From 28c8df5e6383889ac5c7e52afb4377d4c7b087a5 Mon Sep 17 00:00:00 2001 From: pal-sig <88981777+pal-sig@users.noreply.github.com> Date: Tue, 16 Nov 2021 21:13:20 +0530 Subject: [PATCH] Fix(FE):trace page (#356) * chore: Router provider is removed * update: localstorage set get is added * update: AppLayout is updated * fix: adapter type is fixed * fix: Metric and metric application is now fixed * fix: Metrics page application is updated * fix: Tracepage is made fix * fix: app layout is updated * fix: global Time reducer is updated * refactor: getService api is added * update: metrics reducer is added * update: service list is fixed * fix: Metrics page is updated * fix: api for the metrics application are done * fix: metrics reducer is updated * fix: metrics application is updated * fix: content layout shift is removed * fix: Metric application is updated * fix: metrics application is updated * fix: Metrics application is updated * fix: Application tab is updated * chore: graph is updated * chore: Metrics application is updated * fix: chart x-axis is label is now fixed * fix: application tab is updated * fix: Top end points is added and re-redering in stopped * fix: fixed the edge case when user changes the global time then updated data is fetched * fix: Settings page is updated * chore: AppLayout is updated * chore: AppLayout is updated * chore: applayout is updated * chore: changed default loading is true in the global time reducer * chore: Global Time option is fixed * chore: Signup and Applayout is updated * chore: Button text is updated * chore: Button in the metrics application is updated * chore: dashboard menu item position in the side nav is updated * fix: Logo is now redirecting to the Application page * fix: Application page is updated * fix: AppLayout is updated * fix: starting and ending time is fixed * fix: Metrics Application is updated to the previous chart data * update: getDateArrayFromStartAndEnd function is added * update: Empty graph data is added * fix: External Call and DB Call Tabs graph are updated when there is no data a empty data is rendered * fix: onboarding modal condition is fixed and new calling api every 50000 ms to fetch the data * fix: onBoarding condition modal is updated * fix: onBoarding condition modal is updated * fix: onBoarding condition modal is updated * fix: Application chart re rendering issue is fixed * fix: Application page is changed when we change the global time * chore: step size is increased from 30 to 60 * chore: build is now fixed * chore: metrics application page is updated * fix: empty graph is now fixed * fix: application metrics graph is now fixed * update: seperate api for trace page are made * fix: /trace page is updated * chore: Filter of the Trace page is updated * chore: initial trace page is updated * fix: changing the filters,fetches the updated values from the backend * chore: Trace page is updated * update: trace page is updated * fix: trace page is updated * Refresh Text is updated * update: Trace page is updated * update:header is updated * update: Trace page is updated * update: Trace page is updated * update: Trace page is updated * update: Trace page is updated * update: why did you re render is added * update: trace page is updated * update: trace page is updated * update: Loading is updated * update: start and end time is updated * fix: metrics and metrics page redudant calls is reduced * fix: Metrics Application page reducer is reset on the unmount * fix: Trace page reducer is reset when the page is unmounted * fix: Custom Visualizations is now fetching only one api to get the details * fix: Trace page is updated * fix: composeEnhancers is updated * fix: metrics application is updated * chore: webpack eslint fixes are updated * chore: some of the type definition is added * fix(UI): Trace page bug is resolved * chore(UI): if length of the selected tags is zero updated the value over the form * chore(UI): check for the no spans filter is updated --- frontend/package.json | 1 + frontend/src/AppRoutes/pageComponents.ts | 4 + frontend/src/AppRoutes/routes.ts | 6 + 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/getTags.ts | 24 ++ frontend/src/constants/query.ts | 5 + frontend/src/constants/routes.ts | 1 + .../GridGraphLayout/Graph/FullView/index.tsx | 15 +- .../Header/DateTimeSelection/Refresh.tsx | 34 ++ .../Header/DateTimeSelection/index.tsx | 200 +++++---- .../MetricsApplication/Tabs/Application.tsx | 33 +- .../MetricsApplication/TopEndpointsTable.tsx | 28 +- frontend/src/container/MetricsTable/index.tsx | 24 +- frontend/src/container/SideNav/index.tsx | 9 +- frontend/src/container/SideNav/menuItems.ts | 2 +- .../TraceCustomGraph.tsx | 33 ++ .../TraceCustomVisualization/config.ts | 56 +++ .../TraceCustomVisualization/index.tsx | 127 ++++++ .../TraceCustomVisualization/styles.ts | 34 ++ frontend/src/container/TraceFilter/Filter.tsx | 187 +++++++++ .../src/container/TraceFilter/LatencyForm.tsx | 160 ++++++++ frontend/src/container/TraceFilter/config.ts | 15 + frontend/src/container/TraceFilter/index.tsx | 384 ++++++++++++++++++ frontend/src/container/TraceFilter/styles.ts | 34 ++ frontend/src/container/TraceList/index.tsx | 141 +++++++ frontend/src/container/TraceList/styles.ts | 7 + frontend/src/index.tsx | 1 + frontend/src/lib/convertDateToAmAndPm.ts | 1 + frontend/src/lib/createQueryParams.ts | 6 + frontend/src/lib/getMinMax.ts | 57 +++ .../src/modules/Servicemap/SelectService.tsx | 2 +- .../src/modules/Traces/LatencyModalForm.tsx | 1 - .../Traces/TraceCustomVisualizations.tsx | 1 - frontend/src/modules/Traces/TraceDetail.tsx | 29 +- frontend/src/modules/Traces/TraceFilter.tsx | 26 +- .../src/pages/MetricApplication/index.tsx | 54 ++- frontend/src/pages/Metrics/index.tsx | 26 +- frontend/src/pages/SignUp/index.tsx | 7 +- frontend/src/pages/TraceDetails/index.tsx | 76 ++++ frontend/src/store/actions/global.ts | 41 +- .../store/actions/metrics/getInitialData.ts | 37 +- .../src/store/actions/metrics/getService.ts | 33 +- .../store/actions/metrics/resetInitialData.ts | 14 + .../src/store/actions/trace/getInitialData.ts | 201 +++++++++ .../actions/trace/getTraceVisualAgrregates.ts | 92 +++++ frontend/src/store/actions/trace/index.ts | 9 + .../store/actions/trace/loadingCompleted.ts | 12 + .../store/actions/trace/resetTraceDetails.ts | 10 + .../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 + frontend/src/store/reducers/global.ts | 2 + frontend/src/store/reducers/index.ts | 2 + frontend/src/store/reducers/metric.ts | 16 +- frontend/src/store/reducers/trace.ts | 204 ++++++++++ frontend/src/store/reducers/traceFilters.ts | 4 +- frontend/src/types/actions/globalTime.ts | 8 +- frontend/src/types/actions/index.ts | 4 +- frontend/src/types/actions/metrics.ts | 9 +- frontend/src/types/actions/trace.ts | 151 +++++++ .../src/types/api/trace/getServiceList.ts | 1 + .../types/api/trace/getServiceOperation.ts | 5 + .../src/types/api/trace/getSpanAggregate.ts | 20 + frontend/src/types/api/trace/getSpans.ts | 51 +++ frontend/src/types/api/trace/getTags.ts | 10 + frontend/src/types/common/index.ts | 5 +- frontend/src/types/reducer/globalTime.ts | 2 + frontend/src/types/reducer/metrics.ts | 1 + frontend/src/types/reducer/trace.ts | 37 ++ frontend/src/typings/react-graph-vis.d.ts | 2 +- frontend/src/utils/app.ts | 2 +- frontend/src/wdyr.ts | 15 + frontend/webpack.config.prod.ts | 15 +- frontend/webpack.config.ts | 13 +- frontend/yarn.lock | 19 +- 84 files changed, 2991 insertions(+), 377 deletions(-) create mode 100644 frontend/src/api/trace/getServiceList.ts create mode 100644 frontend/src/api/trace/getServiceOperation.ts create mode 100644 frontend/src/api/trace/getSpan.ts create mode 100644 frontend/src/api/trace/getSpanAggregate.ts create mode 100644 frontend/src/api/trace/getTags.ts create mode 100644 frontend/src/container/Header/DateTimeSelection/Refresh.tsx create mode 100644 frontend/src/container/TraceCustomVisualization/TraceCustomGraph.tsx create mode 100644 frontend/src/container/TraceCustomVisualization/config.ts create mode 100644 frontend/src/container/TraceCustomVisualization/index.tsx create mode 100644 frontend/src/container/TraceCustomVisualization/styles.ts create mode 100644 frontend/src/container/TraceFilter/Filter.tsx create mode 100644 frontend/src/container/TraceFilter/LatencyForm.tsx create mode 100644 frontend/src/container/TraceFilter/config.ts create mode 100644 frontend/src/container/TraceFilter/index.tsx create mode 100644 frontend/src/container/TraceFilter/styles.ts create mode 100644 frontend/src/container/TraceList/index.tsx create mode 100644 frontend/src/container/TraceList/styles.ts create mode 100644 frontend/src/lib/createQueryParams.ts create mode 100644 frontend/src/lib/getMinMax.ts create mode 100644 frontend/src/pages/TraceDetails/index.tsx create mode 100644 frontend/src/store/actions/metrics/resetInitialData.ts create mode 100644 frontend/src/store/actions/trace/getInitialData.ts create mode 100644 frontend/src/store/actions/trace/getTraceVisualAgrregates.ts create mode 100644 frontend/src/store/actions/trace/index.ts create mode 100644 frontend/src/store/actions/trace/loadingCompleted.ts create mode 100644 frontend/src/store/actions/trace/resetTraceDetails.ts create mode 100644 frontend/src/store/actions/trace/updateSelectedAggOption.ts create mode 100644 frontend/src/store/actions/trace/updateSelectedData.ts create mode 100644 frontend/src/store/actions/trace/updateSelectedEntity.ts create mode 100644 frontend/src/store/actions/trace/updateSelectedKind.ts create mode 100644 frontend/src/store/actions/trace/updateSelectedLatency.ts create mode 100644 frontend/src/store/actions/trace/updateSelectedOperation.ts create mode 100644 frontend/src/store/actions/trace/updateSelectedService.ts create mode 100644 frontend/src/store/actions/trace/updateSelectedTags.ts create mode 100644 frontend/src/store/actions/trace/updateSpanLoading.ts create mode 100644 frontend/src/store/reducers/trace.ts create mode 100644 frontend/src/types/actions/trace.ts create mode 100644 frontend/src/types/api/trace/getServiceList.ts create mode 100644 frontend/src/types/api/trace/getServiceOperation.ts create mode 100644 frontend/src/types/api/trace/getSpanAggregate.ts create mode 100644 frontend/src/types/api/trace/getSpans.ts create mode 100644 frontend/src/types/api/trace/getTags.ts create mode 100644 frontend/src/types/reducer/trace.ts create mode 100644 frontend/src/wdyr.ts diff --git a/frontend/package.json b/frontend/package.json index 6559a7f74a..905dbe4704 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -145,6 +145,7 @@ "@types/webpack-dev-server": "^4.3.0", "@typescript-eslint/eslint-plugin": "^4.28.2", "@typescript-eslint/parser": "^4.28.2", + "@welldone-software/why-did-you-render": "^6.2.1", "autoprefixer": "^9.0.0", "babel-plugin-styled-components": "^1.12.0", "compression-webpack-plugin": "^9.0.0", diff --git a/frontend/src/AppRoutes/pageComponents.ts b/frontend/src/AppRoutes/pageComponents.ts index 0683c49cbe..c214e2d116 100644 --- a/frontend/src/AppRoutes/pageComponents.ts +++ b/frontend/src/AppRoutes/pageComponents.ts @@ -25,6 +25,10 @@ export const TraceDetailPage = Loadable( ), ); +export const TraceDetailPages = Loadable( + () => import(/* webpackChunkName: "TraceDetailPage" */ 'pages/TraceDetails'), +); + export const TraceGraphPage = Loadable( () => import( diff --git a/frontend/src/AppRoutes/routes.ts b/frontend/src/AppRoutes/routes.ts index f2dc54544d..a73f1c24e2 100644 --- a/frontend/src/AppRoutes/routes.ts +++ b/frontend/src/AppRoutes/routes.ts @@ -12,6 +12,7 @@ import { SettingsPage, SignupPage, TraceDetailPage, + TraceDetailPages, TraceGraphPage, UsageExplorerPage, } from './pageComponents'; @@ -77,6 +78,11 @@ const routes: AppRoutes[] = [ exact: true, component: DashboardWidget, }, + { + path: ROUTES.TRACE, + exact: true, + component: TraceDetailPages, + }, ]; interface AppRoutes { diff --git a/frontend/src/api/trace/getServiceList.ts b/frontend/src/api/trace/getServiceList.ts new file mode 100644 index 0000000000..0fc063e721 --- /dev/null +++ b/frontend/src/api/trace/getServiceList.ts @@ -0,0 +1,24 @@ +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 new file mode 100644 index 0000000000..04ee9c954a --- /dev/null +++ b/frontend/src/api/trace/getServiceOperation.ts @@ -0,0 +1,24 @@ +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 new file mode 100644 index 0000000000..6b36b23cdc --- /dev/null +++ b/frontend/src/api/trace/getSpan.ts @@ -0,0 +1,26 @@ +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 new file mode 100644 index 0000000000..d233dc25e5 --- /dev/null +++ b/frontend/src/api/trace/getSpanAggregate.ts @@ -0,0 +1,26 @@ +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/getTags.ts b/frontend/src/api/trace/getTags.ts new file mode 100644 index 0000000000..430c25381a --- /dev/null +++ b/frontend/src/api/trace/getTags.ts @@ -0,0 +1,24 @@ +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/constants/query.ts b/frontend/src/constants/query.ts index 9e327ed6d4..0f816d1605 100644 --- a/frontend/src/constants/query.ts +++ b/frontend/src/constants/query.ts @@ -6,4 +6,9 @@ export enum METRICS_PAGE_QUERY_PARAM { error = 'error', operation = 'operation', kind = 'kind', + latencyMax = 'latencyMax', + latencyMin = 'latencyMin', + selectedTags = 'selectedTags', + aggregationOption = 'aggregationOption', + entity = 'entity', } diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts index 40ab99fe66..9f84fbf3e1 100644 --- a/frontend/src/constants/routes.ts +++ b/frontend/src/constants/routes.ts @@ -3,6 +3,7 @@ const ROUTES = { SERVICE_METRICS: '/application/:servicename', SERVICE_MAP: '/service-map', TRACES: '/traces', + TRACE: '/trace', TRACE_GRAPH: '/traces/:id', SETTINGS: '/settings', INSTRUMENTATION: '/add-instrumentation', diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/index.tsx b/frontend/src/container/GridGraphLayout/Graph/FullView/index.tsx index d053e621d4..1d66a60488 100644 --- a/frontend/src/container/GridGraphLayout/Graph/FullView/index.tsx +++ b/frontend/src/container/GridGraphLayout/Graph/FullView/index.tsx @@ -13,7 +13,7 @@ import { import getChartData from 'lib/getChartData'; import GetMaxMinTime from 'lib/getMaxMinTime'; import getStartAndEndTime from 'lib/getStartAndEndTime'; -import React, { memo, useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import { GlobalTime } from 'types/actions/globalTime'; @@ -219,15 +219,4 @@ interface FullViewProps { noDataGraph?: boolean; } -export default memo(FullView, (prev, next) => { - if ( - next.widget.query.length !== prev.widget.query.length && - next.widget.query.every((value, index) => { - return value === prev.widget.query[index]; - }) - ) { - return false; - } - - return true; -}); +export default FullView; diff --git a/frontend/src/container/Header/DateTimeSelection/Refresh.tsx b/frontend/src/container/Header/DateTimeSelection/Refresh.tsx new file mode 100644 index 0000000000..647b91070d --- /dev/null +++ b/frontend/src/container/Header/DateTimeSelection/Refresh.tsx @@ -0,0 +1,34 @@ +import React, { useEffect, useState } from 'react'; + +import { RefreshTextContainer, Typography } from './styles'; + +const RefreshText = ({ + onLastRefreshHandler, +}: RefreshTextProps): JSX.Element => { + const [refreshText, setRefreshText] = useState(''); + + // this is to update the refresh text + useEffect(() => { + const interval = setInterval(() => { + const text = onLastRefreshHandler(); + if (refreshText !== text) { + setRefreshText(text); + } + }, 2000); + return (): void => { + clearInterval(interval); + }; + }, [onLastRefreshHandler, refreshText]); + + return ( + + {refreshText} + + ); +}; + +interface RefreshTextProps { + onLastRefreshHandler: () => string; +} + +export default RefreshText; diff --git a/frontend/src/container/Header/DateTimeSelection/index.tsx b/frontend/src/container/Header/DateTimeSelection/index.tsx index bd7046d96f..a706d10cc4 100644 --- a/frontend/src/container/Header/DateTimeSelection/index.tsx +++ b/frontend/src/container/Header/DateTimeSelection/index.tsx @@ -2,13 +2,7 @@ import { Button, Select as DefaultSelect } from 'antd'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { getDefaultOption, getOptions, Time } from './config'; -import { - Container, - Form, - FormItem, - RefreshTextContainer, - Typography, -} from './styles'; +import { Container, Form, FormItem } from './styles'; const { Option } = DefaultSelect; import get from 'api/browser/localstorage/get'; import set from 'api/browser/localstorage/set'; @@ -19,31 +13,72 @@ import { connect, useSelector } from 'react-redux'; import { RouteComponentProps, withRouter } from 'react-router'; import { bindActionCreators, Dispatch } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; -import { UpdateTimeInterval } from 'store/actions'; +import { GlobalTimeLoading, UpdateTimeInterval } from 'store/actions'; import { AppState } from 'store/reducers'; import AppActions from 'types/actions'; import { GlobalReducer } from 'types/reducer/globalTime'; import CustomDateTimeModal, { DateTimeRangeType } from '../CustomDateTimeModal'; +import RefreshText from './Refresh'; const DateTimeSelection = ({ location, updateTimeInterval, + globalTimeLoading, }: Props): JSX.Element => { const [form_dtselector] = Form.useForm(); + + const params = new URLSearchParams(location.search); + const searchStartTime = params.get('startTime'); + const searchEndTime = params.get('endTime'); + + const localstorageStartTime = get('startTime'); + const localstorageEndTime = get('endTime'); + + const getTime = useCallback((): [number, number] | undefined => { + if (searchEndTime && searchStartTime) { + const startMoment = moment( + new Date(parseInt(getTimeString(searchStartTime), 10)), + ); + const endMoment = moment( + new Date(parseInt(getTimeString(searchEndTime), 10)), + ); + + return [ + startMoment.toDate().getTime() || 0, + endMoment.toDate().getTime() || 0, + ]; + } + if (localstorageStartTime && localstorageEndTime) { + const startMoment = moment(localstorageStartTime); + const endMoment = moment(localstorageEndTime); + + return [ + startMoment.toDate().getTime() || 0, + endMoment.toDate().getTime() || 0, + ]; + } + return undefined; + }, [ + localstorageEndTime, + localstorageStartTime, + searchEndTime, + searchStartTime, + ]); + const [startTime, setStartTime] = useState(); const [endTime, setEndTime] = useState(); + const [options, setOptions] = useState(getOptions(location.pathname)); const [refreshButtonHidden, setRefreshButtonHidden] = useState(false); - const [refreshText, setRefreshText] = useState(''); const [customDateTimeVisible, setCustomDTPickerVisible] = useState( false, ); - const isOnSelectHandler = useRef(false); - const { maxTime, loading, minTime } = useSelector( - (state) => state.globalTime, - ); + const { maxTime, minTime, selectedTime, loading } = useSelector< + AppState, + GlobalReducer + >((state) => state.globalTime); const getDefaultTime = (pathName: string): Time => { const defaultSelectedOption = getDefaultOption(pathName); @@ -81,8 +116,6 @@ const DateTimeSelection = ({ }; const onSelectHandler = (value: Time): void => { - isOnSelectHandler.current = true; - if (value !== 'custom') { updateTimeInterval(value); const selectedLabel = getInputLabel(undefined, undefined, value); @@ -168,17 +201,6 @@ const DateTimeSelection = ({ } }; - // this is to update the refresh text - useEffect(() => { - const interval = setInterval(() => { - const text = onLastRefreshHandler(); - setRefreshText(text); - }, 2000); - return (): void => { - clearInterval(interval); - }; - }, [onLastRefreshHandler]); - // this is triggred when we change the routes and based on that we are changing the default options useEffect(() => { const metricsTimeDuration = get(LOCAL_STORAGE.METRICS_TIME_IN_DURATION); @@ -187,85 +209,44 @@ const DateTimeSelection = ({ set(LOCAL_STORAGE.METRICS_TIME_IN_DURATION, JSON.stringify({})); } - if (isOnSelectHandler.current === false) { - const currentRoute = location.pathname; - const params = new URLSearchParams(location.search); - const time = getDefaultTime(currentRoute); + const currentRoute = location.pathname; + const time = getDefaultTime(currentRoute); - const currentOptions = getOptions(currentRoute); - setOptions(currentOptions); + const currentOptions = getOptions(currentRoute); + setOptions(currentOptions); - const searchStartTime = params.get('startTime'); - const searchEndTime = params.get('endTime'); - - const localstorageStartTime = get('startTime'); - const localstorageEndTime = get('endTime'); - - const getUpdatedTime = (time: Time): Time => { - if (searchEndTime !== null && searchStartTime !== null) { - return 'custom'; - } - - if ( - (localstorageEndTime === null || localstorageStartTime === null) && - time === 'custom' - ) { - return getDefaultOption(location.pathname); - } - - return time; - }; - - const updatedTime = getUpdatedTime(time); - - setSelectedTimeInterval(updatedTime); - - const getTime = (): [number, number] | undefined => { - if (searchEndTime && searchStartTime) { - const startMoment = moment( - new Date(parseInt(getTimeString(searchStartTime), 10)), - ); - const endMoment = moment( - new Date(parseInt(getTimeString(searchEndTime), 10)), - ); - - setStartTime(startMoment); - setEndTime(endMoment); - - return [ - startMoment.toDate().getTime() || 0, - endMoment.toDate().getTime() || 0, - ]; - } - if (localstorageStartTime && localstorageEndTime) { - const startMoment = moment(localstorageStartTime); - const endMoment = moment(localstorageEndTime); - - setStartTime(startMoment); - setEndTime(endMoment); - - return [ - startMoment.toDate().getTime() || 0, - endMoment.toDate().getTime() || 0, - ]; - } - return undefined; - }; - - if (loading === true) { - updateTimeInterval(updatedTime, getTime()); + const getCustomOrIntervalTime = (time: Time): Time => { + if (searchEndTime !== null && searchStartTime !== null) { + return 'custom'; } - } else { - isOnSelectHandler.current = false; - } + + if ( + (localstorageEndTime === null || localstorageStartTime === null) && + time === 'custom' + ) { + return getDefaultOption(currentRoute); + } + + return time; + }; + + const updatedTime = getCustomOrIntervalTime(time); + + const [preStartTime = 0, preEndTime = 0] = getTime() || []; + + setStartTime(moment(preStartTime)); + setEndTime(moment(preEndTime)); + + updateTimeInterval(updatedTime, [preStartTime, preEndTime]); }, [ location.pathname, - location.search, - startTime, - endTime, + getTime, + localstorageEndTime, + localstorageStartTime, + searchEndTime, + searchStartTime, updateTimeInterval, - selectedTimeInterval, - loading, + globalTimeLoading, ]); return ( @@ -273,11 +254,11 @@ const DateTimeSelection = ({
onSelectHandler(value as Time)} - value={getInputLabel(startTime, endTime, selectedTimeInterval)} + value={getInputLabel(startTime, endTime, selectedTime)} data-testid="dropDown" > {options.map(({ value, label }) => ( @@ -294,9 +275,11 @@ const DateTimeSelection = ({ - - {refreshText} - + (dispatch: Dispatch) => void; - // globalTimeLoading: () => void; + globalTimeLoading: () => void; } const mapDispatchToProps = ( dispatch: ThunkDispatch, ): DispatchProps => ({ updateTimeInterval: bindActionCreators(UpdateTimeInterval, dispatch), - // globalTimeLoading: bindActionCreators(GlobalTimeLoading, dispatch), + globalTimeLoading: bindActionCreators(GlobalTimeLoading, dispatch), }); 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 122a6a24dd..054e866397 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Application.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Application.tsx @@ -6,13 +6,9 @@ import FullView from 'container/GridGraphLayout/Graph/FullView'; import { colors } from 'lib/getRandomColor'; import history from 'lib/history'; import React, { useRef } from 'react'; -import { connect, useSelector } from 'react-redux'; +import { useSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; -import { bindActionCreators } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { GlobalTimeLoading } from 'store/actions'; import { AppState } from 'store/reducers'; -import AppActions from 'types/actions'; import { Widgets } from 'types/api/dashboard/getAll'; import MetricReducer from 'types/reducer/metrics'; @@ -20,10 +16,7 @@ import { Card, Col, GraphContainer, GraphTitle, Row } from '../styles'; import TopEndpointsTable from '../TopEndpointsTable'; import { Button } from './styles'; -const Application = ({ - globalLoading, - getWidget, -}: DashboardProps): JSX.Element => { +const Application = ({ getWidget }: DashboardProps): JSX.Element => { const { servicename } = useParams<{ servicename?: string }>(); const selectedTimeStamp = useRef(0); @@ -42,8 +35,7 @@ const Application = ({ urlParams.set(METRICS_PAGE_QUERY_PARAM.service, servicename); } - globalLoading(); - history.push(`${ROUTES.TRACES}?${urlParams.toString()}`); + history.push(`${ROUTES.TRACE}?${urlParams.toString()}`); }; const onClickhandler = async ( @@ -74,7 +66,7 @@ const Application = ({ buttonElement.style.display = 'block'; buttonElement.style.left = `${firstPoint.element.x}px`; buttonElement.style.top = `${firstPoint.element.y}px`; - selectedTimeStamp.current = new Date(time).getTime(); + selectedTimeStamp.current = time.getTime(); } } } else { @@ -97,8 +89,7 @@ const Application = ({ } urlParams.set(METRICS_PAGE_QUERY_PARAM.error, 'true'); - globalLoading(); - history.push(`${ROUTES.TRACES}?${urlParams.toString()}`); + history.push(`${ROUTES.TRACE}?${urlParams.toString()}`); }; return ( @@ -238,18 +229,8 @@ const Application = ({ ); }; -interface DispatchProps { - globalLoading: () => void; -} - -const mapDispatchToProps = ( - dispatch: ThunkDispatch, -): DispatchProps => ({ - globalLoading: bindActionCreators(GlobalTimeLoading, dispatch), -}); - -interface DashboardProps extends DispatchProps { +interface DashboardProps { getWidget: (query: Widgets['query']) => Widgets; } -export default connect(null, mapDispatchToProps)(Application); +export default Application; diff --git a/frontend/src/container/MetricsApplication/TopEndpointsTable.tsx b/frontend/src/container/MetricsApplication/TopEndpointsTable.tsx index 7a82d7d2f2..914423f418 100644 --- a/frontend/src/container/MetricsApplication/TopEndpointsTable.tsx +++ b/frontend/src/container/MetricsApplication/TopEndpointsTable.tsx @@ -1,15 +1,12 @@ import { Button, Table, Tooltip } from 'antd'; import { ColumnsType } from 'antd/lib/table'; import { METRICS_PAGE_QUERY_PARAM } from 'constants/query'; +import ROUTES from 'constants/routes'; import React from 'react'; -import { connect, useSelector } from 'react-redux'; +import { useSelector } from 'react-redux'; import { useHistory, useParams } from 'react-router-dom'; -import { bindActionCreators } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { GlobalTimeLoading } from 'store/actions'; import { topEndpointListItem } from 'store/actions/MetricsActions'; import { AppState } from 'store/reducers'; -import AppActions from 'types/actions'; import { GlobalReducer } from 'types/reducer/globalTime'; const TopEndpointsTable = (props: TopEndpointsTableProps): JSX.Element => { @@ -25,19 +22,18 @@ const TopEndpointsTable = (props: TopEndpointsTableProps): JSX.Element => { const { servicename } = params; urlParams.set( METRICS_PAGE_QUERY_PARAM.startTime, - String(Number(minTime) / 1000000), + (minTime / 1000000).toString(), ); urlParams.set( METRICS_PAGE_QUERY_PARAM.endTime, - String(Number(maxTime) / 1000000), + (maxTime / 1000000).toString(), ); if (servicename) { urlParams.set(METRICS_PAGE_QUERY_PARAM.service, servicename); } urlParams.set(METRICS_PAGE_QUERY_PARAM.operation, operation); - props.globalTimeLoading(); - history.push(`/traces?${urlParams.toString()}`); + history.push(`${ROUTES.TRACE}?${urlParams.toString()}`); }; const columns: ColumnsType = [ @@ -105,18 +101,8 @@ const TopEndpointsTable = (props: TopEndpointsTableProps): JSX.Element => { type DataProps = topEndpointListItem; -interface DispatchProps { - globalTimeLoading: () => void; -} - -const mapDispatchToProps = ( - dispatch: ThunkDispatch, -): DispatchProps => ({ - globalTimeLoading: bindActionCreators(GlobalTimeLoading, dispatch), -}); - -interface TopEndpointsTableProps extends DispatchProps { +interface TopEndpointsTableProps { data: topEndpointListItem[]; } -export default connect(null, mapDispatchToProps)(TopEndpointsTable); +export default TopEndpointsTable; diff --git a/frontend/src/container/MetricsTable/index.tsx b/frontend/src/container/MetricsTable/index.tsx index 40074d13f2..153176b35d 100644 --- a/frontend/src/container/MetricsTable/index.tsx +++ b/frontend/src/container/MetricsTable/index.tsx @@ -3,18 +3,15 @@ import { SKIP_ONBOARDING } from 'constants/onboarding'; import ROUTES from 'constants/routes'; import history from 'lib/history'; import React, { useState } from 'react'; -import { connect, useSelector } from 'react-redux'; -import { bindActionCreators } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { GlobalTimeLoading, servicesListItem } from 'store/actions'; +import { useSelector } from 'react-redux'; +import { servicesListItem } from 'store/actions/MetricsActions/metricsInterfaces'; import { AppState } from 'store/reducers'; -import AppActions from 'types/actions'; import MetricReducer from 'types/reducer/metrics'; import SkipBoardModal from './SkipOnBoardModal'; import { Container, Name } from './styles'; -const Metrics = ({ globalTimeLoading }: MetricsProps): JSX.Element => { +const Metrics = (): JSX.Element => { const [skipOnboarding, setSkipOnboarding] = useState( localStorage.getItem(SKIP_ONBOARDING) === 'true', ); @@ -30,7 +27,6 @@ const Metrics = ({ globalTimeLoading }: MetricsProps): JSX.Element => { const onClickHandler = (to: string): void => { history.push(to); - globalTimeLoading(); }; if ( @@ -90,16 +86,4 @@ const Metrics = ({ globalTimeLoading }: MetricsProps): JSX.Element => { type DataProps = servicesListItem; -interface DispatchProps { - globalTimeLoading: () => void; -} - -const mapDispatchToProps = ( - dispatch: ThunkDispatch, -): DispatchProps => ({ - globalTimeLoading: bindActionCreators(GlobalTimeLoading, dispatch), -}); - -type MetricsProps = DispatchProps; - -export default connect(null, mapDispatchToProps)(Metrics); +export default Metrics; diff --git a/frontend/src/container/SideNav/index.tsx b/frontend/src/container/SideNav/index.tsx index 159ae95c1e..ba5c64132c 100644 --- a/frontend/src/container/SideNav/index.tsx +++ b/frontend/src/container/SideNav/index.tsx @@ -7,7 +7,7 @@ import { NavLink } from 'react-router-dom'; import { useLocation } from 'react-router-dom'; import { bindActionCreators } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; -import { GlobalTimeLoading, ToggleDarkMode } from 'store/actions'; +import { ToggleDarkMode } from 'store/actions'; import { AppState } from 'store/reducers'; import AppActions from 'types/actions'; import AppReducer from 'types/reducer/app'; @@ -15,7 +15,7 @@ import AppReducer from 'types/reducer/app'; import menus from './menuItems'; import { Logo, Sider, ThemeSwitcherWrapper } from './styles'; -const SideNav = ({ toggleDarkMode, globalTimeLoading }: Props): JSX.Element => { +const SideNav = ({ toggleDarkMode }: Props): JSX.Element => { const [collapsed, setCollapsed] = useState(false); const { pathname } = useLocation(); const { isDarkMode } = useSelector((state) => state.app); @@ -49,10 +49,9 @@ const SideNav = ({ toggleDarkMode, globalTimeLoading }: Props): JSX.Element => { (to: string) => { if (pathname !== to) { history.push(to); - globalTimeLoading(); } }, - [pathname, globalTimeLoading], + [pathname], ); return ( @@ -86,14 +85,12 @@ type mode = 'darkMode' | 'lightMode'; interface DispatchProps { toggleDarkMode: () => void; - globalTimeLoading: () => void; } const mapDispatchToProps = ( dispatch: ThunkDispatch, ): DispatchProps => ({ toggleDarkMode: bindActionCreators(ToggleDarkMode, dispatch), - globalTimeLoading: bindActionCreators(GlobalTimeLoading, dispatch), }); type Props = DispatchProps; diff --git a/frontend/src/container/SideNav/menuItems.ts b/frontend/src/container/SideNav/menuItems.ts index 8b057dab3a..3cd80a4ab2 100644 --- a/frontend/src/container/SideNav/menuItems.ts +++ b/frontend/src/container/SideNav/menuItems.ts @@ -17,7 +17,7 @@ const menus: SidebarMenu[] = [ }, { Icon: AlignLeftOutlined, - to: ROUTES.TRACES, + to: ROUTES.TRACE, name: 'Traces', }, { diff --git a/frontend/src/container/TraceCustomVisualization/TraceCustomGraph.tsx b/frontend/src/container/TraceCustomVisualization/TraceCustomGraph.tsx new file mode 100644 index 0000000000..a651bd6cec --- /dev/null +++ b/frontend/src/container/TraceCustomVisualization/TraceCustomGraph.tsx @@ -0,0 +1,33 @@ +import Graph from 'components/Graph'; +import { colors } from 'lib/getRandomColor'; +import React, { memo } from 'react'; +import { TraceReducer } from 'types/reducer/trace'; + +import { CustomGraphContainer } from './styles'; + +const TraceCustomGraph = ({ + spansAggregate, +}: TraceCustomGraphProps): JSX.Element => { + return ( + + new Date(s.timestamp / 1000000)), + datasets: [ + { + data: spansAggregate.map((e) => 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 new file mode 100644 index 0000000000..0a3d5fbf9b --- /dev/null +++ b/frontend/src/container/TraceCustomVisualization/config.ts @@ -0,0 +1,56 @@ +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 new file mode 100644 index 0000000000..08ef2f63f3 --- /dev/null +++ b/frontend/src/container/TraceCustomVisualization/index.tsx @@ -0,0 +1,127 @@ +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 { bindActionCreators } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; + +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'; +import { + GetTraceVisualAggregates, + GetTraceVisualAggregatesProps, +} from 'store/actions/trace/getTraceVisualAgrregates'; + +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: any): 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 new file mode 100644 index 0000000000..6a06cbd717 --- /dev/null +++ b/frontend/src/container/TraceCustomVisualization/styles.ts @@ -0,0 +1,34 @@ +import { + Card as CardComponent, + Form, + Space as SpaceComponent, + Typography, +} from 'antd'; +import styled from 'styled-components'; + +export const CustomGraphContainer = styled.div` + min-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 new file mode 100644 index 0000000000..729be85b1a --- /dev/null +++ b/frontend/src/container/TraceFilter/Filter.tsx @@ -0,0 +1,187 @@ +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 { + UpdateSelectedLatency, + UpdateSelectedOperation, + UpdateSelectedService, + 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 new file mode 100644 index 0000000000..593e1fd877 --- /dev/null +++ b/frontend/src/container/TraceFilter/LatencyForm.tsx @@ -0,0 +1,160 @@ +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 new file mode 100644 index 0000000000..8410b1cf54 --- /dev/null +++ b/frontend/src/container/TraceFilter/config.ts @@ -0,0 +1,15 @@ +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 new file mode 100644 index 0000000000..79fd84ef93 --- /dev/null +++ b/frontend/src/container/TraceFilter/index.tsx @@ -0,0 +1,384 @@ +import { Button, Input, Typography, notification } 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]); + + 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 new file mode 100644 index 0000000000..165f48e1b6 --- /dev/null +++ b/frontend/src/container/TraceFilter/styles.ts @@ -0,0 +1,34 @@ +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 new file mode 100644 index 0000000000..cae991d33a --- /dev/null +++ b/frontend/src/container/TraceList/index.tsx @@ -0,0 +1,141 @@ +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.TRACES + '/' + 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 new file mode 100644 index 0000000000..dfde5eaad6 --- /dev/null +++ b/frontend/src/container/TraceList/styles.ts @@ -0,0 +1,7 @@ +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/index.tsx b/frontend/src/index.tsx index 78da8f849e..4385d499de 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -1,3 +1,4 @@ +import './wdyr'; import 'assets/index.css'; import AppRoutes from 'AppRoutes'; diff --git a/frontend/src/lib/convertDateToAmAndPm.ts b/frontend/src/lib/convertDateToAmAndPm.ts index 0a0ad0a5fc..358a3f2667 100644 --- a/frontend/src/lib/convertDateToAmAndPm.ts +++ b/frontend/src/lib/convertDateToAmAndPm.ts @@ -2,6 +2,7 @@ const convertDateToAmAndPm = (date: Date): string => { return date.toLocaleString('en-US', { hour: '2-digit', minute: 'numeric', + second: 'numeric', hour12: true, }); }; diff --git a/frontend/src/lib/createQueryParams.ts b/frontend/src/lib/createQueryParams.ts new file mode 100644 index 0000000000..0f1b3f7ad7 --- /dev/null +++ b/frontend/src/lib/createQueryParams.ts @@ -0,0 +1,6 @@ +const createQueryParams = (params: { [x: string]: string }): string => + Object.keys(params) + .map((k) => `${k}=${encodeURI(params[k])}`) + .join('&'); + +export default createQueryParams; diff --git a/frontend/src/lib/getMinMax.ts b/frontend/src/lib/getMinMax.ts new file mode 100644 index 0000000000..ee369d9fba --- /dev/null +++ b/frontend/src/lib/getMinMax.ts @@ -0,0 +1,57 @@ +import { Time } from 'container/Header/DateTimeSelection/config'; +import { GlobalReducer } from 'types/reducer/globalTime'; + +import getMinAgo from './getStartAndEndTime/getMinAgo'; + +const GetMinMax = ( + interval: Time, + dateTimeRange?: [number, number], +): GetMinMaxPayload => { + let maxTime = new Date().getTime(); + let minTime = 0; + + if (interval === '1min') { + const minTimeAgo = getMinAgo({ minutes: 1 }).getTime(); + minTime = minTimeAgo; + } else if (interval === '15min') { + const minTimeAgo = getMinAgo({ minutes: 15 }).getTime(); + minTime = minTimeAgo; + } else if (interval === '1hr') { + const minTimeAgo = getMinAgo({ minutes: 60 }).getTime(); + minTime = minTimeAgo; + } else if (interval === '30min') { + const minTimeAgo = getMinAgo({ minutes: 30 }).getTime(); + minTime = minTimeAgo; + } else if (interval === '5min') { + const minTimeAgo = getMinAgo({ minutes: 5 }).getTime(); + minTime = minTimeAgo; + } else if (interval === '1day') { + // one day = 24*60(min) + const minTimeAgo = getMinAgo({ minutes: 26 * 60 }).getTime(); + minTime = minTimeAgo; + } else if (interval === '1week') { + // one week = one day * 7 + const minTimeAgo = getMinAgo({ minutes: 26 * 60 * 7 }).getTime(); + minTime = minTimeAgo; + } else if (interval === '6hr') { + const minTimeAgo = getMinAgo({ minutes: 6 * 60 }).getTime(); + minTime = minTimeAgo; + } else if (interval === 'custom') { + maxTime = (dateTimeRange || [])[1] || 0; + minTime = (dateTimeRange || [])[0] || 0; + } else { + throw new Error('invalid time type'); + } + + return { + minTime: minTime * 1000000, + maxTime: maxTime * 1000000, + }; +}; + +interface GetMinMaxPayload { + minTime: GlobalReducer['minTime']; + maxTime: GlobalReducer['maxTime']; +} + +export default GetMinMax; diff --git a/frontend/src/modules/Servicemap/SelectService.tsx b/frontend/src/modules/Servicemap/SelectService.tsx index 016a504fe6..1b5993d897 100644 --- a/frontend/src/modules/Servicemap/SelectService.tsx +++ b/frontend/src/modules/Servicemap/SelectService.tsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import { servicesItem } from "Src/store/actions"; +import { servicesItem } from "store/actions"; import { InfoCircleOutlined } from "@ant-design/icons"; import { Select } from "antd"; import styled from "styled-components"; diff --git a/frontend/src/modules/Traces/LatencyModalForm.tsx b/frontend/src/modules/Traces/LatencyModalForm.tsx index 66fa3b9291..c52993baaf 100644 --- a/frontend/src/modules/Traces/LatencyModalForm.tsx +++ b/frontend/src/modules/Traces/LatencyModalForm.tsx @@ -69,7 +69,6 @@ const LatencyModalForm: React.FC = ({ initialValues={latencyFilterValues} > - {/* */} diff --git a/frontend/src/modules/Traces/TraceDetail.tsx b/frontend/src/modules/Traces/TraceDetail.tsx index 7e26b88356..ede374bae8 100644 --- a/frontend/src/modules/Traces/TraceDetail.tsx +++ b/frontend/src/modules/Traces/TraceDetail.tsx @@ -1,21 +1,10 @@ -import React, { useEffect } from 'react'; -import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; -import { ThunkDispatch } from 'redux-thunk'; -import { GlobalTimeLoading } from 'store/actions'; -import AppActions from 'types/actions'; +import React from 'react'; import { TraceCustomVisualizations } from './TraceCustomVisualizations'; import { TraceFilter } from './TraceFilter'; import { TraceList } from './TraceList'; -const TraceDetail = ({ globalTimeLoading }: Props): JSX.Element => { - useEffect(() => { - return (): void => { - globalTimeLoading(); - }; - }, [globalTimeLoading]); - +const TraceDetail = (): JSX.Element => { return ( <> @@ -25,16 +14,4 @@ const TraceDetail = ({ globalTimeLoading }: Props): JSX.Element => { ); }; -interface DispatchProps { - globalTimeLoading: () => void; -} - -const mapDispatchToProps = ( - dispatch: ThunkDispatch, -): DispatchProps => ({ - globalTimeLoading: bindActionCreators(GlobalTimeLoading, dispatch), -}); - -type Props = DispatchProps; - -export default connect(null, mapDispatchToProps)(TraceDetail); +export default TraceDetail; diff --git a/frontend/src/modules/Traces/TraceFilter.tsx b/frontend/src/modules/Traces/TraceFilter.tsx index 538a257203..4ea95996a7 100644 --- a/frontend/src/modules/Traces/TraceFilter.tsx +++ b/frontend/src/modules/Traces/TraceFilter.tsx @@ -120,15 +120,6 @@ const _TraceFilter = (props: TraceFilterProps): JSX.Element => { const handleApplyFilterForm = useCallback( (values: any): void => { - // setTagKeyValueApplied((tagKeyValueApplied) => [ - // ...tagKeyValueApplied, - // 'service eq' + values.service, - // 'operation eq ' + values.operation, - // 'maxduration eq ' + - // (parseInt(latencyFilterValues.max) / 1000000).toString(), - // 'minduration eq ' + - // (parseInt(latencyFilterValues.min) / 1000000).toString(), - // ]); updateTraceFilters({ service: values.service, operation: values.operation, @@ -272,10 +263,6 @@ const _TraceFilter = (props: TraceFilterProps): JSX.Element => { '&tags=' + encodeURIComponent(JSON.stringify(traceFilters.tags)); - /* - Call the apis only when the route is loaded. - Check this issue: https://github.com/SigNoz/signoz/issues/110 - */ if (loading === false) { fetchTraces(globalTime, request_string); } @@ -305,19 +292,10 @@ const _TraceFilter = (props: TraceFilterProps): JSX.Element => { 'ms'; form_basefilter.setFieldsValue({ latency: latencyButtonText }); - }, [traceFilters.latency, form_basefilter]); - - useEffect(() => { form_basefilter.setFieldsValue({ service: traceFilters.service }); - }, [traceFilters.service, form_basefilter]); - - useEffect(() => { form_basefilter.setFieldsValue({ operation: traceFilters.operation }); - }, [traceFilters.operation, form_basefilter]); - - useEffect(() => { form_basefilter.setFieldsValue({ kind: traceFilters.kind }); - }, [traceFilters.kind, form_basefilter]); + }, [traceFilters, form_basefilter]); const onLatencyButtonClick = (): void => { setModalVisible(true); @@ -438,8 +416,6 @@ const _TraceFilter = (props: TraceFilterProps): JSX.Element => { - {/* // What will be the empty state of card when there is no Tag , it should show something */} - Select Service to get Tag suggestions
{ - const { loading, maxTime, minTime } = useSelector( +const MetricsApplication = ({ + getInitialData, + resetInitialData, +}: MetricsProps): JSX.Element => { + const { selectedTime } = useSelector( (state) => state.globalTime, ); - const { error, errorMessage } = useSelector( - (state) => state.metrics, - ); + const { error, errorMessage, metricsApplicationLoading } = useSelector< + AppState, + MetricReducer + >((state) => state.metrics); const { servicename } = useParams(); - const dispatch = useDispatch>(); - useEffect(() => { - if (servicename !== undefined && loading == false) { + if (servicename !== undefined) { getInitialData({ - end: maxTime, - service: servicename, - start: minTime, - step: 60, + selectedTimeInterval: selectedTime, + serviceName: servicename, }); } - return (): void => { - // setting the data to it's initial this will avoid the re-rendering the graph - dispatch({ - type: 'GET_INTIAL_APPLICATION_DATA', - payload: { - serviceOverview: [], - topEndPoints: [], - }, - }); + return () => { + resetInitialData(); }; - }, [servicename, maxTime, minTime, getInitialData, loading, dispatch]); + }, [servicename, getInitialData, selectedTime]); + + if (metricsApplicationLoading) { + return ; + } if (error) { return {errorMessage}; } - if (loading) { - return ; - } - return ; }; interface DispatchProps { getInitialData: (props: GetInitialDataProps) => void; + resetInitialData: () => void; } interface ServiceProps { @@ -72,6 +67,7 @@ const mapDispatchToProps = ( dispatch: ThunkDispatch, ): DispatchProps => ({ getInitialData: bindActionCreators(GetInitialData, dispatch), + resetInitialData: bindActionCreators(ResetInitialData, dispatch), }); type MetricsProps = DispatchProps; diff --git a/frontend/src/pages/Metrics/index.tsx b/frontend/src/pages/Metrics/index.tsx index aeed58319f..cc0cfa328f 100644 --- a/frontend/src/pages/Metrics/index.tsx +++ b/frontend/src/pages/Metrics/index.tsx @@ -5,16 +5,17 @@ import React, { useEffect } from 'react'; import { connect, useSelector } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; -import { GetService, GetServiceProps } from 'store/actions'; +import { GetService, GetServiceProps } from 'store/actions/metrics'; import { AppState } from 'store/reducers'; import AppActions from 'types/actions'; import { GlobalReducer } from 'types/reducer/globalTime'; import MetricReducer from 'types/reducer/metrics'; const Metrics = ({ getService }: MetricsProps): JSX.Element => { - const { minTime, maxTime, loading } = useSelector( - (state) => state.globalTime, - ); + const { minTime, maxTime, loading, selectedTime } = useSelector< + AppState, + GlobalReducer + >((state) => state.globalTime); const { services } = useSelector( (state) => state.metrics, ); @@ -24,11 +25,10 @@ const Metrics = ({ getService }: MetricsProps): JSX.Element => { useEffect(() => { if (loading === false) { getService({ - start: minTime, - end: maxTime, + selectedTimeInterval: selectedTime, }); } - }, [getService, maxTime, minTime, loading]); + }, [getService, loading, selectedTime]); useEffect(() => { let timeInterval: NodeJS.Timeout; @@ -36,8 +36,7 @@ const Metrics = ({ getService }: MetricsProps): JSX.Element => { if (loading === false && !isSkipped && services.length === 0) { timeInterval = setInterval(() => { getService({ - start: minTime, - end: maxTime, + selectedTimeInterval: selectedTime, }); }, 50000); } @@ -45,7 +44,7 @@ const Metrics = ({ getService }: MetricsProps): JSX.Element => { return (): void => { clearInterval(timeInterval); }; - }, [getService, isSkipped, loading, maxTime, minTime, services]); + }, [getService, isSkipped, loading, maxTime, minTime, services, selectedTime]); if (loading) { return ; @@ -55,10 +54,9 @@ const Metrics = ({ getService }: MetricsProps): JSX.Element => { }; interface DispatchProps { - getService: ({ - end, - start, - }: GetServiceProps) => (dispatch: Dispatch) => void; + getService: ( + props: GetServiceProps, + ) => (dispatch: Dispatch, getState: () => AppState) => void; } const mapDispatchToProps = ( diff --git a/frontend/src/pages/SignUp/index.tsx b/frontend/src/pages/SignUp/index.tsx index 3a15a98026..e451850aad 100644 --- a/frontend/src/pages/SignUp/index.tsx +++ b/frontend/src/pages/SignUp/index.tsx @@ -6,7 +6,7 @@ import React, { useState } from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; -import { GlobalTimeLoading, UserLoggedIn } from 'store/actions'; +import { UserLoggedIn } from 'store/actions'; import AppActions from 'types/actions'; import { @@ -17,7 +17,7 @@ import { Title, } from './styles'; -const Signup = ({ globalLoading, loggedIn }: SignupProps): JSX.Element => { +const Signup = ({ loggedIn }: SignupProps): JSX.Element => { const [state, setState] = useState({ submitted: false }); const [formState, setFormState] = useState({ firstName: { value: '' }, @@ -59,7 +59,6 @@ const Signup = ({ globalLoading, loggedIn }: SignupProps): JSX.Element => { if (response.statusCode === 200) { loggedIn(); - globalLoading(); history.push(ROUTES.APPLICATION); } else { // @TODO throw a error notification here @@ -125,14 +124,12 @@ const Signup = ({ globalLoading, loggedIn }: SignupProps): JSX.Element => { }; interface DispatchProps { - globalLoading: () => void; loggedIn: () => void; } const mapDispatchToProps = ( dispatch: ThunkDispatch, ): DispatchProps => ({ - globalLoading: bindActionCreators(GlobalTimeLoading, dispatch), loggedIn: bindActionCreators(UserLoggedIn, dispatch), }); diff --git a/frontend/src/pages/TraceDetails/index.tsx b/frontend/src/pages/TraceDetails/index.tsx new file mode 100644 index 0000000000..247a87201a --- /dev/null +++ b/frontend/src/pages/TraceDetails/index.tsx @@ -0,0 +1,76 @@ +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, + ResetRaceData, + GetInitialTraceDataProps, +} 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]); + + 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/global.ts b/frontend/src/store/actions/global.ts index 1b5e8e11d8..8299a94fb1 100644 --- a/frontend/src/store/actions/global.ts +++ b/frontend/src/store/actions/global.ts @@ -1,5 +1,5 @@ import { Time } from 'container/Header/DateTimeSelection/config'; -import getMinAgo from 'lib/getStartAndEndTime/getMinAgo'; +import GetMinMax from 'lib/getMinMax'; import { Dispatch } from 'redux'; import AppActions from 'types/actions'; @@ -8,45 +8,14 @@ export const UpdateTimeInterval = ( dateTimeRange: [number, number] = [0, 0], ): ((dispatch: Dispatch) => void) => { return (dispatch: Dispatch): void => { - let maxTime = new Date().getTime(); - let minTime = 0; - - if (interval === '1min') { - const minTimeAgo = getMinAgo({ minutes: 1 }).getTime(); - minTime = minTimeAgo; - } else if (interval === '15min') { - const minTimeAgo = getMinAgo({ minutes: 15 }).getTime(); - minTime = minTimeAgo; - } else if (interval === '1hr') { - const minTimeAgo = getMinAgo({ minutes: 60 }).getTime(); - minTime = minTimeAgo; - } else if (interval === '30min') { - const minTimeAgo = getMinAgo({ minutes: 30 }).getTime(); - minTime = minTimeAgo; - } else if (interval === '5min') { - const minTimeAgo = getMinAgo({ minutes: 5 }).getTime(); - minTime = minTimeAgo; - } else if (interval === '1day') { - // one day = 24*60(min) - const minTimeAgo = getMinAgo({ minutes: 26 * 60 }).getTime(); - minTime = minTimeAgo; - } else if (interval === '1week') { - // one week = one day * 7 - const minTimeAgo = getMinAgo({ minutes: 26 * 60 * 7 }).getTime(); - minTime = minTimeAgo; - } else if (interval === '6hr') { - const minTimeAgo = getMinAgo({ minutes: 6 * 60 }).getTime(); - minTime = minTimeAgo; - } else if (interval === 'custom') { - maxTime = dateTimeRange[1]; - minTime = dateTimeRange[0]; - } + const { maxTime, minTime } = GetMinMax(interval, dateTimeRange); dispatch({ type: 'UPDATE_TIME_INTERVAL', payload: { - maxTime: maxTime * 1000000, // in nano sec, - minTime: minTime * 1000000, + maxTime: maxTime, + minTime: minTime, + selectedTime: interval, }, }); }; diff --git a/frontend/src/store/actions/metrics/getInitialData.ts b/frontend/src/store/actions/metrics/getInitialData.ts index 41c798a88b..18be7f6995 100644 --- a/frontend/src/store/actions/metrics/getInitialData.ts +++ b/frontend/src/store/actions/metrics/getInitialData.ts @@ -5,19 +5,38 @@ import getServiceOverview from 'api/metrics/getServiceOverview'; import getTopEndPoints from 'api/metrics/getTopEndPoints'; import { AxiosError } from 'axios'; +import GetMinMax from 'lib/getMinMax'; import { Dispatch } from 'redux'; +import { AppState } from 'store/reducers'; import AppActions from 'types/actions'; import { Props } from 'types/api/metrics/getDBOverview'; +import { GlobalReducer } from 'types/reducer/globalTime'; export const GetInitialData = ( props: GetInitialDataProps, -): ((dispatch: Dispatch) => void) => { - return async (dispatch: Dispatch): Promise => { +): ((dispatch: Dispatch, getState: () => AppState) => void) => { + return async (dispatch, getState): Promise => { try { + const { globalTime } = getState(); + + /** + * @description This is because we keeping the store as source of truth + */ + if (props.selectedTimeInterval !== globalTime.selectedTime) { + return; + } + dispatch({ type: 'GET_INITIAL_APPLICATION_LOADING', }); + const { maxTime, minTime } = GetMinMax(props.selectedTimeInterval, [ + globalTime.minTime / 1000000, + globalTime.maxTime / 1000000, + ]); + + const step = 60; + const [ // getDBOverViewResponse, // getExternalAverageDurationResponse, @@ -39,10 +58,15 @@ export const GetInitialData = ( // ...props, // }), getServiceOverview({ - ...props, + end: maxTime, + service: props.serviceName, + start: minTime, + step, }), getTopEndPoints({ - ...props, + end: maxTime, + service: props.serviceName, + start: minTime, }), ]); @@ -91,4 +115,7 @@ export const GetInitialData = ( }; }; -export type GetInitialDataProps = Props; +export interface GetInitialDataProps { + serviceName: Props['service']; + selectedTimeInterval: GlobalReducer['selectedTime']; +} diff --git a/frontend/src/store/actions/metrics/getService.ts b/frontend/src/store/actions/metrics/getService.ts index bb839cf2c6..eaf554a9d9 100644 --- a/frontend/src/store/actions/metrics/getService.ts +++ b/frontend/src/store/actions/metrics/getService.ts @@ -1,20 +1,35 @@ import getService from 'api/metrics/getService'; import { AxiosError } from 'axios'; +import GetMinMax from 'lib/getMinMax'; import { Dispatch } from 'redux'; +import { AppState } from 'store/reducers'; import AppActions from 'types/actions'; -import { Props } from 'types/api/metrics/getService'; +import { GlobalReducer } from 'types/reducer/globalTime'; -export const GetService = ({ - end, - start, -}: GetServiceProps): ((dispatch: Dispatch) => void) => { - return async (dispatch: Dispatch): Promise => { +export const GetService = ( + props: GetServiceProps, +): ((dispatch: Dispatch, getState: () => AppState) => void) => { + return async (dispatch, getState): Promise => { try { + const { globalTime } = getState(); + + if (props.selectedTimeInterval !== globalTime.selectedTime) { + return; + } + + const { maxTime, minTime } = GetMinMax(props.selectedTimeInterval, [ + globalTime.minTime / 1000000, + globalTime.maxTime / 1000000, + ]); + dispatch({ type: 'GET_SERVICE_LIST_LOADING_START', }); - const response = await getService({ end, start }); + const response = await getService({ + end: maxTime, + start: minTime, + }); if (response.statusCode === 200) { dispatch({ @@ -40,4 +55,6 @@ export const GetService = ({ }; }; -export type GetServiceProps = Props; +export type GetServiceProps = { + selectedTimeInterval: GlobalReducer['selectedTime']; +}; diff --git a/frontend/src/store/actions/metrics/resetInitialData.ts b/frontend/src/store/actions/metrics/resetInitialData.ts new file mode 100644 index 0000000000..58e77be111 --- /dev/null +++ b/frontend/src/store/actions/metrics/resetInitialData.ts @@ -0,0 +1,14 @@ +import { Dispatch } from 'redux'; +import { AppState } from 'store/reducers'; +import AppActions from 'types/actions'; + +export const ResetInitialData = (): (( + dispatch: Dispatch, + getState: () => AppState, +) => void) => { + return (dispatch, getState): void => { + dispatch({ + type: 'RESET_INITIAL_APPLICATION_DATA', + }); + }; +}; diff --git a/frontend/src/store/actions/trace/getInitialData.ts b/frontend/src/store/actions/trace/getInitialData.ts new file mode 100644 index 0000000000..13851614a8 --- /dev/null +++ b/frontend/src/store/actions/trace/getInitialData.ts @@ -0,0 +1,201 @@ +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/getTraceVisualAgrregates.ts b/frontend/src/store/actions/trace/getTraceVisualAgrregates.ts new file mode 100644 index 0000000000..fa12fc4e47 --- /dev/null +++ b/frontend/src/store/actions/trace/getTraceVisualAgrregates.ts @@ -0,0 +1,92 @@ +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 new file mode 100644 index 0000000000..09567a180d --- /dev/null +++ b/frontend/src/store/actions/trace/index.ts @@ -0,0 +1,9 @@ +export * from './getInitialData'; +export * from './updateSelectedAggOption'; +export * from './updateSelectedEntity'; +export * from './updateSelectedKind'; +export * from './updateSelectedLatency'; +export * from './updateSelectedOperation'; +export * from './updateSelectedService'; +export * from './updateSelectedTags'; +export * from './resetTraceDetails'; diff --git a/frontend/src/store/actions/trace/loadingCompleted.ts b/frontend/src/store/actions/trace/loadingCompleted.ts new file mode 100644 index 0000000000..3a64b2449d --- /dev/null +++ b/frontend/src/store/actions/trace/loadingCompleted.ts @@ -0,0 +1,12 @@ +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/resetTraceDetails.ts b/frontend/src/store/actions/trace/resetTraceDetails.ts new file mode 100644 index 0000000000..80b3e63bc9 --- /dev/null +++ b/frontend/src/store/actions/trace/resetTraceDetails.ts @@ -0,0 +1,10 @@ +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/updateSelectedAggOption.ts b/frontend/src/store/actions/trace/updateSelectedAggOption.ts new file mode 100644 index 0000000000..b5004e6a2b --- /dev/null +++ b/frontend/src/store/actions/trace/updateSelectedAggOption.ts @@ -0,0 +1,16 @@ +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 new file mode 100644 index 0000000000..d9371daa4f --- /dev/null +++ b/frontend/src/store/actions/trace/updateSelectedData.ts @@ -0,0 +1,164 @@ +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 new file mode 100644 index 0000000000..95b10baa6e --- /dev/null +++ b/frontend/src/store/actions/trace/updateSelectedEntity.ts @@ -0,0 +1,16 @@ +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 new file mode 100644 index 0000000000..5954599926 --- /dev/null +++ b/frontend/src/store/actions/trace/updateSelectedKind.ts @@ -0,0 +1,16 @@ +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 new file mode 100644 index 0000000000..636b2e84da --- /dev/null +++ b/frontend/src/store/actions/trace/updateSelectedLatency.ts @@ -0,0 +1,16 @@ +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 new file mode 100644 index 0000000000..e8e1a89f9c --- /dev/null +++ b/frontend/src/store/actions/trace/updateSelectedOperation.ts @@ -0,0 +1,16 @@ +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 new file mode 100644 index 0000000000..c936dfdc58 --- /dev/null +++ b/frontend/src/store/actions/trace/updateSelectedService.ts @@ -0,0 +1,16 @@ +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 new file mode 100644 index 0000000000..1f23c54fac --- /dev/null +++ b/frontend/src/store/actions/trace/updateSelectedTags.ts @@ -0,0 +1,94 @@ +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 new file mode 100644 index 0000000000..d5f87764ab --- /dev/null +++ b/frontend/src/store/actions/trace/updateSpanLoading.ts @@ -0,0 +1,16 @@ +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/reducers/global.ts b/frontend/src/store/reducers/global.ts index 104d3c6ba6..bb0e1fb764 100644 --- a/frontend/src/store/reducers/global.ts +++ b/frontend/src/store/reducers/global.ts @@ -1,3 +1,4 @@ +import { getDefaultOption } from 'container/Header/DateTimeSelection/config'; import { GLOBAL_TIME_LOADING_START, GlobalTimeAction, @@ -9,6 +10,7 @@ const intitalState: GlobalReducer = { maxTime: Date.now() * 1000000, minTime: (Date.now() - 15 * 60 * 1000) * 1000000, loading: true, + selectedTime: getDefaultOption(location.pathname), }; const globalTimeReducer = ( diff --git a/frontend/src/store/reducers/index.ts b/frontend/src/store/reducers/index.ts index adbb190968..6a89c4735a 100644 --- a/frontend/src/store/reducers/index.ts +++ b/frontend/src/store/reducers/index.ts @@ -6,6 +6,7 @@ import globalTimeReducer from './global'; import metricsReducers from './metric'; import { metricsReducer } from './metrics'; import { ServiceMapReducer } from './serviceMap'; +import { traceReducer } from './trace'; import TraceFilterReducer from './traceFilters'; import { traceItemReducer, tracesReducer } from './traces'; import { usageDataReducer } from './usage'; @@ -14,6 +15,7 @@ const reducers = combineReducers({ traceFilters: TraceFilterReducer, traces: tracesReducer, traceItem: traceItemReducer, + trace: traceReducer, usageDate: usageDataReducer, globalTime: globalTimeReducer, metricsData: metricsReducer, diff --git a/frontend/src/store/reducers/metric.ts b/frontend/src/store/reducers/metric.ts index dbe0178ade..18bb456c2f 100644 --- a/frontend/src/store/reducers/metric.ts +++ b/frontend/src/store/reducers/metric.ts @@ -5,6 +5,7 @@ import { GET_SERVICE_LIST_ERROR, GET_SERVICE_LIST_LOADING_START, GET_SERVICE_LIST_SUCCESS, + RESET_INITIAL_APPLICATION_DATA, MetricsActions, } from 'types/actions/metrics'; import InitialValueTypes from 'types/reducer/metrics'; @@ -12,7 +13,8 @@ import InitialValueTypes from 'types/reducer/metrics'; const InitialValue: InitialValueTypes = { error: false, errorMessage: '', - loading: false, + loading: true, + metricsApplicationLoading: true, services: [], dbOverView: [], externalService: [], @@ -56,18 +58,24 @@ const metrics = ( case GET_INITIAL_APPLICATION_LOADING: { return { ...state, - loading: true, + metricsApplicationLoading: true, }; } case GET_INITIAL_APPLICATION_ERROR: { return { ...state, - loading: false, + metricsApplicationLoading: false, errorMessage: action.payload.errorMessage, error: true, }; } + case RESET_INITIAL_APPLICATION_DATA: { + return { + ...InitialValue, + }; + } + case GET_INTIAL_APPLICATION_DATA: { const { // dbOverView, @@ -80,13 +88,13 @@ const metrics = ( return { ...state, - loading: false, // dbOverView, topEndPoints, serviceOverview, // externalService, // externalAverageDuration, // externalError, + metricsApplicationLoading: false, }; } default: diff --git a/frontend/src/store/reducers/trace.ts b/frontend/src/store/reducers/trace.ts new file mode 100644 index 0000000000..c19c27983c --- /dev/null +++ b/frontend/src/store/reducers/trace.ts @@ -0,0 +1,204 @@ +import { + GET_TRACE_INITIAL_DATA_ERROR, + GET_TRACE_INITIAL_DATA_SUCCESS, + GET_TRACE_LOADING_END, + GET_TRACE_LOADING_START, + TraceActions, + 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, + RESET_TRACE_DATA, + UPDATE_AGGREGATES, +} from 'types/actions/trace'; +import { TraceReducer } from 'types/reducer/trace'; + +const intitalState: TraceReducer = { + error: false, + errorMessage: '', + loading: true, + operationsList: [], + selectedKind: '', + selectedLatency: { + max: '', + min: '', + }, + selectedOperation: '', + selectedService: '', + selectedTags: [], + serviceList: [], + spanList: [], + tagsSuggestions: [], + selectedAggOption: 'count', + selectedEntity: 'calls', + spansAggregate: [], + spansLoading: false, +}; + +export const traceReducer = ( + state = intitalState, + action: TraceActions, +): TraceReducer => { + switch (action.type) { + case GET_TRACE_INITIAL_DATA_ERROR: { + return { + ...state, + errorMessage: action.payload.errorMessage, + loading: false, + error: true, + }; + } + + case GET_TRACE_LOADING_START: { + return { + ...state, + loading: true, + spansLoading: true, + }; + } + + case GET_TRACE_INITIAL_DATA_SUCCESS: { + const { + serviceList, + operationList, + tagsSuggestions, + selectedOperation, + selectedService, + selectedTags, + spansList, + selectedKind, + selectedLatency, + spansAggregate, + } = action.payload; + + return { + ...state, + serviceList: serviceList, + tagsSuggestions, + selectedOperation, + selectedService, + selectedTags, + spanList: spansList, + operationsList: operationList, + error: false, + selectedKind, + selectedLatency, + spansAggregate, + spansLoading: false, + }; + } + + case UPDATE_TRACE_SELECTED_KIND: { + return { + ...state, + selectedKind: action.payload.selectedKind, + }; + } + + case UPDATE_TRACE_SELECTED_LATENCY_VALUE: { + return { + ...state, + selectedLatency: action.payload.selectedLatency, + }; + } + + case UPDATE_TRACE_SELECTED_OPERATION: { + return { + ...state, + selectedOperation: action.payload.selectedOperation, + }; + } + + case UPDATE_TRACE_SELECTED_SERVICE: { + return { + ...state, + selectedService: action.payload.selectedService, + }; + } + + case UPDATE_TRACE_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: { + return { + ...state, + spansAggregate: action.payload.spansAggregate, + selectedAggOption: action.payload.selectedAggOption, + selectedEntity: action.payload.selectedEntity, + }; + } + + default: + return state; + } +}; diff --git a/frontend/src/store/reducers/traceFilters.ts b/frontend/src/store/reducers/traceFilters.ts index b18d031fbe..57dc93dad6 100644 --- a/frontend/src/store/reducers/traceFilters.ts +++ b/frontend/src/store/reducers/traceFilters.ts @@ -1,9 +1,11 @@ -import { ActionTypes, TraceFilters } from 'store/actions'; +import { TraceFilters } from 'store/actions/traceFilters'; +import { ActionTypes } from 'store/actions/types'; type ACTION = { type: ActionTypes; payload: TraceFilters; }; + const initialState: TraceFilters = { service: '', tags: [], diff --git a/frontend/src/types/actions/globalTime.ts b/frontend/src/types/actions/globalTime.ts index 31102f0139..8bc148fa1b 100644 --- a/frontend/src/types/actions/globalTime.ts +++ b/frontend/src/types/actions/globalTime.ts @@ -1,3 +1,5 @@ +import { Time } from 'container/Header/DateTimeSelection/config'; + export const UPDATE_TIME_INTERVAL = 'UPDATE_TIME_INTERVAL'; export const GLOBAL_TIME_LOADING_START = 'GLOBAL_TIME_LOADING_START'; @@ -6,9 +8,13 @@ export type GlobalTime = { minTime: number; }; +interface UpdateTime extends GlobalTime { + selectedTime: Time; +} + interface UpdateTimeInterval { type: typeof UPDATE_TIME_INTERVAL; - payload: GlobalTime; + payload: UpdateTime; } interface GlobalTimeLoading { diff --git a/frontend/src/types/actions/index.ts b/frontend/src/types/actions/index.ts index 618e2564b6..da5a26f5fd 100644 --- a/frontend/src/types/actions/index.ts +++ b/frontend/src/types/actions/index.ts @@ -2,11 +2,13 @@ import { AppAction } from './app'; import { DashboardActions } from './dashboard'; import { GlobalTimeAction } from './globalTime'; import { MetricsActions } from './metrics'; +import { TraceActions } from './trace'; type AppActions = | DashboardActions | AppAction | GlobalTimeAction - | MetricsActions; + | MetricsActions + | TraceActions; export default AppActions; diff --git a/frontend/src/types/actions/metrics.ts b/frontend/src/types/actions/metrics.ts index a2454be066..d9dd162218 100644 --- a/frontend/src/types/actions/metrics.ts +++ b/frontend/src/types/actions/metrics.ts @@ -13,7 +13,7 @@ export const GET_INITIAL_APPLICATION_LOADING = 'GET_INITIAL_APPLICATION_LOADING'; export const GET_INITIAL_APPLICATION_ERROR = 'GET_INITIAL_APPLICATION_ERROR'; export const GET_INTIAL_APPLICATION_DATA = 'GET_INTIAL_APPLICATION_DATA'; - +export const RESET_INITIAL_APPLICATION_DATA = 'RESET_INITIAL_APPLICATION_DATA'; export interface GetServiceList { type: typeof GET_SERVICE_LIST_SUCCESS; payload: ServicesList[]; @@ -44,8 +44,13 @@ export interface GetInitialApplicationData { }; } +export interface ResetInitialApplicationData { + type: typeof RESET_INITIAL_APPLICATION_DATA; +} + export type MetricsActions = | GetServiceListError | GetServiceListLoading | GetServiceList - | GetInitialApplicationData; + | GetInitialApplicationData + | ResetInitialApplicationData; diff --git a/frontend/src/types/actions/trace.ts b/frontend/src/types/actions/trace.ts new file mode 100644 index 0000000000..18a4115b7c --- /dev/null +++ b/frontend/src/types/actions/trace.ts @@ -0,0 +1,151 @@ +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; +} + +interface UpdateSpansLoading { + type: typeof UPDATE_SPANS_LOADING; + payload: { + loading: boolean; + }; +} + +interface GetTraceInitialData { + type: typeof GET_TRACE_INITIAL_DATA_SUCCESS; + 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; + 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']; + }; +} + +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; + payload: { + selectedTags: TraceReducer['selectedTags']; + spansList: TraceReducer['spanList']; + spansAggregate: TraceReducer['spansAggregate']; + }; +} + +interface UpdateSelectedAggOption { + type: typeof UPDATE_SELECTED_AGG_OPTION; + payload: { + selectedAggOption: TraceReducer['selectedAggOption']; + }; +} + +interface UpdateSelectedEntity { + type: typeof UPDATE_SELECTED_ENTITY; + payload: { + selectedEntity: TraceReducer['selectedEntity']; + }; +} + +interface UpdateAggregates { + type: typeof UPDATE_AGGREGATES; + payload: { + spansAggregate: TraceReducer['spansAggregate']; + selectedEntity: TraceReducer['selectedEntity']; + selectedAggOption: TraceReducer['selectedAggOption']; + }; +} + +interface ResetTraceData { + type: typeof RESET_TRACE_DATA; +} + +export type TraceActions = + | GetTraceLoading + | GetTraceInitialData + | GetTraceInitialDataError + | UpdateTraceSelectedService + | UpdateTraceSelectedLatencyValue + | UpdateTraceSelectedKind + | UpdateTraceSelectedOperation + | UpdateTraceSelectedTags + | UpdateSelectedDate + | UpdateSelectedAggOption + | UpdateSelectedEntity + | UpdateSpansLoading + | ResetTraceData + | UpdateAggregates; diff --git a/frontend/src/types/api/trace/getServiceList.ts b/frontend/src/types/api/trace/getServiceList.ts new file mode 100644 index 0000000000..b01bb5940f --- /dev/null +++ b/frontend/src/types/api/trace/getServiceList.ts @@ -0,0 +1 @@ +export type PayloadProps = string[]; diff --git a/frontend/src/types/api/trace/getServiceOperation.ts b/frontend/src/types/api/trace/getServiceOperation.ts new file mode 100644 index 0000000000..70460a3658 --- /dev/null +++ b/frontend/src/types/api/trace/getServiceOperation.ts @@ -0,0 +1,5 @@ +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 new file mode 100644 index 0000000000..f81f58b203 --- /dev/null +++ b/frontend/src/types/api/trace/getSpanAggregate.ts @@ -0,0 +1,20 @@ +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; +} + +interface Timestamp { + timestamp: number; + value: number; +} + +export type PayloadProps = Timestamp[]; diff --git a/frontend/src/types/api/trace/getSpans.ts b/frontend/src/types/api/trace/getSpans.ts new file mode 100644 index 0000000000..508e7abb9a --- /dev/null +++ b/frontend/src/types/api/trace/getSpans.ts @@ -0,0 +1,51 @@ +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[]; + +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; +} diff --git a/frontend/src/types/api/trace/getTags.ts b/frontend/src/types/api/trace/getTags.ts new file mode 100644 index 0000000000..9e6c3a4e2e --- /dev/null +++ b/frontend/src/types/api/trace/getTags.ts @@ -0,0 +1,10 @@ +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/common/index.ts b/frontend/src/types/common/index.ts index 7c5414ffc9..a55f38c1af 100644 --- a/frontend/src/types/common/index.ts +++ b/frontend/src/types/common/index.ts @@ -4,6 +4,8 @@ export type Success = 200; export type Forbidden = 403; +export type BadRequest = 400; + export type Unauthorized = 401; export type NotFound = 404; @@ -17,6 +19,7 @@ export type ErrorStatusCode = | Forbidden | Unauthorized | NotFound - | ServerError; + | ServerError + | BadRequest; export type StatusCode = SuccessStatusCode | ErrorStatusCode; diff --git a/frontend/src/types/reducer/globalTime.ts b/frontend/src/types/reducer/globalTime.ts index d24bf26a6c..788b8447b4 100644 --- a/frontend/src/types/reducer/globalTime.ts +++ b/frontend/src/types/reducer/globalTime.ts @@ -1,7 +1,9 @@ +import { Time } from 'container/Header/DateTimeSelection/config'; import { GlobalTime } from 'types/actions/globalTime'; export interface GlobalReducer { maxTime: GlobalTime['maxTime']; minTime: GlobalTime['minTime']; loading: boolean; + selectedTime: Time; } diff --git a/frontend/src/types/reducer/metrics.ts b/frontend/src/types/reducer/metrics.ts index 1fd1e7fe9f..3519954f60 100644 --- a/frontend/src/types/reducer/metrics.ts +++ b/frontend/src/types/reducer/metrics.ts @@ -9,6 +9,7 @@ import { TopEndPoints } from 'types/api/metrics/getTopEndPoints'; interface MetricReducer { services: ServicesList[]; loading: boolean; + metricsApplicationLoading: boolean; error: boolean; errorMessage: string; dbOverView: DBOverView[]; diff --git a/frontend/src/types/reducer/trace.ts b/frontend/src/types/reducer/trace.ts new file mode 100644 index 0000000000..318a3678ba --- /dev/null +++ b/frontend/src/types/reducer/trace.ts @@ -0,0 +1,37 @@ +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; +} + +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; +} diff --git a/frontend/src/typings/react-graph-vis.d.ts b/frontend/src/typings/react-graph-vis.d.ts index afe97d6fdd..c19a3c6301 100644 --- a/frontend/src/typings/react-graph-vis.d.ts +++ b/frontend/src/typings/react-graph-vis.d.ts @@ -6,7 +6,7 @@ declare module 'react-graph-vis' { export { Network, NetworkEvents, Options, Node, Edge, DataSet } from 'vis'; export interface graphEvents { - [event: NetworkEvents]: (params?: any) => void; + [event: NetworkEvents]: (params) => void; } //Doesn't appear that this module supports passing in a vis.DataSet directly. Once it does graph can just use the Data object from vis. diff --git a/frontend/src/utils/app.ts b/frontend/src/utils/app.ts index f2d5b49d37..e183a806c1 100644 --- a/frontend/src/utils/app.ts +++ b/frontend/src/utils/app.ts @@ -1,5 +1,5 @@ import { SKIP_ONBOARDING } from 'constants/onboarding'; -export const isOnboardingSkipped = () => { +export const isOnboardingSkipped = (): boolean => { return localStorage.getItem(SKIP_ONBOARDING) === 'true'; }; diff --git a/frontend/src/wdyr.ts b/frontend/src/wdyr.ts new file mode 100644 index 0000000000..8ba678a4d6 --- /dev/null +++ b/frontend/src/wdyr.ts @@ -0,0 +1,15 @@ +/// +// ^ https://github.com/welldone-software/why-did-you-render/issues/161 +import React from 'react'; + +if (process.env.NODE_ENV === 'development') { + const whyDidYouRender = require('@welldone-software/why-did-you-render'); + whyDidYouRender(React, { + trackAllPureComponents: false, + trackExtraHooks: [[require('react-redux/lib'), 'useSelector']], + include: [/^ConnectFunction/], + logOnDifferentValues: true, + }); +} + +export default ''; diff --git a/frontend/webpack.config.prod.ts b/frontend/webpack.config.prod.ts index f9e1da993c..09fb6f1ce0 100644 --- a/frontend/webpack.config.prod.ts +++ b/frontend/webpack.config.prod.ts @@ -4,8 +4,7 @@ import CopyPlugin from 'copy-webpack-plugin'; import HtmlWebpackPlugin from 'html-webpack-plugin'; import { resolve } from 'path'; import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin'; -import webpack from 'webpack'; -import { WebpackPluginInstance } from 'webpack-dev-middleware/node_modules/webpack'; +import webpack, { WebpackPluginInstance } from 'webpack'; const __dirname = resolve(); @@ -14,7 +13,7 @@ const config: webpack.Configuration = { devtool: 'source-map', entry: resolve(__dirname, './src/index.tsx'), output: { - filename: ({ chunk }: any): string => { + filename: ({ chunk }): string => { const hash = chunk?.hash; const name = chunk?.name; return `js/${name}-${hash}.js`; @@ -53,12 +52,12 @@ const config: webpack.Configuration = { }, plugins: [ new HtmlWebpackPlugin({ template: 'src/index.html.ejs' }), - new CompressionPlugin({ + (new CompressionPlugin({ exclude: /.map$/, - }) as any, - new CopyPlugin({ + }) as unknown) as WebpackPluginInstance, + (new CopyPlugin({ patterns: [{ from: resolve(__dirname, 'public/'), to: '.' }], - }) as any, + }) as unknown) as WebpackPluginInstance, new webpack.ProvidePlugin({ process: 'process/browser', }), @@ -71,4 +70,4 @@ const config: webpack.Configuration = { }, }; -export default config; \ No newline at end of file +export default config; diff --git a/frontend/webpack.config.ts b/frontend/webpack.config.ts index 534a4919fa..3434e5a2a3 100644 --- a/frontend/webpack.config.ts +++ b/frontend/webpack.config.ts @@ -2,13 +2,12 @@ import dotenv from 'dotenv'; import HtmlWebpackPlugin from 'html-webpack-plugin'; import { resolve } from 'path'; +//@ts-ignore +import portFinderSync from 'portfinder-sync'; import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin'; import webpack from 'webpack'; import { Configuration as WebpackDevServerConfiguration } from 'webpack-dev-server'; -// @ts-ignore -import portFinderSync from 'portfinder-sync'; - dotenv.config(); const __dirname = resolve(); @@ -29,10 +28,10 @@ const config: Configuration = { liveReload: true, port: portFinderSync.getPort(3000), static: { - directory: resolve(__dirname, "public"), - publicPath: "/", + directory: resolve(__dirname, 'public'), + publicPath: '/', watch: true, - } + }, }, target: 'web', output: { @@ -86,4 +85,4 @@ const config: Configuration = { }, }; -export default config; \ No newline at end of file +export default config; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 70f3955ae0..ffc8bfb404 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -3118,6 +3118,13 @@ resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.5.1.tgz#b5fde2f0f79c1e120307c415a4c1d5eb15a6f278" integrity sha512-4vSVUiOPJLmr45S8rMGy7WDvpWxfFxfP/Qx/cxZFCfvoypTYpPPL1X8VIZMe0WTA+Jr7blUxwUSEZNkjoMTgSw== +"@welldone-software/why-did-you-render@^6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@welldone-software/why-did-you-render/-/why-did-you-render-6.2.1.tgz#6a87926cc8386b748dc07341cf495caa5be1db28" + integrity sha512-eIVKeK6ueS3tuzCqMVTaaNrPYvb9cA8NHiNgLA7Op8SD4TiT31zqNjxmhzLEK+y3sBxcwr6YhsiQGX9EThrvaw== + dependencies: + lodash "^4" + "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" @@ -5373,10 +5380,10 @@ custom-event-polyfill@^1.0.6: resolved "https://registry.yarnpkg.com/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz#9bc993ddda937c1a30ccd335614c6c58c4f87aee" integrity sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w== -cypress@8.6.0: - version "8.6.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-8.6.0.tgz#8d02fa58878b37cfc45bbfce393aa974fa8a8e22" - integrity sha512-F7qEK/6Go5FsqTueR+0wEw2vOVKNgk5847Mys8vsWkzPoEKdxs+7N9Y1dit+zhaZCLtMPyrMwjfA53ZFy+lSww== +cypress@^8.3.0: + version "8.7.0" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-8.7.0.tgz#2ee371f383d8f233d3425b6cc26ddeec2668b6da" + integrity sha512-b1bMC3VQydC6sXzBMFnSqcvwc9dTZMgcaOzT0vpSD+Gq1yFc+72JDWi55sfUK5eIeNLAtWOGy1NNb6UlhMvB+Q== dependencies: "@cypress/request" "^2.88.6" "@cypress/xvfb" "^1.2.4" @@ -9929,7 +9936,7 @@ lodash.uniq@^4.3.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -"lodash@>=3.5 <5", lodash@>=4.17.21, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: +"lodash@>=3.5 <5", lodash@>=4.17.21, lodash@^4, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -14658,7 +14665,7 @@ uuid@^2.0.1: resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" integrity sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho= -uuid@^3.3.2, uuid@^3.4.0: +uuid@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==