From be8ec756c621f1a260011b8c036077686cda3e37 Mon Sep 17 00:00:00 2001 From: palash-signoz Date: Wed, 9 Feb 2022 11:31:13 +0530 Subject: [PATCH] Feat (UI) :Trace Filter page is updated (#684) * dayjs and less loader is added * webpack config is added * moment is removed * useDebounceFunction hook is made * old components and reducer is removed * search is updated * changes are upadted for the trace page as skeleton is ready * chore: method is change from dayjs * convertObject into params is updated * initial filters are updated * initial and final filter issue is fixed * selection of the filter is updated * filters are now able to selected * checkbox disable when loading is in progress * chore: getFilter filename is updated * feat: clear all and exapanded filter is updated * chore: clearAll and expand panel is updated * feat: useClickOutSide hook is added * chore: get filter url becomes encoded * chore: get tag filters is added * feat: search tags is wip * bug: global max,min on change bug is resolved * chore: getInitial filter is updated * chore: expand panel is updated * chore: get filter is updated * chore: code smells is updated * feat: loader is added in the panel header to show the loading * chore: search tags in wip * chore: button style is updated * chore: search in wip * chore: search ui is updated from the global state * chore: search in wip * chore: search is updated * chore: getSpansAggregate section is updated * useOutside click is updated * useclickoutside hook is updated * useclickoutside hook is updated * parsing is updated * initial filter is updated * feat: trace table is updated * chore: trace table is updated * chore: useClickout side is updated for the search panel * feat: unneccesary re-render and code is removed * chore: trace table is updated * custom component is removed and used antd search component * error state is updated over search component * chore: search bar is updated * chore: left panel search and table component connection is updated * chore: trace filter config is updated * chore: for graph reducer is updated * chore: graph is updated * chore: table is updated * chore: spans is updated * chore: reducer is updated * chore: graph component is updated * chore: number of graph condition is updated * chore: input and range slider is now sync * chore: duration is updated * chore: clearAllFilter is updated * chore: duration slider is updated * chore: duration is updated and panel body loading is updated * chore: slider container is added to add padding from left to right * chore: Select filter is updated * chore: duration filter is updated * chore: Divider is added * chore: none option is added in both the dropdown * chore: icon are updated * chore: added padding in the pages component * chore: none is updated * chore: antd notification is added in the redux action * chore: some of the changes are updated * chore: display value is updated for the filter panel heading * chore: calulation is memorised * chore: utils function are updated in trace reducer * chore: getFilters are updated * tracetable is updated * chore: actions is updated * chore: metrics application is updated * chore: search on clear action is updated * chore: serviceName panel position is updated * chore: added the label in the duration * bug: edge case is fixed * chore: some more changes are updated * chore: some more changes are updated * chore: clear all is fixed * chore: panel heading caret is updated * chore: checkbox is updated * chore: isError handler is updated over initial render * chore: traces is updated * fix: tag search is updated * chore: loading is added in the trace table and soring is introduced in the trace table * bug: multiple render for the key is fixed * Bug(UI): new suggestion is updated * feat: isTraceFilterEnum function is made * bug: new changes are updated * chore: get Filter is updated * chore: application metrics params is updated * chore: error is added in the application metrics * chore: filters is updated * chore: expand panel edge case is updated * chore: expand panel is updated and utls: updateUrl function is updated * chore: reset trace state when unmounted * chore: getFilter action is updated * chore: api duration is updated * chore: useEffect dependency is updated * chore: filter is updated with the new arch * bug: trace table issue is resolved * chore: application rps url is updated for trace * chore: duration filter is updated * chore: search key is updated * chore: filter is added in the search url * bug: filter is fixed * bug: filter is fixed * bug: filter is fixed * chore: reset trace data when unmounted * chore: TopEnd point is added * chore: getInitialSpanAggregate action is updated * chore: application url is updated * chore: no tags placeholder is updated * chore: flow from customer is now fixed * chore: search is updated * chore: select all button is removed * chore: prev filter is removed to show the result * chore: config is updated * chore: checkbox component is updated * chore: span filter is updated * chore: graph issue is resolved * chore: selected is updated * chore: all filter are selected * feat: new trace page is updated * chore: utils is updated * feat: trace filter page is updated * chore: duration is now fixed * chore: duration clear filter is added * chore: onClickCheck is updated * chore: trace filter page is updated * bug: some of bugs are resolved * chore: duration body is updated * chore: topEndPoint and application query is updated * chore: user selection is updated in the duration filter * chore: panel duration is updated * chore: panel duration is updated * chore: duration bug is solved * chore: function display value is updated --- frontend/package.json | 3 + frontend/src/AppRoutes/pageComponents.ts | 2 +- frontend/src/api/alerts/getGroup.ts | 5 +- frontend/src/api/trace/getFilters.ts | 48 +++ frontend/src/api/trace/getServiceList.ts | 24 -- frontend/src/api/trace/getServiceOperation.ts | 24 -- frontend/src/api/trace/getSpan.ts | 26 -- frontend/src/api/trace/getSpanAggregate.ts | 26 -- frontend/src/api/trace/getSpans.ts | 59 +++ frontend/src/api/trace/getSpansAggregate.ts | 60 +++ frontend/src/api/trace/getTagFilter.ts | 38 ++ frontend/src/api/trace/getTags.ts | 24 -- frontend/src/components/DatePicker/index.tsx | 7 + frontend/src/components/Graph/index.tsx | 2 +- .../Header/CustomDateTimeModal/index.tsx | 12 +- .../Header/DateTimeSelection/index.tsx | 55 +-- .../MetricsApplication/Tabs/Application.tsx | 19 +- .../MetricsApplication/TopEndpointsTable.tsx | 10 +- .../Panel/PanelBody/Common/Checkbox.tsx | 191 +++++++++ .../Filters/Panel/PanelBody/Common/styles.ts | 11 + .../Panel/PanelBody/CommonCheckBox/index.tsx | 36 ++ .../Panel/PanelBody/Duration/index.tsx | 212 ++++++++++ .../Panel/PanelBody/Duration/styles.ts | 27 ++ .../Trace/Filters/Panel/PanelBody/index.tsx | 37 ++ .../Trace/Filters/Panel/PanelBody/styles.ts | 3 + .../Filters/Panel/PanelHeading/index.tsx | 317 ++++++++++++++ .../Filters/Panel/PanelHeading/styles.ts | 49 +++ .../container/Trace/Filters/Panel/index.tsx | 28 ++ .../src/container/Trace/Filters/index.tsx | 27 ++ .../src/container/Trace/Filters/styles.ts | 20 + frontend/src/container/Trace/Graph/config.ts | 123 ++++++ frontend/src/container/Trace/Graph/index.tsx | 48 +++ frontend/src/container/Trace/Graph/styles.ts | 19 + .../Trace/Search/AllTags/Tag/TagKey.tsx | 102 +++++ .../Trace/Search/AllTags/Tag/index.tsx | 130 ++++++ .../Trace/Search/AllTags/Tag/styles.ts | 39 ++ .../container/Trace/Search/AllTags/index.tsx | 158 +++++++ .../container/Trace/Search/AllTags/styles.ts | 56 +++ frontend/src/container/Trace/Search/index.tsx | 163 ++++++++ frontend/src/container/Trace/Search/styles.ts | 17 + frontend/src/container/Trace/Search/util.ts | 84 ++++ .../Trace/TraceGraphFilter/config.ts | 91 ++++ .../Trace/TraceGraphFilter/index.tsx | 85 ++++ .../Trace/TraceGraphFilter/styles.ts | 9 + .../src/container/Trace/TraceTable/index.tsx | 155 +++++++ .../TraceCustomGraph.tsx | 44 -- .../TraceCustomVisualization/config.ts | 56 --- .../TraceCustomVisualization/index.tsx | 127 ------ .../TraceCustomVisualization/styles.ts | 34 -- frontend/src/container/TraceFilter/Filter.tsx | 182 -------- .../src/container/TraceFilter/LatencyForm.tsx | 160 ------- frontend/src/container/TraceFilter/config.ts | 15 - frontend/src/container/TraceFilter/index.tsx | 390 ------------------ frontend/src/container/TraceFilter/styles.ts | 34 -- frontend/src/container/TraceList/index.tsx | 141 ------- frontend/src/container/TraceList/styles.ts | 7 - frontend/src/hooks/useClickOutside.ts | 26 ++ frontend/src/hooks/useDebouncedFunction.ts | 34 ++ .../src/lib/query/convertObjectIntoParams.ts | 15 + frontend/src/pages/Trace/index.tsx | 157 +++++++ frontend/src/pages/Trace/styles.ts | 37 ++ frontend/src/pages/TraceDetails/index.tsx | 76 ---- .../src/store/actions/trace/getInitialData.ts | 201 --------- .../store/actions/trace/getInitialFilter.ts | 173 ++++++++ .../actions/trace/getInitialSpansAggregate.ts | 115 ++++++ frontend/src/store/actions/trace/getSpans.ts | 96 +++++ .../actions/trace/getTraceVisualAgrregates.ts | 92 ----- frontend/src/store/actions/trace/index.ts | 9 - .../store/actions/trace/loadingCompleted.ts | 12 - .../actions/trace/parseFilter/current.ts | 36 ++ .../store/actions/trace/parseFilter/filter.ts | 43 ++ .../trace/parseFilter/filterToFetchData.ts | 37 ++ .../store/actions/trace/parseFilter/index.ts | 8 + .../trace/parseFilter/isFilterExclude.ts | 44 ++ .../actions/trace/parseFilter/minMaxTime.ts | 20 + .../trace/parseFilter/selectedFilter.ts | 43 ++ .../actions/trace/parseFilter/selectedTags.ts | 37 ++ .../trace/parseFilter/skippedSelected.ts | 33 ++ .../store/actions/trace/resetTraceDetails.ts | 10 - .../store/actions/trace/selectTraceFilter.ts | 50 +++ .../store/actions/trace/updateIsTagsError.ts | 17 + .../actions/trace/updateSelectedAggOption.ts | 16 - .../store/actions/trace/updateSelectedData.ts | 164 -------- .../actions/trace/updateSelectedEntity.ts | 16 - .../store/actions/trace/updateSelectedKind.ts | 16 - .../actions/trace/updateSelectedLatency.ts | 16 - .../actions/trace/updateSelectedOperation.ts | 16 - .../actions/trace/updateSelectedService.ts | 16 - .../store/actions/trace/updateSelectedTags.ts | 94 ----- .../store/actions/trace/updateSpanLoading.ts | 16 - .../actions/trace/updateTagPanelVisiblity.ts | 17 + .../store/actions/trace/updateTagsSelected.ts | 17 + frontend/src/store/actions/trace/util.ts | 79 ++++ frontend/src/store/reducers/index.ts | 7 +- frontend/src/store/reducers/trace.ts | 301 +++++++------- frontend/src/types/actions/trace.ts | 251 +++++------ frontend/src/types/api/trace/getFilters.ts | 15 + .../src/types/api/trace/getServiceList.ts | 1 - .../types/api/trace/getServiceOperation.ts | 5 - .../src/types/api/trace/getSpanAggregate.ts | 26 +- frontend/src/types/api/trace/getSpans.ts | 67 +-- frontend/src/types/api/trace/getTagFilters.ts | 13 + frontend/src/types/api/trace/getTags.ts | 10 - frontend/src/types/reducer/trace.ts | 138 +++++-- frontend/webpack.config.js | 19 + frontend/webpack.config.prod.js | 19 + frontend/yarn.lock | 92 ++++- 107 files changed, 4206 insertions(+), 2533 deletions(-) create mode 100644 frontend/src/api/trace/getFilters.ts delete mode 100644 frontend/src/api/trace/getServiceList.ts delete mode 100644 frontend/src/api/trace/getServiceOperation.ts delete mode 100644 frontend/src/api/trace/getSpan.ts delete mode 100644 frontend/src/api/trace/getSpanAggregate.ts create mode 100644 frontend/src/api/trace/getSpans.ts create mode 100644 frontend/src/api/trace/getSpansAggregate.ts create mode 100644 frontend/src/api/trace/getTagFilter.ts delete mode 100644 frontend/src/api/trace/getTags.ts create mode 100644 frontend/src/components/DatePicker/index.tsx create mode 100644 frontend/src/container/Trace/Filters/Panel/PanelBody/Common/Checkbox.tsx create mode 100644 frontend/src/container/Trace/Filters/Panel/PanelBody/Common/styles.ts create mode 100644 frontend/src/container/Trace/Filters/Panel/PanelBody/CommonCheckBox/index.tsx create mode 100644 frontend/src/container/Trace/Filters/Panel/PanelBody/Duration/index.tsx create mode 100644 frontend/src/container/Trace/Filters/Panel/PanelBody/Duration/styles.ts create mode 100644 frontend/src/container/Trace/Filters/Panel/PanelBody/index.tsx create mode 100644 frontend/src/container/Trace/Filters/Panel/PanelBody/styles.ts create mode 100644 frontend/src/container/Trace/Filters/Panel/PanelHeading/index.tsx create mode 100644 frontend/src/container/Trace/Filters/Panel/PanelHeading/styles.ts create mode 100644 frontend/src/container/Trace/Filters/Panel/index.tsx create mode 100644 frontend/src/container/Trace/Filters/index.tsx create mode 100644 frontend/src/container/Trace/Filters/styles.ts create mode 100644 frontend/src/container/Trace/Graph/config.ts create mode 100644 frontend/src/container/Trace/Graph/index.tsx create mode 100644 frontend/src/container/Trace/Graph/styles.ts create mode 100644 frontend/src/container/Trace/Search/AllTags/Tag/TagKey.tsx create mode 100644 frontend/src/container/Trace/Search/AllTags/Tag/index.tsx create mode 100644 frontend/src/container/Trace/Search/AllTags/Tag/styles.ts create mode 100644 frontend/src/container/Trace/Search/AllTags/index.tsx create mode 100644 frontend/src/container/Trace/Search/AllTags/styles.ts create mode 100644 frontend/src/container/Trace/Search/index.tsx create mode 100644 frontend/src/container/Trace/Search/styles.ts create mode 100644 frontend/src/container/Trace/Search/util.ts create mode 100644 frontend/src/container/Trace/TraceGraphFilter/config.ts create mode 100644 frontend/src/container/Trace/TraceGraphFilter/index.tsx create mode 100644 frontend/src/container/Trace/TraceGraphFilter/styles.ts create mode 100644 frontend/src/container/Trace/TraceTable/index.tsx delete mode 100644 frontend/src/container/TraceCustomVisualization/TraceCustomGraph.tsx delete mode 100644 frontend/src/container/TraceCustomVisualization/config.ts delete mode 100644 frontend/src/container/TraceCustomVisualization/index.tsx delete mode 100644 frontend/src/container/TraceCustomVisualization/styles.ts delete mode 100644 frontend/src/container/TraceFilter/Filter.tsx delete mode 100644 frontend/src/container/TraceFilter/LatencyForm.tsx delete mode 100644 frontend/src/container/TraceFilter/config.ts delete mode 100644 frontend/src/container/TraceFilter/index.tsx delete mode 100644 frontend/src/container/TraceFilter/styles.ts delete mode 100644 frontend/src/container/TraceList/index.tsx delete mode 100644 frontend/src/container/TraceList/styles.ts create mode 100644 frontend/src/hooks/useClickOutside.ts create mode 100644 frontend/src/hooks/useDebouncedFunction.ts create mode 100644 frontend/src/lib/query/convertObjectIntoParams.ts create mode 100644 frontend/src/pages/Trace/index.tsx create mode 100644 frontend/src/pages/Trace/styles.ts delete mode 100644 frontend/src/pages/TraceDetails/index.tsx delete mode 100644 frontend/src/store/actions/trace/getInitialData.ts create mode 100644 frontend/src/store/actions/trace/getInitialFilter.ts create mode 100644 frontend/src/store/actions/trace/getInitialSpansAggregate.ts create mode 100644 frontend/src/store/actions/trace/getSpans.ts delete mode 100644 frontend/src/store/actions/trace/getTraceVisualAgrregates.ts delete mode 100644 frontend/src/store/actions/trace/index.ts delete mode 100644 frontend/src/store/actions/trace/loadingCompleted.ts create mode 100644 frontend/src/store/actions/trace/parseFilter/current.ts create mode 100644 frontend/src/store/actions/trace/parseFilter/filter.ts create mode 100644 frontend/src/store/actions/trace/parseFilter/filterToFetchData.ts create mode 100644 frontend/src/store/actions/trace/parseFilter/index.ts create mode 100644 frontend/src/store/actions/trace/parseFilter/isFilterExclude.ts create mode 100644 frontend/src/store/actions/trace/parseFilter/minMaxTime.ts create mode 100644 frontend/src/store/actions/trace/parseFilter/selectedFilter.ts create mode 100644 frontend/src/store/actions/trace/parseFilter/selectedTags.ts create mode 100644 frontend/src/store/actions/trace/parseFilter/skippedSelected.ts delete mode 100644 frontend/src/store/actions/trace/resetTraceDetails.ts create mode 100644 frontend/src/store/actions/trace/selectTraceFilter.ts create mode 100644 frontend/src/store/actions/trace/updateIsTagsError.ts delete mode 100644 frontend/src/store/actions/trace/updateSelectedAggOption.ts delete mode 100644 frontend/src/store/actions/trace/updateSelectedData.ts delete mode 100644 frontend/src/store/actions/trace/updateSelectedEntity.ts delete mode 100644 frontend/src/store/actions/trace/updateSelectedKind.ts delete mode 100644 frontend/src/store/actions/trace/updateSelectedLatency.ts delete mode 100644 frontend/src/store/actions/trace/updateSelectedOperation.ts delete mode 100644 frontend/src/store/actions/trace/updateSelectedService.ts delete mode 100644 frontend/src/store/actions/trace/updateSelectedTags.ts delete mode 100644 frontend/src/store/actions/trace/updateSpanLoading.ts create mode 100644 frontend/src/store/actions/trace/updateTagPanelVisiblity.ts create mode 100644 frontend/src/store/actions/trace/updateTagsSelected.ts create mode 100644 frontend/src/store/actions/trace/util.ts create mode 100644 frontend/src/types/api/trace/getFilters.ts delete mode 100644 frontend/src/types/api/trace/getServiceList.ts delete mode 100644 frontend/src/types/api/trace/getServiceOperation.ts create mode 100644 frontend/src/types/api/trace/getTagFilters.ts delete mode 100644 frontend/src/types/api/trace/getTags.ts diff --git a/frontend/package.json b/frontend/package.json index fbde581ba4..4eaf1f5fd8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -42,11 +42,14 @@ "d3": "^6.2.0", "d3-flame-graph": "^3.1.1", "d3-tip": "^0.9.1", + "dayjs": "^1.10.7", "dotenv": "8.2.0", "file-loader": "6.1.1", "history": "4.10.1", "html-webpack-plugin": "5.1.0", "jest": "26.6.0", + "less": "^4.1.2", + "less-loader": "^10.2.0", "mini-css-extract-plugin": "2.4.5", "monaco-editor": "^0.30.0", "react": "17.0.0", diff --git a/frontend/src/AppRoutes/pageComponents.ts b/frontend/src/AppRoutes/pageComponents.ts index ee9095f014..feb99a7c24 100644 --- a/frontend/src/AppRoutes/pageComponents.ts +++ b/frontend/src/AppRoutes/pageComponents.ts @@ -19,7 +19,7 @@ export const ServiceMapPage = Loadable( ); export const TraceDetailPages = Loadable( - () => import(/* webpackChunkName: "TraceDetailPage" */ 'pages/TraceDetails'), + () => import(/* webpackChunkName: "TraceDetailPage" */ 'pages/Trace'), ); export const TraceGraphPage = Loadable( diff --git a/frontend/src/api/alerts/getGroup.ts b/frontend/src/api/alerts/getGroup.ts index f1cb2c6d77..adfd0c18e6 100644 --- a/frontend/src/api/alerts/getGroup.ts +++ b/frontend/src/api/alerts/getGroup.ts @@ -3,14 +3,13 @@ import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; import { AxiosError } from 'axios'; import { ErrorResponse, SuccessResponse } from 'types/api'; import { PayloadProps, Props } from 'types/api/alerts/getGroups'; +import convertObjectIntoParams from 'lib/query/convertObjectIntoParams'; const getGroups = async ( props: Props, ): Promise | ErrorResponse> => { try { - const queryParams = Object.keys(props) - .map((e) => `${e}=${props[e]}`) - .join('&'); + const queryParams = convertObjectIntoParams(props); const response = await AxiosAlertManagerInstance.get( `/alerts/groups?${queryParams}`, diff --git a/frontend/src/api/trace/getFilters.ts b/frontend/src/api/trace/getFilters.ts new file mode 100644 index 0000000000..6fb484673f --- /dev/null +++ b/frontend/src/api/trace/getFilters.ts @@ -0,0 +1,48 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { PayloadProps, Props } from 'types/api/trace/getFilters'; +import omitBy from 'lodash-es/omitBy'; + +const getFilters = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const duration = + omitBy(props.other, (_, key) => !key.startsWith('duration')) || []; + + const nonDuration = omitBy(props.other, (_, key) => + key.startsWith('duration'), + ); + + const exclude: string[] = []; + + props.isFilterExclude.forEach((value, key) => { + if (value) { + exclude.push(key); + } + }); + + const response = await axios.post(`/getSpanFilters`, { + start: props.start, + end: props.end, + getFilters: props.getFilters, + ...nonDuration, + maxDuration: String((duration['duration'] || [])[0] || ''), + minDuration: String((duration['duration'] || [])[1] || ''), + exclude: exclude, + }); + + return { + statusCode: 200, + error: null, + message: 'Success', + payload: response.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default getFilters; diff --git a/frontend/src/api/trace/getServiceList.ts b/frontend/src/api/trace/getServiceList.ts deleted file mode 100644 index 0fc063e721..0000000000 --- a/frontend/src/api/trace/getServiceList.ts +++ /dev/null @@ -1,24 +0,0 @@ -import axios from 'api'; -import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; -import { AxiosError } from 'axios'; -import { ErrorResponse, SuccessResponse } from 'types/api'; -import { PayloadProps } from 'types/api/trace/getServiceList'; - -const getServiceList = async (): Promise< - SuccessResponse | ErrorResponse -> => { - try { - const response = await axios.get('/services/list'); - - return { - statusCode: 200, - error: null, - message: 'Success', - payload: response.data, - }; - } catch (error) { - return ErrorResponseHandler(error as AxiosError); - } -}; - -export default getServiceList; diff --git a/frontend/src/api/trace/getServiceOperation.ts b/frontend/src/api/trace/getServiceOperation.ts deleted file mode 100644 index 04ee9c954a..0000000000 --- a/frontend/src/api/trace/getServiceOperation.ts +++ /dev/null @@ -1,24 +0,0 @@ -import axios from 'api'; -import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; -import { AxiosError } from 'axios'; -import { ErrorResponse, SuccessResponse } from 'types/api'; -import { PayloadProps, Props } from 'types/api/trace/getServiceOperation'; - -const getServiceOperation = async ( - props: Props, -): Promise | ErrorResponse> => { - try { - const response = await axios.get(`/service/${props.service}/operations`); - - return { - statusCode: 200, - error: null, - message: 'Success', - payload: response.data, - }; - } catch (error) { - return ErrorResponseHandler(error as AxiosError); - } -}; - -export default getServiceOperation; diff --git a/frontend/src/api/trace/getSpan.ts b/frontend/src/api/trace/getSpan.ts deleted file mode 100644 index 6b36b23cdc..0000000000 --- a/frontend/src/api/trace/getSpan.ts +++ /dev/null @@ -1,26 +0,0 @@ -import axios from 'api'; -import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; -import { AxiosError } from 'axios'; -import { ErrorResponse, SuccessResponse } from 'types/api'; -import { PayloadProps, Props } from 'types/api/trace/getSpans'; - -const getSpans = async ( - props: Props, -): Promise | ErrorResponse> => { - try { - const response = await axios.get( - `/spans?&start=${props.start}&end=${props.end}&kind=${props.kind}&lookback=${props.lookback}&maxDuration=${props.maxDuration}&minDuration=${props.minDuration}&operation=${props.operation}&service=${props.service}&limit=${props.limit}&tags=${props.tags}`, - ); - - return { - statusCode: 200, - error: null, - message: 'Success', - payload: response.data, - }; - } catch (error) { - return ErrorResponseHandler(error as AxiosError); - } -}; - -export default getSpans; diff --git a/frontend/src/api/trace/getSpanAggregate.ts b/frontend/src/api/trace/getSpanAggregate.ts deleted file mode 100644 index d233dc25e5..0000000000 --- a/frontend/src/api/trace/getSpanAggregate.ts +++ /dev/null @@ -1,26 +0,0 @@ -import axios from 'api'; -import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; -import { AxiosError } from 'axios'; -import { ErrorResponse, SuccessResponse } from 'types/api'; -import { PayloadProps, Props } from 'types/api/trace/getSpanAggregate'; - -const getSpansAggregate = async ( - props: Props, -): Promise | ErrorResponse> => { - try { - const response = await axios.get( - `/spans/aggregates?start=${props.start}&end=${props.end}&aggregation_option=${props.aggregation_option}&dimension=${props.dimension}&kind=${props.kind}&maxDuration=${props.maxDuration}&minDuration=${props.minDuration}&operation=${props.operation}&service=${props.service}&step=${props.step}&tags=${props.tags}`, - ); - - return { - statusCode: 200, - error: null, - message: 'Success', - payload: response.data, - }; - } catch (error) { - return ErrorResponseHandler(error as AxiosError); - } -}; - -export default getSpansAggregate; diff --git a/frontend/src/api/trace/getSpans.ts b/frontend/src/api/trace/getSpans.ts new file mode 100644 index 0000000000..8527eb2732 --- /dev/null +++ b/frontend/src/api/trace/getSpans.ts @@ -0,0 +1,59 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import omitBy from 'lodash-es/omitBy'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { PayloadProps, Props } from 'types/api/trace/getSpans'; + +const getSpans = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const updatedSelectedTags = props.selectedTags.map((e) => ({ + Key: e.Key[0], + Operator: e.Operator, + Values: e.Values, + })); + + const exclude: string[] = []; + + props.isFilterExclude.forEach((value, key) => { + if (value) { + exclude.push(key); + } + }); + + const other = Object.fromEntries(props.selectedFilter); + + const duration = omitBy(other, (_, key) => !key.startsWith('duration')) || []; + + const nonDuration = omitBy(other, (_, key) => key.startsWith('duration')); + + const response = await axios.post( + `/getFilteredSpans/aggregates`, + { + start: String(props.start), + end: String(props.end), + function: props.function, + groupBy: props.groupBy, + step: props.step, + tags: updatedSelectedTags, + ...nonDuration, + maxDuration: String((duration['duration'] || [])[0] || ''), + minDuration: String((duration['duration'] || [])[1] || ''), + exclude, + }, + ); + + return { + statusCode: 200, + error: null, + message: 'Success', + payload: response.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default getSpans; diff --git a/frontend/src/api/trace/getSpansAggregate.ts b/frontend/src/api/trace/getSpansAggregate.ts new file mode 100644 index 0000000000..1f63f034e4 --- /dev/null +++ b/frontend/src/api/trace/getSpansAggregate.ts @@ -0,0 +1,60 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import omitBy from 'lodash-es/omitBy'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { PayloadProps, Props } from 'types/api/trace/getSpanAggregate'; +import { TraceFilterEnum } from 'types/reducer/trace'; + +const getSpanAggregate = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const preProps = { + start: String(props.start), + end: String(props.end), + limit: props.limit, + offset: props.offset, + }; + + const exclude: TraceFilterEnum[] = []; + + props.isFilterExclude.forEach((value, key) => { + if (value) { + exclude.push(key); + } + }); + + const updatedSelectedTags = props.selectedTags.map((e) => ({ + Key: e.Key[0], + Operator: e.Operator, + Values: e.Values, + })); + + const other = Object.fromEntries(props.selectedFilter); + + const duration = omitBy(other, (_, key) => !key.startsWith('duration')) || []; + + const nonDuration = omitBy(other, (_, key) => key.startsWith('duration')); + + const response = await axios.post(`/getFilteredSpans`, { + ...preProps, + tags: updatedSelectedTags, + ...nonDuration, + maxDuration: String((duration['duration'] || [])[0] || ''), + minDuration: String((duration['duration'] || [])[1] || ''), + exclude, + }); + + return { + statusCode: 200, + error: null, + message: 'Success', + payload: response.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default getSpanAggregate; diff --git a/frontend/src/api/trace/getTagFilter.ts b/frontend/src/api/trace/getTagFilter.ts new file mode 100644 index 0000000000..d54ceb8905 --- /dev/null +++ b/frontend/src/api/trace/getTagFilter.ts @@ -0,0 +1,38 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { omitBy } from 'lodash-es'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { PayloadProps, Props } from 'types/api/trace/getTagFilters'; + +const getTagFilters = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const duration = + omitBy(props.other, (_, key) => !key.startsWith('duration')) || []; + + const nonDuration = omitBy(props.other, (_, key) => + key.startsWith('duration'), + ); + + const response = await axios.post(`/getTagFilters`, { + start: String(props.start), + end: String(props.end), + ...nonDuration, + maxDuration: String((duration['duration'] || [])[0] || ''), + minDuration: String((duration['duration'] || [])[1] || ''), + }); + + return { + statusCode: 200, + error: null, + message: 'Success', + payload: response.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default getTagFilters; diff --git a/frontend/src/api/trace/getTags.ts b/frontend/src/api/trace/getTags.ts deleted file mode 100644 index 430c25381a..0000000000 --- a/frontend/src/api/trace/getTags.ts +++ /dev/null @@ -1,24 +0,0 @@ -import axios from 'api'; -import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; -import { AxiosError } from 'axios'; -import { ErrorResponse, SuccessResponse } from 'types/api'; -import { PayloadProps, Props } from 'types/api/trace/getTags'; - -const getTags = async ( - props: Props, -): Promise | ErrorResponse> => { - try { - const response = await axios.get(`/tags?service=${props.service}`); - - return { - statusCode: 200, - error: null, - message: 'Success', - payload: response.data, - }; - } catch (error) { - return ErrorResponseHandler(error as AxiosError); - } -}; - -export default getTags; diff --git a/frontend/src/components/DatePicker/index.tsx b/frontend/src/components/DatePicker/index.tsx new file mode 100644 index 0000000000..f76439d9d2 --- /dev/null +++ b/frontend/src/components/DatePicker/index.tsx @@ -0,0 +1,7 @@ +import { Dayjs } from 'dayjs'; +import dayjsGenerateConfig from 'rc-picker/lib/generate/dayjs'; +import generatePicker from 'antd/es/date-picker/generatePicker'; + +const DatePicker = generatePicker(dayjsGenerateConfig); + +export default DatePicker; diff --git a/frontend/src/components/Graph/index.tsx b/frontend/src/components/Graph/index.tsx index b6905495d4..eac5ae2712 100644 --- a/frontend/src/components/Graph/index.tsx +++ b/frontend/src/components/Graph/index.tsx @@ -148,7 +148,7 @@ const Graph = ({ useEffect(() => { buildChart(); - }, [buildChart]); + }, []); return (
diff --git a/frontend/src/container/Header/CustomDateTimeModal/index.tsx b/frontend/src/container/Header/CustomDateTimeModal/index.tsx index 8ebb25bd32..168a0c8117 100644 --- a/frontend/src/container/Header/CustomDateTimeModal/index.tsx +++ b/frontend/src/container/Header/CustomDateTimeModal/index.tsx @@ -1,8 +1,8 @@ -import { DatePicker, Modal } from 'antd'; -import { Moment } from 'moment'; -import moment from 'moment'; +import { Modal } from 'antd'; import React, { useState } from 'react'; -export type DateTimeRangeType = [Moment | null, Moment | null] | null; +export type DateTimeRangeType = [Dayjs | null, Dayjs | null] | null; +import DatePicker from 'components/DatePicker'; +import dayjs, { Dayjs } from 'dayjs'; const { RangePicker } = DatePicker; @@ -20,8 +20,8 @@ const CustomDateTimeModal = ({ setCustomDateTimeRange(date_time); } - function disabledDate(current: Moment): boolean { - if (current > moment()) { + function disabledDate(current: Dayjs): boolean { + if (current > dayjs()) { return true; } else { return false; diff --git a/frontend/src/container/Header/DateTimeSelection/index.tsx b/frontend/src/container/Header/DateTimeSelection/index.tsx index c6c5ff4015..ce7dcce6d6 100644 --- a/frontend/src/container/Header/DateTimeSelection/index.tsx +++ b/frontend/src/container/Header/DateTimeSelection/index.tsx @@ -8,7 +8,7 @@ import getLocalStorageKey from 'api/browser/localstorage/get'; import setLocalStorageKey from 'api/browser/localstorage/set'; import { LOCAL_STORAGE } from 'constants/localStorage'; import getTimeString from 'lib/getTimeString'; -import moment from 'moment'; +import dayjs, { Dayjs } from 'dayjs'; import { connect, useSelector } from 'react-redux'; import { RouteComponentProps, withRouter } from 'react-router'; import { bindActionCreators, Dispatch } from 'redux'; @@ -37,26 +37,18 @@ const DateTimeSelection = ({ const getTime = useCallback((): [number, number] | undefined => { if (searchEndTime && searchStartTime) { - const startMoment = moment( + const startDate = dayjs( new Date(parseInt(getTimeString(searchStartTime), 10)), ); - const endMoment = moment( - new Date(parseInt(getTimeString(searchEndTime), 10)), - ); + const endDate = dayjs(new Date(parseInt(getTimeString(searchEndTime), 10))); - return [ - startMoment.toDate().getTime() || 0, - endMoment.toDate().getTime() || 0, - ]; + return [startDate.toDate().getTime() || 0, endDate.toDate().getTime() || 0]; } if (localstorageStartTime && localstorageEndTime) { - const startMoment = moment(localstorageStartTime); - const endMoment = moment(localstorageEndTime); + const startDate = dayjs(localstorageStartTime); + const endDate = dayjs(localstorageEndTime); - return [ - startMoment.toDate().getTime() || 0, - endMoment.toDate().getTime() || 0, - ]; + return [startDate.toDate().getTime() || 0, endDate.toDate().getTime() || 0]; } return undefined; }, [ @@ -66,8 +58,8 @@ const DateTimeSelection = ({ searchStartTime, ]); - const [startTime, setStartTime] = useState(); - const [endTime, setEndTime] = useState(); + const [startTime, setStartTime] = useState(); + const [endTime, setEndTime] = useState(); const [options, setOptions] = useState(getOptions(location.pathname)); const [refreshButtonHidden, setRefreshButtonHidden] = useState(false); @@ -136,8 +128,8 @@ const DateTimeSelection = ({ }; const getInputLabel = ( - startTime?: moment.Moment, - endTime?: moment.Moment, + startTime?: Dayjs, + endTime?: Dayjs, timeInterval: Time = '15min', ): string | Time => { if (startTime && endTime && timeInterval === 'custom') { @@ -153,18 +145,18 @@ const DateTimeSelection = ({ }; const onLastRefreshHandler = useCallback(() => { - const currentTime = moment(); + const currentTime = dayjs(); - const lastRefresh = moment( + const lastRefresh = dayjs( selectedTimeInterval === 'custom' ? minTime / 1000000 : maxTime / 1000000, ); - const duration = moment.duration(currentTime.diff(lastRefresh)); - const secondsDiff = Math.floor(duration.asSeconds()); - const minutedDiff = Math.floor(duration.asMinutes()); - const hoursDiff = Math.floor(duration.asHours()); - const daysDiff = Math.floor(duration.asDays()); - const monthsDiff = Math.floor(duration.asMonths()); + const secondsDiff = currentTime.diff(lastRefresh, 'seconds'); + + const minutedDiff = currentTime.diff(lastRefresh, 'minutes'); + const hoursDiff = currentTime.diff(lastRefresh, 'hours'); + const daysDiff = currentTime.diff(lastRefresh, 'days'); + const monthsDiff = currentTime.diff(lastRefresh, 'months'); if (monthsDiff > 0) { return `Last refresh -${monthsDiff} months ago`; @@ -242,8 +234,8 @@ const DateTimeSelection = ({ const [preStartTime = 0, preEndTime = 0] = getTime() || []; - setStartTime(moment(preStartTime)); - setEndTime(moment(preEndTime)); + setStartTime(dayjs(preStartTime)); + setEndTime(dayjs(preEndTime)); updateTimeInterval(updatedTime, [preStartTime, preEndTime]); }, [ @@ -318,8 +310,3 @@ const mapDispatchToProps = ( type Props = DispatchProps & RouteComponentProps; export default connect(null, mapDispatchToProps)(withRouter(DateTimeSelection)); - -// DateTimeSelection.whyDidYouRender = { -// logOnDifferentValues: true, -// customName: 'DateTimeSelection', -// }; diff --git a/frontend/src/container/MetricsApplication/Tabs/Application.tsx b/frontend/src/container/MetricsApplication/Tabs/Application.tsx index 6c126216cc..005033f999 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Application.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Application.tsx @@ -32,11 +32,12 @@ const Application = ({ getWidget }: DashboardProps): JSX.Element => { const urlParams = new URLSearchParams(); urlParams.set(METRICS_PAGE_QUERY_PARAM.startTime, currentTime.toString()); urlParams.set(METRICS_PAGE_QUERY_PARAM.endTime, tPlusOne.toString()); - if (servicename) { - urlParams.set(METRICS_PAGE_QUERY_PARAM.service, servicename); - } - history.push(`${ROUTES.TRACE}?${urlParams.toString()}`); + history.replace( + `${ + ROUTES.TRACE + }?${urlParams.toString()}&selected={"serviceName":["${servicename}"],"status":["ok","error"]}&filterToFetchData=["duration","status","serviceName"]&userSelectedFilter={"status":["error","ok"],"serviceName":["${servicename}"]}&isSelectedFilterSkipped=true`, + ); }; const onClickhandler = async ( @@ -85,12 +86,12 @@ const Application = ({ getWidget }: DashboardProps): JSX.Element => { const urlParams = new URLSearchParams(); urlParams.set(METRICS_PAGE_QUERY_PARAM.startTime, currentTime.toString()); urlParams.set(METRICS_PAGE_QUERY_PARAM.endTime, tPlusOne.toString()); - if (servicename) { - urlParams.set(METRICS_PAGE_QUERY_PARAM.service, servicename); - } - urlParams.set(METRICS_PAGE_QUERY_PARAM.error, 'true'); - history.push(`${ROUTES.TRACE}?${urlParams.toString()}`); + history.replace( + `${ + ROUTES.TRACE + }?${urlParams.toString()}&selected={"serviceName":["${servicename}"],"status":["error"]}&filterToFetchData=["duration","status","serviceName"]&userSelectedFilter={"status":["error"],"serviceName":["${servicename}"]}&isSelectedFilterSkipped=true`, + ); }; return ( diff --git a/frontend/src/container/MetricsApplication/TopEndpointsTable.tsx b/frontend/src/container/MetricsApplication/TopEndpointsTable.tsx index 914423f418..9cd8f54821 100644 --- a/frontend/src/container/MetricsApplication/TopEndpointsTable.tsx +++ b/frontend/src/container/MetricsApplication/TopEndpointsTable.tsx @@ -28,12 +28,12 @@ const TopEndpointsTable = (props: TopEndpointsTableProps): JSX.Element => { METRICS_PAGE_QUERY_PARAM.endTime, (maxTime / 1000000).toString(), ); - if (servicename) { - urlParams.set(METRICS_PAGE_QUERY_PARAM.service, servicename); - } - urlParams.set(METRICS_PAGE_QUERY_PARAM.operation, operation); - history.push(`${ROUTES.TRACE}?${urlParams.toString()}`); + history.push( + `${ + ROUTES.TRACE + }?${urlParams.toString()}&selected={"status":["error","ok"],"serviceName":["${servicename}"],"operation":["${operation}"]}&filterToFetchData=["duration","status","serviceName","operation"]&isSelectedFilterSkipped=true&userSelectedFilter={"status":["error","ok"],"serviceName":["${servicename}"],"operation":["${operation}"]}&isSelectedFilterSkipped=true`, + ); }; const columns: ColumnsType = [ diff --git a/frontend/src/container/Trace/Filters/Panel/PanelBody/Common/Checkbox.tsx b/frontend/src/container/Trace/Filters/Panel/PanelBody/Common/Checkbox.tsx new file mode 100644 index 0000000000..7ee07f2c20 --- /dev/null +++ b/frontend/src/container/Trace/Filters/Panel/PanelBody/Common/Checkbox.tsx @@ -0,0 +1,191 @@ +import React, { useState } from 'react'; +import { CheckBoxContainer } from './styles'; +import { Checkbox, notification, Typography } from 'antd'; +import { connect, useDispatch, useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace'; + +import { SelectedTraceFilter } from 'store/actions/trace/selectTraceFilter'; +import AppActions from 'types/actions'; +import { ThunkDispatch } from 'redux-thunk'; +import { bindActionCreators, Dispatch } from 'redux'; +import { getFilter, updateURL } from 'store/actions/trace/util'; +import getFilters from 'api/trace/getFilters'; +import { AxiosError } from 'axios'; +import { GlobalReducer } from 'types/reducer/globalTime'; +import { UPDATE_ALL_FILTERS } from 'types/actions/trace'; + +const CheckBoxComponent = (props: CheckBoxProps): JSX.Element => { + const { + selectedFilter, + filterLoading, + filterToFetchData, + spansAggregate, + selectedTags, + filter, + userSelectedFilter, + isFilterExclude, + } = useSelector((state) => state.traces); + + const globalTime = useSelector( + (state) => state.globalTime, + ); + + const dispatch = useDispatch>(); + + const [isLoading, setIsLoading] = useState(false); + + const isUserSelected = + (userSelectedFilter.get(props.name) || []).find( + (e) => e === props.keyValue, + ) !== undefined; + + const onCheckHandler = async () => { + try { + setIsLoading(true); + + const newSelectedMap = new Map(selectedFilter); + const preUserSelectedMap = new Map(userSelectedFilter); + const preIsFilterExclude = new Map(isFilterExclude); + + const isTopicPresent = preUserSelectedMap.get(props.name); + + // append the value + if (!isTopicPresent) { + preUserSelectedMap.set(props.name, [props.keyValue]); + } else { + const isValuePresent = + isTopicPresent.find((e) => e === props.keyValue) !== undefined; + + // check the value if present then remove the value or isChecked + if (isValuePresent) { + preUserSelectedMap.set( + props.name, + isTopicPresent.filter((e) => e !== props.keyValue), + ); + } else { + // if not present add into the array of string + preUserSelectedMap.set(props.name, [...isTopicPresent, props.keyValue]); + } + } + + if (newSelectedMap.get(props.name)?.find((e) => e === props.keyValue)) { + newSelectedMap.set(props.name, [ + ...(newSelectedMap.get(props.name) || []).filter( + (e) => e !== props.keyValue, + ), + ]); + } else { + newSelectedMap.set(props.name, [ + ...new Set([...(newSelectedMap.get(props.name) || []), props.keyValue]), + ]); + } + + if (preIsFilterExclude.get(props.name) !== false) { + preIsFilterExclude.set(props.name, true); + } + + const response = await getFilters({ + other: Object.fromEntries(newSelectedMap), + end: String(globalTime.maxTime), + start: String(globalTime.minTime), + getFilters: filterToFetchData.filter((e) => e !== props.name), + isFilterExclude: preIsFilterExclude, + }); + + if (response.statusCode === 200) { + const updatedFilter = getFilter(response.payload); + + updatedFilter.forEach((value, key) => { + if (key !== 'duration' && props.name !== key) { + preUserSelectedMap.set(key, Object.keys(value)); + } + }); + + updatedFilter.set(props.name, { + [`${props.keyValue}`]: '-1', + ...(filter.get(props.name) || {}), + ...(updatedFilter.get(props.name) || {}), + }); + + dispatch({ + type: UPDATE_ALL_FILTERS, + payload: { + selectedTags, + current: spansAggregate.currentPage, + filter: updatedFilter, + filterToFetchData, + selectedFilter: newSelectedMap, + userSelected: preUserSelectedMap, + isFilterExclude: preIsFilterExclude, + }, + }); + + setIsLoading(false); + + updateURL( + newSelectedMap, + filterToFetchData, + spansAggregate.currentPage, + selectedTags, + updatedFilter, + preIsFilterExclude, + preUserSelectedMap, + ); + } else { + setIsLoading(false); + + notification.error({ + message: response.error || 'Something went wrong', + }); + } + } catch (error) { + notification.error({ + message: (error as AxiosError).toString() || 'Something went wrong', + }); + setIsLoading(false); + } + }; + + const isCheckBoxSelected = isUserSelected; + + return ( + + + {props.keyValue} + + {isCheckBoxSelected ? ( + {props.value} + ) : ( + - + )} + + ); +}; + +interface DispatchProps { + selectedTraceFilter: (props: { + topic: TraceFilterEnum; + value: string; + }) => void; +} + +interface CheckBoxProps extends DispatchProps { + keyValue: string; + value: string; + name: TraceFilterEnum; +} + +const mapDispatchToProps = ( + dispatch: ThunkDispatch, +): DispatchProps => ({ + selectedTraceFilter: bindActionCreators(SelectedTraceFilter, dispatch), +}); + +export default connect(null, mapDispatchToProps)(CheckBoxComponent); diff --git a/frontend/src/container/Trace/Filters/Panel/PanelBody/Common/styles.ts b/frontend/src/container/Trace/Filters/Panel/PanelBody/Common/styles.ts new file mode 100644 index 0000000000..33a51a5112 --- /dev/null +++ b/frontend/src/container/Trace/Filters/Panel/PanelBody/Common/styles.ts @@ -0,0 +1,11 @@ +import styled from 'styled-components'; + +export const CheckBoxContainer = styled.div` + display: flex; + justify-content: space-between; + margin-left: 1rem; + margin-right: 1rem; + + margin-top: 0.5rem; + margin-bottom: 0.5rem; +`; diff --git a/frontend/src/container/Trace/Filters/Panel/PanelBody/CommonCheckBox/index.tsx b/frontend/src/container/Trace/Filters/Panel/PanelBody/CommonCheckBox/index.tsx new file mode 100644 index 0000000000..a19e7c4ecd --- /dev/null +++ b/frontend/src/container/Trace/Filters/Panel/PanelBody/CommonCheckBox/index.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace'; +import CheckBoxComponent from '../Common/Checkbox'; + +const CommonCheckBox = (props: CommonCheckBoxProps): JSX.Element => { + const { filter } = useSelector( + (state) => state.traces, + ); + + const status = filter.get(props.name) || {}; + + const statusObj = Object.keys(status); + + return ( + <> + {statusObj.map((e) => ( + + ))} + + ); +}; + +interface CommonCheckBoxProps { + name: TraceFilterEnum; +} + +export default CommonCheckBox; diff --git a/frontend/src/container/Trace/Filters/Panel/PanelBody/Duration/index.tsx b/frontend/src/container/Trace/Filters/Panel/PanelBody/Duration/index.tsx new file mode 100644 index 0000000000..f6025b8fbd --- /dev/null +++ b/frontend/src/container/Trace/Filters/Panel/PanelBody/Duration/index.tsx @@ -0,0 +1,212 @@ +import React, { useState } from 'react'; + +import { Input, Slider } from 'antd'; +import { Container, InputContainer, Text } from './styles'; +import { useDispatch, useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { TraceReducer } from 'types/reducer/trace'; +import useDebouncedFn from 'hooks/useDebouncedFunction'; +import { getFilter, updateURL } from 'store/actions/trace/util'; +import dayjs from 'dayjs'; +import durationPlugin from 'dayjs/plugin/duration'; +import { Dispatch } from 'redux'; +import AppActions from 'types/actions'; +import { UPDATE_ALL_FILTERS } from 'types/actions/trace'; +import getFilters from 'api/trace/getFilters'; +import { GlobalReducer } from 'types/reducer/globalTime'; +import { SliderRangeProps } from 'antd/lib/slider'; + +dayjs.extend(durationPlugin); + +const getMs = (value: string) => { + return dayjs + .duration({ + milliseconds: parseInt(value, 10) / 1000000, + }) + .format('SSS'); +}; + +const Duration = (): JSX.Element => { + const { + filter, + selectedFilter, + filterToFetchData, + spansAggregate, + selectedTags, + userSelectedFilter, + isFilterExclude, + } = useSelector((state) => state.traces); + + const dispatch = useDispatch>(); + const globalTime = useSelector( + (state) => state.globalTime, + ); + + const getDuration = () => { + const selectedDuration = selectedFilter.get('duration'); + + if (selectedDuration) { + return { + maxDuration: selectedDuration[0], + minDuration: selectedDuration[1], + }; + } + + return filter.get('duration') || {}; + }; + + const duration = getDuration(); + + const maxDuration = duration['maxDuration'] || '0'; + const minDuration = duration['minDuration'] || '0'; + + const [localMax, setLocalMax] = useState(maxDuration); + const [localMin, setLocalMin] = useState(minDuration); + + const defaultValue = [parseFloat(minDuration), parseFloat(maxDuration)]; + + const updatedUrl = async (min: number, max: number) => { + const preSelectedFilter = new Map(selectedFilter); + const preUserSelected = new Map(userSelectedFilter); + + preSelectedFilter.set('duration', [String(max), String(min)]); + + console.log('on the update Url'); + const response = await getFilters({ + end: String(globalTime.maxTime), + getFilters: filterToFetchData, + other: Object.fromEntries(preSelectedFilter), + start: String(globalTime.minTime), + isFilterExclude, + }); + + if (response.statusCode === 200) { + const preFilter = getFilter(response.payload); + + preFilter.forEach((value, key) => { + if (key !== 'duration') { + preUserSelected.set(key, Object.keys(value)); + } + }); + + dispatch({ + type: UPDATE_ALL_FILTERS, + payload: { + current: spansAggregate.currentPage, + filter: preFilter, + filterToFetchData, + selectedFilter: preSelectedFilter, + selectedTags, + userSelected: preUserSelected, + isFilterExclude, + }, + }); + + updateURL( + preSelectedFilter, + filterToFetchData, + spansAggregate.currentPage, + selectedTags, + preFilter, + isFilterExclude, + userSelectedFilter, + ); + } + }; + + const onRangeSliderHandler = (number: [number, number]) => { + const [min, max] = number; + + setLocalMin(min.toString()); + setLocalMax(max.toString()); + }; + + const debouncedFunction = useDebouncedFn( + (min, max) => { + console.log('debounce function'); + updatedUrl(min, max); + }, + 500, + undefined, + [], + ); + + const onChangeMaxHandler: React.ChangeEventHandler = ( + event, + ) => { + const value = event.target.value; + const min = parseFloat(localMin); + const max = parseFloat(value) * 1000000; + + console.log('on change in max'); + + onRangeSliderHandler([min, max]); + debouncedFunction(min, max); + }; + + const onChangeMinHandler: React.ChangeEventHandler = ( + event, + ) => { + const value = event.target.value; + const min = parseFloat(value) * 1000000; + const max = parseFloat(localMax); + onRangeSliderHandler([min, max]); + console.log('on change in min'); + debouncedFunction(min, max); + }; + + const onRangeHandler: SliderRangeProps['onChange'] = ([min, max]) => { + updatedUrl(min, max); + }; + + return ( +
+ + + Min + + + + + Max + + + + + + { + if (value === undefined) { + return ''; + } + return
{`${getMs(value.toString())}ms`}
; + }} + onChange={([min, max]) => { + onRangeSliderHandler([min, max]); + }} + onAfterChange={onRangeHandler} + // onAfterChange={([min, max]) => { + // const returnFunction = debounce((min, max) => updatedUrl(min, max)); + + // returnFunction(min, max); + // }} + value={[parseFloat(localMin), parseFloat(localMax)]} + /> +
+
+ ); +}; + +export default Duration; diff --git a/frontend/src/container/Trace/Filters/Panel/PanelBody/Duration/styles.ts b/frontend/src/container/Trace/Filters/Panel/PanelBody/Duration/styles.ts new file mode 100644 index 0000000000..150391fce2 --- /dev/null +++ b/frontend/src/container/Trace/Filters/Panel/PanelBody/Duration/styles.ts @@ -0,0 +1,27 @@ +import styled from 'styled-components'; +import { Typography } from 'antd'; + +export const DurationText = styled.div` + display: flex; + align-items: center; + justify-content: space-around; + min-height: 8vh; + flex-direction: column; +`; + +export const InputContainer = styled.div` + width: 100%; + margin-top: 0.5rem; + margin-bottom: 0.2rem; +`; + +export const Text = styled(Typography)` + &&& { + text-align: left; + } +`; + +export const Container = styled.div` + padding-left: 1rem; + padding-right: 1rem; +`; diff --git a/frontend/src/container/Trace/Filters/Panel/PanelBody/index.tsx b/frontend/src/container/Trace/Filters/Panel/PanelBody/index.tsx new file mode 100644 index 0000000000..98364e14d4 --- /dev/null +++ b/frontend/src/container/Trace/Filters/Panel/PanelBody/index.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace'; +import { Card } from 'antd'; + +import Duration from './Duration'; +import CommonCheckBox from './CommonCheckBox'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import Spinner from 'components/Spinner'; + +const PanelBody = (props: PanelBodyProps): JSX.Element => { + const { type } = props; + + const { filterLoading } = useSelector( + (state) => state.traces, + ); + + if (filterLoading) { + return ( + + + + ); + } + + return ( + + {type === 'duration' ? : } + + ); +}; + +interface PanelBodyProps { + type: TraceFilterEnum; +} + +export default PanelBody; diff --git a/frontend/src/container/Trace/Filters/Panel/PanelBody/styles.ts b/frontend/src/container/Trace/Filters/Panel/PanelBody/styles.ts new file mode 100644 index 0000000000..c33898348f --- /dev/null +++ b/frontend/src/container/Trace/Filters/Panel/PanelBody/styles.ts @@ -0,0 +1,3 @@ +import styled from 'styled-components'; + +export const Container = styled.div``; diff --git a/frontend/src/container/Trace/Filters/Panel/PanelHeading/index.tsx b/frontend/src/container/Trace/Filters/Panel/PanelHeading/index.tsx new file mode 100644 index 0000000000..96fc5d5dc7 --- /dev/null +++ b/frontend/src/container/Trace/Filters/Panel/PanelHeading/index.tsx @@ -0,0 +1,317 @@ +import React, { useState } from 'react'; +import { DownOutlined, RightOutlined } from '@ant-design/icons'; +import { Card, Typography, Divider, notification } from 'antd'; + +import { + ButtonComponent, + ButtonContainer, + Container, + IconContainer, + TextCotainer, +} from './styles'; +import { useDispatch, useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace'; +const { Text } = Typography; + +import { AllPanelHeading } from 'types/reducer/trace'; +import getFilters from 'api/trace/getFilters'; +import { GlobalReducer } from 'types/reducer/globalTime'; +import { getFilter, updateURL } from 'store/actions/trace/util'; +import AppActions from 'types/actions'; +import { Dispatch } from 'redux'; +import { UPDATE_ALL_FILTERS } from 'types/actions/trace'; +import { AxiosError } from 'axios'; + +const PanelHeading = (props: PanelHeadingProps): JSX.Element => { + const { + filterLoading, + filterToFetchData, + selectedFilter, + spansAggregate, + selectedTags, + filter, + isFilterExclude, + userSelectedFilter, + } = useSelector((state) => state.traces); + + const isDefaultOpen = + filterToFetchData.find((e) => e === props.name) !== undefined; + + const [isLoading, setIsLoading] = useState(false); + + const global = useSelector( + (state) => state.globalTime, + ); + + const dispatch = useDispatch>(); + + const onExpandHandler: React.MouseEventHandler = async (e) => { + try { + e.preventDefault(); + e.stopPropagation(); + + setIsLoading(true); + let updatedFilterData: TraceReducer['filterToFetchData'] = []; + const getprepdatedSelectedFilter = new Map(selectedFilter); + const getPreUserSelected = new Map(userSelectedFilter); + + if (!isDefaultOpen) { + updatedFilterData = [props.name]; + } else { + // removing the selected filter + updatedFilterData = [ + ...filterToFetchData.filter((name) => name !== props.name), + ]; + getprepdatedSelectedFilter.delete(props.name); + getPreUserSelected.delete(props.name); + } + + const response = await getFilters({ + end: String(global.maxTime), + start: String(global.minTime), + getFilters: updatedFilterData, + other: Object.fromEntries(getprepdatedSelectedFilter), + isFilterExclude, + }); + + if (response.statusCode === 200) { + const updatedFilter = getFilter(response.payload); + + // is closed + if (!isDefaultOpen) { + // getprepdatedSelectedFilter.set( + // props.name, + // Object.keys(updatedFilter.get(props.name) || {}), + // ); + + getPreUserSelected.set( + props.name, + Object.keys(updatedFilter.get(props.name) || {}), + ); + + updatedFilterData = [...filterToFetchData, props.name]; + } + + // now append the non prop.name trace filter enum over the list + // selectedFilter.forEach((value, key) => { + // if (key !== props.name) { + // getprepdatedSelectedFilter.set(key, value); + // } + // }); + + getPreUserSelected.forEach((value, key) => { + if (key !== props.name) { + getPreUserSelected.set(key, value); + } + }); + filter.forEach((value, key) => { + if (key !== props.name) { + updatedFilter.set(key, value); + } + }); + + dispatch({ + type: UPDATE_ALL_FILTERS, + payload: { + current: spansAggregate.currentPage, + filter: updatedFilter, + filterToFetchData: updatedFilterData, + selectedFilter: getprepdatedSelectedFilter, + selectedTags, + userSelected: getPreUserSelected, + isFilterExclude, + }, + }); + + updateURL( + getprepdatedSelectedFilter, + updatedFilterData, + spansAggregate.currentPage, + selectedTags, + updatedFilter, + isFilterExclude, + getPreUserSelected, + ); + } else { + notification.error({ + message: response.error || 'Something went wrong', + }); + } + + setIsLoading(false); + } catch (error) { + notification.error({ + message: (error as AxiosError).toString() || 'Something went wrong', + }); + } + }; + + const onClearAllHandler = async () => { + try { + setIsLoading(true); + const updatedFilter = new Map(selectedFilter); + const preUserSelected = new Map(userSelectedFilter); + + updatedFilter.delete(props.name); + preUserSelected.delete(props.name); + + const postIsFilterExclude = new Map(isFilterExclude); + + postIsFilterExclude.set(props.name, false); + + const response = await getFilters({ + end: String(global.maxTime), + start: String(global.minTime), + getFilters: filterToFetchData, + other: Object.fromEntries(preUserSelected), + isFilterExclude: postIsFilterExclude, + }); + + if (response.statusCode === 200 && response.payload) { + const getUpatedFilter = getFilter(response.payload); + + dispatch({ + type: UPDATE_ALL_FILTERS, + payload: { + current: spansAggregate.currentPage, + filter: getUpatedFilter, + filterToFetchData, + selectedFilter: updatedFilter, + selectedTags, + userSelected: preUserSelected, + isFilterExclude: postIsFilterExclude, + }, + }); + + updateURL( + updatedFilter, + filterToFetchData, + spansAggregate.currentPage, + selectedTags, + getUpatedFilter, + postIsFilterExclude, + preUserSelected, + ); + } else { + notification.error({ + message: response.error || 'Something went wrong', + }); + } + setIsLoading(false); + } catch (error) { + notification.error({ + message: (error as AxiosError).toString(), + }); + setIsLoading(false); + } + }; + + // const onSelectAllHandler = async () => { + // try { + // setIsLoading(true); + // const preFilter = new Map(filter); + // const preSelectedFilter = new Map(selectedFilter); + + // preSelectedFilter.set( + // props.name, + // Object.keys(preFilter.get(props.name) || {}), + // ); + + // const response = await getFilters({ + // end: String(global.maxTime), + // start: String(global.minTime), + // getFilters: filterToFetchData, + // other: Object.fromEntries(preSelectedFilter), + // }); + + // if (response.statusCode === 200 && response.payload) { + // const getUpatedFilter = getFilter(response.payload); + + // preSelectedFilter.set( + // props.name, + // Object.keys(getUpatedFilter.get(props.name) || {}), + // ); + + // dispatch({ + // type: UPDATE_ALL_FILTERS, + // payload: { + // current: spansAggregate.currentPage, + // filter: preFilter, + // filterToFetchData, + // selectedFilter: preSelectedFilter, + // selectedTags, + // }, + // }); + + // updateURL( + // preSelectedFilter, + // filterToFetchData, + // spansAggregate.currentPage, + // selectedTags, + // preFilter, + // ); + // } + // setIsLoading(false); + // } catch (error) { + // setIsLoading(false); + + // notification.error({ + // message: (error as AxiosError).toString(), + // }); + // } + // }; + + return ( + <> + {props.name !== 'duration' && } + + + + + + {!props.isOpen ? : } + + + + {AllPanelHeading.find((e) => e.key === props.name)?.displayValue || ''} + + + + {props.name !== 'duration' && ( + + {/* + Select All + */} + + + Clear All + + + )} + + + + ); +}; + +interface PanelHeadingProps { + name: TraceFilterEnum; + isOpen: boolean; +} + +export default PanelHeading; diff --git a/frontend/src/container/Trace/Filters/Panel/PanelHeading/styles.ts b/frontend/src/container/Trace/Filters/Panel/PanelHeading/styles.ts new file mode 100644 index 0000000000..aae92d18d6 --- /dev/null +++ b/frontend/src/container/Trace/Filters/Panel/PanelHeading/styles.ts @@ -0,0 +1,49 @@ +import { Button } from 'antd'; +import styled, { css } from 'styled-components'; + +interface Props { + disabled: boolean; +} + +export const Container = styled.div` + &&& { + display: flex; + justify-content: space-between; + align-items: center; + padding-left: 0.5rem; + min-height: 5vh; + + cursor: ${({ disabled }) => disabled && 'not-allowed'}; + + ${({ disabled }) => + disabled && + css` + opacity: 0.5; + `} + } +`; + +export const IconContainer = styled.div` + &&& { + margin-right: 0.5rem; + } +`; + +export const TextCotainer = styled.div` + &&& { + display: flex; + cursor: pointer; + } +`; + +export const ButtonComponent = styled(Button)` + &&& { + font-size: 0.75rem; + } +`; + +export const ButtonContainer = styled.div` + &&& { + display: flex; + } +`; diff --git a/frontend/src/container/Trace/Filters/Panel/index.tsx b/frontend/src/container/Trace/Filters/Panel/index.tsx new file mode 100644 index 0000000000..a6aee59c2d --- /dev/null +++ b/frontend/src/container/Trace/Filters/Panel/index.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace'; + +import PanelBody from './PanelBody'; +import PanelHeading from './PanelHeading'; + +const Panel = (props: PanelProps): JSX.Element => { + const traces = useSelector((state) => state.traces); + + const isDefaultOpen = + traces.filterToFetchData.find((e) => e === props.name) !== undefined; + + return ( + <> + + + {isDefaultOpen && } + + ); +}; + +interface PanelProps { + name: TraceFilterEnum; +} + +export default Panel; diff --git a/frontend/src/container/Trace/Filters/index.tsx b/frontend/src/container/Trace/Filters/index.tsx new file mode 100644 index 0000000000..0090e29726 --- /dev/null +++ b/frontend/src/container/Trace/Filters/index.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { TraceFilterEnum } from 'types/reducer/trace'; + +import Panel from './Panel'; + +export const AllTraceFilterEnum: TraceFilterEnum[] = [ + 'duration', + 'status', + 'serviceName', + 'operation', + 'component', + 'httpCode', + 'httpHost', + 'httpMethod', + 'httpRoute', + 'httpUrl', +]; + +const Filters = (): JSX.Element => ( + + {AllTraceFilterEnum.map((panelName) => ( + + ))} + +); + +export default Filters; diff --git a/frontend/src/container/Trace/Filters/styles.ts b/frontend/src/container/Trace/Filters/styles.ts new file mode 100644 index 0000000000..a5a63ab931 --- /dev/null +++ b/frontend/src/container/Trace/Filters/styles.ts @@ -0,0 +1,20 @@ +import { Button, Input } from 'antd'; +import styled from 'styled-components'; + +export const DurationContainer = styled.div` + display: flex; + justify-content: space-between; + align-items: center; +`; + +export const InputComponent = styled(Input)` + &&& { + margin-left: 0.5rem; + margin-right: 0.5rem; + } +`; + +export const CheckBoxContainer = styled.div` + display: flex; + flex-direction: column; +`; diff --git a/frontend/src/container/Trace/Graph/config.ts b/frontend/src/container/Trace/Graph/config.ts new file mode 100644 index 0000000000..17344b52ef --- /dev/null +++ b/frontend/src/container/Trace/Graph/config.ts @@ -0,0 +1,123 @@ +import { ChartData, ChartDataset, ChartDatasetProperties } from 'chart.js'; +import { TraceReducer } from 'types/reducer/trace'; +import dayjs from 'dayjs'; +import { colors } from 'lib/getRandomColor'; + +function transposeArray(array: number[][], arrayLength: number) { + let newArray: number[][] = []; + for (let i = 0; i < array.length; i++) { + newArray.push([]); + } + + for (let i = 0; i < array.length; i++) { + for (let j = 0; j < arrayLength; j++) { + newArray[j]?.push(array[i][j]); + } + } + + return newArray; +} + +export const getChartData = ( + data: TraceReducer['spansGraph']['payload'], +): ChartData<'line'> => { + const allDataPoints = data.items; + + const chartDataset: ChartDatasetProperties<'line', number[]> = { + data: [], + type: 'line', + }; + + const chartLabels: ChartData<'line'>['labels'] = []; + + Object.keys(allDataPoints).forEach((timestamp) => { + const key = allDataPoints[timestamp]; + if (key.value) { + chartDataset.data.push(key.value); + const date = dayjs(key.timestamp / 1000000); + chartLabels.push(date.toDate().getTime()); + } + }); + + return { + datasets: [ + { + ...chartDataset, + borderWidth: 1.5, + spanGaps: true, + borderColor: colors[0] || 'red', + showLine: true, + pointRadius: 0, + }, + ], + labels: chartLabels, + }; +}; + +export const getChartDataforGroupBy = ( + props: TraceReducer['spansGraph']['payload'], +): ChartData => { + const items = props.items; + + const chartData: ChartData = { + datasets: [], + labels: [], + }; + + let max = 0; + + const allGroupBy = Object.keys(items).map((e) => items[e].groupBy); + + Object.keys(allGroupBy).map((e) => { + const length = Object.keys(allGroupBy[e]).length; + + if (length >= max) { + max = length; + } + }); + + const numberOfGraphs = max; + + const spansGraph: number[][] = []; + + const names: string[] = []; + + // number of data points + Object.keys(items).forEach((item) => { + const spanData = items[item]; + const date = dayjs(Number(item) / 1000000) + .toDate() + .getTime(); + + chartData.labels?.push(date); + + const groupBy = spanData.groupBy; + const preData: number[] = []; + + if (groupBy) { + Object.keys(groupBy).forEach((key) => { + const value = groupBy[key]; + preData.push(value); + names.push(key); + }); + + spansGraph.push(preData); + } + }); + + const updatedName = [...new Set(names)]; + + transposeArray(spansGraph, numberOfGraphs).forEach((values, index) => { + chartData.datasets.push({ + data: values.map((e) => e || 0), + borderWidth: 1.5, + spanGaps: true, + borderColor: colors[index] || 'red', + showLine: true, + pointRadius: 0, + label: updatedName[index], + }); + }); + + return chartData; +}; diff --git a/frontend/src/container/Trace/Graph/index.tsx b/frontend/src/container/Trace/Graph/index.tsx new file mode 100644 index 0000000000..a91fe78930 --- /dev/null +++ b/frontend/src/container/Trace/Graph/index.tsx @@ -0,0 +1,48 @@ +import React, { useMemo } from 'react'; + +import Graph from 'components/Graph'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { TraceReducer } from 'types/reducer/trace'; +import Spinner from 'components/Spinner'; +import { Container } from './styles'; +import { Typography } from 'antd'; +import { getChartData, getChartDataforGroupBy } from './config'; + +const TraceGraph = () => { + const { spansGraph, selectedGroupBy } = useSelector( + (state) => state.traces, + ); + + const { loading, error, errorMessage, payload } = spansGraph; + + const ChartData = useMemo(() => { + return selectedGroupBy.length === 0 + ? getChartData(payload) + : getChartDataforGroupBy(payload); + }, [payload]); + + if (error) { + return ( + + {errorMessage || 'Something went wrong'} + + ); + } + + if (loading || payload === undefined) { + return ( + + + + ); + } + + return ( + + + + ); +}; + +export default TraceGraph; diff --git a/frontend/src/container/Trace/Graph/styles.ts b/frontend/src/container/Trace/Graph/styles.ts new file mode 100644 index 0000000000..51a3e72ffc --- /dev/null +++ b/frontend/src/container/Trace/Graph/styles.ts @@ -0,0 +1,19 @@ +import styled, { css } from 'styled-components'; + +interface Props { + center?: boolean; +} + +export const Container = styled.div` + height: 25vh; + margin-top: 1rem; + margin-bottom: 1rem; + + ${({ center }) => + center && + css` + display: flex; + justify-content: center; + align-items: center; + `} +`; diff --git a/frontend/src/container/Trace/Search/AllTags/Tag/TagKey.tsx b/frontend/src/container/Trace/Search/AllTags/Tag/TagKey.tsx new file mode 100644 index 0000000000..f82a39a291 --- /dev/null +++ b/frontend/src/container/Trace/Search/AllTags/Tag/TagKey.tsx @@ -0,0 +1,102 @@ +import { AutoComplete, AutoCompleteProps, Input, notification } from 'antd'; +import getTagFilters from 'api/trace/getTagFilter'; +import React, { useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { GlobalReducer } from 'types/reducer/globalTime'; +import { TraceReducer } from 'types/reducer/trace'; + +const TagsKey = (props: TagsKeysProps): JSX.Element => { + const [selectLoading, setSelectLoading] = useState(false); + const globalTime = useSelector( + (state) => state.globalTime, + ); + + const [selectedKey, setSelectedKey] = useState(props.tag.Key[0] || ''); + + const traces = useSelector((state) => state.traces); + + const [options, setOptions] = useState([]); + + const onSearchHandler = async () => { + try { + setSelectLoading(true); + const response = await getTagFilters({ + start: globalTime.minTime, + end: globalTime.maxTime, + other: Object.fromEntries(traces.selectedFilter), + }); + + if (response.statusCode === 200) { + if (response.payload === null) { + setOptions([ + { + value: '', + label: 'No tags available', + }, + ]); + } else { + setOptions( + response.payload.map((e) => ({ + value: e.tagKeys, + label: e.tagKeys, + })), + ); + } + } else { + notification.error({ + message: response.error || 'Something went wrong', + }); + } + setSelectLoading(false); + } catch (error) { + notification.error({ + message: 'Something went wrong', + }); + setSelectLoading(false); + } + }; + + useEffect(() => { + onSearchHandler(); + }, []); + + return ( + { + if (options && options.find((option) => option.value === value)) { + setSelectedKey(value); + + props.setLocalSelectedTags((tags) => [ + ...tags.slice(0, props.index), + { + Key: [value], + Operator: props.tag.Operator, + Values: props.tag.Values, + }, + ...tags.slice(props.index + 1, tags.length), + ]); + } else { + setSelectedKey(''); + } + }} + > + + + ); +}; + +interface TagsKeysProps { + index: number; + tag: FlatArray; + setLocalSelectedTags: React.Dispatch< + React.SetStateAction + >; +} + +export default TagsKey; diff --git a/frontend/src/container/Trace/Search/AllTags/Tag/index.tsx b/frontend/src/container/Trace/Search/AllTags/Tag/index.tsx new file mode 100644 index 0000000000..a377de487f --- /dev/null +++ b/frontend/src/container/Trace/Search/AllTags/Tag/index.tsx @@ -0,0 +1,130 @@ +import React from 'react'; + +import { Select } from 'antd'; +import { + Container, + IconContainer, + SelectComponent, + ValueSelect, +} from './styles'; +import { connect, useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { TraceReducer } from 'types/reducer/trace'; +import { CloseOutlined } from '@ant-design/icons'; +import { SelectValue } from 'antd/lib/select'; +import { ThunkDispatch } from 'redux-thunk'; +import AppActions from 'types/actions'; +import { bindActionCreators } from 'redux'; +import { UpdateSelectedTags } from 'store/actions/trace/updateTagsSelected'; +import TagsKey from './TagKey'; +const { Option } = Select; + +type Tags = FlatArray['Operator']; + +const AllMenu: AllMenu[] = [ + { + key: 'in', + value: 'IN', + }, + { + key: 'not in', + value: 'NOT IN', + }, +]; + +interface AllMenu { + key: Tags | ''; + value: string; +} + +const SingleTags = (props: AllTagsProps): JSX.Element => { + const traces = useSelector((state) => state.traces); + const { + Key: selectedKey, + Operator: selectedOperator, + Values: selectedValues, + } = props.tag; + + const onDeleteTagHandler = (index: number) => { + props.onCloseHandler(index); + }; + + const onChangeOperatorHandler = (key: SelectValue) => { + props.setLocalSelectedTags([ + ...traces.selectedTags.slice(0, props.index), + { + Key: selectedKey, + Values: selectedValues, + Operator: key as Tags, + }, + ...traces.selectedTags.slice(props.index + 1, traces.selectedTags.length), + ]); + }; + + return ( + <> + + + + e.key === selectedOperator)?.value || ''} + > + {AllMenu.map((e) => ( + + ))} + + + { + props.setLocalSelectedTags((tags) => [ + ...tags.slice(0, props.index), + { + Key: selectedKey, + Operator: selectedOperator, + Values: value as string[], + }, + ...tags.slice(props.index + 1, tags.length), + ]); + }} + mode="tags" + /> + + onDeleteTagHandler(props.index)} + > + + + + + ); +}; + +interface DispatchProps { + updateSelectedTags: (props: TraceReducer['selectedTags']) => void; +} + +const mapDispatchToProps = ( + dispatch: ThunkDispatch, +): DispatchProps => ({ + updateSelectedTags: bindActionCreators(UpdateSelectedTags, dispatch), +}); + +interface AllTagsProps extends DispatchProps { + onCloseHandler: (index: number) => void; + index: number; + tag: FlatArray; + setLocalSelectedTags: React.Dispatch< + React.SetStateAction + >; +} + +export default connect(null, mapDispatchToProps)(SingleTags); diff --git a/frontend/src/container/Trace/Search/AllTags/Tag/styles.ts b/frontend/src/container/Trace/Search/AllTags/Tag/styles.ts new file mode 100644 index 0000000000..91da16b124 --- /dev/null +++ b/frontend/src/container/Trace/Search/AllTags/Tag/styles.ts @@ -0,0 +1,39 @@ +import styled from 'styled-components'; +import { Button, Select, Space } from 'antd'; + +export const SpaceComponent = styled(Space)` + &&& { + width: 100%; + } +`; + +export const SelectComponent = styled(Select)` + &&& { + min-width: 170px; + margin-right: 21.91px; + margin-left: 21.92px; + } +`; + +export const ValueSelect = styled(Select)` + &&& { + width: 100%; + } +`; + +export const Container = styled.div` + &&& { + display: flex; + margin-top: 1rem; + margin-bottom: 1rem; + } +`; + +export const IconContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + + margin-left: 1.125rem; +`; diff --git a/frontend/src/container/Trace/Search/AllTags/index.tsx b/frontend/src/container/Trace/Search/AllTags/index.tsx new file mode 100644 index 0000000000..e2a449ff6d --- /dev/null +++ b/frontend/src/container/Trace/Search/AllTags/index.tsx @@ -0,0 +1,158 @@ +import React, { useEffect, useState } from 'react'; + +import { Button, Space, Typography } from 'antd'; +import { CaretRightFilled } from '@ant-design/icons'; +import { + Container, + ButtonContainer, + CurrentTagsContainer, + Wrapper, + ErrorContainer, +} from './styles'; +import Tags from './Tag'; +const { Text } = Typography; +import { PlusOutlined } from '@ant-design/icons'; +import { connect, useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { TraceReducer } from 'types/reducer/trace'; +import { bindActionCreators } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; +import AppActions from 'types/actions'; +import { UpdateTagIsError } from 'store/actions/trace/updateIsTagsError'; +import { parseTagsToQuery } from '../util'; +import { isEqual } from 'lodash-es'; +import { UpdateTagVisiblity } from 'store/actions/trace/updateTagPanelVisiblity'; + +const { Paragraph } = Typography; + +const AllTags = ({ + updateTagIsError, + onChangeHandler, + updateTagVisiblity, + updateFilters, +}: AllTagsProps): JSX.Element => { + const traces = useSelector((state) => state.traces); + + const [localSelectedTags, setLocalSelectedTags] = useState< + TraceReducer['selectedTags'] + >(traces.selectedTags); + + const onTagAddHandler = () => { + setLocalSelectedTags((tags) => [ + ...tags, + { + Key: [], + Operator: 'in', + Values: [], + }, + ]); + }; + + useEffect(() => { + if (!isEqual(traces.selectedTags, localSelectedTags)) { + setLocalSelectedTags(traces.selectedTags); + } + }, [traces.selectedTags]); + + const onCloseHandler = (index: number) => { + setLocalSelectedTags([ + ...localSelectedTags.slice(0, index), + ...localSelectedTags.slice(index + 1, localSelectedTags.length), + ]); + }; + + const onRunQueryHandler = () => { + const parsedQuery = parseTagsToQuery(localSelectedTags); + + if (parsedQuery.isError) { + updateTagIsError(true); + } else { + onChangeHandler(parsedQuery.payload); + updateFilters(localSelectedTags); + updateTagIsError(false); + updateTagVisiblity(false); + } + }; + + const onResetHandler = () => { + setLocalSelectedTags([]); + }; + + if (traces.isTagModalError) { + return ( + + + Unrecognised query format. Please reset your query by clicking `X` in the + search bar above. + + + + Please click on the search bar to get a drop down to select relevant tags + + + ); + } + + return ( + <> + + + Tags + + + {localSelectedTags.map((tags, index) => ( + onCloseHandler(index)} + setLocalSelectedTags={setLocalSelectedTags} + /> + ))} + + + + + + + Results will include spans with ALL the specified tags ( Rows are `anded` + ) + + + + + + + + + + + ); +}; + +interface DispatchProps { + updateTagIsError: (value: boolean) => void; + updateTagVisiblity: (value: boolean) => void; +} + +const mapDispatchToProps = ( + dispatch: ThunkDispatch, +): DispatchProps => ({ + updateTagIsError: bindActionCreators(UpdateTagIsError, dispatch), + updateTagVisiblity: bindActionCreators(UpdateTagVisiblity, dispatch), +}); + +interface AllTagsProps extends DispatchProps { + updateFilters: (tags: TraceReducer['selectedTags']) => void; + onChangeHandler: (search: string) => void; +} + +export default connect(null, mapDispatchToProps)(AllTags); diff --git a/frontend/src/container/Trace/Search/AllTags/styles.ts b/frontend/src/container/Trace/Search/AllTags/styles.ts new file mode 100644 index 0000000000..ef875287a1 --- /dev/null +++ b/frontend/src/container/Trace/Search/AllTags/styles.ts @@ -0,0 +1,56 @@ +import styled from 'styled-components'; +import { Card } from 'antd'; + +export const Container = styled(Card)` + top: 120%; + min-height: 20vh; + width: 100%; + z-index: 2; + position: absolute; + + .ant-card-body { + padding-bottom: 0; + padding-right: 0; + padding-left: 0; + } +`; + +export const ErrorContainer = styled(Card)` + top: 120%; + min-height: 20vh; + width: 100%; + z-index: 2; + position: absolute; + + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +`; + +export const Wrapper = styled.div` + &&& { + padding-right: 2rem; + padding-left: 2rem; + } +`; + +export const ButtonContainer = styled.div` + display: flex; + justify-content: flex-end; + align-items: center; + background-color: #303030; + padding-top: 11px; + padding-bottom: 11px; + padding-right: 38.01px; + + margin-top: 1rem; + + > button:nth-child(1) { + margin-right: 1rem; + } +`; + +export const CurrentTagsContainer = styled.div` + margin-bottom: 1rem; +`; diff --git a/frontend/src/container/Trace/Search/index.tsx b/frontend/src/container/Trace/Search/index.tsx new file mode 100644 index 0000000000..1b6e59eba6 --- /dev/null +++ b/frontend/src/container/Trace/Search/index.tsx @@ -0,0 +1,163 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { Space } from 'antd'; +import { Container, SearchComponent } from './styles'; +import useClickOutside from 'hooks/useClickOutside'; +import Tags from './AllTags'; +import { connect, useDispatch, useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { TraceReducer } from 'types/reducer/trace'; +import { ThunkDispatch } from 'redux-thunk'; +import AppActions from 'types/actions'; +import { bindActionCreators, Dispatch } from 'redux'; +import { UpdateTagVisiblity } from 'store/actions/trace/updateTagPanelVisiblity'; +import { parseQueryToTags, parseTagsToQuery } from './util'; +import { UpdateTagIsError } from 'store/actions/trace/updateIsTagsError'; +import { CaretRightFilled } from '@ant-design/icons'; +import { updateURL } from 'store/actions/trace/util'; +import { UPDATE_ALL_FILTERS } from 'types/actions/trace'; + +const Search = ({ + updateTagVisiblity, + updateTagIsError, +}: SearchProps): JSX.Element => { + const traces = useSelector((state) => state.traces); + + const [value, setValue] = useState(''); + const dispatch = useDispatch>(); + + useEffect(() => { + if (traces.filterLoading) { + const initialTags = parseTagsToQuery(traces.selectedTags); + if (!initialTags.isError) { + setValue(initialTags.payload); + } + } + }, [traces.selectedTags, traces.filterLoading]); + + useEffect(() => { + if (value.length === 0 && traces.isTagModalError) { + updateTagIsError(false); + } + }, [traces.isTagModalError, value]); + + const tagRef = useRef(null); + + useClickOutside(tagRef, (e: HTMLElement) => { + // using this hack as overlay span is voilating this condition + if ( + e.nodeName === 'svg' || + e.nodeName === 'path' || + e.nodeName === 'span' || + e.nodeName === 'button' + ) { + return; + } + + if ( + e.nodeName === 'DIV' && + ![ + 'ant-select-item-option-content', + 'ant-empty-image', + 'ant-select-item', + 'ant-col', + 'ant-select-item-option-active', + ].find((p) => p.indexOf(e.className) !== -1) && + !(e.ariaSelected === 'true') && + traces.isTagModalOpen + ) { + updateTagVisiblity(false); + } + }); + + const onChangeHandler = (search: string) => { + setValue(search); + }; + + const setIsTagsModalHandler = (value: boolean) => { + updateTagVisiblity(value); + }; + + const onFocusHandler: React.FocusEventHandler = (e) => { + e.preventDefault(); + setIsTagsModalHandler(true); + }; + + const updateFilters = async (selectedTags: TraceReducer['selectedTags']) => { + dispatch({ + type: UPDATE_ALL_FILTERS, + payload: { + selectedTags, + current: traces.spansAggregate.currentPage, + filter: traces.filter, + filterToFetchData: traces.filterToFetchData, + selectedFilter: traces.selectedFilter, + userSelected: traces.userSelectedFilter, + isFilterExclude: traces.isFilterExclude, + }, + }); + + updateURL( + traces.selectedFilter, + traces.filterToFetchData, + traces.spansAggregate.currentPage, + selectedTags, + traces.filter, + traces.isFilterExclude, + traces.userSelectedFilter, + ); + }; + + return ( + + + onChangeHandler(event.target.value)} + value={value} + allowClear + disabled={traces.filterLoading} + onFocus={onFocusHandler} + placeholder="Click to filter by tags" + type={'search'} + enterButton={} + onSearch={(string) => { + if (string.length === 0) { + updateTagVisiblity(false); + updateFilters([]); + return; + } + + const { isError, payload } = parseQueryToTags(string); + + if (isError) { + updateTagIsError(true); + } else { + updateTagIsError(false); + updateTagVisiblity(false); + updateFilters(payload); + } + }} + /> + + {traces.isTagModalOpen && ( + + )} + + + ); +}; + +interface DispatchProps { + updateTagVisiblity: (value: boolean) => void; + updateTagIsError: (value: boolean) => void; +} + +const mapDispatchToProps = ( + dispatch: ThunkDispatch, +): DispatchProps => ({ + updateTagVisiblity: bindActionCreators(UpdateTagVisiblity, dispatch), + updateTagIsError: bindActionCreators(UpdateTagIsError, dispatch), +}); + +type SearchProps = DispatchProps; + +export default connect(null, mapDispatchToProps)(Search); diff --git a/frontend/src/container/Trace/Search/styles.ts b/frontend/src/container/Trace/Search/styles.ts new file mode 100644 index 0000000000..f6f342aca9 --- /dev/null +++ b/frontend/src/container/Trace/Search/styles.ts @@ -0,0 +1,17 @@ +import styled from 'styled-components'; +import { Input } from 'antd'; + +const { Search } = Input; + +export const Container = styled.div` + display: flex; + position: relative; +`; + +export const SearchComponent = styled(Search)` + .ant-btn-primary { + svg { + transform: scale(1.5); + } + } +`; diff --git a/frontend/src/container/Trace/Search/util.ts b/frontend/src/container/Trace/Search/util.ts new file mode 100644 index 0000000000..d74a133229 --- /dev/null +++ b/frontend/src/container/Trace/Search/util.ts @@ -0,0 +1,84 @@ +import { TraceReducer } from 'types/reducer/trace'; + +type Tags = TraceReducer['selectedTags']; + +interface PayloadProps { + isError: boolean; + payload: T; +} + +export const parseQueryToTags = (query: string): PayloadProps => { + let isError = false; + + const noOfTags = query.split(' AND '); + + const tags: Tags = noOfTags.map((filter) => { + const isInPresent = filter.includes('IN'); + const isNotInPresent = filter.includes('NOT_IN'); + + if (!isNotInPresent || !isInPresent) { + isError = true; + } + + const splitBy = isNotInPresent ? 'NOT_IN' : isInPresent ? 'IN' : ''; + + if (splitBy.length === 0) { + isError = true; + } + + const filteredtags = filter.split(splitBy).map((e) => e.trim()); + + if (filteredtags.length !== 2) { + isError = true; + } + + const filterForTags = filteredtags[1]; + + if (!filterForTags) { + isError = true; + } + + const removingFirstAndLastBrackets = `${filterForTags?.slice(1, -1)}`; + + const noofFilters = removingFirstAndLastBrackets.split(','); + + noofFilters.forEach((e) => { + const firstChar = e.charAt(0); + const lastChar = e.charAt(e.length - 1); + + if (!(firstChar === '"' && lastChar === '"')) { + isError = true; + } + }); + + return { + Key: [filteredtags[0]], + Values: noofFilters, + Operator: splitBy as FlatArray['Operator'], + }; + }); + + return { + isError, + payload: tags, + }; +}; + +export const parseTagsToQuery = (tags: Tags): PayloadProps => { + let isError = false; + + const payload = tags + .map(({ Values, Key, Operator }) => { + if (Key[0] === undefined) { + isError = true; + } + + return `${Key[0]} ${Operator} (${Values.map((e) => `"${e}"`).join(',')})`; + }) + .join(' AND '); + + return { + isError, + payload, + }; +}; diff --git a/frontend/src/container/Trace/TraceGraphFilter/config.ts b/frontend/src/container/Trace/TraceGraphFilter/config.ts new file mode 100644 index 0000000000..5d7a942b35 --- /dev/null +++ b/frontend/src/container/Trace/TraceGraphFilter/config.ts @@ -0,0 +1,91 @@ +interface Dropdown { + key: string; + displayValue: string; +} + +export const groupBy: Dropdown[] = [ + { + key: '', + displayValue: 'None', + }, + { + key: 'serviceName', + displayValue: 'Service Name', + }, + + { + displayValue: 'Operation', + key: 'operation', + }, + { + displayValue: 'HTTP url', + key: 'httpUrl', + }, + { + displayValue: 'HTTP method', + key: 'httpMethod', + }, + { + displayValue: 'HTTP host', + key: 'httpHost', + }, + { + displayValue: 'HTTP route', + key: 'httpRoute', + }, + { + displayValue: 'HTTP status code', + key: 'httpCode', + }, + { + displayValue: 'Database name', + key: 'dbName', + }, + { + displayValue: 'Database operation', + key: 'dbSystem', + }, + { + displayValue: 'Messaging System', + key: 'msgSystem', + }, + { + displayValue: 'Messaging Operation', + key: 'msgOperation', + }, + { + displayValue: 'Component', + key: 'component', + }, +]; + +export const functions: Dropdown[] = [ + { displayValue: 'Count', key: 'count' }, + { displayValue: 'Rate per sec', key: 'ratePerSec' }, + { displayValue: 'Sum(duration in ns)', key: 'sum' }, + { displayValue: 'Avg(duration in ns)', key: 'avg' }, + { + displayValue: 'Max(duration in ns)', + key: 'max', + }, + { + displayValue: 'Min(duration in ns)', + key: 'min', + }, + { + displayValue: '50th percentile(duration in ns)', + key: 'p50', + }, + { + displayValue: '90th percentile(duration in ns', + key: 'p90', + }, + { + displayValue: '95th percentile(duration in ns)', + key: 'p95', + }, + { + displayValue: '99th percentile(duration in ns)', + key: 'p99', + }, +]; diff --git a/frontend/src/container/Trace/TraceGraphFilter/index.tsx b/frontend/src/container/Trace/TraceGraphFilter/index.tsx new file mode 100644 index 0000000000..bd06764aad --- /dev/null +++ b/frontend/src/container/Trace/TraceGraphFilter/index.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { Space, SelectProps } from 'antd'; +import { functions, groupBy } from './config'; +import { useDispatch, useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { TraceReducer } from 'types/reducer/trace'; +import AppActions from 'types/actions'; +import { + UPDATE_SELECTED_FUNCTION, + UPDATE_SELECTED_GROUP_BY, +} from 'types/actions/trace'; +import { Dispatch } from 'redux'; +import { SelectComponent } from './styles'; +import { SelectValue } from 'antd/lib/select'; + +const { Option } = SelectComponent; + +const TraceGraphFilter = () => { + const { selectedFunction, selectedGroupBy } = useSelector< + AppState, + TraceReducer + >((state) => state.traces); + const dispatch = useDispatch>(); + + const onClickSelectedFunctionHandler: SelectProps['onChange'] = ( + ev, + ) => { + const selected = functions.find((e) => e.key === ev); + if (selected) { + dispatch({ + type: UPDATE_SELECTED_FUNCTION, + payload: { + selectedFunction: selected.key, + }, + }); + } + }; + + const onClickSelectedGroupByHandler: SelectProps['onChange'] = ( + ev, + ) => { + const selected = groupBy.find((e) => e.key === ev); + if (selected) { + dispatch({ + type: UPDATE_SELECTED_GROUP_BY, + payload: { + selectedGroupBy: selected.key, + }, + }); + } + }; + + return ( + + + + selectedFunction === e.key)?.displayValue} + onChange={onClickSelectedFunctionHandler} + > + {functions.map((value) => ( + + ))} + + + + selectedGroupBy === e.key)?.displayValue} + onChange={onClickSelectedGroupByHandler} + > + {groupBy.map((value) => ( + + ))} + + + ); +}; + +export default TraceGraphFilter; diff --git a/frontend/src/container/Trace/TraceGraphFilter/styles.ts b/frontend/src/container/Trace/TraceGraphFilter/styles.ts new file mode 100644 index 0000000000..be712d4cba --- /dev/null +++ b/frontend/src/container/Trace/TraceGraphFilter/styles.ts @@ -0,0 +1,9 @@ +import { Select } from 'antd'; + +import styled from 'styled-components'; + +export const SelectComponent = styled(Select)` + &&& { + min-width: 10rem; + } +`; diff --git a/frontend/src/container/Trace/TraceTable/index.tsx b/frontend/src/container/Trace/TraceTable/index.tsx new file mode 100644 index 0000000000..ce6133a089 --- /dev/null +++ b/frontend/src/container/Trace/TraceTable/index.tsx @@ -0,0 +1,155 @@ +import React from 'react'; + +import Table, { ColumnsType } from 'antd/lib/table'; +import { TableProps, Tag } from 'antd'; + +import { connect, useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { TraceReducer } from 'types/reducer/trace'; +import { bindActionCreators } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; +import AppActions from 'types/actions'; +import { + GetSpansAggregate, + GetSpansAggregateProps, +} from 'store/actions/trace/getInitialSpansAggregate'; +import { GlobalReducer } from 'types/reducer/globalTime'; +import dayjs from 'dayjs'; +import duration from 'dayjs/plugin/duration'; +import history from 'lib/history'; +import ROUTES from 'constants/routes'; +dayjs.extend(duration); + +const TraceTable = ({ getSpansAggregate }: TraceProps) => { + const { + spansAggregate, + selectedFilter, + selectedTags, + filterLoading, + } = useSelector((state) => state.traces); + + const globalTime = useSelector( + (state) => state.globalTime, + ); + + const { loading, total } = spansAggregate; + + type TableType = FlatArray; + + const columns: ColumnsType = [ + { + title: 'Date', + dataIndex: 'timestamp', + key: 'timestamp', + render: (value: TableType['timestamp']) => { + const day = dayjs(value); + return
{day.format('DD/MM/YYYY HH:MM:ss A')}
; + }, + sorter: (a, b) => dayjs(a.timestamp).diff(dayjs(b.timestamp)), + }, + { + title: 'Service', + dataIndex: 'serviceName', + key: 'serviceName', + sorter: (a, b) => a.serviceName.length - b.serviceName.length, + }, + { + title: 'Operation', + dataIndex: 'operation', + key: 'operation', + }, + { + title: 'Duration', + dataIndex: 'durationNano', + key: 'durationNano', + sorter: (a, b) => a.durationNano - b.durationNano, + render: (value: TableType['durationNano']) => { + return ( +
+ {`${dayjs + .duration({ milliseconds: value / 1000000 }) + .asMilliseconds()} ms`} +
+ ); + }, + }, + { + title: 'Method', + dataIndex: 'httpMethod', + key: 'httpMethod', + render: (value: TableType['httpMethod']) => { + if (value.length === 0) { + return
-
; + } + return {value}; + }, + }, + { + title: 'Status Code', + dataIndex: 'httpCode', + key: 'httpCode', + sorter: (a, b) => a.httpCode.length - b.httpCode.length, + render: (value: TableType['httpCode']) => { + if (value.length === 0) { + return
-
; + } + return {value}; + }, + }, + ]; + + const onChangeHandler: TableProps['onChange'] = (props) => { + if (props.current && props.pageSize) { + getSpansAggregate({ + maxTime: globalTime.maxTime, + minTime: globalTime.minTime, + selectedFilter, + current: props.current, + pageSize: props.pageSize, + selectedTags, + }); + } + }; + + return ( + ({ + onClick: () => { + history.push({ + pathname: ROUTES.TRACE + '/' + record.traceID, + state: { + spanId: record.spanID, + }, + }); + }, + })} + size="middle" + rowKey={'timestamp'} + pagination={{ + current: spansAggregate.currentPage, + pageSize: spansAggregate.pageSize, + responsive: true, + position: ['bottomLeft'], + total: total, + }} + /> + ); +}; + +interface DispatchProps { + getSpansAggregate: (props: GetSpansAggregateProps) => void; +} + +const mapDispatchToProps = ( + dispatch: ThunkDispatch, +): DispatchProps => ({ + getSpansAggregate: bindActionCreators(GetSpansAggregate, dispatch), +}); + +type TraceProps = DispatchProps; + +export default connect(null, mapDispatchToProps)(TraceTable); diff --git a/frontend/src/container/TraceCustomVisualization/TraceCustomGraph.tsx b/frontend/src/container/TraceCustomVisualization/TraceCustomGraph.tsx deleted file mode 100644 index 9f9fd79746..0000000000 --- a/frontend/src/container/TraceCustomVisualization/TraceCustomGraph.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import Graph from 'components/Graph'; -import convertToNanoSecondsToSecond from 'lib/convertToNanoSecondsToSecond'; -import { colors } from 'lib/getRandomColor'; -import React, { memo } from 'react'; -import { useSelector } from 'react-redux'; -import { AppState } from 'store/reducers'; -import { TraceReducer } from 'types/reducer/trace'; - -import { CustomGraphContainer } from './styles'; - -const TraceCustomGraph = ({ - spansAggregate, -}: TraceCustomGraphProps): JSX.Element => { - const { selectedEntity } = useSelector( - (state) => state.trace, - ); - - return ( - - new Date(s.timestamp / 1000000)), - datasets: [ - { - data: spansAggregate.map((e) => - selectedEntity === 'duration' - ? parseFloat(convertToNanoSecondsToSecond(e.value)) - : e.value, - ), - borderColor: colors[0], - }, - ], - }} - /> - - ); -}; - -interface TraceCustomGraphProps { - spansAggregate: TraceReducer['spansAggregate']; -} - -export default memo(TraceCustomGraph); diff --git a/frontend/src/container/TraceCustomVisualization/config.ts b/frontend/src/container/TraceCustomVisualization/config.ts deleted file mode 100644 index 0a3d5fbf9b..0000000000 --- a/frontend/src/container/TraceCustomVisualization/config.ts +++ /dev/null @@ -1,56 +0,0 @@ -export const entity = [ - { - title: 'Calls', - key: 'calls', - dataindex: 'calls', - }, - { - title: 'Duration', - key: 'duration', - dataindex: 'duration', - }, - { - title: 'Error', - key: 'error', - dataindex: 'error', - }, - { - title: 'Status Code', - key: 'status_code', - dataindex: 'status_code', - }, -]; - -export const aggregation_options = [ - { - linked_entity: 'calls', - default_selected: { title: 'count', dataindex: 'count' }, - options_available: [ - { title: 'Count', dataindex: 'count' }, - { title: 'Rate (per sec)', dataindex: 'rate_per_sec' }, - ], - }, - { - linked_entity: 'duration', - default_selected: { title: 'p99', dataindex: 'p99' }, - // options_available: [ {title:'Avg', dataindex:'avg'}, {title:'Max', dataindex:'max'},{title:'Min', dataindex:'min'}, {title:'p50', dataindex:'p50'},{title:'p95', dataindex:'p95'}, {title:'p95', dataindex:'p95'}] - options_available: [ - { title: 'p50', dataindex: 'p50' }, - { title: 'p95', dataindex: 'p95' }, - { title: 'p99', dataindex: 'p99' }, - ], - }, - { - linked_entity: 'error', - default_selected: { title: 'count', dataindex: 'count' }, - options_available: [ - { title: 'count', dataindex: 'count' }, - { title: 'Rate (per sec)', dataindex: 'rate_per_sec' }, - ], - }, - { - linked_entity: 'status_code', - default_selected: { title: 'count', dataindex: 'count' }, - options_available: [{ title: 'count', dataindex: 'count' }], - }, -]; diff --git a/frontend/src/container/TraceCustomVisualization/index.tsx b/frontend/src/container/TraceCustomVisualization/index.tsx deleted file mode 100644 index dbc4c46ed0..0000000000 --- a/frontend/src/container/TraceCustomVisualization/index.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { Form, Select } from 'antd'; -import Spinner from 'components/Spinner'; -import React from 'react'; -import { connect, useSelector } from 'react-redux'; -import { AppState } from 'store/reducers'; -const { Option } = Select; -import { Store } from 'rc-field-form/lib/interface'; -import { bindActionCreators } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { - GetTraceVisualAggregates, - GetTraceVisualAggregatesProps, -} from 'store/actions/trace/getTraceVisualAgrregates'; -import AppActions from 'types/actions'; -import { TraceReducer } from 'types/reducer/trace'; - -import { aggregation_options, entity } from './config'; -import { Card, CustomVisualizationsTitle, FormItem, Space } from './styles'; -import TraceCustomGraph from './TraceCustomGraph'; - -const TraceCustomVisualisation = ({ - getTraceVisualAggregates, -}: TraceCustomVisualisationProps): JSX.Element => { - const { - selectedEntity, - spansLoading, - selectedAggOption, - spansAggregate, - } = useSelector((state) => state.trace); - - const [form] = Form.useForm(); - - if (spansLoading) { - return ; - } - - const handleFormValuesChange = (changedValues: Store): void => { - const formFieldName = Object.keys(changedValues)[0]; - if (formFieldName === 'entity') { - const temp_entity = aggregation_options.filter( - (item) => item.linked_entity === changedValues[formFieldName], - )[0]; - - form.setFieldsValue({ - agg_options: temp_entity.default_selected.title, - }); - - const values = form.getFieldsValue(['agg_options', 'entity']); - - getTraceVisualAggregates({ - selectedAggOption: values.agg_options, - selectedEntity: values.entity, - }); - } - - if (formFieldName === 'agg_options') { - getTraceVisualAggregates({ - selectedAggOption: changedValues[formFieldName], - selectedEntity, - }); - } - }; - - return ( - - Custom Visualizations -
- - - - - - - - - - - - -
- ); -}; - -interface DispatchProps { - getTraceVisualAggregates: (props: GetTraceVisualAggregatesProps) => void; -} - -const mapDispatchToProps = ( - dispatch: ThunkDispatch, -): DispatchProps => ({ - getTraceVisualAggregates: bindActionCreators( - GetTraceVisualAggregates, - dispatch, - ), -}); - -type TraceCustomVisualisationProps = DispatchProps; - -export default connect(null, mapDispatchToProps)(TraceCustomVisualisation); diff --git a/frontend/src/container/TraceCustomVisualization/styles.ts b/frontend/src/container/TraceCustomVisualization/styles.ts deleted file mode 100644 index 069feaad73..0000000000 --- a/frontend/src/container/TraceCustomVisualization/styles.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { - Card as CardComponent, - Form, - Space as SpaceComponent, - Typography, -} from 'antd'; -import styled from 'styled-components'; - -export const CustomGraphContainer = styled.div` - height: 30vh; -`; - -export const Card = styled(CardComponent)` - .ant-card-body { - padding-bottom: 0; - } -`; - -export const CustomVisualizationsTitle = styled(Typography)` - margin-bottom: 1rem; -`; - -export const FormItem = styled(Form.Item)` - &&& { - margin: 0; - } -`; - -export const Space = styled(SpaceComponent)` - &&& { - display: flex; - flex-wrap: wrap; - } -`; diff --git a/frontend/src/container/TraceFilter/Filter.tsx b/frontend/src/container/TraceFilter/Filter.tsx deleted file mode 100644 index ca4a2754a1..0000000000 --- a/frontend/src/container/TraceFilter/Filter.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import { Tag } from 'antd'; -import { METRICS_PAGE_QUERY_PARAM } from 'constants/query'; -import React from 'react'; -import { connect, useSelector } from 'react-redux'; -import { bindActionCreators, Dispatch } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { TagItem } from 'store/actions'; -import { UpdateSelectedTags } from 'store/actions/trace'; -import { - UpdateSelectedData, - UpdateSelectedDataProps, -} from 'store/actions/trace/updateSelectedData'; -import { AppState } from 'store/reducers'; -import AppActions from 'types/actions'; -import { TraceReducer } from 'types/reducer/trace'; - -import { Card } from './styles'; - -const Filter = ({ - updatedQueryParams, - updateSelectedData, - updateSelectedTags, -}: FilterProps): JSX.Element => { - const { - selectedService, - selectedOperation, - selectedLatency, - selectedTags, - selectedKind, - selectedEntity, - selectedAggOption, - } = useSelector((state) => state.trace); - - function handleCloseTag(value: string): void { - if (value === 'service') { - updatedQueryParams([''], [METRICS_PAGE_QUERY_PARAM.service]); - updateSelectedData({ - selectedAggOption, - selectedEntity, - selectedKind, - selectedLatency, - selectedOperation, - selectedService: '', - }); - } - if (value === 'operation') { - updatedQueryParams([''], [METRICS_PAGE_QUERY_PARAM.operation]); - updateSelectedData({ - selectedAggOption, - selectedEntity, - selectedKind, - selectedLatency, - selectedOperation: '', - selectedService, - }); - } - if (value === 'maxLatency') { - updatedQueryParams([''], [METRICS_PAGE_QUERY_PARAM.latencyMax]); - updateSelectedData({ - selectedAggOption, - selectedEntity, - selectedKind, - selectedLatency: { - min: selectedLatency.min, - max: '', - }, - selectedOperation, - selectedService, - }); - } - if (value === 'minLatency') { - updatedQueryParams([''], [METRICS_PAGE_QUERY_PARAM.latencyMin]); - updateSelectedData({ - selectedAggOption, - selectedEntity, - selectedKind, - selectedLatency: { - min: '', - max: selectedLatency.max, - }, - selectedOperation, - selectedService, - }); - } - } - - function handleCloseTagElement(item: TagItem): void { - const updatedSelectedtags = selectedTags.filter((e) => e.key !== item.key); - - updatedQueryParams( - [updatedSelectedtags], - [METRICS_PAGE_QUERY_PARAM.selectedTags], - ); - updateSelectedTags(updatedSelectedtags); - } - - return ( - - {selectedService.length !== 0 && ( - { - e.preventDefault(); - handleCloseTag('service'); - }} - > - service:{selectedService} - - )} - - {selectedOperation.length !== 0 && ( - { - e.preventDefault(); - handleCloseTag('operation'); - }} - > - operation:{selectedOperation} - - )} - - {selectedLatency?.min.length !== 0 && ( - { - e.preventDefault(); - handleCloseTag('minLatency'); - }} - > - minLatency: - {(parseInt(selectedLatency?.min || '0') / 1000000).toString()}ms - - )} - {selectedLatency?.max.length !== 0 && ( - { - e.preventDefault(); - handleCloseTag('maxLatency'); - }} - > - maxLatency: - {(parseInt(selectedLatency?.max || '0') / 1000000).toString()}ms - - )} - - {selectedTags.map((item) => ( - { - e.preventDefault(); - handleCloseTagElement(item); - }} - > - {item.key} {item.operator} {item.value} - - ))} - - ); -}; - -interface DispatchProps { - updateSelectedTags: ( - selectedTags: TraceReducer['selectedTags'], - ) => (dispatch: Dispatch) => void; - updateSelectedData: (props: UpdateSelectedDataProps) => void; -} - -const mapDispatchToProps = ( - dispatch: ThunkDispatch, -): DispatchProps => ({ - updateSelectedTags: bindActionCreators(UpdateSelectedTags, dispatch), - updateSelectedData: bindActionCreators(UpdateSelectedData, dispatch), -}); - -interface FilterProps extends DispatchProps { - updatedQueryParams: (updatedValue: string[], key: string[]) => void; -} - -export default connect(null, mapDispatchToProps)(Filter); diff --git a/frontend/src/container/TraceFilter/LatencyForm.tsx b/frontend/src/container/TraceFilter/LatencyForm.tsx deleted file mode 100644 index 593e1fd877..0000000000 --- a/frontend/src/container/TraceFilter/LatencyForm.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { Col, Form, InputNumber, Modal, notification, Row } from 'antd'; -import { METRICS_PAGE_QUERY_PARAM } from 'constants/query'; -import { FormInstance, RuleObject } from 'rc-field-form/lib/interface'; -import React from 'react'; -import { connect, useSelector } from 'react-redux'; -import { bindActionCreators, Dispatch } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { UpdateSelectedLatency } from 'store/actions/trace'; -import { - UpdateSelectedData, - UpdateSelectedDataProps, -} from 'store/actions/trace/updateSelectedData'; -import { AppState } from 'store/reducers'; -import AppActions from 'types/actions'; -import { TraceReducer } from 'types/reducer/trace'; - -const LatencyForm = ({ - onCancel, - visible, - updateSelectedLatency, - onLatencyButtonClick, - updatedQueryParams, - updateSelectedData, -}: LatencyModalFormProps): JSX.Element => { - const [form] = Form.useForm(); - const [notifications, Element] = notification.useNotification(); - const { - selectedLatency, - selectedKind, - selectedOperation, - selectedService, - selectedAggOption, - selectedEntity, - } = useSelector((state) => state.trace); - - const validateMinValue = (form: FormInstance): RuleObject => ({ - validator(_: RuleObject, value): Promise { - const { getFieldValue } = form; - const minValue = getFieldValue('min'); - const maxValue = getFieldValue('max'); - - if (value <= maxValue && value >= minValue) { - return Promise.resolve(); - } - return Promise.reject(new Error('Min value should be less than Max value')); - }, - }); - - const validateMaxValue = (form: FormInstance): RuleObject => ({ - validator(_, value): Promise { - const { getFieldValue } = form; - - const minValue = getFieldValue('min'); - const maxValue = getFieldValue('max'); - - if (value >= minValue && value <= maxValue) { - return Promise.resolve(); - } - return Promise.reject( - new Error('Max value should be greater than Min value'), - ); - }, - }); - - const onOkHandler = (): void => { - form - .validateFields() - .then((values) => { - const maxValue = (values.max * 1000000).toString(); - const minValue = (values.min * 1000000).toString(); - - onLatencyButtonClick(); - updatedQueryParams( - [maxValue, minValue], - [METRICS_PAGE_QUERY_PARAM.latencyMax, METRICS_PAGE_QUERY_PARAM.latencyMin], - ); - updateSelectedLatency({ - max: maxValue, - min: minValue, - }); - updateSelectedData({ - selectedKind, - selectedLatency: { - max: maxValue, - min: minValue, - }, - selectedOperation, - selectedService, - selectedAggOption, - selectedEntity, - }); - }) - .catch((info) => { - notifications.error({ - message: info.toString(), - }); - }); - }; - - return ( - <> - {Element} - - -
- -
- - - - - - - - - - - - - - ); -}; - -interface DispatchProps { - updateSelectedLatency: ( - selectedLatency: TraceReducer['selectedLatency'], - ) => (dispatch: Dispatch) => void; - updateSelectedData: (props: UpdateSelectedDataProps) => void; -} - -const mapDispatchToProps = ( - dispatch: ThunkDispatch, -): DispatchProps => ({ - updateSelectedLatency: bindActionCreators(UpdateSelectedLatency, dispatch), - updateSelectedData: bindActionCreators(UpdateSelectedData, dispatch), -}); - -interface LatencyModalFormProps extends DispatchProps { - onCancel: () => void; - visible: boolean; - onLatencyButtonClick: () => void; - updatedQueryParams: (updatedValue: string[], value: string[]) => void; -} - -export default connect(null, mapDispatchToProps)(LatencyForm); diff --git a/frontend/src/container/TraceFilter/config.ts b/frontend/src/container/TraceFilter/config.ts deleted file mode 100644 index 8410b1cf54..0000000000 --- a/frontend/src/container/TraceFilter/config.ts +++ /dev/null @@ -1,15 +0,0 @@ -interface SpanKindList { - label: 'SERVER' | 'CLIENT'; - value: string; -} - -export const spanKindList: SpanKindList[] = [ - { - label: 'SERVER', - value: '2', - }, - { - label: 'CLIENT', - value: '3', - }, -]; diff --git a/frontend/src/container/TraceFilter/index.tsx b/frontend/src/container/TraceFilter/index.tsx deleted file mode 100644 index 3d4f7d2f43..0000000000 --- a/frontend/src/container/TraceFilter/index.tsx +++ /dev/null @@ -1,390 +0,0 @@ -import { Button, Input, notification, Typography } from 'antd'; -import { SelectValue } from 'antd/lib/select'; -import React, { useCallback, useEffect, useState } from 'react'; -import { connect, useSelector } from 'react-redux'; -import { AppState } from 'store/reducers'; -import { TagItem, TraceReducer } from 'types/reducer/trace'; - -import { spanKindList } from './config'; -import Filter from './Filter'; -import LatencyForm from './LatencyForm'; -import { AutoComplete, Form, InfoWrapper, Select } from './styles'; -const { Option } = Select; -import { METRICS_PAGE_QUERY_PARAM } from 'constants/query'; -import ROUTES from 'constants/routes'; -import createQueryParams from 'lib/createQueryParams'; -import history from 'lib/history'; -import { useLocation } from 'react-router'; -import { bindActionCreators, Dispatch } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { UpdateSelectedTags } from 'store/actions/trace'; -import { - UpdateSelectedData, - UpdateSelectedDataProps, -} from 'store/actions/trace/updateSelectedData'; -import AppActions from 'types/actions'; - -const FormItem = Form.Item; - -const TraceList = ({ - updateSelectedTags, - updateSelectedData, -}: TraceListProps): JSX.Element => { - const [ - notificationInstance, - NotificationElement, - ] = notification.useNotification(); - - const [visible, setVisible] = useState(false); - const [form] = Form.useForm(); - const [form_basefilter] = Form.useForm(); - - const { search } = useLocation(); - - const params = new URLSearchParams(search); - - const onLatencyButtonClick = useCallback(() => { - setVisible((visible) => !visible); - }, []); - - const { - operationsList, - serviceList, - tagsSuggestions, - selectedTags, - selectedService, - selectedOperation, - selectedLatency, - selectedKind, - selectedAggOption, - selectedEntity, - } = useSelector((state) => state.trace); - - const paramsInObject = (params: URLSearchParams): { [x: string]: string } => { - const updatedParamas: { [x: string]: string } = {}; - params.forEach((value, key) => { - updatedParamas[key] = value; - }); - return updatedParamas; - }; - - const updatedQueryParams = (updatedValue: string[], key: string[]): void => { - const updatedParams = paramsInObject(params); - - updatedValue.forEach((_, index) => { - updatedParams[key[index]] = updatedValue[index]; - }); - - const queryParams = createQueryParams(updatedParams); - history.push(ROUTES.TRACE + `?${queryParams}`); - }; - - const getUpdatedSelectedData = (props: UpdateSelectedDataProps): void => { - const { - selectedKind, - selectedLatency, - selectedOperation, - selectedService, - } = props; - - updateSelectedData({ - selectedKind, - selectedLatency, - selectedOperation, - selectedService, - selectedAggOption, - selectedEntity, - }); - }; - - const onTagSubmitTagHandler = (values: Item): void => { - if (values.tag_key.length === 0 || values.tag_value.length === 0) { - return; - } - - // check whether it is pre-existing in the array or not - - const isFound = selectedTags.find((tags) => { - return ( - tags.key === values.tag_key && - tags.value === values.tag_value && - tags.operator === values.operator - ); - }); - - if (!isFound) { - const preSelectedTags = [ - ...selectedTags, - { - operator: values.operator, - key: values.tag_key, - value: values.tag_value, - }, - ]; - - updatedQueryParams( - [JSON.stringify(preSelectedTags)], - [METRICS_PAGE_QUERY_PARAM.selectedTags], - ); - - updateSelectedTags(preSelectedTags); - } else { - notificationInstance.error({ - message: 'Tag Already Present', - }); - } - }; - - const onChangeTagKey = (data: string): void => { - form.setFieldsValue({ tag_key: data }); - }; - - const updateSelectedServiceHandler = (value: string): void => { - updatedQueryParams([value], [METRICS_PAGE_QUERY_PARAM.service]); - getUpdatedSelectedData({ - selectedKind, - selectedLatency, - selectedOperation, - selectedService: value, - selectedAggOption, - selectedEntity, - }); - }; - - const updateSelectedOperationHandler = (value: string): void => { - updatedQueryParams([value], [METRICS_PAGE_QUERY_PARAM.operation]); - getUpdatedSelectedData({ - selectedKind, - selectedLatency, - selectedOperation: value, - selectedService, - selectedAggOption, - selectedEntity, - }); - }; - - const updateSelectedKindHandler = (value: string): void => { - updatedQueryParams([value], [METRICS_PAGE_QUERY_PARAM.kind]); - getUpdatedSelectedData({ - selectedKind: value, - selectedLatency, - selectedOperation, - selectedService, - selectedAggOption, - selectedEntity, - }); - }; - - useEffect(() => { - if (selectedService.length !== 0) { - form_basefilter.setFieldsValue({ - service: selectedService, - }); - } else { - form_basefilter.setFieldsValue({ - service: '', - }); - } - - if (selectedOperation.length !== 0) { - form_basefilter.setFieldsValue({ - operation: selectedOperation, - }); - } else { - form_basefilter.setFieldsValue({ - operation: '', - }); - } - - if (selectedKind.length !== 0) { - form_basefilter.setFieldsValue({ - spanKind: selectedKind, - }); - } else { - form_basefilter.setFieldsValue({ - spanKind: '', - }); - } - - if (selectedLatency.max.length === 0 && selectedLatency.min.length === 0) { - form_basefilter.setFieldsValue({ - latency: 'Latency', - }); - } - - if (selectedLatency.max.length !== 0 && selectedLatency.min.length === 0) { - form_basefilter.setFieldsValue({ - latency: `Latency < Max Latency: ${ - parseInt(selectedLatency.max, 10) / 1000000 - } ms`, - }); - } - - if (selectedLatency.max.length === 0 && selectedLatency.min.length !== 0) { - form_basefilter.setFieldsValue({ - latency: `Min Latency: ${ - parseInt(selectedLatency.min, 10) / 1000000 - } ms < Latency`, - }); - } - - if (selectedLatency.max.length !== 0 && selectedLatency.min.length !== 0) { - form_basefilter.setFieldsValue({ - latency: `Min Latency: ${ - parseInt(selectedLatency.min, 10) / 1000000 - } ms < Latency < Max Latency: ${ - parseInt(selectedLatency.min, 10) / 1000000 - } ms`, - }); - } - }, [ - selectedService, - selectedOperation, - selectedKind, - selectedLatency, - form_basefilter, - ]); - - return ( - <> - {NotificationElement} - - Filter Traces -
- - - - - - - - - - - - - - - - - - {(selectedTags.length !== 0 || - selectedService.length !== 0 || - selectedOperation.length !== 0 || - selectedLatency.max.length !== 0 || - selectedLatency.min.length !== 0) && ( - - )} - - Select Service to get Tag suggestions -
- - { - return { value: s.tagKeys }; - })} - onChange={onChangeTagKey} - filterOption={(inputValue, option): boolean => - option?.value.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1 - } - placeholder="Tag Key" - /> - - - - - - - - - - - - - - - { - setVisible(false); - }} - updatedQueryParams={updatedQueryParams} - visible={visible} - onLatencyButtonClick={onLatencyButtonClick} - /> - - ); -}; - -interface Item { - tag_key: string; - tag_value: string; - operator: TagItem['operator']; -} - -interface DispatchProps { - updateSelectedTags: ( - selectedTags: TraceReducer['selectedTags'], - ) => (dispatch: Dispatch) => void; - updateSelectedData: (props: UpdateSelectedDataProps) => void; -} - -const mapDispatchToProps = ( - dispatch: ThunkDispatch, -): DispatchProps => ({ - updateSelectedTags: bindActionCreators(UpdateSelectedTags, dispatch), - updateSelectedData: bindActionCreators(UpdateSelectedData, dispatch), -}); - -type TraceListProps = DispatchProps; - -export default connect(null, mapDispatchToProps)(TraceList); diff --git a/frontend/src/container/TraceFilter/styles.ts b/frontend/src/container/TraceFilter/styles.ts deleted file mode 100644 index 165f48e1b6..0000000000 --- a/frontend/src/container/TraceFilter/styles.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { - AutoComplete as AutoCompleteComponent, - Card as CardComponent, - Form as FormComponent, - Select as SelectComponent, - Typography, -} from 'antd'; -import styled from 'styled-components'; - -export const InfoWrapper = styled(Typography)` - padding-top: 1rem; - font-style: italic; - font-size: 0.75rem; -`; - -export const Select = styled(SelectComponent)` - min-width: 180px; -`; - -export const AutoComplete = styled(AutoCompleteComponent)` - min-width: 180px; -`; - -export const Form = styled(FormComponent)` - margin-top: 1rem; - margin-bottom: 1rem; - gap: 0.5rem; -`; - -export const Card = styled(CardComponent)` - .ant-card-body { - padding: 0.5rem; - } -`; diff --git a/frontend/src/container/TraceList/index.tsx b/frontend/src/container/TraceList/index.tsx deleted file mode 100644 index 9f655dd964..0000000000 --- a/frontend/src/container/TraceList/index.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import { Space, Table, Typography } from 'antd'; -import { ColumnsType } from 'antd/lib/table/Table'; -import ROUTES from 'constants/routes'; -import convertDateToAmAndPm from 'lib/convertDateToAmAndPm'; -import getFormattedDate from 'lib/getFormatedDate'; -import history from 'lib/history'; -import React from 'react'; -import { useSelector } from 'react-redux'; -import { AppState } from 'store/reducers'; -import { pushDStree } from 'types/api/trace/getSpans'; -import { TraceReducer } from 'types/reducer/trace'; -import { isOnboardingSkipped } from 'utils/app'; - -import { TitleContainer } from './styles'; - -const TraceDetails = (): JSX.Element => { - const { spanList } = useSelector( - (state) => state.trace, - ); - - const spans: TableDataSourceItem[] = spanList[0]?.events?.map( - (item: (number | string | string[] | pushDStree[])[], index) => { - if ( - typeof item[0] === 'number' && - typeof item[4] === 'string' && - typeof item[6] === 'string' && - typeof item[1] === 'string' && - typeof item[2] === 'string' && - typeof item[3] === 'string' - ) { - return { - startTime: item[0], - operationName: item[4], - duration: parseInt(item[6]), - spanid: item[1], - traceid: item[2], - key: index.toString(), - service: item[3], - }; - } - return { - duration: 0, - key: '', - operationName: '', - service: '', - spanid: '', - startTime: 0, - traceid: '', - }; - }, - ); - - const columns: ColumnsType = [ - { - title: 'Start Time', - dataIndex: 'startTime', - key: 'startTime', - sorter: (a, b): number => a.startTime - b.startTime, - sortDirections: ['descend', 'ascend'], - render: (value: number): string => { - const date = new Date(value); - const result = `${getFormattedDate(date)} ${convertDateToAmAndPm(date)}`; - return result; - }, - }, - { - title: 'Service', - dataIndex: 'service', - key: 'service', - }, - { - title: 'Operation', - dataIndex: 'operationName', - key: 'operationName', - }, - { - title: 'Duration (in ms)', - dataIndex: 'duration', - key: 'duration', - sorter: (a, b): number => a.duration - b.duration, - sortDirections: ['descend', 'ascend'], - render: (value: number): string => (value / 1000000).toFixed(2), - }, - ]; - - if (isOnboardingSkipped() && spans?.length === 0) { - return ( - - No spans found. Please add instrumentation (follow this - - guide - - ) - - ); - } - - if (spans?.length === 0) { - return No spans found for given filter!; - } - - return ( - <> - List of filtered spans - -
=> ({ - onClick: (): void => { - history.push({ - pathname: ROUTES.TRACE + '/' + record.traceid, - state: { - spanId: record.spanid, - }, - }); - }, - })} - /> - - ); -}; - -export interface TableDataSourceItem { - key: string; - spanid: string; - traceid: string; - operationName: string; - startTime: number; - duration: number; - service: string; -} - -export default TraceDetails; diff --git a/frontend/src/container/TraceList/styles.ts b/frontend/src/container/TraceList/styles.ts deleted file mode 100644 index dfde5eaad6..0000000000 --- a/frontend/src/container/TraceList/styles.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Typography } from 'antd'; -import styled from 'styled-components'; - -export const TitleContainer = styled(Typography)` - margin-top: 1rem; - margin-bottom: 1rem; -`; diff --git a/frontend/src/hooks/useClickOutside.ts b/frontend/src/hooks/useClickOutside.ts new file mode 100644 index 0000000000..d3d7ce83c7 --- /dev/null +++ b/frontend/src/hooks/useClickOutside.ts @@ -0,0 +1,26 @@ +import React, { useEffect } from 'react'; + +const useClickOutside = ( + ref: React.RefObject, + callback: (e: HTMLElement) => void | null, +) => { + const listener = (e: Event) => { + const node = e?.target as HTMLElement; + + if (ref.current && !ref.current.contains(node)) { + if (callback) { + callback(node); + } + } + }; + + useEffect(() => { + document.addEventListener('click', listener); + + return () => { + document.removeEventListener('click', listener); + }; + }, [ref, callback]); +}; + +export default useClickOutside; diff --git a/frontend/src/hooks/useDebouncedFunction.ts b/frontend/src/hooks/useDebouncedFunction.ts new file mode 100644 index 0000000000..4a26fbc46f --- /dev/null +++ b/frontend/src/hooks/useDebouncedFunction.ts @@ -0,0 +1,34 @@ +import { useCallback } from 'react'; +import debounce from 'lodash-es/debounce'; + +export interface DebouncedFunc any> { + (...args: Parameters): ReturnType | undefined; + + cancel(): void; + + flush(): ReturnType | undefined; +} + +export type DebounceOptions = { + leading?: boolean; + maxWait?: number; + trailing?: boolean; +}; + +const defaultOptions: DebounceOptions = { + leading: false, + trailing: true, +}; + +const useDebouncedFn = any>( + fn: T, + wait: number = 100, + options: DebounceOptions = defaultOptions, + dependencies?: ReadonlyArray, +): DebouncedFunc => { + const debounced = debounce(fn, wait, options); + + return useCallback(debounced, dependencies || []); +}; + +export default useDebouncedFn; diff --git a/frontend/src/lib/query/convertObjectIntoParams.ts b/frontend/src/lib/query/convertObjectIntoParams.ts new file mode 100644 index 0000000000..fa6dc3f6f5 --- /dev/null +++ b/frontend/src/lib/query/convertObjectIntoParams.ts @@ -0,0 +1,15 @@ +const convertObjectIntoParams = ( + props: Record, + stringify = false, +) => { + return Object.keys(props) + .map( + (e) => + `${e}=${ + stringify ? encodeURIComponent(JSON.stringify(props[e])) : props[e] + }`, + ) + .join('&'); +}; + +export default convertObjectIntoParams; diff --git a/frontend/src/pages/Trace/index.tsx b/frontend/src/pages/Trace/index.tsx new file mode 100644 index 0000000000..5fd2894365 --- /dev/null +++ b/frontend/src/pages/Trace/index.tsx @@ -0,0 +1,157 @@ +import { Card } from 'antd'; +import ROUTES from 'constants/routes'; +import Filters from 'container/Trace/Filters'; +import TraceGraph from 'container/Trace/Graph'; +import Search from 'container/Trace/Search'; +import TraceGraphFilter from 'container/Trace/TraceGraphFilter'; +import TraceTable from 'container/Trace/TraceTable'; +import history from 'lib/history'; +import React, { useCallback, useEffect, useState } from 'react'; +import { connect, useDispatch, useSelector } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; +import { GetInitialTraceFilter } from 'store/actions/trace/getInitialFilter'; +import { + GetSpansAggregate, + GetSpansAggregateProps, +} from 'store/actions/trace/getInitialSpansAggregate'; +import { GetSpans, GetSpansProps } from 'store/actions/trace/getSpans'; +import { AppState } from 'store/reducers'; +import AppActions from 'types/actions'; +import { RESET_TRACE_FILTER } from 'types/actions/trace'; +import { GlobalReducer } from 'types/reducer/globalTime'; +import { TraceReducer } from 'types/reducer/trace'; + +import { + Container, + LeftContainer, + RightContainer, + ClearAllFilter, +} from './styles'; + +const Trace = ({ + getSpansAggregate, + getSpans, + getInitialFilter, +}: Props): JSX.Element => { + const { maxTime, minTime } = useSelector( + (state) => state.globalTime, + ); + + const dispatch = useDispatch>(); + + const [isChanged, setIsChanged] = useState(true); + + const { + selectedFilter, + spansAggregate, + selectedTags, + selectedFunction, + selectedGroupBy, + isFilterExclude, + } = useSelector((state) => state.traces); + + useEffect(() => { + getInitialFilter(minTime, maxTime); + }, [maxTime, minTime, getInitialFilter, isChanged]); + + useEffect(() => { + getSpansAggregate({ + maxTime: maxTime, + minTime: minTime, + selectedFilter, + current: spansAggregate.currentPage, + pageSize: spansAggregate.pageSize, + selectedTags, + }); + }, [selectedTags, selectedFilter, maxTime, minTime]); + + useEffect(() => { + getSpans({ + end: maxTime, + function: selectedFunction, + groupBy: selectedGroupBy, + selectedFilter, + selectedTags, + start: minTime, + step: 60, + isFilterExclude, + }); + }, [ + selectedFunction, + selectedGroupBy, + selectedFilter, + selectedTags, + maxTime, + minTime, + ]); + + useEffect(() => { + return () => { + dispatch({ + type: RESET_TRACE_FILTER, + }); + }; + }, []); + + const onClickHandler = useCallback((e) => { + e.preventDefault(); + e.stopPropagation(); + + history.replace(ROUTES.TRACE); + + dispatch({ + type: RESET_TRACE_FILTER, + }); + + setIsChanged((state) => !state); + }, []); + + return ( + <> + + +
+ + Clear all filters + + + + +
+ + + + + + + + + + + +
+ + ); +}; + +interface DispatchProps { + getSpansAggregate: (props: GetSpansAggregateProps) => void; + getSpans: (props: GetSpansProps) => void; + getInitialFilter: ( + minTime: GlobalReducer['minTime'], + maxTime: GlobalReducer['maxTime'], + ) => void; +} + +const mapDispatchToProps = ( + dispatch: ThunkDispatch, +): DispatchProps => ({ + getInitialFilter: bindActionCreators(GetInitialTraceFilter, dispatch), + getSpansAggregate: bindActionCreators(GetSpansAggregate, dispatch), + getSpans: bindActionCreators(GetSpans, dispatch), +}); + +type Props = DispatchProps; + +export default connect(null, mapDispatchToProps)(Trace); diff --git a/frontend/src/pages/Trace/styles.ts b/frontend/src/pages/Trace/styles.ts new file mode 100644 index 0000000000..f124268258 --- /dev/null +++ b/frontend/src/pages/Trace/styles.ts @@ -0,0 +1,37 @@ +import styled from 'styled-components'; +import { Button, Card } from 'antd'; + +export const Container = styled.div` + display: flex; + flex: 1; + min-height: 80vh; + + margin-top: 1rem; +`; + +export const LeftContainer = styled(Card)` + flex: 0.5; + width: 95%; + padding-right: 0.5rem; + + .ant-card-body { + padding: 0; + } +`; + +export const RightContainer = styled(Card)` + &&& { + flex: 2; + } + + .ant-card-body { + padding: 0.5rem; + } +`; + +export const ClearAllFilter = styled(Button)` + &&& { + width: 95%; + margin-bottom: 0.5rem; + } +`; diff --git a/frontend/src/pages/TraceDetails/index.tsx b/frontend/src/pages/TraceDetails/index.tsx deleted file mode 100644 index 4f58d3c554..0000000000 --- a/frontend/src/pages/TraceDetails/index.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { Typography } from 'antd'; -import Spinner from 'components/Spinner'; -import TraceCustomVisualisation from 'container/TraceCustomVisualization'; -import TraceFilter from 'container/TraceFilter'; -import TraceList from 'container/TraceList'; -import React, { useEffect } from 'react'; -import { connect, useSelector } from 'react-redux'; -import { bindActionCreators } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { - GetInitialTraceData, - GetInitialTraceDataProps, - ResetRaceData, -} from 'store/actions/trace'; -import { AppState } from 'store/reducers'; -import AppActions from 'types/actions'; -import { GlobalReducer } from 'types/reducer/globalTime'; -import { TraceReducer } from 'types/reducer/trace'; - -const TraceDetail = ({ - getInitialTraceData, - resetTraceData, -}: TraceDetailProps): JSX.Element => { - const { loading, selectedTime } = useSelector( - (state) => state.globalTime, - ); - - const { loading: TraceLoading, error, errorMessage } = useSelector< - AppState, - TraceReducer - >((state) => state.trace); - - useEffect(() => { - if (!loading) { - getInitialTraceData({ - selectedTime, - }); - } - - return (): void => { - resetTraceData(); - }; - }, [getInitialTraceData, loading, selectedTime, resetTraceData]); - - if (error) { - return {errorMessage}; - } - - if (loading || TraceLoading) { - return ; - } - - return ( - <> - - - - - ); -}; - -interface DispatchProps { - getInitialTraceData: (props: GetInitialTraceDataProps) => void; - resetTraceData: () => void; -} - -const mapDispatchToProps = ( - dispatch: ThunkDispatch, -): DispatchProps => ({ - getInitialTraceData: bindActionCreators(GetInitialTraceData, dispatch), - resetTraceData: bindActionCreators(ResetRaceData, dispatch), -}); - -type TraceDetailProps = DispatchProps; - -export default connect(null, mapDispatchToProps)(TraceDetail); diff --git a/frontend/src/store/actions/trace/getInitialData.ts b/frontend/src/store/actions/trace/getInitialData.ts deleted file mode 100644 index 13851614a8..0000000000 --- a/frontend/src/store/actions/trace/getInitialData.ts +++ /dev/null @@ -1,201 +0,0 @@ -import getServiceList from 'api/trace/getServiceList'; -import getServiceOperation from 'api/trace/getServiceOperation'; -import getSpan from 'api/trace/getSpan'; -import getSpansAggregate from 'api/trace/getSpanAggregate'; -import getTags from 'api/trace/getTags'; -import { AxiosError } from 'axios'; -import { METRICS_PAGE_QUERY_PARAM } from 'constants/query'; -import history from 'lib/history'; -import { Dispatch } from 'redux'; -import store from 'store'; -import AppActions from 'types/actions'; -import { ErrorResponse, SuccessResponse } from 'types/api'; -import { PayloadProps as ServiceOperationPayloadProps } from 'types/api/trace/getServiceOperation'; -import { PayloadProps as TagPayloadProps } from 'types/api/trace/getTags'; -import { GlobalReducer } from 'types/reducer/globalTime'; -import { TraceReducer } from 'types/reducer/trace'; - -export const GetInitialTraceData = ({ - selectedTime, -}: GetInitialTraceDataProps): ((dispatch: Dispatch) => void) => { - return async (dispatch: Dispatch): Promise => { - try { - const { globalTime, trace } = store.getState(); - const { minTime, maxTime, selectedTime: globalSelectedTime } = globalTime; - const { selectedAggOption, selectedEntity } = trace; - - // keeping the redux as source of truth - if (selectedTime !== globalSelectedTime) { - return; - } - - dispatch({ - type: 'UPDATE_SPANS_LOADING', - payload: { - loading: true, - }, - }); - - const urlParams = new URLSearchParams(history.location.search.split('?')[1]); - const operationName = urlParams.get(METRICS_PAGE_QUERY_PARAM.operation); - const serviceName = urlParams.get(METRICS_PAGE_QUERY_PARAM.service); - const errorTag = urlParams.get(METRICS_PAGE_QUERY_PARAM.error); - const kindTag = urlParams.get(METRICS_PAGE_QUERY_PARAM.kind); - const latencyMin = urlParams.get(METRICS_PAGE_QUERY_PARAM.latencyMin); - const latencyMax = urlParams.get(METRICS_PAGE_QUERY_PARAM.latencyMax); - const selectedTags = urlParams.get(METRICS_PAGE_QUERY_PARAM.selectedTags); - const aggregationOption = urlParams.get( - METRICS_PAGE_QUERY_PARAM.aggregationOption, - ); - const selectedEntityOption = urlParams.get(METRICS_PAGE_QUERY_PARAM.entity); - - const isCustomSelected = selectedTime === 'custom'; - - const end = isCustomSelected - ? globalTime.maxTime + 15 * 60 * 1000000000 - : maxTime; - - const start = isCustomSelected - ? globalTime.minTime - 15 * 60 * 1000000000 - : minTime; - - const [ - serviceListResponse, - spanResponse, - spanAggregateResponse, - ] = await Promise.all([ - getServiceList(), - getSpan({ - start, - end, - kind: kindTag || '', - limit: '100', - lookback: '2d', - maxDuration: latencyMax || '', - minDuration: latencyMin || '', - operation: operationName || '', - service: serviceName || '', - tags: selectedTags || '[]', - }), - getSpansAggregate({ - aggregation_option: aggregationOption || selectedAggOption, - dimension: selectedEntityOption || selectedEntity, - end, - start, - kind: kindTag || '', - maxDuration: latencyMax || '', - minDuration: latencyMin || '', - operation: operationName || '', - service: serviceName || '', - step: '60', - tags: selectedTags || '[]', - }), - ]); - - let tagResponse: - | SuccessResponse - | ErrorResponse - | undefined; - - let serviceOperationResponse: - | SuccessResponse - | ErrorResponse - | undefined; - - if (serviceName !== null && serviceName.length !== 0) { - [tagResponse, serviceOperationResponse] = await Promise.all([ - getTags({ - service: serviceName, - }), - getServiceOperation({ - service: serviceName, - }), - ]); - } - - const getSelectedTags = (): TraceReducer['selectedTags'] => { - const selectedTag = JSON.parse(selectedTags || '[]'); - - if (typeof selectedTag !== 'object' && Array.isArray(selectedTag)) { - return []; - } - - if (errorTag) { - return [ - ...selectedTag, - { - key: METRICS_PAGE_QUERY_PARAM.error, - operator: 'equals', - value: errorTag, - }, - ]; - } - - return [...selectedTag]; - }; - - const getCondition = (): boolean => { - const basicCondition = - serviceListResponse.statusCode === 200 && - spanResponse.statusCode === 200 && - (spanAggregateResponse.statusCode === 200 || - spanAggregateResponse.statusCode === 400); - - if (serviceName === null || serviceName.length === 0) { - return basicCondition; - } - - return ( - basicCondition && - tagResponse?.statusCode === 200 && - serviceOperationResponse?.statusCode === 200 - ); - }; - - const condition = getCondition(); - - if (condition) { - dispatch({ - type: 'GET_TRACE_INITIAL_DATA_SUCCESS', - payload: { - serviceList: serviceListResponse.payload || [], - operationList: serviceOperationResponse?.payload || [], - tagsSuggestions: tagResponse?.payload || [], - spansList: spanResponse?.payload || [], - selectedService: serviceName || '', - selectedOperation: operationName || '', - selectedTags: getSelectedTags(), - selectedKind: kindTag || '', - selectedLatency: { - max: latencyMax || '', - min: latencyMin || '', - }, - spansAggregate: spanAggregateResponse.payload || [], - }, - }); - - dispatch({ - type: 'GET_TRACE_LOADING_END', - }); - } else { - dispatch({ - type: 'GET_TRACE_INITIAL_DATA_ERROR', - payload: { - errorMessage: serviceListResponse?.error || 'Something went wrong', - }, - }); - } - } catch (error) { - dispatch({ - type: 'GET_TRACE_INITIAL_DATA_ERROR', - payload: { - errorMessage: (error as AxiosError).toString() || 'Something went wrong', - }, - }); - } - }; -}; - -export interface GetInitialTraceDataProps { - selectedTime: GlobalReducer['selectedTime']; -} diff --git a/frontend/src/store/actions/trace/getInitialFilter.ts b/frontend/src/store/actions/trace/getInitialFilter.ts new file mode 100644 index 0000000000..99e3fee6c5 --- /dev/null +++ b/frontend/src/store/actions/trace/getInitialFilter.ts @@ -0,0 +1,173 @@ +import { Dispatch, Store } from 'redux'; +import { AppState } from 'store/reducers'; +import AppActions from 'types/actions'; +import { GlobalReducer } from 'types/reducer/globalTime'; +import getFiltersApi from 'api/trace/getFilters'; +import { + parseSelectedFilter, + parseFilterToFetchData, + parseQueryIntoCurrent, + parseQueryIntoSelectedTags, + isTraceFilterEnum, + parseQueryIntoFilter, + parseIsSkippedSelection, + parseFilterExclude, +} from './util'; +import { + UPDATE_ALL_FILTERS, + UPDATE_TRACE_FILTER_LOADING, +} from 'types/actions/trace'; +import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace'; +import { notification } from 'antd'; +import xor from 'lodash-es/xor'; + +export const GetInitialTraceFilter = ( + minTime: GlobalReducer['minTime'], + maxTime: GlobalReducer['maxTime'], +): (( + dispatch: Dispatch, + getState: Store['getState'], +) => void) => { + return async (dispatch, getState): Promise => { + try { + const query = location.search; + + const { traces, globalTime } = getState(); + + if (globalTime.maxTime !== maxTime && globalTime.minTime !== minTime) { + return; + } + + const getSelectedFilter = parseSelectedFilter( + query, + traces.selectedFilter, + true, + ); + + const getFilterToFetchData = parseFilterToFetchData( + query, + traces.filterToFetchData, + ); + + const getUserSelected = parseSelectedFilter( + query, + traces.userSelectedFilter, + ); + + const getIsFilterExcluded = parseFilterExclude( + query, + traces.isFilterExclude, + ); + + const parsedQueryCurrent = parseQueryIntoCurrent( + query, + traces.spansAggregate.currentPage, + ); + + const isSelectionSkipped = parseIsSkippedSelection(query); + + const parsedSelectedTags = parseQueryIntoSelectedTags( + query, + traces.selectedTags, + ); + + const parsedFilter = parseQueryIntoFilter(query, traces.filter); + + // now filter are not matching we need to fetch the data and make in sync + dispatch({ + type: UPDATE_TRACE_FILTER_LOADING, + payload: { + filterLoading: true, + }, + }); + + const response = await getFiltersApi({ + end: String(maxTime), + getFilters: getFilterToFetchData.currentValue, + start: String(minTime), + other: Object.fromEntries(getSelectedFilter.currentValue), + isFilterExclude: getIsFilterExcluded.currentValue, + }); + + let preSelectedFilter: Map = new Map( + getSelectedFilter.currentValue, + ); + + if (response.payload && !isSelectionSkipped.currentValue) { + const diff = + query.length === 0 + ? traces.filterToFetchData + : xor(traces.filterToFetchData, getFilterToFetchData.currentValue); + + Object.keys(response.payload).map((key) => { + const value = response.payload[key]; + Object.keys(value) + // remove maxDuration and minDuration filter from initial selection logic + .filter((e) => !['maxDuration', 'minDuration'].includes(e)) + .map((preKey) => { + if (isTraceFilterEnum(key) && diff.find((v) => v === key)) { + // const preValue = preSelectedFilter?.get(key) || []; + const preValue = getUserSelected.currentValue?.get(key) || []; + // preSelectedFilter?.set(key, [...new Set([...preValue, preKey])]); + getUserSelected.currentValue.set(key, [ + ...new Set([...preValue, preKey]), + ]); + } + }); + }); + } + + if (response.statusCode === 200) { + const preResponseSelected: TraceReducer['filterResponseSelected'] = new Set(); + + const initialFilter = new Map>( + parsedFilter.currentValue, + ); + + Object.keys(response.payload).forEach((key) => { + const value = response.payload[key]; + if (isTraceFilterEnum(key)) { + Object.keys(value).forEach((e) => preResponseSelected.add(e)); + + initialFilter.set(key, { + ...initialFilter.get(key), + ...value, + }); + } + }); + + dispatch({ + type: UPDATE_ALL_FILTERS, + payload: { + filter: initialFilter, + selectedFilter: preSelectedFilter, + filterToFetchData: getFilterToFetchData.currentValue, + current: parsedQueryCurrent.currentValue, + selectedTags: parsedSelectedTags.currentValue, + userSelected: getUserSelected.currentValue, + isFilterExclude: getIsFilterExcluded.currentValue, + }, + }); + } else { + notification.error({ + message: response.error || 'Something went wrong', + }); + } + + dispatch({ + type: UPDATE_TRACE_FILTER_LOADING, + payload: { + filterLoading: false, + }, + }); + } catch (error) { + console.log(error); + dispatch({ + type: UPDATE_TRACE_FILTER_LOADING, + payload: { + filterLoading: false, + }, + }); + } + }; +}; diff --git a/frontend/src/store/actions/trace/getInitialSpansAggregate.ts b/frontend/src/store/actions/trace/getInitialSpansAggregate.ts new file mode 100644 index 0000000000..0ae6362eea --- /dev/null +++ b/frontend/src/store/actions/trace/getInitialSpansAggregate.ts @@ -0,0 +1,115 @@ +import { Dispatch, Store } from 'redux'; +import { AppState } from 'store/reducers'; +import AppActions from 'types/actions'; +import { UPDATE_SPANS_AGGREEGATE } from 'types/actions/trace'; +import getSpansAggregate from 'api/trace/getSpansAggregate'; +import { GlobalReducer } from 'types/reducer/globalTime'; +import { TraceReducer } from 'types/reducer/trace'; +import { notification } from 'antd'; + +export const GetSpansAggregate = ( + props: GetSpansAggregateProps, +): (( + dispatch: Dispatch, + getState: Store['getState'], +) => void) => { + return async (dispatch, getState): Promise => { + const { traces, globalTime } = getState(); + const { spansAggregate } = traces; + + if ( + globalTime.maxTime !== props.maxTime && + globalTime.minTime !== props.minTime + ) { + return; + } + + if (traces.filterLoading) { + return; + } + + try { + // triggering loading + dispatch({ + type: UPDATE_SPANS_AGGREEGATE, + payload: { + spansAggregate: { + currentPage: props.current, + loading: true, + data: spansAggregate.data, + error: false, + total: spansAggregate.total, + pageSize: props.pageSize, + }, + }, + }); + + const response = await getSpansAggregate({ + end: props.maxTime, + start: props.minTime, + selectedFilter: props.selectedFilter, + limit: props.pageSize, + offset: props.current * props.pageSize - props.pageSize, + selectedTags: props.selectedTags, + isFilterExclude: traces.isFilterExclude, + }); + + if (response.statusCode === 200) { + dispatch({ + type: UPDATE_SPANS_AGGREEGATE, + payload: { + spansAggregate: { + currentPage: props.current, + loading: false, + data: response.payload.spans, + error: false, + total: response.payload.totalSpans, + pageSize: props.pageSize, + }, + }, + }); + } else { + notification.error({ + message: response.error || 'Something went wrong', + }); + + dispatch({ + type: UPDATE_SPANS_AGGREEGATE, + payload: { + spansAggregate: { + currentPage: props.current, + loading: false, + data: spansAggregate.data, + error: true, + total: spansAggregate.total, + pageSize: props.pageSize, + }, + }, + }); + } + } catch (error) { + dispatch({ + type: UPDATE_SPANS_AGGREEGATE, + payload: { + spansAggregate: { + currentPage: props.current, + loading: false, + data: spansAggregate.data, + error: true, + total: spansAggregate.total, + pageSize: props.pageSize, + }, + }, + }); + } + }; +}; + +export interface GetSpansAggregateProps { + maxTime: GlobalReducer['maxTime']; + minTime: GlobalReducer['minTime']; + selectedFilter: TraceReducer['selectedFilter']; + current: TraceReducer['spansAggregate']['currentPage']; + pageSize: TraceReducer['spansAggregate']['pageSize']; + selectedTags: TraceReducer['selectedTags']; +} diff --git a/frontend/src/store/actions/trace/getSpans.ts b/frontend/src/store/actions/trace/getSpans.ts new file mode 100644 index 0000000000..6cee9a95c8 --- /dev/null +++ b/frontend/src/store/actions/trace/getSpans.ts @@ -0,0 +1,96 @@ +import { Dispatch, Store } from 'redux'; +import { AppState } from 'store/reducers'; +import AppActions from 'types/actions'; +import { + UPDATE_TRACE_GRAPH_ERROR, + UPDATE_TRACE_GRAPH_LOADING, + UPDATE_TRACE_GRAPH_SUCCESS, +} from 'types/actions/trace'; +import getSpans from 'api/trace/getSpans'; +import { Props } from 'types/api/trace/getSpans'; +import { notification } from 'antd'; + +export const GetSpans = ( + props: GetSpansProps, +): (( + dispatch: Dispatch, + getState: Store['getState'], +) => void) => { + return async (dispatch, getState): Promise => { + try { + const { traces, globalTime } = getState(); + const { spansGraph } = traces; + + if (globalTime.maxTime !== props.end && globalTime.minTime !== props.start) { + return; + } + + const { selectedTime } = globalTime; + + if (traces.filterLoading) { + return; + } + + // @TODO refactor this logic when share url functionlity is updated + const isCustomSelected = selectedTime === 'custom'; + + const end = isCustomSelected + ? globalTime.maxTime + 15 * 60 * 1000000000 + : props.end; + + const start = isCustomSelected + ? globalTime.minTime - 15 * 60 * 1000000000 + : props.start; + + if (!spansGraph.loading) { + dispatch({ + type: UPDATE_TRACE_GRAPH_LOADING, + payload: { + loading: true, + }, + }); + } + + const response = await getSpans({ + end: end, + function: props.function, + groupBy: props.groupBy, + selectedFilter: props.selectedFilter, + selectedTags: props.selectedTags, + start: start, + step: props.step, + isFilterExclude: props.isFilterExclude, + }); + + if (response.statusCode === 200) { + dispatch({ + type: UPDATE_TRACE_GRAPH_SUCCESS, + payload: { + data: response.payload, + }, + }); + } else { + notification.error({ + message: response.error || 'Something went wrong', + }); + dispatch({ + type: UPDATE_TRACE_GRAPH_ERROR, + payload: { + error: true, + errorMessage: response.error || 'Something went wrong', + }, + }); + } + } catch (error) { + dispatch({ + type: UPDATE_TRACE_GRAPH_ERROR, + payload: { + error: true, + errorMessage: (error as Error)?.toString() || 'Something went wrong', + }, + }); + } + }; +}; + +export type GetSpansProps = Props; diff --git a/frontend/src/store/actions/trace/getTraceVisualAgrregates.ts b/frontend/src/store/actions/trace/getTraceVisualAgrregates.ts deleted file mode 100644 index fa12fc4e47..0000000000 --- a/frontend/src/store/actions/trace/getTraceVisualAgrregates.ts +++ /dev/null @@ -1,92 +0,0 @@ -import getSpansAggregate from 'api/trace/getSpanAggregate'; -import { AxiosError } from 'axios'; -import { Dispatch } from 'redux'; -import store from 'store'; -import AppActions from 'types/actions'; -import { TraceReducer } from 'types/reducer/trace'; - -export const GetTraceVisualAggregates = ({ - selectedEntity, - selectedAggOption, -}: GetTraceVisualAggregatesProps): (( - dispatch: Dispatch, -) => void) => { - return async (dispatch: Dispatch): Promise => { - try { - dispatch({ - type: 'UPDATE_SPANS_LOADING', - payload: { - loading: true, - }, - }); - - const { trace, globalTime } = store.getState(); - - const { - selectedKind, - selectedLatency, - selectedOperation, - selectedService, - selectedTags, - } = trace; - - const { selectedTime, maxTime, minTime } = globalTime; - - const isCustomSelected = selectedTime === 'custom'; - - const end = isCustomSelected - ? globalTime.maxTime + 15 * 60 * 1000000000 - : maxTime; - - const start = isCustomSelected - ? globalTime.minTime - 15 * 60 * 1000000000 - : minTime; - - const [spanAggregateResponse] = await Promise.all([ - getSpansAggregate({ - aggregation_option: selectedAggOption, - dimension: selectedEntity, - end, - start, - kind: selectedKind || '', - maxDuration: selectedLatency.max || '', - minDuration: selectedLatency.min || '', - operation: selectedOperation || '', - service: selectedService || '', - step: '60', - tags: JSON.stringify(selectedTags) || '[]', - }), - ]); - - if (spanAggregateResponse.statusCode === 200) { - dispatch({ - type: 'UPDATE_AGGREGATES', - payload: { - spansAggregate: spanAggregateResponse.payload, - selectedAggOption, - selectedEntity, - }, - }); - } - - dispatch({ - type: 'UPDATE_SPANS_LOADING', - payload: { - loading: false, - }, - }); - } catch (error) { - dispatch({ - type: 'GET_TRACE_INITIAL_DATA_ERROR', - payload: { - errorMessage: (error as AxiosError).toString() || 'Something went wrong', - }, - }); - } - }; -}; - -export interface GetTraceVisualAggregatesProps { - selectedAggOption: TraceReducer['selectedAggOption']; - selectedEntity: TraceReducer['selectedEntity']; -} diff --git a/frontend/src/store/actions/trace/index.ts b/frontend/src/store/actions/trace/index.ts deleted file mode 100644 index f8d65f3eff..0000000000 --- a/frontend/src/store/actions/trace/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * from './getInitialData'; -export * from './resetTraceDetails'; -export * from './updateSelectedAggOption'; -export * from './updateSelectedEntity'; -export * from './updateSelectedKind'; -export * from './updateSelectedLatency'; -export * from './updateSelectedOperation'; -export * from './updateSelectedService'; -export * from './updateSelectedTags'; diff --git a/frontend/src/store/actions/trace/loadingCompleted.ts b/frontend/src/store/actions/trace/loadingCompleted.ts deleted file mode 100644 index 3a64b2449d..0000000000 --- a/frontend/src/store/actions/trace/loadingCompleted.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Dispatch } from 'redux'; -import AppActions from 'types/actions'; - -export const LoadingCompleted = (): (( - dispatch: Dispatch, -) => void) => { - return (dispatch: Dispatch): void => { - dispatch({ - type: 'GET_TRACE_LOADING_END', - }); - }; -}; diff --git a/frontend/src/store/actions/trace/parseFilter/current.ts b/frontend/src/store/actions/trace/parseFilter/current.ts new file mode 100644 index 0000000000..e8faae05c1 --- /dev/null +++ b/frontend/src/store/actions/trace/parseFilter/current.ts @@ -0,0 +1,36 @@ +import { TraceReducer } from 'types/reducer/trace'; +import { ParsedUrl } from '../util'; + +export const parseQueryIntoCurrent = ( + query: string, + stateCurrent: TraceReducer['spansAggregate']['currentPage'], +): ParsedUrl => { + const url = new URLSearchParams(query); + + let current = 1; + + const selected = url.get('current'); + + if (selected) { + try { + const parsedValue = JSON.parse(decodeURIComponent(selected)); + if (Number.isInteger(parsedValue)) { + current = parseInt(parsedValue, 10); + } + } catch (error) { + console.log('error while parsing json'); + } + } + + if (selected) { + return { + currentValue: parseInt(selected, 10), + urlValue: current, + }; + } + + return { + currentValue: stateCurrent, + urlValue: current, + }; +}; diff --git a/frontend/src/store/actions/trace/parseFilter/filter.ts b/frontend/src/store/actions/trace/parseFilter/filter.ts new file mode 100644 index 0000000000..0d91dfc9e6 --- /dev/null +++ b/frontend/src/store/actions/trace/parseFilter/filter.ts @@ -0,0 +1,43 @@ +import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace'; +import { isTraceFilterEnum, ParsedUrl } from '../util'; + +export const parseQueryIntoFilter = ( + query: string, + stateFilter: TraceReducer['filter'], +): ParsedUrl => { + const urlFilter = new Map>(); + const url = new URLSearchParams(query); + + const selected = url.get('filter'); + + if (selected) { + try { + const parsedValue = JSON.parse(selected); + + if (typeof parsedValue === 'object') { + Object.keys(parsedValue).forEach((key) => { + if (isTraceFilterEnum(key)) { + const value = parsedValue[key]; + if (typeof value === 'object') { + urlFilter.set(key, value); + } + } + }); + } + } catch (error) { + console.log(error); + } + } + + if (selected) { + return { + currentValue: urlFilter, + urlValue: urlFilter, + }; + } + + return { + currentValue: stateFilter, + urlValue: urlFilter, + }; +}; diff --git a/frontend/src/store/actions/trace/parseFilter/filterToFetchData.ts b/frontend/src/store/actions/trace/parseFilter/filterToFetchData.ts new file mode 100644 index 0000000000..5016bc7702 --- /dev/null +++ b/frontend/src/store/actions/trace/parseFilter/filterToFetchData.ts @@ -0,0 +1,37 @@ +import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace'; +import { ParsedUrl } from '../util'; + +export const parseFilterToFetchData = ( + query: string, + stateTraceFilterData: TraceReducer['filterToFetchData'], +): ParsedUrl => { + const url = new URLSearchParams(query); + + let filterToFetchData: TraceFilterEnum[] = []; + + const selected = url.get('filterToFetchData'); + + if (selected) { + try { + const parsedValue = JSON.parse(decodeURIComponent(selected)); + + if (Array.isArray(parsedValue)) { + filterToFetchData.push(...parsedValue); + } + } catch (error) { + //error while parsing json + } + } + + if (selected) { + return { + currentValue: filterToFetchData, + urlValue: filterToFetchData, + }; + } + + return { + currentValue: stateTraceFilterData, + urlValue: filterToFetchData, + }; +}; diff --git a/frontend/src/store/actions/trace/parseFilter/index.ts b/frontend/src/store/actions/trace/parseFilter/index.ts new file mode 100644 index 0000000000..e9fb1f3b19 --- /dev/null +++ b/frontend/src/store/actions/trace/parseFilter/index.ts @@ -0,0 +1,8 @@ +export * from './minMaxTime'; +export * from './selectedFilter'; +export * from './filterToFetchData'; +export * from './selectedTags'; +export * from './filter'; +export * from './skippedSelected'; +export * from './current'; +export * from './isFilterExclude'; diff --git a/frontend/src/store/actions/trace/parseFilter/isFilterExclude.ts b/frontend/src/store/actions/trace/parseFilter/isFilterExclude.ts new file mode 100644 index 0000000000..a07bc4b3b0 --- /dev/null +++ b/frontend/src/store/actions/trace/parseFilter/isFilterExclude.ts @@ -0,0 +1,44 @@ +import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace'; +import { isTraceFilterEnum, ParsedUrl } from '../util'; + +export const parseFilterExclude = ( + query: string, + stateFilterExclude: TraceReducer['isFilterExclude'], +): ParsedUrl => { + const currentFilter = new Map(); + + const url = new URLSearchParams(query); + + const isPresent = url.get('isFilterExclude'); + + if (isPresent) { + try { + const parsedValue = JSON.parse(isPresent); + + if (typeof parsedValue === 'object') { + Object.keys(parsedValue).forEach((key) => { + if (isTraceFilterEnum(key)) { + const keyValue = parsedValue[key]; + if (typeof keyValue === 'boolean') { + currentFilter.set(key, keyValue); + } + } + }); + } + } catch (error) { + // parsing the value + } + } + + if (isPresent) { + return { + currentValue: currentFilter, + urlValue: currentFilter, + }; + } + + return { + currentValue: stateFilterExclude, + urlValue: currentFilter, + }; +}; diff --git a/frontend/src/store/actions/trace/parseFilter/minMaxTime.ts b/frontend/src/store/actions/trace/parseFilter/minMaxTime.ts new file mode 100644 index 0000000000..9953fb18c6 --- /dev/null +++ b/frontend/src/store/actions/trace/parseFilter/minMaxTime.ts @@ -0,0 +1,20 @@ +import { GlobalTime } from 'types/actions/globalTime'; + +export const parseMinMaxTime = (query: string): GlobalTime => { + const url = new URLSearchParams(query); + let maxTime = 0; + let minTime = 0; + + const urlMaxTime = url.get('minTime'); + const urlMinTime = url.get('maxTime'); + + if (urlMaxTime && urlMinTime) { + maxTime = parseInt(urlMaxTime); + minTime = parseInt(urlMinTime); + } + + return { + maxTime, + minTime, + }; +}; diff --git a/frontend/src/store/actions/trace/parseFilter/selectedFilter.ts b/frontend/src/store/actions/trace/parseFilter/selectedFilter.ts new file mode 100644 index 0000000000..973e4ba331 --- /dev/null +++ b/frontend/src/store/actions/trace/parseFilter/selectedFilter.ts @@ -0,0 +1,43 @@ +import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace'; +import { isTraceFilterEnum, ParsedUrl } from '../util'; + +export const parseSelectedFilter = ( + query: string, + selectedFilter: TraceReducer['selectedFilter'], + isUserSelected = false, +): ParsedUrl> => { + const url = new URLSearchParams(query); + + const filters = new Map(); + + const title = isUserSelected ? 'selected' : 'userSelectedFilter'; + + const selected = url.get(title); + + if (selected) { + try { + const parsedValue = JSON.parse(decodeURIComponent(selected)); + if (typeof parsedValue === 'object') { + Object.keys(parsedValue).forEach((e) => { + if (isTraceFilterEnum(e)) { + filters.set(e, parsedValue[e]); + } + }); + } + } catch (error) { + // if the parsing error happens + } + } + + if (selected) { + return { + urlValue: filters, + currentValue: filters, + }; + } + + return { + urlValue: filters, + currentValue: selectedFilter, + }; +}; diff --git a/frontend/src/store/actions/trace/parseFilter/selectedTags.ts b/frontend/src/store/actions/trace/parseFilter/selectedTags.ts new file mode 100644 index 0000000000..a98cbda921 --- /dev/null +++ b/frontend/src/store/actions/trace/parseFilter/selectedTags.ts @@ -0,0 +1,37 @@ +import { TraceReducer } from 'types/reducer/trace'; +import { ParsedUrl } from '../util'; + +export const parseQueryIntoSelectedTags = ( + query: string, + stateSelectedTags: TraceReducer['selectedTags'], +): ParsedUrl => { + const url = new URLSearchParams(query); + + let selectedTags: TraceReducer['selectedTags'] = []; + + const querySelectedTags = url.get('selectedTags'); + + if (querySelectedTags) { + try { + const parsedQuerySelectedTags = JSON.parse(querySelectedTags); + + if (Array.isArray(parsedQuerySelectedTags)) { + selectedTags = parsedQuerySelectedTags; + } + } catch (error) { + //error while parsing + } + } + + if (querySelectedTags) { + return { + currentValue: selectedTags, + urlValue: selectedTags, + }; + } + + return { + currentValue: stateSelectedTags, + urlValue: selectedTags, + }; +}; diff --git a/frontend/src/store/actions/trace/parseFilter/skippedSelected.ts b/frontend/src/store/actions/trace/parseFilter/skippedSelected.ts new file mode 100644 index 0000000000..fd4b110ba9 --- /dev/null +++ b/frontend/src/store/actions/trace/parseFilter/skippedSelected.ts @@ -0,0 +1,33 @@ +import { ParsedUrl } from '../util'; + +export const parseIsSkippedSelection = (query: string): ParsedUrl => { + const url = new URLSearchParams(query); + + let current = false; + + const isSkippedSelected = url.get('isSelectedFilterSkipped'); + + if (isSkippedSelected) { + try { + const parsedValue = JSON.parse(isSkippedSelected); + + if (typeof parsedValue === 'boolean') { + current = parsedValue; + } + } catch (error) { + current = false; + } + } + + if (isSkippedSelected) { + return { + currentValue: current, + urlValue: current, + }; + } + + return { + currentValue: current, + urlValue: current, + }; +}; diff --git a/frontend/src/store/actions/trace/resetTraceDetails.ts b/frontend/src/store/actions/trace/resetTraceDetails.ts deleted file mode 100644 index 80b3e63bc9..0000000000 --- a/frontend/src/store/actions/trace/resetTraceDetails.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Dispatch } from 'redux'; -import AppActions from 'types/actions'; - -export const ResetRaceData = (): ((dispatch: Dispatch) => void) => { - return (dispatch: Dispatch): void => { - dispatch({ - type: 'RESET_TRACE_DATA', - }); - }; -}; diff --git a/frontend/src/store/actions/trace/selectTraceFilter.ts b/frontend/src/store/actions/trace/selectTraceFilter.ts new file mode 100644 index 0000000000..b18443ad3e --- /dev/null +++ b/frontend/src/store/actions/trace/selectTraceFilter.ts @@ -0,0 +1,50 @@ +import { Dispatch, Store } from 'redux'; +import { AppState } from 'store/reducers'; +import AppActions from 'types/actions'; +import { TraceFilterEnum } from 'types/reducer/trace'; +import { updateURL } from './util'; + +export const SelectedTraceFilter = (props: { + topic: TraceFilterEnum; + value: string; +}): (( + dispatch: Dispatch, + getState: Store['getState'], +) => void) => { + return (_, getState): void => { + const { topic, value } = props; + const { traces } = getState(); + + const filter = traces.selectedFilter; + + const isTopicPresent = filter.get(topic); + + // append the value + if (!isTopicPresent) { + filter.set(props.topic, [props.value]); + } else { + const isValuePresent = + isTopicPresent.find((e) => e === props.value) !== undefined; + + // check the value if present then remove the value + if (isValuePresent) { + filter.set( + props.topic, + isTopicPresent.filter((e) => e !== value), + ); + } else { + // if not present add into the array of string + filter.set(props.topic, [...isTopicPresent, props.value]); + } + } + + updateURL( + filter, + traces.filterToFetchData, + traces.spansAggregate.currentPage, + traces.selectedTags, + traces.filter, + traces.isFilterExclude, + ); + }; +}; diff --git a/frontend/src/store/actions/trace/updateIsTagsError.ts b/frontend/src/store/actions/trace/updateIsTagsError.ts new file mode 100644 index 0000000000..be7b0b5f89 --- /dev/null +++ b/frontend/src/store/actions/trace/updateIsTagsError.ts @@ -0,0 +1,17 @@ +import { Dispatch } from 'redux'; +import AppActions from 'types/actions'; +import { TraceReducer } from 'types/reducer/trace'; +import { UPDATE_IS_TAG_ERROR } from 'types/actions/trace'; + +export const UpdateTagIsError = ( + isTagModalError: TraceReducer['isTagModalError'], +): ((dispatch: Dispatch) => void) => { + return (dispatch): void => { + dispatch({ + type: UPDATE_IS_TAG_ERROR, + payload: { + isTagModalError, + }, + }); + }; +}; diff --git a/frontend/src/store/actions/trace/updateSelectedAggOption.ts b/frontend/src/store/actions/trace/updateSelectedAggOption.ts deleted file mode 100644 index b5004e6a2b..0000000000 --- a/frontend/src/store/actions/trace/updateSelectedAggOption.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Dispatch } from 'redux'; -import AppActions from 'types/actions'; -import { TraceReducer } from 'types/reducer/trace'; - -export const UpdateSelectedAggOption = ( - selectedAggOption: TraceReducer['selectedAggOption'], -): ((dispatch: Dispatch) => void) => { - return (dispatch: Dispatch): void => { - dispatch({ - type: 'UPDATE_SELECTED_AGG_OPTION', - payload: { - selectedAggOption, - }, - }); - }; -}; diff --git a/frontend/src/store/actions/trace/updateSelectedData.ts b/frontend/src/store/actions/trace/updateSelectedData.ts deleted file mode 100644 index d9371daa4f..0000000000 --- a/frontend/src/store/actions/trace/updateSelectedData.ts +++ /dev/null @@ -1,164 +0,0 @@ -import getServiceOperation from 'api/trace/getServiceOperation'; -import getSpan from 'api/trace/getSpan'; -import getSpansAggregate from 'api/trace/getSpanAggregate'; -import getTags from 'api/trace/getTags'; -import { AxiosError } from 'axios'; -import { Dispatch } from 'redux'; -import store from 'store'; -import AppActions from 'types/actions'; -import { ErrorResponse, SuccessResponse } from 'types/api'; -import { PayloadProps as ServiceOperationPayloadProps } from 'types/api/trace/getServiceOperation'; -import { PayloadProps as TagPayloadProps } from 'types/api/trace/getTags'; -import { TraceReducer } from 'types/reducer/trace'; - -export const UpdateSelectedData = ({ - selectedKind, - selectedService, - selectedLatency, - selectedOperation, - selectedAggOption, - selectedEntity, -}: UpdateSelectedDataProps): ((dispatch: Dispatch) => void) => { - return async (dispatch: Dispatch): Promise => { - try { - dispatch({ - type: 'UPDATE_SPANS_LOADING', - payload: { - loading: true, - }, - }); - const { trace, globalTime } = store.getState(); - const { minTime, maxTime, selectedTime } = globalTime; - - const { selectedTags } = trace; - - const isCustomSelected = selectedTime === 'custom'; - - const end = isCustomSelected - ? globalTime.maxTime + 15 * 60 * 1000000000 - : maxTime; - - const start = isCustomSelected - ? globalTime.minTime - 15 * 60 * 1000000000 - : minTime; - - const [spanResponse, getSpanAggregateResponse] = await Promise.all([ - getSpan({ - start, - end, - kind: selectedKind || '', - limit: '100', - lookback: '2d', - maxDuration: selectedLatency.max || '', - minDuration: selectedLatency.min || '', - operation: selectedOperation || '', - service: selectedService || '', - tags: JSON.stringify(selectedTags), - }), - getSpansAggregate({ - aggregation_option: selectedAggOption || '', - dimension: selectedEntity || '', - end, - kind: selectedKind || '', - maxDuration: selectedLatency.max || '', - minDuration: selectedLatency.min || '', - operation: selectedOperation || '', - service: selectedService || '', - start, - step: '60', - tags: JSON.stringify(selectedTags), - }), - ]); - - let tagResponse: - | SuccessResponse - | ErrorResponse - | undefined; - - let serviceOperationResponse: - | SuccessResponse - | ErrorResponse - | undefined; - - if (selectedService !== null && selectedService.length !== 0) { - [tagResponse, serviceOperationResponse] = await Promise.all([ - getTags({ - service: selectedService, - }), - getServiceOperation({ - service: selectedService, - }), - ]); - } - - const spanAggregateCondition = - getSpanAggregateResponse.statusCode === 200 || - getSpanAggregateResponse.statusCode === 400; - - const getCondition = (): boolean => { - const basicCondition = - spanResponse.statusCode === 200 && spanAggregateCondition; - - if (selectedService === null || selectedService.length === 0) { - return basicCondition; - } - - return ( - basicCondition && - tagResponse?.statusCode === 200 && - serviceOperationResponse?.statusCode === 200 - ); - }; - - const condition = getCondition(); - - if (condition) { - dispatch({ - type: 'UPDATE_SELECTED_TRACE_DATA', - payload: { - operationList: serviceOperationResponse?.payload || [], - spansList: spanResponse.payload || [], - tagsSuggestions: tagResponse?.payload || [], - selectedKind, - selectedService, - selectedLatency, - selectedOperation, - spansAggregate: spanAggregateCondition - ? getSpanAggregateResponse.payload || [] - : [], - }, - }); - } else { - dispatch({ - type: 'GET_TRACE_INITIAL_DATA_ERROR', - payload: { - errorMessage: 'Something went wrong', - }, - }); - } - - dispatch({ - type: 'UPDATE_SPANS_LOADING', - payload: { - loading: false, - }, - }); - } catch (error) { - dispatch({ - type: 'GET_TRACE_INITIAL_DATA_ERROR', - payload: { - errorMessage: (error as AxiosError).toString() || 'Something went wrong', - }, - }); - } - }; -}; - -export interface UpdateSelectedDataProps { - selectedKind: TraceReducer['selectedKind']; - selectedService: TraceReducer['selectedService']; - selectedLatency: TraceReducer['selectedLatency']; - selectedOperation: TraceReducer['selectedOperation']; - selectedEntity: TraceReducer['selectedEntity']; - selectedAggOption: TraceReducer['selectedAggOption']; -} diff --git a/frontend/src/store/actions/trace/updateSelectedEntity.ts b/frontend/src/store/actions/trace/updateSelectedEntity.ts deleted file mode 100644 index 95b10baa6e..0000000000 --- a/frontend/src/store/actions/trace/updateSelectedEntity.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Dispatch } from 'redux'; -import AppActions from 'types/actions'; -import { TraceReducer } from 'types/reducer/trace'; - -export const UpdateSelectedEntity = ( - selectedEntity: TraceReducer['selectedEntity'], -): ((dispatch: Dispatch) => void) => { - return (dispatch: Dispatch): void => { - dispatch({ - type: 'UPDATE_SELECTED_ENTITY', - payload: { - selectedEntity, - }, - }); - }; -}; diff --git a/frontend/src/store/actions/trace/updateSelectedKind.ts b/frontend/src/store/actions/trace/updateSelectedKind.ts deleted file mode 100644 index 5954599926..0000000000 --- a/frontend/src/store/actions/trace/updateSelectedKind.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Dispatch } from 'redux'; -import AppActions from 'types/actions'; -import { TraceReducer } from 'types/reducer/trace'; - -export const UpdateSelectedKind = ( - selectedKind: TraceReducer['selectedKind'], -): ((dispatch: Dispatch) => void) => { - return (dispatch: Dispatch): void => { - dispatch({ - type: 'UPDATE_TRACE_SELECTED_KIND', - payload: { - selectedKind, - }, - }); - }; -}; diff --git a/frontend/src/store/actions/trace/updateSelectedLatency.ts b/frontend/src/store/actions/trace/updateSelectedLatency.ts deleted file mode 100644 index 636b2e84da..0000000000 --- a/frontend/src/store/actions/trace/updateSelectedLatency.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Dispatch } from 'redux'; -import AppActions from 'types/actions'; -import { TraceReducer } from 'types/reducer/trace'; - -export const UpdateSelectedLatency = ( - selectedLatency: TraceReducer['selectedLatency'], -): ((dispatch: Dispatch) => void) => { - return (dispatch: Dispatch): void => { - dispatch({ - type: 'UPDATE_TRACE_SELECTED_LATENCY_VALUE', - payload: { - selectedLatency, - }, - }); - }; -}; diff --git a/frontend/src/store/actions/trace/updateSelectedOperation.ts b/frontend/src/store/actions/trace/updateSelectedOperation.ts deleted file mode 100644 index e8e1a89f9c..0000000000 --- a/frontend/src/store/actions/trace/updateSelectedOperation.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Dispatch } from 'redux'; -import AppActions from 'types/actions'; -import { TraceReducer } from 'types/reducer/trace'; - -export const UpdateSelectedOperation = ( - selectedOperation: TraceReducer['selectedOperation'], -): ((dispatch: Dispatch) => void) => { - return (dispatch: Dispatch): void => { - dispatch({ - type: 'UPDATE_TRACE_SELECTED_OPERATION', - payload: { - selectedOperation, - }, - }); - }; -}; diff --git a/frontend/src/store/actions/trace/updateSelectedService.ts b/frontend/src/store/actions/trace/updateSelectedService.ts deleted file mode 100644 index c936dfdc58..0000000000 --- a/frontend/src/store/actions/trace/updateSelectedService.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Dispatch } from 'redux'; -import AppActions from 'types/actions'; -import { TraceReducer } from 'types/reducer/trace'; - -export const UpdateSelectedService = ( - selectedService: TraceReducer['selectedService'], -): ((dispatch: Dispatch) => void) => { - return (dispatch: Dispatch): void => { - dispatch({ - type: 'UPDATE_TRACE_SELECTED_SERVICE', - payload: { - selectedService, - }, - }); - }; -}; diff --git a/frontend/src/store/actions/trace/updateSelectedTags.ts b/frontend/src/store/actions/trace/updateSelectedTags.ts deleted file mode 100644 index 1f23c54fac..0000000000 --- a/frontend/src/store/actions/trace/updateSelectedTags.ts +++ /dev/null @@ -1,94 +0,0 @@ -import getSpan from 'api/trace/getSpan'; -import getSpansAggregate from 'api/trace/getSpanAggregate'; -import { AxiosError } from 'axios'; -import { Dispatch } from 'redux'; -import store from 'store'; -import AppActions from 'types/actions'; -import { TraceReducer } from 'types/reducer/trace'; - -export const UpdateSelectedTags = ( - selectedTags: TraceReducer['selectedTags'], -): ((dispatch: Dispatch) => void) => { - return async (dispatch: Dispatch): Promise => { - try { - dispatch({ - type: 'UPDATE_SPANS_LOADING', - payload: { - loading: true, - }, - }); - - const { trace, globalTime } = store.getState(); - const { - selectedKind, - selectedLatency, - selectedOperation, - selectedService, - selectedAggOption, - selectedEntity, - spansAggregate, - } = trace; - - const { maxTime, minTime } = globalTime; - - const [spanResponse, spansAggregateResponse] = await Promise.all([ - getSpan({ - start: minTime, - end: maxTime, - kind: selectedKind || '', - limit: '100', - lookback: '2d', - maxDuration: selectedLatency.max || '', - minDuration: selectedLatency.min || '', - operation: selectedOperation || '', - service: selectedService || '', - tags: JSON.stringify(selectedTags), - }), - getSpansAggregate({ - aggregation_option: selectedAggOption, - dimension: selectedEntity, - end: maxTime, - kind: selectedKind || '', - maxDuration: selectedLatency.max || '', - minDuration: selectedLatency.min || '', - operation: selectedOperation || '', - service: selectedService || '', - start: minTime, - step: '60', - tags: JSON.stringify(selectedTags), - }), - ]); - - const condition = - spansAggregateResponse.statusCode === 200 || - spansAggregateResponse.statusCode === 400; - - if (spanResponse.statusCode === 200 && condition) { - dispatch({ - type: 'UPDATE_TRACE_SELECTED_TAGS', - payload: { - selectedTags, - spansList: spanResponse.payload, - spansAggregate: - spansAggregateResponse.statusCode === 400 - ? spansAggregate - : spansAggregateResponse.payload || [], - }, - }); - } - dispatch({ - type: 'UPDATE_SPANS_LOADING', - payload: { - loading: false, - }, - }); - } catch (error) { - dispatch({ - type: 'GET_TRACE_INITIAL_DATA_ERROR', - payload: { - errorMessage: (error as AxiosError).toString() || 'Something went wrong', - }, - }); - } - }; -}; diff --git a/frontend/src/store/actions/trace/updateSpanLoading.ts b/frontend/src/store/actions/trace/updateSpanLoading.ts deleted file mode 100644 index d5f87764ab..0000000000 --- a/frontend/src/store/actions/trace/updateSpanLoading.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Dispatch } from 'redux'; -import AppActions from 'types/actions'; -import { TraceReducer } from 'types/reducer/trace'; - -export const UpdateSpanLoading = ( - spansLoading: TraceReducer['spansLoading'], -): ((dispatch: Dispatch) => void) => { - return (dispatch: Dispatch): void => { - dispatch({ - type: 'UPDATE_SPANS_LOADING', - payload: { - loading: spansLoading, - }, - }); - }; -}; diff --git a/frontend/src/store/actions/trace/updateTagPanelVisiblity.ts b/frontend/src/store/actions/trace/updateTagPanelVisiblity.ts new file mode 100644 index 0000000000..0ff8321670 --- /dev/null +++ b/frontend/src/store/actions/trace/updateTagPanelVisiblity.ts @@ -0,0 +1,17 @@ +import { Dispatch } from 'redux'; +import AppActions from 'types/actions'; +import { TraceReducer } from 'types/reducer/trace'; +import { UPDATE_TAG_MODAL_VISIBLITY } from 'types/actions/trace'; + +export const UpdateTagVisiblity = ( + isTagModalOpen: TraceReducer['isTagModalOpen'], +): ((dispatch: Dispatch) => void) => { + return (dispatch): void => { + dispatch({ + type: UPDATE_TAG_MODAL_VISIBLITY, + payload: { + isTagModalOpen: isTagModalOpen, + }, + }); + }; +}; diff --git a/frontend/src/store/actions/trace/updateTagsSelected.ts b/frontend/src/store/actions/trace/updateTagsSelected.ts new file mode 100644 index 0000000000..459aaca1a6 --- /dev/null +++ b/frontend/src/store/actions/trace/updateTagsSelected.ts @@ -0,0 +1,17 @@ +import { Dispatch } from 'redux'; +import AppActions from 'types/actions'; +import { TraceReducer } from 'types/reducer/trace'; +import { UPDATE_SELECTED_TAGS } from 'types/actions/trace'; + +export const UpdateSelectedTags = ( + selectedTags: TraceReducer['selectedTags'], +): ((dispatch: Dispatch) => void) => { + return (dispatch): void => { + dispatch({ + type: UPDATE_SELECTED_TAGS, + payload: { + selectedTags: selectedTags, + }, + }); + }; +}; diff --git a/frontend/src/store/actions/trace/util.ts b/frontend/src/store/actions/trace/util.ts new file mode 100644 index 0000000000..8b3cab27b0 --- /dev/null +++ b/frontend/src/store/actions/trace/util.ts @@ -0,0 +1,79 @@ +import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace'; +import history from 'lib/history'; +import { AllTraceFilterEnum } from 'container/Trace/Filters'; +import { PayloadProps as GetFilterPayload } from 'types/api/trace/getFilters'; +export * from './parseFilter'; +export interface ParsedUrl { + currentValue: T; + urlValue: T; +} + +export function isTraceFilterEnum( + value: TraceFilterEnum | string, +): value is TraceFilterEnum { + if (AllTraceFilterEnum.find((enums) => enums === value)) { + return true; + } + return false; +} + +export const updateURL = ( + selectedFilter: TraceReducer['selectedFilter'], + filterToFetchData: TraceReducer['filterToFetchData'], + current: TraceReducer['spansAggregate']['total'], + selectedTags: TraceReducer['selectedTags'], + filter: TraceReducer['filter'], + isFilterExclude: TraceReducer['isFilterExclude'], + userSelectedFilter: TraceReducer['userSelectedFilter'], +) => { + const search = new URLSearchParams(location.search); + const preResult: { key: string; value: string }[] = []; + + const keyToSkip = [ + 'selected', + 'filterToFetchData', + 'current', + 'selectedTags', + 'filter', + 'isFilterExclude', + 'userSelectedFilter', + ]; + + search.forEach((value, key) => { + if (!keyToSkip.includes(key)) { + preResult.push({ + key, + value, + }); + } + }); + + history.replace( + `${history.location.pathname}?selected=${JSON.stringify( + Object.fromEntries(selectedFilter), + )}&filterToFetchData=${JSON.stringify( + filterToFetchData, + )}¤t=${current}&selectedTags=${JSON.stringify( + selectedTags, + )}&filter=${JSON.stringify(Object.fromEntries(filter))}&${preResult + .map((e) => `${e.key}=${e.value}`) + .join('&')}&isFilterExclude=${JSON.stringify( + Object.fromEntries(isFilterExclude), + )}&userSelectedFilter=${JSON.stringify( + Object.fromEntries(userSelectedFilter), + )}`, + ); +}; + +export const getFilter = (data: GetFilterPayload): TraceReducer['filter'] => { + const filter = new Map>(); + + Object.keys(data).forEach((key) => { + const value = data[key]; + if (isTraceFilterEnum(key)) { + filter.set(key, value); + } + }); + + return filter; +}; diff --git a/frontend/src/store/reducers/index.ts b/frontend/src/store/reducers/index.ts index 4eba388f72..9c2bdecb1f 100644 --- a/frontend/src/store/reducers/index.ts +++ b/frontend/src/store/reducers/index.ts @@ -5,16 +5,15 @@ import dashboardReducer from './dashboard'; import globalTimeReducer from './global'; import metricsReducers from './metric'; import { ServiceMapReducer } from './serviceMap'; -import { traceReducer } from './trace'; +import traceReducer from './trace'; import TraceFilterReducer from './traceFilters'; -import { traceItemReducer, tracesReducer } from './traces'; +import { traceItemReducer } from './traces'; import { usageDataReducer } from './usage'; const reducers = combineReducers({ traceFilters: TraceFilterReducer, - traces: tracesReducer, + traces: traceReducer, traceItem: traceItemReducer, - trace: traceReducer, usageDate: usageDataReducer, globalTime: globalTimeReducer, serviceMap: ServiceMapReducer, diff --git a/frontend/src/store/reducers/trace.ts b/frontend/src/store/reducers/trace.ts index ae9e27e343..cbccb6e7af 100644 --- a/frontend/src/store/reducers/trace.ts +++ b/frontend/src/store/reducers/trace.ts @@ -1,200 +1,199 @@ import { - GET_TRACE_INITIAL_DATA_ERROR, - GET_TRACE_INITIAL_DATA_SUCCESS, - GET_TRACE_LOADING_END, - GET_TRACE_LOADING_START, - RESET_TRACE_DATA, + SELECT_TRACE_FILTER, TraceActions, - UPDATE_AGGREGATES, - UPDATE_SELECTED_AGG_OPTION, - UPDATE_SELECTED_ENTITY, - UPDATE_SELECTED_TRACE_DATA, - UPDATE_SPANS_LOADING, - UPDATE_TRACE_SELECTED_KIND, - UPDATE_TRACE_SELECTED_LATENCY_VALUE, - UPDATE_TRACE_SELECTED_OPERATION, - UPDATE_TRACE_SELECTED_SERVICE, - UPDATE_TRACE_SELECTED_TAGS, + UPDATE_TRACE_FILTER, + UPDATE_TRACE_FILTER_LOADING, + UPDATE_ALL_FILTERS, + UPDATE_SELECTED_TAGS, + UPDATE_SPANS_AGGREEGATE, + UPDATE_TAG_MODAL_VISIBLITY, + UPDATE_IS_TAG_ERROR, + UPDATE_SELECTED_FUNCTION, + UPDATE_SELECTED_GROUP_BY, + UPDATE_TRACE_GRAPH_LOADING, + UPDATE_TRACE_GRAPH_ERROR, + UPDATE_TRACE_GRAPH_SUCCESS, + RESET_TRACE_FILTER, + UPDATE_FILTER_RESPONSE_SELECTED, + UPDATE_FILTER_EXCLUDE, } from 'types/actions/trace'; -import { TraceReducer } from 'types/reducer/trace'; +import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace'; -const intitalState: TraceReducer = { - error: false, - errorMessage: '', - loading: true, - operationsList: [], - selectedKind: '', - selectedLatency: { - max: '', - min: '', - }, - selectedOperation: '', - selectedService: '', +const initialValue: TraceReducer = { + filter: new Map(), + filterToFetchData: ['duration', 'status', 'serviceName'], + filterLoading: true, + filterResponseSelected: new Set(), + selectedFilter: new Map(), selectedTags: [], - serviceList: [], - spanList: [], - tagsSuggestions: [], - selectedAggOption: 'count', - selectedEntity: 'calls', - spansAggregate: [], - spansLoading: false, + isTagModalOpen: false, + isTagModalError: false, + isFilterExclude: new Map([]), + userSelectedFilter: new Map(), + spansAggregate: { + currentPage: 1, + loading: false, + data: [], + error: false, + total: 0, + pageSize: 10, + }, + selectedGroupBy: '', + selectedFunction: 'count', + spansGraph: { + error: false, + errorMessage: '', + loading: true, + payload: { items: {} }, + }, }; -export const traceReducer = ( - state = intitalState, +const traceReducer = ( + state = initialValue, action: TraceActions, ): TraceReducer => { switch (action.type) { - case GET_TRACE_INITIAL_DATA_ERROR: { + case UPDATE_TRACE_FILTER: { return { ...state, - errorMessage: action.payload.errorMessage, - loading: false, - error: true, + filter: action.payload.filter, }; } - case GET_TRACE_LOADING_START: { - return { - ...state, - loading: true, - spansLoading: true, - }; - } - - case GET_TRACE_INITIAL_DATA_SUCCESS: { + case UPDATE_ALL_FILTERS: { + const { payload } = action; const { - serviceList, - operationList, - tagsSuggestions, - selectedOperation, - selectedService, + filter, + filterToFetchData, + selectedFilter, + current, selectedTags, - spansList, - selectedKind, - selectedLatency, - spansAggregate, - } = action.payload; + userSelected, + isFilterExclude, + } = payload; return { ...state, - serviceList: serviceList, - tagsSuggestions, - selectedOperation, - selectedService, + filter, + filterToFetchData, + selectedFilter, selectedTags, - spanList: spansList, - operationsList: operationList, - error: false, - selectedKind, - selectedLatency, - spansAggregate, - spansLoading: false, + userSelectedFilter: userSelected, + isFilterExclude, + spansAggregate: { + ...state.spansAggregate, + currentPage: current, + }, }; } - case UPDATE_TRACE_SELECTED_KIND: { + case UPDATE_TRACE_FILTER_LOADING: { return { ...state, - selectedKind: action.payload.selectedKind, + filterLoading: action.payload.filterLoading, }; } - case UPDATE_TRACE_SELECTED_LATENCY_VALUE: { + case SELECT_TRACE_FILTER: { return { ...state, - selectedLatency: action.payload.selectedLatency, + selectedFilter: action.payload.selectedFilter, }; } - case UPDATE_TRACE_SELECTED_OPERATION: { + case RESET_TRACE_FILTER: { return { - ...state, - selectedOperation: action.payload.selectedOperation, + ...initialValue, }; } - case UPDATE_TRACE_SELECTED_SERVICE: { - return { - ...state, - selectedService: action.payload.selectedService, - }; - } - - case UPDATE_TRACE_SELECTED_TAGS: { + case UPDATE_SELECTED_TAGS: { return { ...state, selectedTags: action.payload.selectedTags, - spanList: action.payload.spansList, - spansAggregate: action.payload.spansAggregate, }; } - case UPDATE_SELECTED_TRACE_DATA: { - const { - spansList, - tagsSuggestions, - operationList, - selectedOperation, - selectedLatency, - selectedService, - selectedKind, - spansAggregate, - } = action.payload; - - return { - ...state, - spanList: spansList, - tagsSuggestions, - operationsList: operationList, - selectedOperation, - selectedLatency, - selectedService, - selectedKind, - spansAggregate, - }; - } - - case GET_TRACE_LOADING_END: { - return { - ...state, - loading: false, - }; - } - - case UPDATE_SELECTED_AGG_OPTION: { - return { - ...state, - selectedAggOption: action.payload.selectedAggOption, - }; - } - - case UPDATE_SELECTED_ENTITY: { - return { - ...state, - selectedEntity: action.payload.selectedEntity, - }; - } - - case UPDATE_SPANS_LOADING: { - return { - ...state, - spansLoading: action.payload.loading, - }; - } - - case RESET_TRACE_DATA: { - return { - ...intitalState, - }; - } - - case UPDATE_AGGREGATES: { + case UPDATE_SPANS_AGGREEGATE: { return { ...state, spansAggregate: action.payload.spansAggregate, - selectedAggOption: action.payload.selectedAggOption, - selectedEntity: action.payload.selectedEntity, + }; + } + + case UPDATE_TAG_MODAL_VISIBLITY: { + return { + ...state, + isTagModalOpen: action.payload.isTagModalOpen, + }; + } + + case UPDATE_IS_TAG_ERROR: { + return { + ...state, + isTagModalError: action.payload.isTagModalError, + }; + } + + case UPDATE_SELECTED_FUNCTION: { + return { + ...state, + selectedFunction: action.payload.selectedFunction, + }; + } + + case UPDATE_SELECTED_GROUP_BY: { + return { + ...state, + selectedGroupBy: action.payload.selectedGroupBy, + }; + } + + case UPDATE_TRACE_GRAPH_LOADING: { + return { + ...state, + spansGraph: { + ...state.spansGraph, + loading: action.payload.loading, + }, + }; + } + + case UPDATE_TRACE_GRAPH_ERROR: { + return { + ...state, + spansGraph: { + ...state.spansGraph, + error: action.payload.error, + errorMessage: action.payload.errorMessage, + loading: false, + }, + }; + } + + case UPDATE_TRACE_GRAPH_SUCCESS: { + return { + ...state, + spansGraph: { + ...state.spansGraph, + payload: action.payload.data, + loading: false, + error: false, + }, + }; + } + + case UPDATE_FILTER_RESPONSE_SELECTED: { + return { + ...state, + filterResponseSelected: action.payload.filterResponseSelected, + }; + } + + case UPDATE_FILTER_EXCLUDE: { + return { + ...state, + isFilterExclude: action.payload.isFilterExclude, }; } @@ -202,3 +201,5 @@ export const traceReducer = ( return state; } }; + +export default traceReducer; diff --git a/frontend/src/types/actions/trace.ts b/frontend/src/types/actions/trace.ts index 18a4115b7c..a31a676a93 100644 --- a/frontend/src/types/actions/trace.ts +++ b/frontend/src/types/actions/trace.ts @@ -1,151 +1,172 @@ -export const GET_TRACE_INITIAL_DATA_SUCCESS = 'GET_TRACE_INITIAL_DATA_SUCCESS'; -export const GET_TRACE_INITIAL_DATA_ERROR = 'GET_TRACE_INITIAL_DATA_ERROR'; -export const GET_TRACE_LOADING_START = 'GET_TRACE_LOADING_START'; -export const GET_TRACE_LOADING_END = 'GET_TRACE_LOADING_END'; - -export const UPDATE_TRACE_SELECTED_SERVICE = 'UPDATE_TRACE_SELECTED_SERVICE'; -export const UPDATE_TRACE_SELECTED_OPERATION = - 'UPDATE_TRACE_SELECTED_OPERATION'; -export const UPDATE_TRACE_SELECTED_LATENCY_VALUE = - 'UPDATE_TRACE_SELECTED_LATENCY_VALUE'; -export const UPDATE_TRACE_SELECTED_KIND = 'UPDATE_TRACE_SELECTED_KIND'; -export const UPDATE_TRACE_SELECTED_TAGS = 'UPDATE_TRACE_SELECTED_TAGS'; - -export const UPDATE_SELECTED_AGG_OPTION = 'UPDATE_SELECTED_AGG_OPTION'; -export const UPDATE_SELECTED_ENTITY = 'UPDATE_SELECTED_ENTITY'; -export const UPDATE_SPANS_LOADING = 'UPDATE_SPANS_LOADING'; - -export const UPDATE_SELECTED_TRACE_DATA = 'UPDATE_SELECTED_TRACE_DATA'; -export const UPDATE_AGGREGATES = 'UPDATE_AGGREGATES'; - -export const RESET_TRACE_DATA = 'RESET_TRACE_DATA'; - import { TraceReducer } from 'types/reducer/trace'; -interface GetTraceLoading { - type: typeof GET_TRACE_LOADING_START | typeof GET_TRACE_LOADING_END; -} +export const UPDATE_TRACE_FILTER = 'UPDATE_TRACE_FILTER'; +export const GET_TRACE_FILTER = 'GET_TRACE_FILTER'; +export const UPDATE_TRACE_FILTER_LOADING = 'UPDATE_TRACE_FILTER_LOADING'; -interface UpdateSpansLoading { - type: typeof UPDATE_SPANS_LOADING; +export const SELECT_TRACE_FILTER = 'SELECT_TRACE_FILTER'; +export const UPDATE_ALL_FILTERS = 'UPDATE_ALL_FILTERS'; +export const UPDATE_SELECTED_TAGS = 'UPDATE_SELECTED_TAGS'; +export const UPDATE_TAG_MODAL_VISIBLITY = 'UPDATE_TAG_MODAL_VISIBLITY'; + +export const UPDATE_SPANS_AGGREEGATE = 'UPDATE_SPANS_AGGREEGATE'; + +export const UPDATE_IS_TAG_ERROR = 'UPDATE_IS_TAG_ERROR'; + +export const UPDATE_SELECTED_FUNCTION = 'UPDATE_SELECTED_FUNCTION'; +export const UPDATE_SELECTED_GROUP_BY = 'UPDATE_SELECTED_GROUP_BY'; + +export const UPDATE_TRACE_GRAPH_LOADING = 'UPDATE_TRACE_GRAPH_LOADING'; +export const UPDATE_TRACE_GRAPH_ERROR = 'UPDATE_TRACE_GRAPH_ERROR'; +export const UPDATE_TRACE_GRAPH_SUCCESS = 'UPDATE_TRACE_GRAPH_SUCCESS'; + +export const RESET_TRACE_FILTER = 'RESET_TRACE_FILTER'; +export const UPDATE_FILTER_RESPONSE_SELECTED = + 'UPDATE_FILTER_RESPONSE_SELECTED'; +export const UPDATE_FILTER_EXCLUDE = 'UPDATE_FILTER_EXCLUDE'; + +export interface UpdateFilter { + type: typeof UPDATE_TRACE_FILTER; payload: { - loading: boolean; + filter: TraceReducer['filter']; }; } -interface GetTraceInitialData { - type: typeof GET_TRACE_INITIAL_DATA_SUCCESS; +export interface UpdateSpansAggregate { + type: typeof UPDATE_SPANS_AGGREEGATE; payload: { - serviceList: TraceReducer['serviceList']; - selectedTags: TraceReducer['selectedTags']; - operationList: TraceReducer['operationsList']; - tagsSuggestions: TraceReducer['tagsSuggestions']; - spansList: TraceReducer['spanList']; - selectedService: TraceReducer['selectedService']; - selectedOperation: TraceReducer['selectedOperation']; - selectedLatency: TraceReducer['selectedLatency']; - selectedKind: TraceReducer['selectedKind']; spansAggregate: TraceReducer['spansAggregate']; }; } -interface UpdateSelectedDate { - type: typeof UPDATE_SELECTED_TRACE_DATA; +export interface UpdateTagVisiblity { + type: typeof UPDATE_TAG_MODAL_VISIBLITY; payload: { - operationList: TraceReducer['operationsList']; - tagsSuggestions: TraceReducer['tagsSuggestions']; - spansList: TraceReducer['spanList']; - selectedKind: TraceReducer['selectedKind']; - selectedService: TraceReducer['selectedService']; - selectedLatency: TraceReducer['selectedLatency']; - selectedOperation: TraceReducer['selectedOperation']; - spansAggregate: TraceReducer['spansAggregate']; + isTagModalOpen: TraceReducer['isTagModalOpen']; }; } -export interface GetTraceInitialDataError { - type: typeof GET_TRACE_INITIAL_DATA_ERROR; - payload: { - errorMessage: string; - }; -} - -interface UpdateTraceSelectedService { - type: typeof UPDATE_TRACE_SELECTED_SERVICE; - payload: { - selectedService: TraceReducer['selectedService']; - }; -} - -interface UpdateTraceSelectedOperation { - type: typeof UPDATE_TRACE_SELECTED_OPERATION; - payload: { - selectedOperation: TraceReducer['selectedOperation']; - }; -} - -interface UpdateTraceSelectedKind { - type: typeof UPDATE_TRACE_SELECTED_KIND; - payload: { - selectedKind: TraceReducer['selectedKind']; - }; -} - -interface UpdateTraceSelectedLatencyValue { - type: typeof UPDATE_TRACE_SELECTED_LATENCY_VALUE; - payload: { - selectedLatency: TraceReducer['selectedLatency']; - }; -} - -interface UpdateTraceSelectedTags { - type: typeof UPDATE_TRACE_SELECTED_TAGS; +export interface UpdateSelectedTags { + type: typeof UPDATE_SELECTED_TAGS; payload: { selectedTags: TraceReducer['selectedTags']; - spansList: TraceReducer['spanList']; - spansAggregate: TraceReducer['spansAggregate']; }; } -interface UpdateSelectedAggOption { - type: typeof UPDATE_SELECTED_AGG_OPTION; +export interface UpdateSelected { + type: typeof UPDATE_FILTER_RESPONSE_SELECTED; payload: { - selectedAggOption: TraceReducer['selectedAggOption']; + filterResponseSelected: TraceReducer['filterResponseSelected']; }; } -interface UpdateSelectedEntity { - type: typeof UPDATE_SELECTED_ENTITY; +export interface UpdateAllFilters { + type: typeof UPDATE_ALL_FILTERS; payload: { - selectedEntity: TraceReducer['selectedEntity']; + filter: TraceReducer['filter']; + selectedFilter: TraceReducer['selectedFilter']; + filterToFetchData: TraceReducer['filterToFetchData']; + current: TraceReducer['spansAggregate']['currentPage']; + selectedTags: TraceReducer['selectedTags']; + userSelected: TraceReducer['userSelectedFilter']; + isFilterExclude: TraceReducer['isFilterExclude']; }; } -interface UpdateAggregates { - type: typeof UPDATE_AGGREGATES; +export interface UpdateFilterLoading { + type: typeof UPDATE_TRACE_FILTER_LOADING; payload: { - spansAggregate: TraceReducer['spansAggregate']; - selectedEntity: TraceReducer['selectedEntity']; - selectedAggOption: TraceReducer['selectedAggOption']; + filterLoading: TraceReducer['filterLoading']; }; } -interface ResetTraceData { - type: typeof RESET_TRACE_DATA; +export interface SelectTraceFilter { + type: typeof SELECT_TRACE_FILTER; + payload: { + selectedFilter: TraceReducer['selectedFilter']; + }; +} + +export interface ResetTraceFilter { + type: typeof RESET_TRACE_FILTER; +} + +export interface GetTraceFilter { + type: typeof GET_TRACE_FILTER; + payload: { + filter: TraceReducer['filter']; + }; +} + +export interface UpdateIsTagError { + type: typeof UPDATE_IS_TAG_ERROR; + payload: { + isTagModalError: TraceReducer['isTagModalError']; + }; +} + +export interface UpdateSelectedGroupBy { + type: typeof UPDATE_SELECTED_GROUP_BY; + payload: { + selectedGroupBy: TraceReducer['selectedGroupBy']; + }; +} + +export interface UpdateSelectedFunction { + type: typeof UPDATE_SELECTED_FUNCTION; + payload: { + selectedFunction: TraceReducer['selectedFunction']; + }; +} + +export interface UpdateSpanLoading { + type: typeof UPDATE_TRACE_GRAPH_LOADING; + payload: { + loading: TraceReducer['spansGraph']['loading']; + }; +} + +export interface UpdateSpansError { + type: typeof UPDATE_TRACE_GRAPH_ERROR; + payload: { + error: TraceReducer['spansGraph']['error']; + errorMessage: TraceReducer['spansGraph']['errorMessage']; + }; +} + +export interface UpdateFilterExclude { + type: typeof UPDATE_FILTER_EXCLUDE; + payload: { + isFilterExclude: TraceReducer['isFilterExclude']; + }; +} + +export interface UpdateSpans { + type: typeof UPDATE_TRACE_GRAPH_SUCCESS; + payload: { + data: TraceReducer['spansGraph']['payload']; + }; +} + +export interface ResetTraceFilter { + type: typeof RESET_TRACE_FILTER; } export type TraceActions = - | GetTraceLoading - | GetTraceInitialData - | GetTraceInitialDataError - | UpdateTraceSelectedService - | UpdateTraceSelectedLatencyValue - | UpdateTraceSelectedKind - | UpdateTraceSelectedOperation - | UpdateTraceSelectedTags - | UpdateSelectedDate - | UpdateSelectedAggOption - | UpdateSelectedEntity - | UpdateSpansLoading - | ResetTraceData - | UpdateAggregates; + | UpdateFilter + | GetTraceFilter + | UpdateFilterLoading + | SelectTraceFilter + | UpdateAllFilters + | UpdateSelectedTags + | UpdateTagVisiblity + | UpdateSpansAggregate + | UpdateIsTagError + | UpdateSelectedGroupBy + | UpdateSelectedFunction + | UpdateSpanLoading + | UpdateSpansError + | UpdateSpans + | ResetTraceFilter + | UpdateSelected + | UpdateFilterExclude; diff --git a/frontend/src/types/api/trace/getFilters.ts b/frontend/src/types/api/trace/getFilters.ts new file mode 100644 index 0000000000..422e360831 --- /dev/null +++ b/frontend/src/types/api/trace/getFilters.ts @@ -0,0 +1,15 @@ +import { TraceReducer } from 'types/reducer/trace'; + +export interface Props { + start: string; + end: string; + getFilters: string[]; + other: { + [k: string]: string[]; + }; + isFilterExclude: TraceReducer['isFilterExclude']; +} + +export interface PayloadProps { + [key: string]: Record; +} diff --git a/frontend/src/types/api/trace/getServiceList.ts b/frontend/src/types/api/trace/getServiceList.ts deleted file mode 100644 index b01bb5940f..0000000000 --- a/frontend/src/types/api/trace/getServiceList.ts +++ /dev/null @@ -1 +0,0 @@ -export type PayloadProps = string[]; diff --git a/frontend/src/types/api/trace/getServiceOperation.ts b/frontend/src/types/api/trace/getServiceOperation.ts deleted file mode 100644 index 70460a3658..0000000000 --- a/frontend/src/types/api/trace/getServiceOperation.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type PayloadProps = string[]; - -export interface Props { - service: string; -} diff --git a/frontend/src/types/api/trace/getSpanAggregate.ts b/frontend/src/types/api/trace/getSpanAggregate.ts index f81f58b203..0c2232efa9 100644 --- a/frontend/src/types/api/trace/getSpanAggregate.ts +++ b/frontend/src/types/api/trace/getSpanAggregate.ts @@ -1,20 +1,16 @@ +import { TraceReducer } from 'types/reducer/trace'; + export interface Props { start: number; end: number; - service: string; - operation: string; - maxDuration: string; - minDuration: string; - kind: string; - tags: string; - dimension: string; - aggregation_option: string; - step: string; + selectedFilter: TraceReducer['selectedFilter']; + limit: number; + offset: number; + selectedTags: TraceReducer['selectedTags']; + isFilterExclude: TraceReducer['isFilterExclude']; } -interface Timestamp { - timestamp: number; - value: number; -} - -export type PayloadProps = Timestamp[]; +export type PayloadProps = { + spans: TraceReducer['spansAggregate']['data']; + totalSpans: number; +}; diff --git a/frontend/src/types/api/trace/getSpans.ts b/frontend/src/types/api/trace/getSpans.ts index 508e7abb9a..9143f2a693 100644 --- a/frontend/src/types/api/trace/getSpans.ts +++ b/frontend/src/types/api/trace/getSpans.ts @@ -1,51 +1,22 @@ -import { GlobalTime } from 'types/actions/globalTime'; - -export interface TraceTagItem { - key: string; - value: string; -} - -export interface pushDStree { - id: string; - name: string; - value: number; - time: number; - startTime: number; - tags: TraceTagItem[]; - children: pushDStree[]; -} - -export type span = [ - number, - string, - string, - string, - string, - string, - string, - string | string[], - string | string[], - string | string[], - pushDStree[], -]; - -export interface SpanList { - events: span[]; - segmentID: string; - columns: string[]; -} - -export type PayloadProps = SpanList[]; +import { TraceReducer } from 'types/reducer/trace'; export interface Props { - start: GlobalTime['minTime']; - end: GlobalTime['maxTime']; - lookback: string; - service: string; - operation: string; - maxDuration: string; - minDuration: string; - kind: string; - limit: string; - tags: string; + start: number; + end: number; + function: TraceReducer['selectedFunction']; + step: number; + groupBy: TraceReducer['selectedGroupBy']; + selectedFilter: TraceReducer['selectedFilter']; + selectedTags: TraceReducer['selectedTags']; + isFilterExclude: TraceReducer['isFilterExclude']; +} + +export interface PayloadProps { + items: Record; +} + +interface SpanData { + timestamp: number; + value?: number; + groupBy?: Record; } diff --git a/frontend/src/types/api/trace/getTagFilters.ts b/frontend/src/types/api/trace/getTagFilters.ts new file mode 100644 index 0000000000..31581e98ba --- /dev/null +++ b/frontend/src/types/api/trace/getTagFilters.ts @@ -0,0 +1,13 @@ +export interface Props { + start: number; + end: number; + other: { + [k: string]: string[]; + }; +} + +interface TagsKeys { + tagKeys: string; +} + +export type PayloadProps = TagsKeys[]; diff --git a/frontend/src/types/api/trace/getTags.ts b/frontend/src/types/api/trace/getTags.ts deleted file mode 100644 index 9e6c3a4e2e..0000000000 --- a/frontend/src/types/api/trace/getTags.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Props as Prop } from './getServiceOperation'; - -interface TagKeys { - tagCount: number; - tagKeys: string; -} - -export type PayloadProps = TagKeys[]; - -export type Props = Prop; diff --git a/frontend/src/types/reducer/trace.ts b/frontend/src/types/reducer/trace.ts index 318a3678ba..a77f4a4b87 100644 --- a/frontend/src/types/reducer/trace.ts +++ b/frontend/src/types/reducer/trace.ts @@ -1,37 +1,107 @@ -import { PayloadProps as ServicePayload } from 'types/api/trace/getServiceList'; -import { PayloadProps as OperationsPayload } from 'types/api/trace/getServiceOperation'; -import { PayloadProps as GetSpansAggregatePayload } from 'types/api/trace/getSpanAggregate'; -import { PayloadProps as GetSpansPayloadProps } from 'types/api/trace/getSpans'; -import { PayloadProps as TagsPayload } from 'types/api/trace/getTags'; - -type TagItemOperator = 'equals' | 'contains' | 'regex'; -export interface TagItem { - key: string; - value: string; - operator: TagItemOperator; -} - -export interface LatencyValue { - min: string; - max: string; -} +import { PayloadProps } from 'types/api/trace/getSpans'; export interface TraceReducer { - selectedService: string; - selectedLatency: LatencyValue; - selectedOperation: string; - selectedKind: '' | '2' | '3' | string; - selectedTags: TagItem[]; - tagsSuggestions: TagsPayload; - errorMessage: string; - serviceList: ServicePayload; - spanList: GetSpansPayloadProps; - operationsList: OperationsPayload; - error: boolean; - loading: boolean; - - selectedAggOption: string; - selectedEntity: string; - spansAggregate: GetSpansAggregatePayload; - spansLoading: boolean; + filter: Map>; + filterToFetchData: TraceFilterEnum[]; + filterLoading: boolean; + selectedFilter: Map; + userSelectedFilter: Map; + isFilterExclude: Map; + selectedTags: Tags[]; + isTagModalOpen: boolean; + filterResponseSelected: Set; + isTagModalError: boolean; + spansAggregate: { + loading: boolean; + currentPage: number; + data: SpansAggregateData[]; + error: boolean; + total: number; + pageSize: number; + }; + selectedGroupBy: string; + selectedFunction: string; + spansGraph: { + loading: boolean; + error: boolean; + errorMessage: string; + payload: PayloadProps; + }; } + +interface SpansAggregateData { + timestamp: string; + spanID: string; + traceID: string; + serviceName: string; + operation: string; + durationNano: number; + httpCode: string; + httpMethod: string; +} + +export interface Tags { + Key: string[]; + Operator: OperatorValues; + Values: string[]; +} + +type OperatorValues = 'not in' | 'in'; + +export type TraceFilterEnum = + | 'component' + | 'duration' + | 'httpCode' + | 'httpHost' + | 'httpMethod' + | 'httpRoute' + | 'httpUrl' + | 'operation' + | 'serviceName' + | 'status'; + +export const AllPanelHeading: { + key: TraceFilterEnum; + displayValue: string; +}[] = [ + { + displayValue: 'Component', + key: 'component', + }, + { + key: 'duration', + displayValue: 'Duration', + }, + { + displayValue: 'HTTP Code', + key: 'httpCode', + }, + { + key: 'httpHost', + displayValue: 'HTTP Host', + }, + { + key: 'httpMethod', + displayValue: 'HTTP Method', + }, + { + displayValue: 'HTTP Route', + key: 'httpRoute', + }, + { + key: 'httpUrl', + displayValue: 'HTTP URL', + }, + { + key: 'operation', + displayValue: 'Operation', + }, + { + key: 'serviceName', + displayValue: 'Service Name', + }, + { + key: 'status', + displayValue: 'Status', + }, +]; diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js index 3c2b8488fe..1eb43d9ecf 100644 --- a/frontend/webpack.config.js +++ b/frontend/webpack.config.js @@ -73,6 +73,25 @@ const config = { test: /\.(ttf|eot|woff|woff2)$/, use: ['file-loader'], }, + { + test: /\.less$/i, + use: [ + { + loader: 'style-loader', + }, + { + loader: 'css-loader', + }, + { + loader: 'less-loader', + options: { + lessOptions: { + javascriptEnabled: true, + }, + }, + }, + ], + }, ], }, plugins: plugins, diff --git a/frontend/webpack.config.prod.js b/frontend/webpack.config.prod.js index ffc6579085..026781a76b 100644 --- a/frontend/webpack.config.prod.js +++ b/frontend/webpack.config.prod.js @@ -86,6 +86,25 @@ const config = { test: /\.(ttf|eot|woff|woff2)$/, use: ['file-loader'], }, + { + test: /\.less$/i, + use: [ + { + loader: 'style-loader', + }, + { + loader: 'css-loader', + }, + { + loader: 'less-loader', + options: { + lessOptions: { + javascriptEnabled: true, + }, + }, + }, + ], + }, ], }, plugins: plugins, diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 5911c63739..9bdfc03964 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -4208,6 +4208,13 @@ cookie@0.4.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== +copy-anything@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-2.0.3.tgz#842407ba02466b0df844819bbe3baebbe5d45d87" + integrity sha512-GK6QUtisv4fNS+XcI7shX0Gx9ORg7QqIznyfho79JTnX1XhLiyZHfftvGiziqzRiEi/Bjhgpi+D2o7HxJFPnDQ== + dependencies: + is-what "^3.12.0" + copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" @@ -5130,7 +5137,7 @@ date-fns@2.x: resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.27.0.tgz#e1ff3c3ddbbab8a2eaadbb6106be2929a5a2d92b" integrity sha512-sj+J0Mo2p2X1e306MHq282WS4/A8Pz/95GIFcsPNMPMZVI3EUrAdSv90al1k+p74WGLCruMXk23bfEDZa71X9Q== -dayjs@1.x, dayjs@^1.10.4: +dayjs@1.x, dayjs@^1.10.4, dayjs@^1.10.7: version "1.10.7" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.7.tgz#2cf5f91add28116748440866a0a1d26f3a6ce468" integrity sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig== @@ -5154,7 +5161,7 @@ debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1, d dependencies: ms "2.1.2" -debug@^3.1.0, debug@^3.1.1, debug@^3.2.7: +debug@^3.1.0, debug@^3.1.1, debug@^3.2.6, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== @@ -5540,6 +5547,13 @@ envinfo@^7.7.3: resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== +errno@^0.1.1: + version "0.1.8" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" + integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A== + dependencies: + prr "~1.0.1" + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -6830,7 +6844,7 @@ husky@4.3.8: slash "^3.0.0" which-pm-runs "^1.0.0" -iconv-lite@0.4, iconv-lite@0.4.24: +iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.4: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -6870,6 +6884,11 @@ iltorb@^2.4.3: prebuild-install "^5.3.3" which-pm-runs "^1.0.0" +image-size@~0.5.0: + version "0.5.5" + resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" + integrity sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w= + import-fresh@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" @@ -7284,6 +7303,11 @@ is-weakref@^1.0.1: dependencies: call-bind "^1.0.0" +is-what@^3.12.0: + version "3.14.1" + resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.14.1.tgz#e1222f46ddda85dead0fd1c9df131760e77755c1" + integrity sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA== + is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -7962,6 +7986,11 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== +klona@^2.0.4: + version "2.0.5" + resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.5.tgz#d166574d90076395d9963aa7a928fabb8d76afbc" + integrity sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ== + layout-bmfont-text@^1.2.0: version "1.3.4" resolved "https://registry.yarnpkg.com/layout-bmfont-text/-/layout-bmfont-text-1.3.4.tgz#f20f2c5464774f48da6ce8a997fbce6d46945b81" @@ -7976,6 +8005,13 @@ lazy-ass@^1.6.0: resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" integrity sha1-eZllXoZGwX8In90YfRUNMyTVRRM= +less-loader@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/less-loader/-/less-loader-10.2.0.tgz#97286d8797dc3dc05b1d16b0ecec5f968bdd4e32" + integrity sha512-AV5KHWvCezW27GT90WATaDnfXBv99llDbtaj4bshq6DvAihMdNjaPDcUMa6EXKLRF+P2opFenJp89BXg91XLYg== + dependencies: + klona "^2.0.4" + less-plugin-npm-import@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/less-plugin-npm-import/-/less-plugin-npm-import-2.1.0.tgz#823e6986c93318a98171ca858848b6bead55bf3e" @@ -7984,6 +8020,23 @@ less-plugin-npm-import@^2.1.0: promise "~7.0.1" resolve "~1.1.6" +less@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/less/-/less-4.1.2.tgz#6099ee584999750c2624b65f80145f8674e4b4b0" + integrity sha512-EoQp/Et7OSOVu0aJknJOtlXZsnr8XE8KwuzTHOLeVSEx8pVWUICc8Q0VYRHgzyjX78nMEyC/oztWFbgyhtNfDA== + dependencies: + copy-anything "^2.0.1" + parse-node-version "^1.0.1" + tslib "^2.3.0" + optionalDependencies: + errno "^0.1.1" + graceful-fs "^4.1.2" + image-size "~0.5.0" + make-dir "^2.1.0" + mime "^1.4.1" + needle "^2.5.2" + source-map "~0.6.0" + leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -8216,7 +8269,7 @@ lz-string@^1.4.4: resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" integrity sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY= -make-dir@^2.0.0: +make-dir@^2.0.0, make-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== @@ -8343,7 +8396,7 @@ mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, dependencies: mime-db "1.51.0" -mime@1.6.0, mime@^1.3.4: +mime@1.6.0, mime@^1.3.4, mime@^1.4.1: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== @@ -8383,7 +8436,7 @@ mini-create-react-context@^0.4.0: "@babel/runtime" "^7.12.1" tiny-warning "^1.0.3" -mini-css-extract-plugin@^2.4.5: +mini-css-extract-plugin@2.4.5: version "2.4.5" resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.4.5.tgz#191d6c170226037212c483af1180b4010b7b9eef" integrity sha512-oEIhRucyn1JbT/1tU2BhnwO6ft1jjH1iCX9Gc59WFMg0n5773rQU0oyQ0zzeYFFuBfONaRbQJyGoPtuNseMxjA== @@ -8517,6 +8570,15 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= +needle@^2.5.2: + version "2.9.1" + resolved "https://registry.yarnpkg.com/needle/-/needle-2.9.1.tgz#22d1dffbe3490c2b83e301f7709b6736cd8f2684" + integrity sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ== + dependencies: + debug "^3.2.6" + iconv-lite "^0.4.4" + sax "^1.2.4" + negotiator@0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" @@ -9030,6 +9092,14 @@ parse-json@^5.0.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" + +parse-node-version@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b" + integrity sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA== + +parse5@6.0.1: + parse5-htmlparser2-tree-adapter@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6" @@ -9038,6 +9108,7 @@ parse5-htmlparser2-tree-adapter@^6.0.1: parse5 "^6.0.1" parse5@6.0.1, parse5@^6.0.1: + version "6.0.1" resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== @@ -9649,6 +9720,11 @@ proxy-from-env@1.0.0: resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" integrity sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4= +prr@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" + integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= + psl@^1.1.28, psl@^1.1.33: version "1.8.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" @@ -10633,7 +10709,7 @@ sane@^4.0.3: minimist "^1.1.1" walker "~1.0.5" -sax@>=0.6.0: +sax@>=0.6.0, sax@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -11690,7 +11766,7 @@ tslib@^1.8.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.3: +tslib@^2.0.3, tslib@^2.3.0: version "2.3.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==