diff --git a/frontend/src/AppRoutes/index.tsx b/frontend/src/AppRoutes/index.tsx index 78ce774500..777098951a 100644 --- a/frontend/src/AppRoutes/index.tsx +++ b/frontend/src/AppRoutes/index.tsx @@ -1,8 +1,7 @@ import NotFound from 'components/NotFound'; import Spinner from 'components/Spinner'; +import AppLayout from 'container/AppLayout'; import history from 'lib/history'; -import AppLayout from 'modules/AppLayout'; -import { RouteProvider } from 'modules/RouteProvider'; import React, { Suspense } from 'react'; import { Route, Router, Switch } from 'react-router-dom'; @@ -10,20 +9,18 @@ import routes from './routes'; const App = (): JSX.Element => ( - - - }> - - {routes.map(({ path, component, exact }, index) => { - return ( - - ); - })} - - - - - + + }> + + {routes.map(({ path, component, exact }, index) => { + return ( + + ); + })} + + + + ); diff --git a/frontend/src/AppRoutes/pageComponents.ts b/frontend/src/AppRoutes/pageComponents.ts index 7a351c744b..84b92d6f15 100644 --- a/frontend/src/AppRoutes/pageComponents.ts +++ b/frontend/src/AppRoutes/pageComponents.ts @@ -1,9 +1,13 @@ import Loadable from 'components/Loadable'; +export const ServicesTablePage = Loadable( + () => import(/* webpackChunkName: "ServicesTablePage" */ 'pages/Metrics'), +); + export const ServiceMetricsPage = Loadable( () => import( - /* webpackChunkName: "ServiceMetricsPage" */ 'modules/Metrics/ServiceMetricsDef' + /* webpackChunkName: "ServiceMetricsPage" */ 'pages/MetricApplication' ), ); @@ -35,13 +39,6 @@ export const UsageExplorerPage = Loadable( ), ); -export const ServicesTablePage = Loadable( - () => - import( - /* webpackChunkName: "ServicesTablePage" */ 'modules/Metrics/ServicesTableDef' - ), -); - export const SignupPage = Loadable( () => import(/* webpackChunkName: "SignupPage" */ 'pages/SignUp'), ); diff --git a/frontend/src/api/browser/localstorage/get.ts b/frontend/src/api/browser/localstorage/get.ts new file mode 100644 index 0000000000..675d6995c4 --- /dev/null +++ b/frontend/src/api/browser/localstorage/get.ts @@ -0,0 +1,5 @@ +const get = (key: string): string | null => { + return localStorage.getItem(key); +}; + +export default get; diff --git a/frontend/src/api/browser/localstorage/remove.ts b/frontend/src/api/browser/localstorage/remove.ts new file mode 100644 index 0000000000..6e0546ff1e --- /dev/null +++ b/frontend/src/api/browser/localstorage/remove.ts @@ -0,0 +1,5 @@ +const remove = (key: string): void => { + window.localStorage.removeItem(key); +}; + +export default remove; diff --git a/frontend/src/api/browser/localstorage/set.ts b/frontend/src/api/browser/localstorage/set.ts new file mode 100644 index 0000000000..b0910a0c96 --- /dev/null +++ b/frontend/src/api/browser/localstorage/set.ts @@ -0,0 +1,5 @@ +const set = (key: string, value: string): void => { + localStorage.setItem(key, value); +}; + +export default set; diff --git a/frontend/src/api/metrics/getDBOverView.ts b/frontend/src/api/metrics/getDBOverView.ts new file mode 100644 index 0000000000..7afd56d75d --- /dev/null +++ b/frontend/src/api/metrics/getDBOverView.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/metrics/getDBOverview'; + +const getDBOverView = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const response = await axios.get( + `/service/dbOverview?&start=${props.start}&end=${props.end}&service=${props.service}&step=${props.step}`, + ); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default getDBOverView; diff --git a/frontend/src/api/metrics/getExternalAverageDuration.ts b/frontend/src/api/metrics/getExternalAverageDuration.ts new file mode 100644 index 0000000000..51be375d94 --- /dev/null +++ b/frontend/src/api/metrics/getExternalAverageDuration.ts @@ -0,0 +1,29 @@ +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/metrics/getExternalAverageDuration'; + +const getExternalAverageDuration = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const response = await axios.get( + `/service/externalAvgDuration?&start=${props.start}&end=${props.end}&service=${props.service}&step=${props.step}`, + ); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default getExternalAverageDuration; diff --git a/frontend/src/api/metrics/getExternalError.ts b/frontend/src/api/metrics/getExternalError.ts new file mode 100644 index 0000000000..3587639bb9 --- /dev/null +++ b/frontend/src/api/metrics/getExternalError.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/metrics/getExternalError'; + +const getExternalError = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const response = await axios.get( + `/service/externalErrors?&start=${props.start}&end=${props.end}&service=${props.service}&step=${props.step}`, + ); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default getExternalError; diff --git a/frontend/src/api/metrics/getExternalService.ts b/frontend/src/api/metrics/getExternalService.ts new file mode 100644 index 0000000000..de9bf65173 --- /dev/null +++ b/frontend/src/api/metrics/getExternalService.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/metrics/getExternalService'; + +const getExternalService = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const response = await axios.get( + `/service/external?&start=${props.start}&end=${props.end}&service=${props.service}&step=${props.step}`, + ); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default getExternalService; diff --git a/frontend/src/api/metrics/getService.ts b/frontend/src/api/metrics/getService.ts new file mode 100644 index 0000000000..29909b2905 --- /dev/null +++ b/frontend/src/api/metrics/getService.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/metrics/getService'; + +const getService = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const response = await axios.get( + `/services?&start=${props.start}&end=${props.end}`, + ); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default getService; diff --git a/frontend/src/api/metrics/getServiceOverview.ts b/frontend/src/api/metrics/getServiceOverview.ts new file mode 100644 index 0000000000..3ceb794e0e --- /dev/null +++ b/frontend/src/api/metrics/getServiceOverview.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/metrics/getServiceOverview'; + +const getServiceOverview = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const response = await axios.get( + `/service/overview?&start=${props.start}&end=${props.end}&service=${props.service}&step=${props.step}`, + ); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default getServiceOverview; diff --git a/frontend/src/api/metrics/getTopEndPoints.ts b/frontend/src/api/metrics/getTopEndPoints.ts new file mode 100644 index 0000000000..a2973d1866 --- /dev/null +++ b/frontend/src/api/metrics/getTopEndPoints.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/metrics/getTopEndPoints'; + +const getTopEndPoints = async ( + props: Props, +): Promise | ErrorResponse> => { + try { + const response = await axios.get( + `/service/top_endpoints?&start=${props.start}&end=${props.end}&service=${props.service}`, + ); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default getTopEndPoints; diff --git a/frontend/src/components/Graph/index.tsx b/frontend/src/components/Graph/index.tsx index ee1fcce187..a7a901bc06 100644 --- a/frontend/src/components/Graph/index.tsx +++ b/frontend/src/components/Graph/index.tsx @@ -1,8 +1,11 @@ import { + ActiveElement, BarController, BarElement, CategoryScale, Chart, + ChartData, + ChartEvent, ChartOptions, ChartType, Decimation, @@ -13,7 +16,6 @@ import { LineController, LineElement, PointElement, - ScaleOptions, SubTitle, TimeScale, TimeSeriesScale, @@ -53,8 +55,6 @@ const Graph = ({ type, title, isStacked, - label, - xAxisType, onClickHandler, }: GraphProps): JSX.Element => { const { isDarkMode } = useSelector((state) => state.app); @@ -97,25 +97,18 @@ const Graph = ({ legend: { // just making sure that label is present display: !( - data.datasets.find((e) => e.label !== undefined) === undefined + data.datasets.find((e) => { + if (e.label?.length === 0) { + return false; + } + return e.label !== undefined; + }) === undefined ), labels: { usePointStyle: true, pointStyle: 'circle', }, position: 'bottom', - // labels: { - // generateLabels: (chart: Chart): LegendItem[] => { - // return (data.datasets || []).map((e, index) => { - // return { - // text: e.label || '', - // datasetIndex: index, - // }; - // }); - // }, - // pointStyle: 'circle', - // usePointStyle: true, - // }, }, }, layout: { @@ -123,16 +116,17 @@ const Graph = ({ }, scales: { x: { - animate: false, grid: { display: true, color: getGridColor(), }, - labels: label, adapters: { date: chartjsAdapter, }, - type: xAxisType, + time: { + unit: 'minute', + }, + type: 'timeseries', }, y: { display: true, @@ -151,77 +145,26 @@ const Graph = ({ cubicInterpolationMode: 'monotone', }, }, - onClick: onClickHandler, + onClick: (event, element, chart) => { + if (onClickHandler) { + onClickHandler(event, element, chart, data); + } + }, }; lineChartRef.current = new Chart(chartRef.current, { type: type, data: data, options, - // plugins: [ - // { - // id: 'htmlLegendPlugin', - // afterUpdate: (chart: Chart): void => { - // if ( - // chart && - // chart.options && - // chart.options.plugins && - // chart.options.plugins.legend && - // chart.options.plugins.legend.labels && - // chart.options.plugins.legend.labels.generateLabels - // ) { - // const labels = chart.options.plugins?.legend?.labels?.generateLabels( - // chart, - // ); - - // const id = 'htmlLegend'; - - // const response = document.getElementById(id); - - // if (labels && response && response?.childNodes.length === 0) { - // const labelComponent = labels.map((e, index) => { - // return { - // element: Legends({ - // text: e.text, - // color: colors[index] || 'white', - // }), - // dataIndex: e.datasetIndex, - // }; - // }); - - // labelComponent.map((e) => { - // const el = stringToHTML(e.element); - - // if (el) { - // el.addEventListener('click', () => { - // chart.setDatasetVisibility( - // e.dataIndex, - // !chart.isDatasetVisible(e.dataIndex), - // ); - // chart.update(); - // }); - // response.append(el); - // } - // }); - // } - // } - // }, - // }, - // ], }); } - }, [chartRef, data, type, title, isStacked, label, xAxisType, getGridColor]); + }, [chartRef, data, type, title, isStacked, getGridColor, onClickHandler]); useEffect(() => { buildChart(); }, [buildChart]); - return ( - <> - - {/* */} - > - ); + return ; }; interface GraphProps { @@ -230,8 +173,14 @@ interface GraphProps { title?: string; isStacked?: boolean; label?: string[]; - xAxisType?: ScaleOptions['type']; - onClickHandler?: ChartOptions['onClick']; + onClickHandler?: graphOnClickHandler; } +export type graphOnClickHandler = ( + event: ChartEvent, + elements: ActiveElement[], + chart: Chart, + data: ChartData, +) => void; + export default Graph; diff --git a/frontend/src/modules/AppLayout.tsx b/frontend/src/container/AppLayout/index.tsx similarity index 68% rename from frontend/src/modules/AppLayout.tsx rename to frontend/src/container/AppLayout/index.tsx index f2f707cf31..66cc5f1fc4 100644 --- a/frontend/src/modules/AppLayout.tsx +++ b/frontend/src/container/AppLayout/index.tsx @@ -1,39 +1,38 @@ import { Layout } from 'antd'; -import SideNav from 'components/SideNav'; +import get from 'api/browser/localstorage/get'; import ROUTES from 'constants/routes'; +import TopNav from 'container/Header'; +import SideNav from 'container/SideNav'; import history from 'lib/history'; import React, { ReactNode, useEffect } from 'react'; import { useSelector } from 'react-redux'; -import { useLocation } from 'react-router-dom'; import { AppState } from 'store/reducers'; import AppReducer from 'types/reducer/app'; -import TopNav from './Nav/TopNav'; -import { useRoute } from './RouteProvider'; - const { Content, Footer } = Layout; - interface BaseLayoutProps { children: ReactNode; } const BaseLayout: React.FC = ({ children }) => { - const location = useLocation(); - const { dispatch } = useRoute(); - const currentYear = new Date().getFullYear(); const { isLoggedIn } = useSelector((state) => state.app); + const isLoggedInLocalStorage = get('isLoggedIn'); useEffect(() => { - dispatch({ type: 'ROUTE_IS_LOADED', payload: location.pathname }); - }, [location, dispatch]); + if (isLoggedIn && history.location.pathname === '/') { + history.push(ROUTES.APPLICATION); + } - useEffect(() => { - if (isLoggedIn) { + if (!isLoggedIn && isLoggedInLocalStorage !== null) { history.push(ROUTES.APPLICATION); } else { - history.push(ROUTES.SIGN_UP); + if (isLoggedInLocalStorage === null) { + history.push(ROUTES.SIGN_UP); + } } - }, [isLoggedIn]); + }, [isLoggedIn, isLoggedInLocalStorage]); + + const currentYear = new Date().getFullYear(); return ( diff --git a/frontend/src/container/AppLayout/styles.ts b/frontend/src/container/AppLayout/styles.ts new file mode 100644 index 0000000000..5a3a409479 --- /dev/null +++ b/frontend/src/container/AppLayout/styles.ts @@ -0,0 +1,26 @@ +import { Layout as LayoutComponent } from 'antd'; +import styled from 'styled-components'; + +export const Layout = styled.div` + &&& { + min-height: 100vh; + display: flex; + } +`; + +export const Content = styled(LayoutComponent.Content)` + &&& { + margin: 0 1rem; + } +`; + +export const Footer = styled(LayoutComponent.Footer)` + &&& { + text-align: center; + font-size: 0.7rem; + } +`; + +export const Main = styled.main` + min-height: 80vh; +`; diff --git a/frontend/src/container/GridGraphComponent/index.tsx b/frontend/src/container/GridGraphComponent/index.tsx index 4831bf7794..94da26c2f2 100644 --- a/frontend/src/container/GridGraphComponent/index.tsx +++ b/frontend/src/container/GridGraphComponent/index.tsx @@ -1,6 +1,6 @@ import { Typography } from 'antd'; -import { ChartData } from 'chart.js'; -import Graph from 'components/Graph'; +import { ChartData, ChartOptions } from 'chart.js'; +import Graph, { graphOnClickHandler } from 'components/Graph'; import ValueGraph from 'components/ValueGraph'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import history from 'lib/history'; @@ -14,6 +14,7 @@ const GridGraphComponent = ({ title, opacity, isStacked, + onClickHandler, }: GridGraphComponentProps): JSX.Element | null => { const location = history.location.pathname; @@ -29,6 +30,7 @@ const GridGraphComponent = ({ isStacked, opacity, xAxisType: 'time', + onClickHandler: onClickHandler, }} /> ); @@ -66,6 +68,7 @@ export interface GridGraphComponentProps { title?: string; opacity?: string; isStacked?: boolean; + onClickHandler?: graphOnClickHandler; } export default GridGraphComponent; diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/EmptyGraph.tsx b/frontend/src/container/GridGraphLayout/Graph/FullView/EmptyGraph.tsx new file mode 100644 index 0000000000..3a6144ac9f --- /dev/null +++ b/frontend/src/container/GridGraphLayout/Graph/FullView/EmptyGraph.tsx @@ -0,0 +1,90 @@ +import Graph, { graphOnClickHandler } from 'components/Graph'; +import { timePreferance } from 'container/NewWidget/RightContainer/timeItems'; +import GetMaxMinTime from 'lib/getMaxMinTime'; +import { colors } from 'lib/getRandomColor'; +import getStartAndEndTime from 'lib/getStartAndEndTime'; +import getTimeString from 'lib/getTimeString'; +import React, { useCallback } from 'react'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { Widgets } from 'types/api/dashboard/getAll'; +import { GlobalReducer } from 'types/reducer/globalTime'; + +const EmptyGraph = ({ + selectedTime, + widget, + onClickHandler, +}: EmptyGraphProps): JSX.Element => { + const { minTime, maxTime, loading } = useSelector( + (state) => state.globalTime, + ); + + const maxMinTime = GetMaxMinTime({ + graphType: widget.panelTypes, + maxTime, + minTime, + }); + + const { end, start } = getStartAndEndTime({ + type: selectedTime.enum, + maxTime: maxMinTime.maxTime, + minTime: maxMinTime.minTime, + }); + + const dateFunction = useCallback(() => { + if (!loading) { + const dates: Date[] = []; + + const startString = getTimeString(start); + const endString = getTimeString(end); + + const parsedStart = parseInt(startString, 10); + const parsedEnd = parseInt(endString, 10); + + let startDate = parsedStart; + const endDate = parsedEnd; + + while (endDate >= startDate) { + const newDate = new Date(startDate); + + startDate = startDate + 20000; + + dates.push(newDate); + } + return dates; + } + return []; + }, [start, end, loading]); + + const date = dateFunction(); + + return ( + + ); +}; + +interface EmptyGraphProps { + selectedTime: timePreferance; + widget: Widgets; + onClickHandler: graphOnClickHandler | undefined; +} + +export default EmptyGraph; diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/index.tsx b/frontend/src/container/GridGraphLayout/Graph/FullView/index.tsx index 5f9310d312..d053e621d4 100644 --- a/frontend/src/container/GridGraphLayout/Graph/FullView/index.tsx +++ b/frontend/src/container/GridGraphLayout/Graph/FullView/index.tsx @@ -2,6 +2,7 @@ import { Button, Typography } from 'antd'; import getQueryResult from 'api/widgets/getQuery'; import { AxiosError } from 'axios'; import { ChartData } from 'chart.js'; +import { graphOnClickHandler } from 'components/Graph'; import Spinner from 'components/Spinner'; import TimePreference from 'components/TimePreferenceDropDown'; import GridGraphComponent from 'container/GridGraphComponent'; @@ -12,15 +13,21 @@ import { import getChartData from 'lib/getChartData'; import GetMaxMinTime from 'lib/getMaxMinTime'; import getStartAndEndTime from 'lib/getStartAndEndTime'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, { memo, useCallback, useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; -import { GlobalTime } from 'store/actions'; import { AppState } from 'store/reducers'; +import { GlobalTime } from 'types/actions/globalTime'; import { Widgets } from 'types/api/dashboard/getAll'; +import EmptyGraph from './EmptyGraph'; import { GraphContainer, NotFoundContainer, TimeContainer } from './styles'; -const FullView = ({ widget }: FullViewProps): JSX.Element => { +const FullView = ({ + widget, + fullViewOptions = true, + onClickHandler, + noDataGraph = false, +}: FullViewProps): JSX.Element => { const { minTime, maxTime } = useSelector( (state) => state.globalTime, ); @@ -65,7 +72,7 @@ const FullView = ({ widget }: FullViewProps): JSX.Element => { end, query: query.query, start: start, - step: '30', + step: '60', }); return { query: query.query, @@ -118,39 +125,69 @@ const FullView = ({ widget }: FullViewProps): JSX.Element => { onFetchDataHandler(); }, [onFetchDataHandler]); + if (state.error && !state.loading) { + return ( + + {state.errorMessage} + + ); + } + if (state.loading || state.payload === undefined) { - return ; + return ( + + + + ); } if (state.loading === false && state.payload.datasets.length === 0) { return ( <> - - - No Data - + {fullViewOptions && ( + + + + Refresh + + + )} + + {noDataGraph ? ( + + ) : ( + + No Data + + )} > ); } return ( <> - - - - Refresh - - + {fullViewOptions && ( + + + + Refresh + + + )} { isStacked: widget.isStacked, opacity: widget.opacity, title: widget.title, + onClickHandler: onClickHandler, }} /> @@ -176,6 +214,20 @@ interface FullViewState { interface FullViewProps { widget: Widgets; + fullViewOptions?: boolean; + onClickHandler?: graphOnClickHandler; + noDataGraph?: boolean; } -export default FullView; +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; +}); diff --git a/frontend/src/container/GridGraphLayout/Graph/FullView/styles.ts b/frontend/src/container/GridGraphLayout/Graph/FullView/styles.ts index 10d21cd4da..7b85723a29 100644 --- a/frontend/src/container/GridGraphLayout/Graph/FullView/styles.ts +++ b/frontend/src/container/GridGraphLayout/Graph/FullView/styles.ts @@ -6,6 +6,7 @@ export const GraphContainer = styled.div` justify-content: center; display: flex; flex-direction: column; + width: 100%; `; export const NotFoundContainer = styled.div` diff --git a/frontend/src/container/GridGraphLayout/Graph/index.tsx b/frontend/src/container/GridGraphLayout/Graph/index.tsx index 05adbd4654..e1a21c383c 100644 --- a/frontend/src/container/GridGraphLayout/Graph/index.tsx +++ b/frontend/src/container/GridGraphLayout/Graph/index.tsx @@ -12,13 +12,13 @@ import { useSelector } from 'react-redux'; import { connect } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; -import { GlobalTime } from 'store/actions'; import { DeleteWidget, DeleteWidgetProps, } from 'store/actions/dashboard/deleteWidget'; import { AppState } from 'store/reducers'; import AppActions from 'types/actions'; +import { GlobalTime } from 'types/actions/globalTime'; import { Widgets } from 'types/api/dashboard/getAll'; import Bar from './Bar'; @@ -65,7 +65,7 @@ const GridCardGraph = ({ end, query: query.query, start: start, - step: '30', + step: '60', }); return { diff --git a/frontend/src/container/Header/Breadcrumbs/index.tsx b/frontend/src/container/Header/Breadcrumbs/index.tsx new file mode 100644 index 0000000000..b28a933af4 --- /dev/null +++ b/frontend/src/container/Header/Breadcrumbs/index.tsx @@ -0,0 +1,48 @@ +import { Breadcrumb } from 'antd'; +import ROUTES from 'constants/routes'; +import React from 'react'; +import { Link } from 'react-router-dom'; + +const breadcrumbNameMap = { + [ROUTES.APPLICATION]: 'Application', + [ROUTES.TRACES]: 'Traces', + [ROUTES.SERVICE_MAP]: 'Service Map', + [ROUTES.USAGE_EXPLORER]: 'Usage Explorer', + [ROUTES.INSTRUMENTATION]: 'Add instrumentation', + [ROUTES.SETTINGS]: 'Settings', + [ROUTES.DASHBOARD]: 'Dashboard', +}; + +import { RouteComponentProps, withRouter } from 'react-router'; + +const ShowBreadcrumbs = (props: RouteComponentProps): JSX.Element => { + const pathArray = props.location.pathname.split('/').filter((i) => i); + + const extraBreadcrumbItems = pathArray.map((_, index) => { + const url = `/${pathArray.slice(0, index + 1).join('/')}`; + + if (breadcrumbNameMap[url] === undefined) { + return ( + + {url.split('/').slice(-1)[0]} + + ); + } else { + return ( + + {breadcrumbNameMap[url]} + + ); + } + }); + + const breadcrumbItems = [ + + Home + , + ].concat(extraBreadcrumbItems); + + return {breadcrumbItems}; +}; + +export default withRouter(ShowBreadcrumbs); diff --git a/frontend/src/modules/Nav/TopNav/CustomDateTimeModal.tsx b/frontend/src/container/Header/CustomDateTimeModal/index.tsx similarity index 81% rename from frontend/src/modules/Nav/TopNav/CustomDateTimeModal.tsx rename to frontend/src/container/Header/CustomDateTimeModal/index.tsx index f1ce19078a..8ebb25bd32 100644 --- a/frontend/src/modules/Nav/TopNav/CustomDateTimeModal.tsx +++ b/frontend/src/container/Header/CustomDateTimeModal/index.tsx @@ -2,22 +2,15 @@ import { DatePicker, Modal } from 'antd'; import { Moment } from 'moment'; import moment from 'moment'; import React, { useState } from 'react'; -import { DateTimeRangeType } from 'store/actions'; +export type DateTimeRangeType = [Moment | null, Moment | null] | null; const { RangePicker } = DatePicker; -interface CustomDateTimeModalProps { - visible: boolean; - onCreate: (dateTimeRange: DateTimeRangeType) => void; //Store is defined in antd forms library - onCancel: () => void; -} - -const CustomDateTimeModal: React.FC = ({ - //destructuring props +const CustomDateTimeModal = ({ visible, onCreate, onCancel, -}) => { +}: CustomDateTimeModalProps): JSX.Element => { const [ customDateTimeRange, setCustomDateTimeRange, @@ -26,6 +19,7 @@ const CustomDateTimeModal: React.FC = ({ function handleRangePickerOk(date_time: DateTimeRangeType): void { setCustomDateTimeRange(date_time); } + function disabledDate(current: Moment): boolean { if (current > moment()) { return true; @@ -53,4 +47,10 @@ const CustomDateTimeModal: React.FC = ({ ); }; +interface CustomDateTimeModalProps { + visible: boolean; + onCreate: (dateTimeRange: DateTimeRangeType) => void; + onCancel: () => void; +} + export default CustomDateTimeModal; diff --git a/frontend/src/container/Header/DateTimeSelection/config.ts b/frontend/src/container/Header/DateTimeSelection/config.ts new file mode 100644 index 0000000000..500267ac1c --- /dev/null +++ b/frontend/src/container/Header/DateTimeSelection/config.ts @@ -0,0 +1,60 @@ +import ROUTES from 'constants/routes'; + +type fiveMin = '5min'; +type fifteenMin = '15min'; +type thrityMin = '30min'; +type oneMin = '1min'; +type sixHour = '6hr'; +type oneHour = '1hr'; +type oneDay = '1day'; +type oneWeek = '1week'; +type custom = 'custom'; + +export type Time = + | fiveMin + | fifteenMin + | thrityMin + | oneMin + | sixHour + | oneHour + | custom + | oneWeek + | oneDay; + +export const Options: Option[] = [ + { value: '5min', label: 'Last 5 min' }, + { value: '15min', label: 'Last 15 min' }, + { value: '30min', label: 'Last 30 min' }, + { value: '1hr', label: 'Last 1 hour' }, + { value: '6hr', label: 'Last 6 hour' }, + { value: '1day', label: 'Last 1 day' }, + { value: '1week', label: 'Last 1 week' }, + { value: 'custom', label: 'Custom' }, +]; + +export interface Option { + value: Time; + label: string; +} + +export const ServiceMapOptions: Option[] = [ + { value: '1min', label: 'Last 1 min' }, + { value: '5min', label: 'Last 5 min' }, +]; + +export const getDefaultOption = (route: string): Time => { + if (route === ROUTES.SERVICE_MAP) { + return ServiceMapOptions[0].value; + } + if (route === ROUTES.APPLICATION) { + return Options[0].value; + } + return Options[2].value; +}; + +export const getOptions = (routes: string): Option[] => { + if (routes === ROUTES.SERVICE_MAP) { + return ServiceMapOptions; + } + return Options; +}; diff --git a/frontend/src/container/Header/DateTimeSelection/index.tsx b/frontend/src/container/Header/DateTimeSelection/index.tsx new file mode 100644 index 0000000000..2cfd86d758 --- /dev/null +++ b/frontend/src/container/Header/DateTimeSelection/index.tsx @@ -0,0 +1,328 @@ +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'; +const { Option } = DefaultSelect; +import get from 'api/browser/localstorage/get'; +import set from 'api/browser/localstorage/set'; +import { LOCAL_STORAGE } from 'constants/localStorage'; +import getTimeString from 'lib/getTimeString'; +import moment from 'moment'; +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 { AppState } from 'store/reducers'; +import AppActions from 'types/actions'; +import { GlobalReducer } from 'types/reducer/globalTime'; + +import CustomDateTimeModal, { DateTimeRangeType } from '../CustomDateTimeModal'; + +const DateTimeSelection = ({ + location, + updateTimeInterval, +}: Props): JSX.Element => { + const [form_dtselector] = Form.useForm(); + 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 getDefaultTime = (pathName: string): Time => { + const defaultSelectedOption = getDefaultOption(pathName); + + const routes = get(LOCAL_STORAGE.METRICS_TIME_IN_DURATION); + + if (routes !== null) { + const routesObject = JSON.parse(routes); + const selectedTime = routesObject[pathName]; + + if (selectedTime) { + return selectedTime; + } + } + + return defaultSelectedOption; + }; + + const [selectedTimeInterval, setSelectedTimeInterval] = useState( + getDefaultTime(location.pathname), + ); + + const updateLocalStorageForRoutes = (value: Time): void => { + const preRoutes = get(LOCAL_STORAGE.METRICS_TIME_IN_DURATION); + if (preRoutes !== null) { + const preRoutesObject = JSON.parse(preRoutes); + + const preRoute = { + ...preRoutesObject, + }; + preRoute[location.pathname] = value; + + set(LOCAL_STORAGE.METRICS_TIME_IN_DURATION, JSON.stringify(preRoute)); + } + }; + + const onSelectHandler = (value: Time): void => { + isOnSelectHandler.current = true; + + if (value !== 'custom') { + updateTimeInterval(value); + const selectedLabel = getInputLabel(undefined, undefined, value); + setSelectedTimeInterval(selectedLabel as Time); + updateLocalStorageForRoutes(value); + } else { + setRefreshButtonHidden(true); + setCustomDTPickerVisible(true); + } + }; + + const onRefreshHandler = (): void => { + onSelectHandler(selectedTimeInterval); + onLastRefreshHandler(); + }; + + const getInputLabel = ( + startTime?: moment.Moment, + endTime?: moment.Moment, + timeInterval: Time = '15min', + ): string | Time => { + if (startTime && endTime && timeInterval === 'custom') { + const format = 'YYYY/MM/DD HH:mm'; + + const startString = startTime.format(format); + const endString = endTime.format(format); + + return `${startString} - ${endString}`; + } + + return timeInterval; + }; + + const onLastRefreshHandler = useCallback(() => { + const currentTime = moment(); + + const lastRefresh = moment( + selectedTimeInterval === 'custom' ? minTime / 1000000 : maxTime / 1000000, + ); + const duration = moment.duration(currentTime.diff(lastRefresh)); + + const secondsDiff = Math.floor(duration.asSeconds()); + const minutedDiff = Math.floor(duration.asMinutes()); + const hoursDiff = Math.floor(duration.asHours()); + const daysDiff = Math.floor(duration.asDays()); + const monthsDiff = Math.floor(duration.asMonths()); + + if (monthsDiff > 0) { + return `Last refresh -${monthsDiff} months ago`; + } + + if (daysDiff > 0) { + return `Last refresh - ${daysDiff} days ago`; + } + + if (hoursDiff > 0) { + return `Last refresh - ${hoursDiff} hrs ago`; + } + + if (minutedDiff > 0) { + return `Last refresh - ${minutedDiff} mins ago`; + } + + return `Last refresh - ${secondsDiff} sec ago`; + }, [maxTime, minTime, selectedTimeInterval]); + + const onCustomDateHandler = (dateTimeRange: DateTimeRangeType): void => { + if (dateTimeRange !== null) { + const [startTimeMoment, endTimeMoment] = dateTimeRange; + if (startTimeMoment && endTimeMoment) { + setSelectedTimeInterval('custom'); + setStartTime(startTimeMoment); + setEndTime(endTimeMoment); + setCustomDTPickerVisible(false); + updateTimeInterval('custom', [ + startTimeMoment?.toDate().getTime() || 0, + endTimeMoment?.toDate().getTime() || 0, + ]); + set('startTime', startTimeMoment.toString()); + set('endTime', endTimeMoment.toString()); + updateLocalStorageForRoutes('custom'); + } + } + }; + + // 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); + + if (metricsTimeDuration === null) { + 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 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()); + } + } else { + isOnSelectHandler.current = false; + } + }, [ + location.pathname, + location.search, + startTime, + endTime, + updateTimeInterval, + selectedTimeInterval, + loading, + ]); + + return ( + + + onSelectHandler(value as Time)} + value={getInputLabel(startTime, endTime, selectedTimeInterval)} + > + {options.map(({ value, label }) => ( + + {label} + + ))} + + + + + Refresh + + + + + + {refreshText} + + + { + setCustomDTPickerVisible(false); + }} + /> + + ); +}; + +interface DispatchProps { + updateTimeInterval: ( + interval: Time, + dateTimeRange?: [number, number], + ) => (dispatch: Dispatch) => void; + // globalTimeLoading: () => void; +} + +const mapDispatchToProps = ( + dispatch: ThunkDispatch, +): DispatchProps => ({ + updateTimeInterval: bindActionCreators(UpdateTimeInterval, dispatch), + // globalTimeLoading: bindActionCreators(GlobalTimeLoading, dispatch), +}); + +type Props = DispatchProps & RouteComponentProps; + +export default connect(null, mapDispatchToProps)(withRouter(DateTimeSelection)); diff --git a/frontend/src/container/Header/DateTimeSelection/styles.ts b/frontend/src/container/Header/DateTimeSelection/styles.ts new file mode 100644 index 0000000000..7993831894 --- /dev/null +++ b/frontend/src/container/Header/DateTimeSelection/styles.ts @@ -0,0 +1,28 @@ +import { Form as FormComponent, Typography as TypographyComponent } from 'antd'; +import styled from 'styled-components'; + +export const Container = styled.div` + justify-content: flex-end; +`; + +export const Form = styled(FormComponent)` + &&& { + justify-content: flex-end; + } +`; + +export const Typography = styled(TypographyComponent)` + &&& { + text-align: right; + } +`; + +export const FormItem = styled(Form.Item)` + &&& { + margin: 0; + } +`; + +export const RefreshTextContainer = styled.div` + min-height: 2rem; +`; diff --git a/frontend/src/modules/Nav/TopNav/index.tsx b/frontend/src/container/Header/index.tsx similarity index 65% rename from frontend/src/modules/Nav/TopNav/index.tsx rename to frontend/src/container/Header/index.tsx index e9f2ba6c33..ef6359f075 100644 --- a/frontend/src/modules/Nav/TopNav/index.tsx +++ b/frontend/src/container/Header/index.tsx @@ -1,17 +1,19 @@ -import { Col, Row } from 'antd'; +import { Col } from 'antd'; import ROUTES from 'constants/routes'; import history from 'lib/history'; import React from 'react'; -import DateTimeSelector from './DateTimeSelector'; -import ShowBreadcrumbs from './ShowBreadcrumbs'; +import ShowBreadcrumbs from './Breadcrumbs'; +import DateTimeSelector from './DateTimeSelection'; +import { Container } from './styles'; const TopNav = (): JSX.Element | null => { if (history.location.pathname === ROUTES.SIGN_UP) { return null; } + return ( - + @@ -19,7 +21,7 @@ const TopNav = (): JSX.Element | null => { - + ); }; diff --git a/frontend/src/container/Header/styles.ts b/frontend/src/container/Header/styles.ts new file mode 100644 index 0000000000..ef3cb15c37 --- /dev/null +++ b/frontend/src/container/Header/styles.ts @@ -0,0 +1,8 @@ +import { Row } from 'antd'; +import styled from 'styled-components'; + +export const Container = styled(Row)` + &&& { + margin-top: 2rem; + } +`; diff --git a/frontend/src/modules/Nav/TopNav/utils.ts b/frontend/src/container/Header/utils.ts similarity index 100% rename from frontend/src/modules/Nav/TopNav/utils.ts rename to frontend/src/container/Header/utils.ts diff --git a/frontend/src/container/MetricsApplication/Tabs/Application.tsx b/frontend/src/container/MetricsApplication/Tabs/Application.tsx new file mode 100644 index 0000000000..122a6a24dd --- /dev/null +++ b/frontend/src/container/MetricsApplication/Tabs/Application.tsx @@ -0,0 +1,255 @@ +import { ActiveElement, Chart, ChartData, ChartEvent } from 'chart.js'; +import Graph from 'components/Graph'; +import { METRICS_PAGE_QUERY_PARAM } from 'constants/query'; +import ROUTES from 'constants/routes'; +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 { 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'; + +import { Card, Col, GraphContainer, GraphTitle, Row } from '../styles'; +import TopEndpointsTable from '../TopEndpointsTable'; +import { Button } from './styles'; + +const Application = ({ + globalLoading, + getWidget, +}: DashboardProps): JSX.Element => { + const { servicename } = useParams<{ servicename?: string }>(); + const selectedTimeStamp = useRef(0); + + const { topEndPoints, serviceOverview } = useSelector( + (state) => state.metrics, + ); + + const onTracePopupClick = (timestamp: number): void => { + const currentTime = timestamp; + const tPlusOne = timestamp + 1 * 60 * 1000; + + const urlParams = new URLSearchParams(); + urlParams.set(METRICS_PAGE_QUERY_PARAM.startTime, currentTime.toString()); + urlParams.set(METRICS_PAGE_QUERY_PARAM.endTime, tPlusOne.toString()); + if (servicename) { + urlParams.set(METRICS_PAGE_QUERY_PARAM.service, servicename); + } + + globalLoading(); + history.push(`${ROUTES.TRACES}?${urlParams.toString()}`); + }; + + const onClickhandler = async ( + event: ChartEvent, + elements: ActiveElement[], + chart: Chart, + data: ChartData, + from: string, + ): Promise => { + if (event.native) { + const points = chart.getElementsAtEventForMode( + event.native, + 'nearest', + { intersect: true }, + true, + ); + + const id = `${from}_button`; + const buttonElement = document.getElementById(id); + + if (points.length !== 0) { + const firstPoint = points[0]; + + if (data.labels) { + const time = data?.labels[firstPoint.index] as Date; + + if (buttonElement) { + buttonElement.style.display = 'block'; + buttonElement.style.left = `${firstPoint.element.x}px`; + buttonElement.style.top = `${firstPoint.element.y}px`; + selectedTimeStamp.current = new Date(time).getTime(); + } + } + } else { + if (buttonElement && buttonElement.style.display === 'block') { + buttonElement.style.display = 'none'; + } + } + } + }; + + const onErrorTrackHandler = (timestamp: number): void => { + const currentTime = timestamp; + const tPlusOne = timestamp + 1 * 60 * 1000; + + const urlParams = new URLSearchParams(); + urlParams.set(METRICS_PAGE_QUERY_PARAM.startTime, currentTime.toString()); + urlParams.set(METRICS_PAGE_QUERY_PARAM.endTime, tPlusOne.toString()); + if (servicename) { + urlParams.set(METRICS_PAGE_QUERY_PARAM.service, servicename); + } + urlParams.set(METRICS_PAGE_QUERY_PARAM.error, 'true'); + + globalLoading(); + history.push(`${ROUTES.TRACES}?${urlParams.toString()}`); + }; + + return ( + <> + + + { + onTracePopupClick(selectedTimeStamp.current); + }} + > + View Traces + + + Application latency in ms + + { + onClickhandler(ChartEvent, activeElements, chart, data, 'Application'); + }} + type="line" + data={{ + datasets: [ + { + data: serviceOverview.map((e) => e.p99), + borderColor: colors[0], + label: 'p99 Latency', + showLine: true, + borderWidth: 1.5, + spanGaps: true, + pointRadius: 1.5, + }, + { + data: serviceOverview.map((e) => e.p95), + borderColor: colors[1], + label: 'p95 Latency', + showLine: true, + borderWidth: 1.5, + spanGaps: true, + pointRadius: 1.5, + }, + { + data: serviceOverview.map((e) => e.p50), + borderColor: colors[2], + label: 'p50 Latency', + showLine: true, + borderWidth: 1.5, + spanGaps: true, + pointRadius: 1.5, + }, + ], + labels: serviceOverview.map((e) => { + return new Date(e.timestamp / 1000000); + }), + }} + /> + + + + + + { + onTracePopupClick(selectedTimeStamp.current); + }} + > + View Traces + + + Request per sec + + { + onClickhandler(event, element, chart, data, 'Request'); + }} + widget={getWidget([ + { + query: `sum(rate(signoz_latency_count{service_name="${servicename}", span_kind="SPAN_KIND_SERVER"}[2m]))`, + legend: 'Request per second', + }, + ])} + /> + + + + + + + { + onErrorTrackHandler(selectedTimeStamp.current); + }} + > + View Traces + + + + + Error Percentage (%) + + { + onClickhandler(ChartEvent, activeElements, chart, data, 'Error'); + }} + widget={getWidget([ + { + query: `sum(rate(signoz_calls_total{service_name="${servicename}", span_kind="SPAN_KIND_SERVER", status_code="STATUS_CODE_ERROR"}[1m]) OR rate(signoz_calls_total{service_name="${servicename}", span_kind="SPAN_KIND_SERVER", http_status_code=~"5.."}[1m]) OR vector(0))*100/sum(rate(signoz_calls_total{service_name="${servicename}", span_kind="SPAN_KIND_SERVER"}[1m]))`, + legend: 'Error Percentage (%)', + }, + ])} + /> + + + + + + + + + + + + > + ); +}; + +interface DispatchProps { + globalLoading: () => void; +} + +const mapDispatchToProps = ( + dispatch: ThunkDispatch, +): DispatchProps => ({ + globalLoading: bindActionCreators(GlobalTimeLoading, dispatch), +}); + +interface DashboardProps extends DispatchProps { + getWidget: (query: Widgets['query']) => Widgets; +} + +export default connect(null, mapDispatchToProps)(Application); diff --git a/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx b/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx new file mode 100644 index 0000000000..45c081d1da --- /dev/null +++ b/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx @@ -0,0 +1,59 @@ +import { Col } from 'antd'; +import FullView from 'container/GridGraphLayout/Graph/FullView'; +import React from 'react'; +import { useParams } from 'react-router-dom'; +import { Widgets } from 'types/api/dashboard/getAll'; + +import { Card, GraphContainer, GraphTitle, Row } from '../styles'; + +const DBCall = ({ getWidget }: DBCallProps): JSX.Element => { + const { servicename } = useParams<{ servicename?: string }>(); + + return ( + <> + + + + Database Calls RPS + + + + + + + + + Database Calls Avg Duration (in ms) + + + + + + + > + ); +}; + +interface DBCallProps { + getWidget: (query: Widgets['query']) => Widgets; +} + +export default DBCall; diff --git a/frontend/src/container/MetricsApplication/Tabs/External.tsx b/frontend/src/container/MetricsApplication/Tabs/External.tsx new file mode 100644 index 0000000000..0274657733 --- /dev/null +++ b/frontend/src/container/MetricsApplication/Tabs/External.tsx @@ -0,0 +1,97 @@ +import { Col } from 'antd'; +import FullView from 'container/GridGraphLayout/Graph/FullView'; +import React from 'react'; +import { useParams } from 'react-router-dom'; +import { Widgets } from 'types/api/dashboard/getAll'; + +import { Card, GraphContainer, GraphTitle, Row } from '../styles'; + +const External = ({ getWidget }: ExternalProps): JSX.Element => { + const { servicename } = useParams<{ servicename?: string }>(); + + return ( + <> + + + + External Call Error Percentage (%) + + + + + + + + + External Call duration + + + + + + + + + + + External Call RPS(by Address) + + + + + + + + + External Call duration(by Address) + + + + + + + > + ); +}; + +interface ExternalProps { + getWidget: (query: Widgets['query']) => Widgets; +} + +export default External; diff --git a/frontend/src/container/MetricsApplication/Tabs/styles.ts b/frontend/src/container/MetricsApplication/Tabs/styles.ts new file mode 100644 index 0000000000..017ab95419 --- /dev/null +++ b/frontend/src/container/MetricsApplication/Tabs/styles.ts @@ -0,0 +1,10 @@ +import { Button as ButtonComponent } from 'antd'; +import styled from 'styled-components'; + +export const Button = styled(ButtonComponent)` + &&& { + position: absolute; + z-index: 999; + display: none; + } +`; diff --git a/frontend/src/container/MetricsApplication/TopEndpointsTable.tsx b/frontend/src/container/MetricsApplication/TopEndpointsTable.tsx new file mode 100644 index 0000000000..7a82d7d2f2 --- /dev/null +++ b/frontend/src/container/MetricsApplication/TopEndpointsTable.tsx @@ -0,0 +1,122 @@ +import { Button, Table, Tooltip } from 'antd'; +import { ColumnsType } from 'antd/lib/table'; +import { METRICS_PAGE_QUERY_PARAM } from 'constants/query'; +import React from 'react'; +import { connect, 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 => { + const { minTime, maxTime } = useSelector( + (state) => state.globalTime, + ); + + const history = useHistory(); + const params = useParams<{ servicename: string }>(); + + const handleOnClick = (operation: string): void => { + const urlParams = new URLSearchParams(); + const { servicename } = params; + urlParams.set( + METRICS_PAGE_QUERY_PARAM.startTime, + String(Number(minTime) / 1000000), + ); + urlParams.set( + METRICS_PAGE_QUERY_PARAM.endTime, + String(Number(maxTime) / 1000000), + ); + 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()}`); + }; + + const columns: ColumnsType = [ + { + title: 'Name', + dataIndex: 'name', + key: 'name', + + // eslint-disable-next-line react/display-name + render: (text: string): JSX.Element => ( + + handleOnClick(text)} + > + {text} + + + ), + }, + { + title: 'P50 (in ms)', + dataIndex: 'p50', + key: 'p50', + sorter: (a: DataProps, b: DataProps): number => a.p50 - b.p50, + render: (value: number): string => (value / 1000000).toFixed(2), + }, + { + title: 'P95 (in ms)', + dataIndex: 'p95', + key: 'p95', + sorter: (a: DataProps, b: DataProps): number => a.p95 - b.p95, + render: (value: number): string => (value / 1000000).toFixed(2), + }, + { + title: 'P99 (in ms)', + dataIndex: 'p99', + key: 'p99', + sorter: (a: DataProps, b: DataProps): number => a.p99 - b.p99, + render: (value: number): string => (value / 1000000).toFixed(2), + }, + { + title: 'Number of Calls', + dataIndex: 'numCalls', + key: 'numCalls', + sorter: (a: topEndpointListItem, b: topEndpointListItem): number => + a.numCalls - b.numCalls, + }, + ]; + + return ( + { + return 'Top Endpoints'; + }} + dataSource={props.data} + columns={columns} + pagination={false} + rowKey="name" + /> + ); +}; + +type DataProps = topEndpointListItem; + +interface DispatchProps { + globalTimeLoading: () => void; +} + +const mapDispatchToProps = ( + dispatch: ThunkDispatch, +): DispatchProps => ({ + globalTimeLoading: bindActionCreators(GlobalTimeLoading, dispatch), +}); + +interface TopEndpointsTableProps extends DispatchProps { + data: topEndpointListItem[]; +} + +export default connect(null, mapDispatchToProps)(TopEndpointsTable); diff --git a/frontend/src/container/MetricsApplication/index.tsx b/frontend/src/container/MetricsApplication/index.tsx new file mode 100644 index 0000000000..6fcda7c9e9 --- /dev/null +++ b/frontend/src/container/MetricsApplication/index.tsx @@ -0,0 +1,50 @@ +import { Tabs } from 'antd'; +import React from 'react'; +import { Widgets } from 'types/api/dashboard/getAll'; + +import Application from './Tabs/Application'; +import DBCall from './Tabs/DBCall'; +import External from './Tabs/External'; + +const { TabPane } = Tabs; + +const ServiceMetrics = (): JSX.Element => { + const getWidget = (query: Widgets['query']): Widgets => { + return { + description: '', + id: '', + isStacked: false, + nullZeroValues: '', + opacity: '0', + panelTypes: 'TIME_SERIES', + query: query, + queryData: { + data: [], + error: false, + errorMessage: '', + loading: false, + }, + timePreferance: 'GLOBAL_TIME', + title: '', + stepSize: 60, + }; + }; + + return ( + + + + + + + + + + + + + + ); +}; + +export default ServiceMetrics; diff --git a/frontend/src/container/MetricsApplication/styles.ts b/frontend/src/container/MetricsApplication/styles.ts new file mode 100644 index 0000000000..836be987b5 --- /dev/null +++ b/frontend/src/container/MetricsApplication/styles.ts @@ -0,0 +1,46 @@ +import { + Card as CardComponent, + Col as ColComponent, + Row as RowComponent, + Typography, +} from 'antd'; +import styled from 'styled-components'; + +export const Card = styled(CardComponent)` + &&& { + padding: 10px; + } + + .ant-card-body { + padding: 0; + min-height: 40vh; + } +`; + +export const Row = styled(RowComponent)` + &&& { + padding: 1rem; + } +`; + +export const Col = styled(ColComponent)` + display &&& { + position: relative; + } +`; + +export const GraphContainer = styled.div` + min-height: 40vh; + max-height: 40vh; + + div { + min-height: 40vh; + max-height: 40vh; + } +`; + +export const GraphTitle = styled(Typography)` + &&& { + text-align: center; + } +`; diff --git a/frontend/src/container/MetricsTable/SkipOnBoardModal/index.tsx b/frontend/src/container/MetricsTable/SkipOnBoardModal/index.tsx new file mode 100644 index 0000000000..a63440022a --- /dev/null +++ b/frontend/src/container/MetricsTable/SkipOnBoardModal/index.tsx @@ -0,0 +1,48 @@ +import { Button, Typography } from 'antd'; +import Modal from 'components/Modal'; +import React from 'react'; + +const SkipOnBoardingModal = ({ onContinueClick }: Props): JSX.Element => { + return ( + + Continue without instrumentation + , + ]} + > + <> + + + No instrumentation data. + + Please instrument your application as mentioned + + here + + + + > + + ); +}; + +interface Props { + onContinueClick: () => void; +} + +export default SkipOnBoardingModal; diff --git a/frontend/src/container/MetricsTable/index.tsx b/frontend/src/container/MetricsTable/index.tsx new file mode 100644 index 0000000000..40074d13f2 --- /dev/null +++ b/frontend/src/container/MetricsTable/index.tsx @@ -0,0 +1,105 @@ +import Table, { ColumnsType } from 'antd/lib/table'; +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 { 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 [skipOnboarding, setSkipOnboarding] = useState( + localStorage.getItem(SKIP_ONBOARDING) === 'true', + ); + + const { services, loading, error } = useSelector( + (state) => state.metrics, + ); + + const onContinueClick = (): void => { + localStorage.setItem(SKIP_ONBOARDING, 'true'); + setSkipOnboarding(true); + }; + + const onClickHandler = (to: string): void => { + history.push(to); + globalTimeLoading(); + }; + + if ( + (services.length === 0 && loading === false && !skipOnboarding) || + (loading == false && error === true) + ) { + return ; + } + + const columns: ColumnsType = [ + { + title: 'Application', + dataIndex: 'serviceName', + key: 'serviceName', + // eslint-disable-next-line react/display-name + render: (text: string): JSX.Element => ( + onClickHandler(ROUTES.APPLICATION + '/' + text)}> + {text} + + ), + }, + { + title: 'P99 latency (in ms)', + dataIndex: 'p99', + key: 'p99', + sorter: (a: DataProps, b: DataProps): number => a.p99 - b.p99, + render: (value: number): string => (value / 1000000).toFixed(2), + }, + { + title: 'Error Rate (in %)', + dataIndex: 'errorRate', + key: 'errorRate', + sorter: (a: DataProps, b: DataProps): number => a.errorRate - b.errorRate, + render: (value: number): string => value.toFixed(2), + }, + { + title: 'Requests Per Second', + dataIndex: 'callRate', + key: 'callRate', + sorter: (a: DataProps, b: DataProps): number => a.callRate - b.callRate, + render: (value: number): string => value.toFixed(2), + }, + ]; + + return ( + + + + ); +}; + +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); diff --git a/frontend/src/container/MetricsTable/styles.ts b/frontend/src/container/MetricsTable/styles.ts new file mode 100644 index 0000000000..3b64520707 --- /dev/null +++ b/frontend/src/container/MetricsTable/styles.ts @@ -0,0 +1,15 @@ +import { Typography } from 'antd'; +import styled from 'styled-components'; + +export const Container = styled.div` + margin-top: 2rem; +`; + +export const Name = styled(Typography)` + &&& { + text-transform: capitalize; + font-weight: 600; + color: #177ddc; + cursor: pointer; + } +`; diff --git a/frontend/src/container/NewWidget/index.tsx b/frontend/src/container/NewWidget/index.tsx index 5bfb59641a..d91ba1d7b4 100644 --- a/frontend/src/container/NewWidget/index.tsx +++ b/frontend/src/container/NewWidget/index.tsx @@ -8,11 +8,7 @@ import { connect, useSelector } from 'react-redux'; import { useHistory, useLocation, useParams } from 'react-router'; import { bindActionCreators, Dispatch } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; -import { - ApplySettingsToPanel, - ApplySettingsToPanelProps, - GlobalTime, -} from 'store/actions'; +import { ApplySettingsToPanel, ApplySettingsToPanelProps } from 'store/actions'; import { GetQueryResults, GetQueryResultsProps, @@ -23,6 +19,7 @@ import { } from 'store/actions/dashboard/saveDashboard'; import { AppState } from 'store/reducers'; import AppActions from 'types/actions'; +import { GlobalTime } from 'types/actions/globalTime'; import DashboardReducer from 'types/reducer/dashboards'; import LeftContainer from './LeftContainer'; diff --git a/frontend/src/components/SideNav/index.tsx b/frontend/src/container/SideNav/index.tsx similarity index 84% rename from frontend/src/components/SideNav/index.tsx rename to frontend/src/container/SideNav/index.tsx index 439db6ed67..159ae95c1e 100644 --- a/frontend/src/components/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 { ToggleDarkMode } from 'store/actions'; +import { GlobalTimeLoading, 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 }: Props): JSX.Element => { +const SideNav = ({ toggleDarkMode, globalTimeLoading }: Props): JSX.Element => { const [collapsed, setCollapsed] = useState(false); const { pathname } = useLocation(); const { isDarkMode } = useSelector((state) => state.app); @@ -45,16 +45,22 @@ const SideNav = ({ toggleDarkMode }: Props): JSX.Element => { setCollapsed((collapsed) => !collapsed); }, []); - const onClickHandler = useCallback((to: string) => { - history.push(to); - }, []); + const onClickHandler = useCallback( + (to: string) => { + if (pathname !== to) { + history.push(to); + globalTimeLoading(); + } + }, + [pathname, globalTimeLoading], + ); return ( - + @@ -80,12 +86,14 @@ 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/components/SideNav/menuItems.ts b/frontend/src/container/SideNav/menuItems.ts similarity index 100% rename from frontend/src/components/SideNav/menuItems.ts rename to frontend/src/container/SideNav/menuItems.ts index acbf400d0c..8b057dab3a 100644 --- a/frontend/src/components/SideNav/menuItems.ts +++ b/frontend/src/container/SideNav/menuItems.ts @@ -20,6 +20,11 @@ const menus: SidebarMenu[] = [ to: ROUTES.TRACES, name: 'Traces', }, + { + Icon: DashboardFilled, + to: ROUTES.ALL_DASHBOARD, + name: 'Dashboard', + }, { to: ROUTES.SERVICE_MAP, name: 'Service Map', @@ -40,11 +45,6 @@ const menus: SidebarMenu[] = [ to: ROUTES.INSTRUMENTATION, name: 'Add instrumentation', }, - { - Icon: DashboardFilled, - to: ROUTES.ALL_DASHBOARD, - name: 'Dashboard', - }, ]; interface SidebarMenu { diff --git a/frontend/src/components/SideNav/styles.ts b/frontend/src/container/SideNav/styles.ts similarity index 100% rename from frontend/src/components/SideNav/styles.ts rename to frontend/src/container/SideNav/styles.ts diff --git a/frontend/src/lib/getChartData.ts b/frontend/src/lib/getChartData.ts index 6830666cab..e20df3db49 100644 --- a/frontend/src/lib/getChartData.ts +++ b/frontend/src/lib/getChartData.ts @@ -7,7 +7,7 @@ import { colors } from './getRandomColor'; const getChartData = ({ queryData }: GetChartDataProps): ChartData => { const response = queryData.data.map(({ query, queryData, legend }) => { - return queryData.map((e, index) => { + return queryData.map((e) => { const { values = [], metric } = e || {}; const labelNames = getLabelName( metric, @@ -18,13 +18,13 @@ const getChartData = ({ queryData }: GetChartDataProps): ChartData => { const dataValue = values?.map((e) => { const [first = 0, second = ''] = e || []; return { - first: new Date(parseInt(convertIntoEpoc(first), 10)), + first: new Date(parseInt(convertIntoEpoc(first * 1000), 10)), // converting in ms second: Number(parseFloat(second).toFixed(2)), }; }); return { - label: labelNames, + label: labelNames !== 'undefined' ? labelNames : '', first: dataValue.map((e) => e.first), second: dataValue.map((e) => e.second), }; diff --git a/frontend/src/lib/getLabelName.ts b/frontend/src/lib/getLabelName.ts index 5f55bcacb9..0208e80920 100644 --- a/frontend/src/lib/getLabelName.ts +++ b/frontend/src/lib/getLabelName.ts @@ -40,7 +40,9 @@ const getLabelName = ( const post = postArray.map((e) => `${e}="${metric[e]}"`).join(','); const pre = preArray.map((e) => `${e}="${metric[e]}"`).join(','); - const result = `${metric[keysArray[index]]}`; + const value = metric[keysArray[index]]; + + const result = `${value === undefined ? '' : value}`; if (post.length === 0 && pre.length === 0) { return result; diff --git a/frontend/src/lib/getMaxMinTime.ts b/frontend/src/lib/getMaxMinTime.ts index 5d3a2cfa7b..5185046c9a 100644 --- a/frontend/src/lib/getMaxMinTime.ts +++ b/frontend/src/lib/getMaxMinTime.ts @@ -1,4 +1,4 @@ -import { GlobalTime } from 'store/actions'; +import { GlobalTime } from 'types/actions/globalTime'; import { Widgets } from 'types/api/dashboard/getAll'; const GetMaxMinTime = ({ diff --git a/frontend/src/lib/getTimeString.ts b/frontend/src/lib/getTimeString.ts new file mode 100644 index 0000000000..2502a7deb2 --- /dev/null +++ b/frontend/src/lib/getTimeString.ts @@ -0,0 +1,15 @@ +const getTimeString = (time: string): string => { + const timeString = time.split('.').join('').slice(0, 13); + + if (timeString.length < 13) { + const lengthMissing = timeString.length - 13; + + const numberZero = new Array(Math.abs(lengthMissing)).fill(0).join(''); + + return timeString + numberZero; + } + + return timeString; +}; + +export default getTimeString; diff --git a/frontend/src/modules/Metrics/ErrorRateChart.tsx b/frontend/src/modules/Metrics/ErrorRateChart.tsx deleted file mode 100644 index 72b2b54b3a..0000000000 --- a/frontend/src/modules/Metrics/ErrorRateChart.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { ActiveElement, Chart, ChartEvent } from 'chart.js'; -import Graph from 'components/Graph'; -import React, { useMemo, useState } from 'react'; -import { metricItem } from 'store/actions/MetricsActions'; -import styled from 'styled-components'; - -import { GraphContainer } from './styles'; - -const ChartPopUpUnique = styled.div<{ - ycoordinate: number; - xcoordinate: number; -}>` - background-color: white; - border: 1px solid rgba(219, 112, 147, 0.5); - z-index: 10; - position: absolute; - top: ${(props): number => props.ycoordinate}px; - left: ${(props): number => props.xcoordinate}px; - font-size: 12px; - border-radius: 2px; -`; - -const PopUpElements = styled.p` - color: black; - margin-bottom: 0px; - padding-left: 4px; - padding-right: 4px; - &:hover { - cursor: pointer; - } -`; - -interface ErrorRateChartProps { - data: metricItem[]; - onTracePopupClick: (props: number) => void; -} - -const ErrorRateChart = ({ - data, - onTracePopupClick, -}: ErrorRateChartProps): JSX.Element => { - const [state, setState] = useState({ - xcoordinate: 0, - ycoordinate: 0, - showpopUp: false, - firstpoint_ts: 0, - }); - - const gotoTracesHandler = (): void => { - onTracePopupClick(state.firstpoint_ts); - }; - - const data_chartJS: Chart['data'] = useMemo(() => { - return { - labels: data.map((s) => new Date(s.timestamp / 1000000)), - datasets: [ - { - label: 'Error Percentage (%)', - data: data.map((s: { errorRate: any }) => s.errorRate), - pointRadius: 0.5, - borderColor: 'rgba(227, 74, 51,1)', // Can also add transparency in border color - borderWidth: 2, - }, - ], - }; - }, [data]); - - const onClickhandler = ( - event: ChartEvent, - elements: ActiveElement[], - chart: Chart, - ): void => { - { - if (event.native) { - const points = chart.getElementsAtEventForMode( - event.native, - 'nearest', - { intersect: true }, - true, - ); - if (points.length) { - const firstPoint = points[0]; - setState({ - xcoordinate: firstPoint.element.x, - ycoordinate: firstPoint.element.y, - showpopUp: true, - firstpoint_ts: data[firstPoint.index].timestamp, - }); - } - } - } - }; - - return ( - - {state.showpopUp && ( - - View Traces - - )} - Error Percentage (%) - - - - - - ); -}; - -export default ErrorRateChart; diff --git a/frontend/src/modules/Metrics/ExternalApi/ExternalApiGraph.tsx b/frontend/src/modules/Metrics/ExternalApi/ExternalApiGraph.tsx deleted file mode 100644 index 8412ca6595..0000000000 --- a/frontend/src/modules/Metrics/ExternalApi/ExternalApiGraph.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import Graph from 'components/Graph'; -import { filter, uniqBy } from 'lodash'; -import React from 'react'; -import { withRouter } from 'react-router'; -import { RouteComponentProps } from 'react-router-dom'; -import { externalMetricsItem } from 'store/actions/MetricsActions'; - -import { GraphContainer } from '../styles'; -import { borderColors } from './graphConfig'; - -interface ExternalApiGraphProps extends RouteComponentProps { - data: externalMetricsItem[]; - keyIdentifier?: string; - label?: string; - title: string; - dataIdentifier: string; - fnDataIdentifier?: (s: number | string) => number | string; -} - -interface ExternalApiGraph { - chartRef: any; -} - -class ExternalApiGraph extends React.Component { - constructor(props: ExternalApiGraphProps) { - super(props); - this.chartRef = React.createRef(); - } - - state = { - xcoordinate: 0, - ycoordinate: 0, - showpopUp: false, - firstpoint_ts: 0, - // graphInfo:{} - }; - - render() { - const { - title, - label, - data, - dataIdentifier, - keyIdentifier, - fnDataIdentifier, - } = this.props; - const getDataSets = () => { - if (!keyIdentifier) { - return [ - { - label: label || '', - data: data.map((s: externalMetricsItem) => - fnDataIdentifier - ? fnDataIdentifier(s[dataIdentifier]) - : s[dataIdentifier], - ), - pointRadius: 0.5, - borderColor: borderColors[0], - borderWidth: 2, - }, - ]; - } - const uniq = uniqBy(data, keyIdentifier); - return uniq.map((obj: externalMetricsItem, i: number) => { - const _data = filter( - data, - (s: externalMetricsItem) => s[keyIdentifier] === obj[keyIdentifier], - ); - return { - label: obj[keyIdentifier], - data: _data.map((s: externalMetricsItem) => - fnDataIdentifier - ? fnDataIdentifier(s[dataIdentifier]) - : s[dataIdentifier], - ), - pointRadius: 0.5, - borderColor: borderColors[i] || borderColors[0], // Can also add transparency in border color - borderWidth: 2, - }; - }); - }; - - const data_chartJS = () => { - const uniqTimestamp = uniqBy(data, 'timestamp'); - - return { - labels: uniqTimestamp.map( - (s: externalMetricsItem) => new Date(s.timestamp / 1000000), - ), - datasets: getDataSets(), - }; - }; - - return ( - <> - {title} - - - - > - ); - } -} - -export default withRouter(ExternalApiGraph); diff --git a/frontend/src/modules/Metrics/ExternalApi/graphConfig.ts b/frontend/src/modules/Metrics/ExternalApi/graphConfig.ts deleted file mode 100644 index 2419c3255e..0000000000 --- a/frontend/src/modules/Metrics/ExternalApi/graphConfig.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { ChartOptions } from 'chart.js'; - -export const getOptions = (theme: string): ChartOptions => { - return { - maintainAspectRatio: true, - responsive: true, - - title: { - display: true, - text: '', - fontSize: 20, - position: 'top', - padding: 8, - fontFamily: 'Arial', - fontStyle: 'regular', - fontColor: theme === 'dark' ? 'rgb(200, 200, 200)' : 'rgb(20, 20, 20)', - }, - - legend: { - display: true, - position: 'bottom', - align: 'center', - - labels: { - fontColor: theme === 'dark' ? 'rgb(200, 200, 200)' : 'rgb(20, 20, 20)', - fontSize: 10, - boxWidth: 10, - usePointStyle: true, - }, - }, - - tooltips: { - mode: 'label', - bodyFontSize: 12, - titleFontSize: 12, - - callbacks: { - label: function (tooltipItem, data) { - if (typeof tooltipItem.yLabel === 'number') { - return ( - data.datasets![tooltipItem.datasetIndex!].label + - ' : ' + - tooltipItem.yLabel.toFixed(2) - ); - } else { - return ''; - } - }, - }, - }, - - scales: { - yAxes: [ - { - stacked: false, - ticks: { - beginAtZero: false, - fontSize: 10, - autoSkip: true, - maxTicksLimit: 6, - }, - - gridLines: { - // You can change the color, the dash effect, the main axe color, etc. - borderDash: [1, 4], - color: '#D3D3D3', - lineWidth: 0.25, - }, - }, - ], - xAxes: [ - { - type: 'time', - // time: { - // unit: 'second' - // }, - distribution: 'linear', - //'linear': data are spread according to their time (distances can vary) - // From https://www.chartjs.org/docs/latest/axes/cartesian/time.html - ticks: { - beginAtZero: false, - fontSize: 10, - autoSkip: true, - maxTicksLimit: 10, - }, - // gridLines: false, --> not a valid option - }, - ], - }, - }; -}; - -export const borderColors = [ - '#00feff', - 'rgba(227, 74, 51, 1.0)', - 'rgba(250,174,50,1)', - '#058b00', - '#a47f00', - 'rgba(57, 255, 20, 1.0)', - '#45a1ff', - '#ffe900', - '#30e60b', - '#8000d7', - '#ededf0', -]; diff --git a/frontend/src/modules/Metrics/ExternalApi/index.tsx b/frontend/src/modules/Metrics/ExternalApi/index.tsx deleted file mode 100644 index 425ef1d510..0000000000 --- a/frontend/src/modules/Metrics/ExternalApi/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from './ExternalApiGraph'; diff --git a/frontend/src/modules/Metrics/LatencyLineChart/LatencyLine.tsx b/frontend/src/modules/Metrics/LatencyLineChart/LatencyLine.tsx deleted file mode 100644 index 1fa4e6e4ae..0000000000 --- a/frontend/src/modules/Metrics/LatencyLineChart/LatencyLine.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { ChartData, ChartOptions } from 'chart.js'; -import Graph from 'components/Graph'; -import React from 'react'; - -import { GraphContainer } from '../styles'; - -const LatencyChart = ({ - data, - onClickhandler, -}: LatencyChartProps): JSX.Element => { - return ( - - - - ); -}; - -interface LatencyChartProps { - data: ChartData; - onClickhandler: ChartOptions['onClick']; -} - -export default LatencyChart; diff --git a/frontend/src/modules/Metrics/LatencyLineChart/index.tsx b/frontend/src/modules/Metrics/LatencyLineChart/index.tsx deleted file mode 100644 index 05026d548b..0000000000 --- a/frontend/src/modules/Metrics/LatencyLineChart/index.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { - ActiveElement, - Chart, - ChartData, - ChartEvent, - ChartOptions, -} from 'chart.js'; -import React, { useMemo, useState } from 'react'; -import { metricItem } from 'store/actions/MetricsActions'; -import styled from 'styled-components'; - -import { GraphTitle } from '../styles'; -import LatencyLine from './LatencyLine'; - -const ChartPopUpUnique = styled.div<{ - ycoordinate: number; - xcoordinate: number; - showPopUp: boolean; -}>` - background-color: white; - border: 1px solid rgba(219, 112, 147, 0.5); - z-index: 10; - position: absolute; - top: ${(props): number => props.ycoordinate}px; - left: ${(props): number => props.xcoordinate}px; - font-size: 12px; - border-radius: 2px; - display: ${({ showPopUp }): string => (showPopUp ? 'block' : 'none')}; -`; - -const PopUpElements = styled.p` - color: black; - margin-bottom: 0px; - padding-left: 4px; - padding-right: 4px; - &:hover { - cursor: pointer; - } -`; - -interface LatencyLineChartProps { - data: metricItem[]; - popupClickHandler: (props: any) => void; -} - -const LatencyLineChart = ({ - data, - popupClickHandler, -}: LatencyLineChartProps): JSX.Element => { - const [state, setState] = useState({ - xcoordinate: 0, - ycoordinate: 0, - showpopUp: false, - firstpoint_ts: 0, - }); - - const onClickhandler: ChartOptions['onClick'] = async ( - event: ChartEvent, - elements: ActiveElement[], - chart: Chart, - ): Promise => { - if (event.native) { - const points = chart.getElementsAtEventForMode( - event.native, - 'nearest', - { intersect: true }, - true, - ); - - if (points.length) { - const firstPoint = points[0]; - - setState({ - xcoordinate: firstPoint.element.x, - ycoordinate: firstPoint.element.y, - showpopUp: true, - firstpoint_ts: data[firstPoint.index].timestamp, - }); - } else { - if (state.showpopUp) { - setState((state) => ({ - ...state, - showpopUp: false, - })); - } - } - } - }; - - const chartData: ChartData<'line'> = useMemo(() => { - return { - labels: data.map((s) => new Date(s.timestamp / 1000000)), - datasets: [ - { - label: 'p99 Latency', - data: data.map((s) => s.p99 / 1000000), //converting latency from nano sec to ms - pointRadius: 0.5, - borderColor: 'rgba(250,174,50,1)', // Can also add transparency in border color - borderWidth: 2, - }, - { - label: 'p95 Latency', - data: data.map((s) => s.p95 / 1000000), //converting latency from nano sec to ms - pointRadius: 0.5, - borderColor: 'rgba(227, 74, 51, 1.0)', - borderWidth: 2, - }, - { - label: 'p50 Latency', - data: data.map((s) => s.p50 / 1000000), //converting latency from nano sec to ms - pointRadius: 0.5, - borderColor: 'rgba(57, 255, 20, 1.0)', - borderWidth: 2, - }, - ], - }; - }, [data]); - - return ( - <> - - { - popupClickHandler(state.firstpoint_ts); - }} - > - View Traces - - - - Application latency in ms - - - > - ); -}; - -export default LatencyLineChart; diff --git a/frontend/src/modules/Metrics/RequestRateChart.tsx b/frontend/src/modules/Metrics/RequestRateChart.tsx deleted file mode 100644 index 27a5927377..0000000000 --- a/frontend/src/modules/Metrics/RequestRateChart.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { ActiveElement, Chart, ChartEvent } from 'chart.js'; -import Graph from 'components/Graph'; -import ROUTES from 'constants/routes'; -import history from 'lib/history'; -import React, { useMemo, useState } from 'react'; -import { metricItem } from 'store/actions/MetricsActions'; -import styled from 'styled-components'; - -import { GraphContainer } from './styles'; - -const ChartPopUpUnique = styled.div<{ - ycoordinate: number; - xcoordinate: number; -}>` - background-color: white; - border: 1px solid rgba(219, 112, 147, 0.5); - z-index: 10; - position: absolute; - top: ${(props): number => props.ycoordinate}px; - left: ${(props): number => props.xcoordinate}px; - font-size: 12px; - border-radius: 2px; -`; - -const PopUpElements = styled.p` - color: black; - margin-bottom: 0px; - padding-left: 4px; - padding-right: 4px; - &:hover { - cursor: pointer; - } -`; - -interface RequestRateChartProps { - data: metricItem[]; -} - -const RequestRateChart = ({ data }: RequestRateChartProps): JSX.Element => { - const [state, setState] = useState({ - xcoordinate: 0, - ycoordinate: 0, - showpopUp: false, - }); - const gotoTracesHandler = (): void => { - history.push(ROUTES.TRACES); - }; - - const onClickHandler = async ( - event: ChartEvent, - elements: ActiveElement[], - charts: Chart, - ): Promise => { - if (event.native) { - const points = charts.getElementsAtEventForMode( - event.native, - 'nearest', - { intersect: true }, - true, - ); - - if (points.length) { - const firstPoint = points[0]; - - setState({ - xcoordinate: firstPoint.element.x, - ycoordinate: firstPoint.element.y, - showpopUp: true, - }); - } else { - if (state.showpopUp) { - setState((state) => ({ - ...state, - showpopUp: false, - })); - } - } - } - }; - - const GraphTracePopUp = (): JSX.Element | null => { - if (state.showpopUp) { - return ( - - View Traces - - ); - } else return null; - }; - - const data_chartJS: Chart['data'] = useMemo(() => { - return { - labels: data.map((s) => new Date(s.timestamp / 1000000)), - datasets: [ - { - label: 'Request per sec', - data: data.map((s) => s.callRate), - pointRadius: 0.5, - borderColor: 'rgba(250,174,50,1)', // Can also add transparency in border color - borderWidth: 2, - }, - ], - }; - }, [data]); - - return ( - <> - - {GraphTracePopUp()} - Request per sec - - - - - > - ); -}; - -export default RequestRateChart; diff --git a/frontend/src/modules/Metrics/ServiceMetrics/index.tsx b/frontend/src/modules/Metrics/ServiceMetrics/index.tsx deleted file mode 100644 index 8827e736c8..0000000000 --- a/frontend/src/modules/Metrics/ServiceMetrics/index.tsx +++ /dev/null @@ -1,260 +0,0 @@ -import { Col, Tabs } from 'antd'; -import Spinner from 'components/Spinner'; -import { METRICS_PAGE_QUERY_PARAM } from 'constants/query'; -import ROUTES from 'constants/routes'; -import history from 'lib/history'; -import React, { useEffect } from 'react'; -import { connect } from 'react-redux'; -import { useParams } from 'react-router-dom'; -import { GlobalTime, updateTimeInterval } from 'store/actions'; -import { - dbOverviewMetricsItem, - externalErrCodeMetricsItem, - externalMetricsAvgDurationItem, - externalMetricsItem, - getInitialMerticDataProps, - metricItem, - topEndpointListItem, -} from 'store/actions/MetricsActions'; -import { - getDbOverViewMetrics, - getExternalAvgDurationMetrics, - getExternalErrCodeMetrics, - getExternalMetrics, - getInitialMerticData, - getServicesMetrics, - getTopEndpoints, -} from 'store/actions/MetricsActions'; -import { AppState } from 'store/reducers'; - -import ErrorRateChart from '../ErrorRateChart'; -import ExternalApiGraph from '../ExternalApi'; -import LatencyLineChart from '../LatencyLineChart'; -import RequestRateChart from '../RequestRateChart'; -import TopEndpointsTable from '../TopEndpointsTable'; -import { Card, Row } from './styles'; -const { TabPane } = Tabs; - -const _ServiceMetrics = (props: ServicesMetricsProps): JSX.Element => { - const { servicename } = useParams<{ servicename?: string }>(); - const { globalTime, getInitialMerticData } = props; - - useEffect(() => { - if (servicename !== undefined) { - getInitialMerticData({ - globalTime: globalTime, - serviceName: servicename, - }); - } - }, [getInitialMerticData, servicename, globalTime]); - - const onTracePopupClick = (timestamp: number): void => { - const currentTime = timestamp / 1000000; - const tPlusOne = timestamp / 1000000 + 1 * 60 * 1000; - - updateTimeInterval('custom', [currentTime, tPlusOne]); // updateTimeInterval takes second range in ms -- give -5 min to selected time, - - const urlParams = new URLSearchParams(); - urlParams.set(METRICS_PAGE_QUERY_PARAM.startTime, currentTime.toString()); - urlParams.set(METRICS_PAGE_QUERY_PARAM.endTime, tPlusOne.toString()); - if (servicename) { - urlParams.set(METRICS_PAGE_QUERY_PARAM.service, servicename); - } - - history.push(`${ROUTES.TRACES}?${urlParams.toString()}`); - }; - - const onErrTracePopupClick = (timestamp: number): void => { - const currentTime = timestamp / 1000000; - const tPlusOne = timestamp / 1000000 + 1 * 60 * 1000; - - updateTimeInterval('custom', [currentTime, tPlusOne]); // updateTimeInterval takes second range in ms -- give -5 min to selected time, - - const urlParams = new URLSearchParams(); - urlParams.set(METRICS_PAGE_QUERY_PARAM.startTime, currentTime.toString()); - urlParams.set(METRICS_PAGE_QUERY_PARAM.endTime, tPlusOne.toString()); - if (servicename) { - urlParams.set(METRICS_PAGE_QUERY_PARAM.service, servicename); - } - urlParams.set(METRICS_PAGE_QUERY_PARAM.error, 'true'); - - history.push(`${ROUTES.TRACES}?${urlParams.toString()}`); - }; - - if (props.loading) { - return ; - } - - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Number(s) / 1000000} - data={props.externalAvgDurationMetrics} - /> - - - - - - - - - - - - - - Number(s) / 1000000} - data={props.externalMetrics} - /> - - - - - - - - - - - - - - - - Number(s) / 1000000} - data={props.dbOverviewMetrics} - /> - - - - - - ); -}; - -interface ServicesMetricsProps { - serviceMetrics: metricItem[]; - dbOverviewMetrics: dbOverviewMetricsItem[]; - getServicesMetrics: () => void; - getExternalMetrics: () => void; - getExternalErrCodeMetrics: () => void; - getExternalAvgDurationMetrics: () => void; - getDbOverViewMetrics: () => void; - externalMetrics: externalMetricsItem[]; - topEndpointsList: topEndpointListItem[]; - externalAvgDurationMetrics: externalMetricsAvgDurationItem[]; - externalErrCodeMetrics: externalErrCodeMetricsItem[]; - getTopEndpoints: () => void; - globalTime: GlobalTime; - updateTimeInterval: () => void; - getInitialMerticData: (props: getInitialMerticDataProps) => void; - loading: boolean; -} - -const mapStateToProps = ( - state: AppState, -): { - serviceMetrics: metricItem[]; - topEndpointsList: topEndpointListItem[]; - externalAvgDurationMetrics: externalMetricsAvgDurationItem[]; - externalErrCodeMetrics: externalErrCodeMetricsItem[]; - externalMetrics: externalMetricsItem[]; - dbOverviewMetrics: dbOverviewMetricsItem[]; - globalTime: GlobalTime; - loading: boolean; -} => { - return { - externalErrCodeMetrics: state.metricsData.externalErrCodeMetricsItem, - serviceMetrics: state.metricsData.metricItems, - topEndpointsList: state.metricsData.topEndpointListItem, - externalMetrics: state.metricsData.externalMetricsItem, - globalTime: state.globalTime, - dbOverviewMetrics: state.metricsData.dbOverviewMetricsItem, - externalAvgDurationMetrics: state.metricsData.externalMetricsAvgDurationItem, - loading: state.metricsData.loading, - }; -}; - -export const ServiceMetrics = connect(mapStateToProps, { - getServicesMetrics: getServicesMetrics, - getExternalMetrics: getExternalMetrics, - getExternalErrCodeMetrics: getExternalErrCodeMetrics, - getExternalAvgDurationMetrics: getExternalAvgDurationMetrics, - getTopEndpoints: getTopEndpoints, - updateTimeInterval: updateTimeInterval, - getDbOverViewMetrics: getDbOverViewMetrics, - getInitialMerticData: getInitialMerticData, -})(_ServiceMetrics); diff --git a/frontend/src/modules/Metrics/ServiceMetrics/styles.ts b/frontend/src/modules/Metrics/ServiceMetrics/styles.ts deleted file mode 100644 index 98efc4662d..0000000000 --- a/frontend/src/modules/Metrics/ServiceMetrics/styles.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Card as CardComponent, Row as RowComponent } from 'antd'; -import styled from 'styled-components'; - -export const Card = styled(CardComponent)` - &&& { - padding: 10px; - } - - .ant-card-body { - padding: 0; - } -`; - -export const Row = styled(RowComponent)` - &&& { - padding: 1rem; - } -`; diff --git a/frontend/src/modules/Metrics/ServiceMetricsDef.tsx b/frontend/src/modules/Metrics/ServiceMetricsDef.tsx deleted file mode 100644 index 13f16b71a3..0000000000 --- a/frontend/src/modules/Metrics/ServiceMetricsDef.tsx +++ /dev/null @@ -1 +0,0 @@ -export { ServiceMetrics as default } from './ServiceMetrics'; diff --git a/frontend/src/modules/Metrics/ServiceTable/index.tsx b/frontend/src/modules/Metrics/ServiceTable/index.tsx deleted file mode 100644 index 643166e34a..0000000000 --- a/frontend/src/modules/Metrics/ServiceTable/index.tsx +++ /dev/null @@ -1,186 +0,0 @@ -import { Button, Space, Table } from 'antd'; -import { ColumnsType } from 'antd/lib/table'; -import Modal from 'components/Modal'; -import Spinner from 'components/Spinner'; -import { SKIP_ONBOARDING } from 'constants/onboarding'; -import ROUTES from 'constants/routes'; -import React, { useCallback, useEffect, useState } from 'react'; -import { connect } from 'react-redux'; -import { NavLink } from 'react-router-dom'; -import { getServicesList, GlobalTime } from 'store/actions'; -import { servicesListItem } from 'store/actions/MetricsActions'; -import { AppState } from 'store/reducers'; - -import { Wrapper } from './styles'; - -const _ServicesTable = (props: ServicesTableProps): JSX.Element => { - const [initialDataFetch, setDataFetched] = useState(false); - const [errorObject, setErrorObject] = useState({ - message: '', - isError: false, - }); - const isEmptyServiceList = - !initialDataFetch && props.servicesList.length === 0; - const refetchFromBackend = isEmptyServiceList || errorObject.isError; - const [skipOnboarding, setSkipOnboarding] = useState( - localStorage.getItem(SKIP_ONBOARDING) === 'true', - ); - - const onContinueClick = (): void => { - localStorage.setItem(SKIP_ONBOARDING, 'true'); - setSkipOnboarding(true); - }; - - const { globalTime, getServicesList } = props; - - const getApiServiceData = useCallback(() => { - getServicesList(globalTime) - .then(() => { - setDataFetched(true); - setErrorObject({ message: '', isError: false }); - }) - .catch((e: string) => { - setErrorObject({ message: e, isError: true }); - setDataFetched(true); - }); - }, [globalTime, getServicesList]); - - useEffect(() => { - getApiServiceData(); - }, [globalTime, getApiServiceData]); - - useEffect(() => { - if (props.servicesList.length > 1) { - localStorage.removeItem(SKIP_ONBOARDING); - } - }, [props.servicesList, errorObject]); - - if (!initialDataFetch) { - return ; - } - - if (refetchFromBackend && !skipOnboarding) { - return ( - - Continue without instrumentation - , - ]} - > - - - - No instrumentation data. - - Please instrument your application as mentioned{' '} - - here - - - - - ); - } - - const columns: ColumnsType = [ - { - title: 'Application', - dataIndex: 'serviceName', - key: 'serviceName', - // eslint-disable-next-line react/display-name - render: (text: string): JSX.Element => ( - - {text} - - ), - }, - { - title: 'P99 latency (in ms)', - dataIndex: 'p99', - key: 'p99', - sorter: (a: DataProps, b: DataProps): number => a.p99 - b.p99, - render: (value: number): string => (value / 1000000).toFixed(2), - }, - { - title: 'Error Rate (in %)', - dataIndex: 'errorRate', - key: 'errorRate', - sorter: (a: DataProps, b: DataProps): number => a.errorRate - b.errorRate, - render: (value: number): string => value.toFixed(2), - }, - { - title: 'Requests Per Second', - dataIndex: 'callRate', - key: 'callRate', - sorter: (a: DataProps, b: DataProps): number => a.callRate - b.callRate, - render: (value: number): string => value.toFixed(2), - }, - ]; - - return ( - - - - {props.servicesList[0] !== undefined && - props.servicesList[0].numCalls === 0 && ( - - No applications present. Please add instrumentation (follow this - - guide - - ) - - )} - - ); -}; - -type DataProps = servicesListItem; - -interface ServicesTableProps { - servicesList: servicesListItem[]; - getServicesList: (props: GlobalTime) => Promise; - globalTime: GlobalTime; -} - -const mapStateToProps = ( - state: AppState, -): { servicesList: servicesListItem[]; globalTime: GlobalTime } => { - return { - servicesList: state.metricsData.serviceList, - globalTime: state.globalTime, - }; -}; - -export const ServicesTable = connect(mapStateToProps, { - getServicesList: getServicesList, -})(_ServicesTable); diff --git a/frontend/src/modules/Metrics/ServiceTable/styles.ts b/frontend/src/modules/Metrics/ServiceTable/styles.ts deleted file mode 100644 index 704bec81dd..0000000000 --- a/frontend/src/modules/Metrics/ServiceTable/styles.ts +++ /dev/null @@ -1,5 +0,0 @@ -import styled from 'styled-components'; - -export const Wrapper = styled.div` - padding: 40px; -`; diff --git a/frontend/src/modules/Metrics/ServicesTableDef.tsx b/frontend/src/modules/Metrics/ServicesTableDef.tsx deleted file mode 100644 index 7d5c2d568d..0000000000 --- a/frontend/src/modules/Metrics/ServicesTableDef.tsx +++ /dev/null @@ -1 +0,0 @@ -export { ServicesTable as default } from './ServiceTable'; diff --git a/frontend/src/modules/Metrics/TopEndpointsTable.css b/frontend/src/modules/Metrics/TopEndpointsTable.css deleted file mode 100644 index 526a2f8349..0000000000 --- a/frontend/src/modules/Metrics/TopEndpointsTable.css +++ /dev/null @@ -1,12 +0,0 @@ -@media only screen and (min-width: 768px) { - .topEndpointsButton { - white-space: nowrap; - padding: 0; - } - - .topEndpointsButton span { - text-overflow: ellipsis; - overflow: hidden; - max-width: 120px; - } -} diff --git a/frontend/src/modules/Metrics/TopEndpointsTable.tsx b/frontend/src/modules/Metrics/TopEndpointsTable.tsx deleted file mode 100644 index 3633e1b350..0000000000 --- a/frontend/src/modules/Metrics/TopEndpointsTable.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import './TopEndpointsTable.css'; - -import { Button, Table, Tooltip } from 'antd'; -import { METRICS_PAGE_QUERY_PARAM } from 'constants/query'; -import React from 'react'; -import { connect } from 'react-redux'; -import { useHistory, useParams } from 'react-router-dom'; -import { GlobalTime } from 'store/actions'; -import { topEndpointListItem } from 'store/actions/MetricsActions'; -import { AppState } from 'store/reducers'; -import styled from 'styled-components'; - -const Wrapper = styled.div` - padding-top: 10px; - padding-bottom: 10px; - padding-left: 8px; - padding-right: 8px; - @media only screen and (max-width: 767px) { - padding: 0; - } - .ant-table table { - font-size: 12px; - } - .ant-table tfoot > tr > td, - .ant-table tfoot > tr > th, - .ant-table-tbody > tr > td, - .ant-table-thead > tr > th { - padding: 10px; - } - .ant-table-column-sorters { - padding: 6px; - } -`; - -interface TopEndpointsTableProps { - data: topEndpointListItem[]; - globalTime: GlobalTime; -} - -const _TopEndpointsTable = (props: TopEndpointsTableProps) => { - const history = useHistory(); - const params = useParams<{ servicename: string }>(); - const handleOnClick = (operation: string) => { - const urlParams = new URLSearchParams(); - const { servicename } = params; - const { maxTime, minTime } = props.globalTime; - urlParams.set( - METRICS_PAGE_QUERY_PARAM.startTime, - String(Number(minTime) / 1000000), - ); - urlParams.set( - METRICS_PAGE_QUERY_PARAM.endTime, - String(Number(maxTime) / 1000000), - ); - if (servicename) { - urlParams.set(METRICS_PAGE_QUERY_PARAM.service, servicename); - } - urlParams.set(METRICS_PAGE_QUERY_PARAM.operation, operation); - history.push(`/traces?${urlParams.toString()}`); - }; - - const columns: any = [ - { - title: 'Name', - dataIndex: 'name', - key: 'name', - - render: (text: string) => ( - - handleOnClick(text)} - > - {text} - - - ), - }, - { - title: 'P50 (in ms)', - dataIndex: 'p50', - key: 'p50', - sorter: (a: any, b: any) => a.p50 - b.p50, - // sortDirections: ['descend', 'ascend'], - render: (value: number) => (value / 1000000).toFixed(2), - }, - { - title: 'P95 (in ms)', - dataIndex: 'p95', - key: 'p95', - sorter: (a: any, b: any) => a.p95 - b.p95, - // sortDirections: ['descend', 'ascend'], - render: (value: number) => (value / 1000000).toFixed(2), - }, - { - title: 'P99 (in ms)', - dataIndex: 'p99', - key: 'p99', - sorter: (a: any, b: any) => a.p99 - b.p99, - // sortDirections: ['descend', 'ascend'], - render: (value: number) => (value / 1000000).toFixed(2), - }, - { - title: 'Number of Calls', - dataIndex: 'numCalls', - key: 'numCalls', - sorter: (a: any, b: any) => a.numCalls - b.numCalls, - }, - ]; - - return ( - - Top Endpoints - - - ); -}; - -const mapStateToProps = ( - state: AppState, -): { - globalTime: GlobalTime; -} => { - return { globalTime: state.globalTime }; -}; - -export const TopEndpointsTable = connect( - mapStateToProps, - null, -)(_TopEndpointsTable); - -export default TopEndpointsTable; diff --git a/frontend/src/modules/Metrics/styles.ts b/frontend/src/modules/Metrics/styles.ts deleted file mode 100644 index 7f46ae4811..0000000000 --- a/frontend/src/modules/Metrics/styles.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Typography } from 'antd'; -import styled from 'styled-components'; - -export const GraphContainer = styled.div` - min-height: 27vh; -`; - -export const GraphTitle = styled(Typography)` - &&& { - text-align: center; - } -`; diff --git a/frontend/src/modules/Nav/TopNav/DateTimeSelector.tsx b/frontend/src/modules/Nav/TopNav/DateTimeSelector.tsx deleted file mode 100644 index 784a97cf6a..0000000000 --- a/frontend/src/modules/Nav/TopNav/DateTimeSelector.tsx +++ /dev/null @@ -1,349 +0,0 @@ -import { Button, Form, Select as DefaultSelect, Space } from 'antd'; -import FormItem from 'antd/lib/form/FormItem'; -import { LOCAL_STORAGE } from 'constants/localStorage'; -import { METRICS_PAGE_QUERY_PARAM } from 'constants/query'; -import ROUTES from 'constants/routes'; -import history from 'lib/history'; -import { cloneDeep } from 'lodash'; -import moment from 'moment'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { connect } from 'react-redux'; -import { useLocation } from 'react-router-dom'; -import { GlobalTime, updateTimeInterval } from 'store/actions'; -import { DateTimeRangeType } from 'store/actions'; -import { AppState } from 'store/reducers'; -import styled from 'styled-components'; - -import { - DefaultOptionsBasedOnRoute, - Options, - ServiceMapOptions, -} from './config'; -import CustomDateTimeModal from './CustomDateTimeModal'; -import { getLocalStorageRouteKey } from './utils'; -const { Option } = DefaultSelect; - -const DateTimeWrapper = styled.div` - margin-top: 20px; - justify-content: flex-end !important; -`; -interface DateTimeSelectorProps { - currentpath?: string; - updateTimeInterval: ( - interval: string, - datetimeRange?: [number, number], - ) => void; - globalTime: GlobalTime; -} - -/* -This components is mounted all the time. Use event listener to track changes. - */ -const _DateTimeSelector = (props: DateTimeSelectorProps): JSX.Element => { - const location = useLocation(); - const LocalStorageRouteKey: string = getLocalStorageRouteKey( - location.pathname, - ); - const { globalTime, updateTimeInterval } = props; - - const timeDurationInLocalStorage = useMemo(() => { - return ( - JSON.parse(localStorage.getItem(LOCAL_STORAGE.METRICS_TIME_IN_DURATION)) || - {} - ); - }, []); - - const options = - location.pathname === ROUTES.SERVICE_MAP ? ServiceMapOptions : Options; - let defaultTime = DefaultOptionsBasedOnRoute[LocalStorageRouteKey] - ? DefaultOptionsBasedOnRoute[LocalStorageRouteKey] - : DefaultOptionsBasedOnRoute.default; - - if (timeDurationInLocalStorage[LocalStorageRouteKey]) { - defaultTime = timeDurationInLocalStorage[LocalStorageRouteKey]; - } - - const getDefaultTime = useCallback(() => { - if (DefaultOptionsBasedOnRoute[LocalStorageRouteKey]) { - return DefaultOptionsBasedOnRoute[LocalStorageRouteKey]; - } - if (timeDurationInLocalStorage[LocalStorageRouteKey]) { - return timeDurationInLocalStorage[LocalStorageRouteKey]; - } - return DefaultOptionsBasedOnRoute.default; - }, [LocalStorageRouteKey, timeDurationInLocalStorage]); - - const [currentLocalStorageRouteKey, setCurrentLocalStorageRouteKey] = useState( - LocalStorageRouteKey, - ); - - const [customDTPickerVisible, setCustomDTPickerVisible] = useState(false); - const [timeInterval, setTimeInterval] = useState(getDefaultTime()); - const [startTime, setStartTime] = useState(null); - const [endTime, setEndTime] = useState(null); - const [refreshButtonHidden, setRefreshButtonHidden] = useState(false); - const [refreshText, setRefreshText] = useState(''); - const [refreshButtonClick, setRefreshButtonClick] = useState(0); - const [form_dtselector] = Form.useForm(); - - const setToLocalStorage = useCallback( - (val: string) => { - let timeDurationInLocalStorageObj = cloneDeep(timeDurationInLocalStorage); - if (timeDurationInLocalStorageObj) { - timeDurationInLocalStorageObj[LocalStorageRouteKey] = val; - } else { - timeDurationInLocalStorageObj = { - [LocalStorageRouteKey]: val, - }; - } - window.localStorage.setItem( - LOCAL_STORAGE.METRICS_TIME_IN_DURATION, - JSON.stringify(timeDurationInLocalStorageObj), - ); - }, - [LocalStorageRouteKey, timeDurationInLocalStorage], - ); - - const setMetricsTimeInterval = useCallback( - (value: string) => { - updateTimeInterval(value); - setTimeInterval(value); - setEndTime(null); - setStartTime(null); - setToLocalStorage(value); - }, - [setToLocalStorage, updateTimeInterval], - ); - - const setCustomTime = useCallback( - (startTime: moment.Moment, endTime: moment.Moment) => { - updateTimeInterval('custom', [startTime.valueOf(), endTime.valueOf()]); - setEndTime(endTime); - setStartTime(startTime); - }, - [updateTimeInterval], - ); - - const updateTimeOnQueryParamChange = useCallback(() => { - const urlParams = new URLSearchParams(location.search); - const intervalInQueryParam = urlParams.get(METRICS_PAGE_QUERY_PARAM.interval); - const startTimeString = urlParams.get(METRICS_PAGE_QUERY_PARAM.startTime); - const endTimeString = urlParams.get(METRICS_PAGE_QUERY_PARAM.endTime); - - // first pref: handle both startTime and endTime - if ( - startTimeString && - startTimeString.length > 0 && - endTimeString && - endTimeString.length > 0 - ) { - const startTime = moment(Number(startTimeString)); - const endTime = moment(Number(endTimeString)); - setCustomTime(startTime, endTime); - } else if (currentLocalStorageRouteKey !== LocalStorageRouteKey) { - setMetricsTimeInterval(defaultTime); - setCurrentLocalStorageRouteKey(LocalStorageRouteKey); - } - // first pref: handle intervalInQueryParam - else if (intervalInQueryParam) { - setMetricsTimeInterval(intervalInQueryParam); - } - }, [ - LocalStorageRouteKey, - currentLocalStorageRouteKey, - setCustomTime, - defaultTime, - setMetricsTimeInterval, - location, - ]); - - useEffect(() => { - setMetricsTimeInterval(defaultTime); - }, [defaultTime, setMetricsTimeInterval]); - - // On URL Change - useEffect(() => { - updateTimeOnQueryParamChange(); - }, [location, updateTimeOnQueryParamChange]); - - const updateUrlForTimeInterval = (value: string): void => { - const preSearch = new URLSearchParams(location.search); - - const widgetId = preSearch.get('widgetId'); - const graphType = preSearch.get('graphType'); - - let result = ''; - - if (widgetId !== null) { - result = result + `&widgetId=${widgetId}`; - } - - if (graphType !== null) { - result = result + `&graphType=${graphType}`; - } - - history.push({ - search: `?${METRICS_PAGE_QUERY_PARAM.interval}=${value}${result}`, - }); //pass time in URL query param for all choices except custom in datetime picker - }; - - const updateUrlForCustomTime = ( - startTime: moment.Moment, - endTime: moment.Moment, - ): void => { - const preSearch = new URLSearchParams(location.search); - - const widgetId = preSearch.get('widgetId'); - const graphType = preSearch.get('graphType'); - - let result = ''; - - if (widgetId !== null) { - result = result + `&widgetId=${widgetId}`; - } - - if (graphType !== null) { - result = result + `&graphType=${graphType}`; - } - - history.push( - `?${METRICS_PAGE_QUERY_PARAM.startTime}=${startTime.valueOf()}&${ - METRICS_PAGE_QUERY_PARAM.endTime - }=${endTime.valueOf()}${result}`, - ); - }; - - const handleOnSelect = (value: string): void => { - if (value === 'custom') { - setCustomDTPickerVisible(true); - } else { - updateUrlForTimeInterval(value); - setRefreshButtonHidden(false); // for normal intervals, show refresh button - } - }; - - //function called on clicking apply in customDateTimeModal - const handleCustomDate = (dateTimeRange: DateTimeRangeType): void => { - // pass values in ms [minTime, maxTime] - if ( - dateTimeRange !== null && - dateTimeRange !== undefined && - dateTimeRange[0] !== null && - dateTimeRange[1] !== null - ) { - const startTime = dateTimeRange[0].valueOf(); - const endTime = dateTimeRange[1].valueOf(); - - updateUrlForCustomTime(moment(startTime), moment(endTime)); - //setting globaltime - setRefreshButtonHidden(true); - form_dtselector.setFieldsValue({ - interval: - dateTimeRange[0].format('YYYY/MM/DD HH:mm') + - '-' + - dateTimeRange[1].format('YYYY/MM/DD HH:mm'), - }); - } - setCustomDTPickerVisible(false); - }; - - const timeSinceLastRefresh = useCallback(() => { - const currentTime = moment(); - const lastRefresh = moment(globalTime.maxTime / 1000000); - const duration = moment.duration(currentTime.diff(lastRefresh)); - - const secondsDiff = Math.floor(duration.asSeconds()); - const minutedDiff = Math.floor(duration.asMinutes()); - const hoursDiff = Math.floor(duration.asHours()); - - if (hoursDiff > 0) { - return `Last refresh - ${hoursDiff} hrs ago`; - } else if (minutedDiff > 0) { - return `Last refresh - ${minutedDiff} mins ago`; - } - return `Last refresh - ${secondsDiff} sec ago`; - }, [globalTime]); - - const handleRefresh = (): void => { - setRefreshButtonClick(refreshButtonClick + 1); - setMetricsTimeInterval(timeInterval); - }; - - useEffect(() => { - setRefreshText(''); - const interval = setInterval(() => { - setRefreshText(timeSinceLastRefresh()); - }, 2000); - return (): void => { - clearInterval(interval); - }; - }, [refreshButtonClick, timeSinceLastRefresh]); - - if (history.location.pathname.startsWith(ROUTES.USAGE_EXPLORER)) { - return <>>; - } else { - const inputLabeLToShow = - startTime && endTime - ? `${startTime.format('YYYY/MM/DD HH:mm')} - ${endTime.format( - 'YYYY/MM/DD HH:mm', - )}` - : timeInterval; - - return ( - - - - - - {options.map(({ value, label }) => ( - - {label} - - ))} - - - - - Refresh - - - - - - {refreshText} - - { - setCustomDTPickerVisible(false); - }} - /> - - - ); - } -}; -const mapStateToProps = (state: AppState): { globalTime: GlobalTime } => { - return { globalTime: state.globalTime }; -}; - -export const DateTimeSelector = connect(mapStateToProps, { - updateTimeInterval: updateTimeInterval, -})(_DateTimeSelector); - -export default DateTimeSelector; diff --git a/frontend/src/modules/Nav/TopNav/ShowBreadcrumbs.tsx b/frontend/src/modules/Nav/TopNav/ShowBreadcrumbs.tsx deleted file mode 100644 index f3f0e931c3..0000000000 --- a/frontend/src/modules/Nav/TopNav/ShowBreadcrumbs.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Breadcrumb } from 'antd'; -import ROUTES from 'constants/routes'; -import React from 'react'; -import { Link, withRouter } from 'react-router-dom'; -import styled from 'styled-components'; - -const BreadCrumbWrapper = styled.div` - padding-top: 20px; - padding-left: 20px; -`; - -const breadcrumbNameMap: any = { - // PNOTE - TO DO - Remove any and do typechecking - like https://stackoverflow.com/questions/56568423/typescript-no-index-signature-with-a-parameter-of-type-string-was-found-on-ty - [ROUTES.APPLICATION]: 'Application', - [ROUTES.TRACES]: 'Traces', - [ROUTES.SERVICE_MAP]: 'Service Map', - [ROUTES.USAGE_EXPLORER]: 'Usage Explorer', - [ROUTES.INSTRUMENTATION]: 'Add instrumentation', - [ROUTES.SETTINGS]: 'Settings', -}; -import history from 'lib/history'; - -const ShowBreadcrumbs = (): JSX.Element => { - // const { location } = props; - const location = history.location; - const pathSnippets = location.pathname.split('/').filter((i) => i); - const extraBreadcrumbItems = pathSnippets.map((_, index) => { - const url = `/${pathSnippets.slice(0, index + 1).join('/')}`; - if (breadcrumbNameMap[url] === undefined) { - return ( - - {url.split('/').slice(-1)[0]} - - ); - } else { - return ( - - {breadcrumbNameMap[url]} - - ); - } - }); - const breadcrumbItems = [ - - Home - , - ].concat(extraBreadcrumbItems); - return ( - - {breadcrumbItems} - - ); -}; - -export default ShowBreadcrumbs; diff --git a/frontend/src/modules/Nav/TopNav/config.ts b/frontend/src/modules/Nav/TopNav/config.ts deleted file mode 100644 index 61a29b203d..0000000000 --- a/frontend/src/modules/Nav/TopNav/config.ts +++ /dev/null @@ -1,24 +0,0 @@ -import ROUTES from 'constants/routes'; - -export const Options = [ - { value: '5min', label: 'Last 5 min' }, - { value: '15min', label: 'Last 15 min' }, - { value: '30min', label: 'Last 30 min' }, - { value: '1hr', label: 'Last 1 hour' }, - { value: '6hr', label: 'Last 6 hour' }, - { value: '1day', label: 'Last 1 day' }, - { value: '1week', label: 'Last 1 week' }, - { value: 'custom', label: 'Custom' }, -]; - -export const ServiceMapOptions = [ - { value: '1min', label: 'Last 1 min' }, - { value: '5min', label: 'Last 5 min' }, -]; - -export const DefaultOptionsBasedOnRoute = { - [ROUTES.SERVICE_MAP]: ServiceMapOptions[0].value, - [ROUTES.APPLICATION]: Options[0].value, - [ROUTES.SERVICE_METRICS]: Options[2].value, - default: Options[2].value, -}; diff --git a/frontend/src/modules/RouteProvider.tsx b/frontend/src/modules/RouteProvider.tsx deleted file mode 100644 index eff5595614..0000000000 --- a/frontend/src/modules/RouteProvider.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import ROUTES from 'constants/routes'; -import React, { createContext, Dispatch, ReactNode, useContext } from 'react'; - -type State = { - [key: string]: { - route: string; - isLoaded: boolean; - }; -}; - -enum ActionTypes { - UPDATE_IS_LOADED = 'ROUTE_IS_LOADED', -} - -type Action = { - type: ActionTypes; - payload: string; -}; - -interface ContextType { - state: State; - dispatch: Dispatch; -} - -const RouteContext = createContext(null); - -interface RouteProviderProps { - children: ReactNode; -} -interface RouteObj { - [key: string]: { - route: string; - isLoaded: boolean; - }; -} - -const updateLocation = (state: State, action: Action): State => { - if (action.type === ActionTypes.UPDATE_IS_LOADED) { - /* - Update the isLoaded property in routes obj - if the route matches the current pathname - - Why: Checkout this issue https://github.com/SigNoz/signoz/issues/110 - To avoid calling the api's twice for Date picker, - We will only call once the route is changed - */ - Object.keys(ROUTES).map((items) => { - state[items].isLoaded = state[items].route === action.payload; - }); - return { - ...state, - }; - } - return { - ...state, - }; -}; - -const getInitialState = () => { - const routes: RouteObj = {}; - Object.keys(ROUTES).map((items) => { - routes[items] = { - route: `${ROUTES[items]}`, - isLoaded: false, - }; - }); - return routes; -}; - -const RouteProvider: React.FC = ({ children }) => { - const [state, dispatch] = React.useReducer(updateLocation, getInitialState()); - const value = { state, dispatch }; - return {children}; -}; - -const useRoute = (): ContextType => { - const context = useContext(RouteContext); - if (context === undefined) { - throw new Error('useRoute must be used within a RouteProvider'); - } - return context as ContextType; -}; -export { RouteProvider, useRoute }; diff --git a/frontend/src/modules/Servicemap/ServiceMap.tsx b/frontend/src/modules/Servicemap/ServiceMap.tsx index f4f4f2adca..4f3b397cf6 100644 --- a/frontend/src/modules/Servicemap/ServiceMap.tsx +++ b/frontend/src/modules/Servicemap/ServiceMap.tsx @@ -1,5 +1,4 @@ import Spinner from 'components/Spinner'; -import { useRoute } from 'modules/RouteProvider'; import React, { useEffect, useRef } from 'react'; import { ForceGraph2D } from 'react-force-graph'; import { connect } from 'react-redux'; @@ -7,11 +6,11 @@ import { RouteComponentProps, withRouter } from 'react-router-dom'; import { getDetailedServiceMapItems, getServiceMapItems, - GlobalTime, serviceMapStore, } from 'store/actions'; import { AppState } from 'store/reducers'; import styled from 'styled-components'; +import { GlobalTime } from 'types/actions/globalTime'; import SelectService from './SelectService'; import { getGraphData, getTooltip, getZoomPx, transformLabel } from './utils'; @@ -54,7 +53,6 @@ export interface graphDataType { const ServiceMap = (props: ServiceMapProps) => { const fgRef = useRef(); - const { state } = useRoute(); const { getDetailedServiceMapItems, @@ -68,10 +66,8 @@ const ServiceMap = (props: ServiceMapProps) => { Call the apis only when the route is loaded. Check this issue: https://github.com/SigNoz/signoz/issues/110 */ - if (state.SERVICE_MAP.isLoaded) { - getServiceMapItems(globalTime); - getDetailedServiceMapItems(globalTime); - } + getServiceMapItems(globalTime); + getDetailedServiceMapItems(globalTime); }, [globalTime]); useEffect(() => { diff --git a/frontend/src/modules/Traces/TraceCustomVisualizations.tsx b/frontend/src/modules/Traces/TraceCustomVisualizations.tsx index 58c14b4598..892ab12a1f 100644 --- a/frontend/src/modules/Traces/TraceCustomVisualizations.tsx +++ b/frontend/src/modules/Traces/TraceCustomVisualizations.tsx @@ -1,14 +1,15 @@ import { Form, Select, Space } from 'antd'; import Graph from 'components/Graph'; -import { useRoute } from 'modules/RouteProvider'; import React, { useEffect, useState } from 'react'; -import { connect } from 'react-redux'; -import { GlobalTime, TraceFilters } from 'store/actions'; +import { connect, useSelector } from 'react-redux'; +import { TraceFilters } from 'store/actions'; import { getFilteredTraceMetrics } from 'store/actions/MetricsActions'; import { customMetricsItem } from 'store/actions/MetricsActions'; import { AppState } from 'store/reducers'; const { Option } = Select; import { colors } from 'lib/getRandomColor'; +import { GlobalTime } from 'types/actions/globalTime'; +import { GlobalReducer } from 'types/reducer/globalTime'; import { Card, @@ -85,7 +86,6 @@ const _TraceCustomVisualizations = ( ): JSX.Element => { const [selectedEntity, setSelectedEntity] = useState('calls'); const [selectedAggOption, setSelectedAggOption] = useState('count'); - const { state } = useRoute(); const [form] = Form.useForm(); const selectedStep = '60'; const { @@ -94,6 +94,9 @@ const _TraceCustomVisualizations = ( globalTime, traceFilters, } = props; + const { loading } = useSelector( + (state) => state.globalTime, + ); // Step should be multiples of 60, 60 -> 1 min useEffect(() => { @@ -129,16 +132,16 @@ const _TraceCustomVisualizations = ( Call the apis only when the route is loaded. Check this issue: https://github.com/SigNoz/signoz/issues/110 */ - if (state.TRACES.isLoaded) { + if (loading === false) { getFilteredTraceMetrics(request_string, plusMinus15); } }, [ selectedEntity, selectedAggOption, traceFilters, - globalTime, getFilteredTraceMetrics, - state.TRACES.isLoaded, + globalTime, + loading, ]); //Custom metrics API called if time, tracefilters, selected entity or agg option changes diff --git a/frontend/src/modules/Traces/TraceDetail.tsx b/frontend/src/modules/Traces/TraceDetail.tsx index ede374bae8..7e26b88356 100644 --- a/frontend/src/modules/Traces/TraceDetail.tsx +++ b/frontend/src/modules/Traces/TraceDetail.tsx @@ -1,10 +1,21 @@ -import React from 'react'; +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 { TraceCustomVisualizations } from './TraceCustomVisualizations'; import { TraceFilter } from './TraceFilter'; import { TraceList } from './TraceList'; -const TraceDetail = (): JSX.Element => { +const TraceDetail = ({ globalTimeLoading }: Props): JSX.Element => { + useEffect(() => { + return (): void => { + globalTimeLoading(); + }; + }, [globalTimeLoading]); + return ( <> @@ -14,4 +25,16 @@ const TraceDetail = (): JSX.Element => { ); }; -export default TraceDetail; +interface DispatchProps { + globalTimeLoading: () => void; +} + +const mapDispatchToProps = ( + dispatch: ThunkDispatch, +): DispatchProps => ({ + globalTimeLoading: bindActionCreators(GlobalTimeLoading, dispatch), +}); + +type Props = DispatchProps; + +export default connect(null, mapDispatchToProps)(TraceDetail); diff --git a/frontend/src/modules/Traces/TraceFilter.tsx b/frontend/src/modules/Traces/TraceFilter.tsx index 75ddf6bbf4..538a257203 100644 --- a/frontend/src/modules/Traces/TraceFilter.tsx +++ b/frontend/src/modules/Traces/TraceFilter.tsx @@ -3,8 +3,6 @@ import FormItem from 'antd/lib/form/FormItem'; import { Store } from 'antd/lib/form/interface'; import api from 'api'; import { METRICS_PAGE_QUERY_PARAM } from 'constants/query'; -import useMountedState from 'hooks/useMountedState'; -import { useRoute } from 'modules/RouteProvider'; import React, { useCallback, useEffect, @@ -12,16 +10,13 @@ import React, { useRef, useState, } from 'react'; -import { connect } from 'react-redux'; +import { connect, useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; -import { - fetchTraces, - GlobalTime, - TraceFilters, - updateTraceFilters, -} from 'store/actions'; +import { fetchTraces, TraceFilters, updateTraceFilters } from 'store/actions'; import { AppState } from 'store/reducers'; import styled from 'styled-components'; +import { GlobalTime } from 'types/actions/globalTime'; +import { GlobalReducer } from 'types/reducer/globalTime'; import { FilterStateDisplay } from './FilterStateDisplay'; import LatencyModalForm from './LatencyModalForm'; @@ -59,11 +54,11 @@ const _TraceFilter = (props: TraceFilterProps): JSX.Element => { const urlParams = useMemo(() => { return new URLSearchParams(location.search.split('?')[1]); }, [location.search]); - const isMount = useMountedState(); - const isMounted = isMount(); + const { loading } = useSelector( + (state) => state.globalTime, + ); - const { state } = useRoute(); const { updateTraceFilters, traceFilters, globalTime, fetchTraces } = props; const [modalVisible, setModalVisible] = useState(false); @@ -86,12 +81,30 @@ const _TraceFilter = (props: TraceFilterProps): JSX.Element => { [traceFilters, updateTraceFilters], ); + const populateData = useCallback( + (value: string) => { + if (loading === false) { + const service_request = '/service/' + value + '/operations'; + api.get(service_request).then((response) => { + // form_basefilter.resetFields(['operation',]) + setOperationsList(response.data); + }); + + const tagkeyoptions_request = '/tags?service=' + value; + api.get(tagkeyoptions_request).then((response) => { + setTagKeyOptions(response.data); + }); + } + }, + [loading], + ); + const handleChangeService = useCallback( (value: string) => { populateData(value); updateTraceFilters({ ...traceFilters, service: value }); }, - [traceFilters, updateTraceFilters], + [traceFilters, updateTraceFilters, populateData], ); const spanKindList: ISpanKind[] = [ @@ -181,7 +194,7 @@ const _TraceFilter = (props: TraceFilterProps): JSX.Element => { const counter = useRef(0); useEffect(() => { - if (isMounted && counter.current === 0) { + if (loading === false && counter.current === 0) { counter.current = 1; api .get(`/services/list`) @@ -237,7 +250,8 @@ const _TraceFilter = (props: TraceFilterProps): JSX.Element => { traceFilters, urlParams, updateTraceFilters, - isMounted, + populateData, + loading, ]); useEffect(() => { @@ -262,10 +276,10 @@ const _TraceFilter = (props: TraceFilterProps): JSX.Element => { Call the apis only when the route is loaded. Check this issue: https://github.com/SigNoz/signoz/issues/110 */ - if (state.TRACES.isLoaded) { + if (loading === false) { fetchTraces(globalTime, request_string); } - }, [globalTime, traceFilters, fetchTraces, state]); + }, [traceFilters, fetchTraces, loading, globalTime]); useEffect(() => { let latencyButtonText = 'Latency'; @@ -305,19 +319,6 @@ const _TraceFilter = (props: TraceFilterProps): JSX.Element => { form_basefilter.setFieldsValue({ kind: traceFilters.kind }); }, [traceFilters.kind, form_basefilter]); - function populateData(value: string): void { - const service_request = '/service/' + value + '/operations'; - api.get(service_request).then((response) => { - // form_basefilter.resetFields(['operation',]) - setOperationsList(response.data); - }); - - const tagkeyoptions_request = '/tags?service=' + value; - api.get(tagkeyoptions_request).then((response) => { - setTagKeyOptions(response.data); - }); - } - const onLatencyButtonClick = (): void => { setModalVisible(true); }; diff --git a/frontend/src/modules/Usage/UsageExplorer.tsx b/frontend/src/modules/Usage/UsageExplorer.tsx index 0259b96366..3fadaba522 100644 --- a/frontend/src/modules/Usage/UsageExplorer.tsx +++ b/frontend/src/modules/Usage/UsageExplorer.tsx @@ -1,20 +1,15 @@ import { Select, Space } from 'antd'; -// import { Bar } from 'react-chartjs-2'; import Graph from 'components/Graph'; -import { useRoute } from 'modules/RouteProvider'; -import moment from 'moment'; import React, { useEffect, useState } from 'react'; -import { connect } from 'react-redux'; -import { - getServicesList, - getUsageData, - GlobalTime, - usageDataItem, -} from 'store/actions'; +import { connect, useSelector } from 'react-redux'; +import { getServicesList, getUsageData, usageDataItem } from 'store/actions'; import { servicesListItem } from 'store/actions/MetricsActions'; import { AppState } from 'store/reducers'; import { isOnboardingSkipped } from 'utils/app'; const { Option } = Select; +import { GlobalTime } from 'types/actions/globalTime'; +import { GlobalReducer } from 'types/reducer/globalTime'; + import { Card } from './styles'; interface UsageExplorerProps { @@ -56,8 +51,9 @@ const _UsageExplorer = (props: UsageExplorerProps) => { const [selectedTime, setSelectedTime] = useState(timeDaysOptions[1]); const [selectedInterval, setSelectedInterval] = useState(interval[2]); const [selectedService, setSelectedService] = useState(''); - - const { state } = useRoute(); + const { loading } = useSelector( + (state) => state.globalTime, + ); useEffect(() => { if (selectedTime && selectedInterval) { @@ -78,15 +74,13 @@ const _UsageExplorer = (props: UsageExplorerProps) => { Call the apis only when the route is loaded. Check this issue: https://github.com/SigNoz/signoz/issues/110 */ - if (state.USAGE_EXPLORER.isLoaded) { + if (loading) { props.getServicesList(props.globalTime); } - }, []); + }, [loading, props]); const data = { - labels: props.usageData.map((s) => - moment(s.timestamp / 1000000).format('MMM Do h a'), - ), + labels: props.usageData.map((s) => new Date(s.timestamp / 1000000)), datasets: [ { label: 'Span Count', @@ -98,22 +92,6 @@ const _UsageExplorer = (props: UsageExplorerProps) => { ], }; - const options = { - scales: { - yAxes: [ - { - ticks: { - beginAtZero: true, - fontSize: 10, - }, - }, - ], - }, - legend: { - display: false, - }, - }; - return ( {/* PNOTE - TODO - Keep it in reponsive row column tab */} diff --git a/frontend/src/pages/MetricApplication/index.tsx b/frontend/src/pages/MetricApplication/index.tsx new file mode 100644 index 0000000000..e6a9870d88 --- /dev/null +++ b/frontend/src/pages/MetricApplication/index.tsx @@ -0,0 +1,79 @@ +import { Typography } from 'antd'; +import Spinner from 'components/Spinner'; +import MetricsApplicationContainer from 'container/MetricsApplication'; +import React, { useEffect } from 'react'; +import { connect, useDispatch, useSelector } from 'react-redux'; +import { useParams } from 'react-router-dom'; +import { bindActionCreators, Dispatch } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; +import { + GetInitialData, + GetInitialDataProps, +} from 'store/actions/metrics/getInitialData'; +import { AppState } from 'store/reducers'; +import AppActions from 'types/actions'; +import { GlobalReducer } from 'types/reducer/globalTime'; +import MetricReducer from 'types/reducer/metrics'; + +const MetricsApplication = ({ getInitialData }: MetricsProps): JSX.Element => { + const { loading, maxTime, minTime } = useSelector( + (state) => state.globalTime, + ); + const { error, errorMessage } = useSelector( + (state) => state.metrics, + ); + + const { servicename } = useParams(); + + const dispatch = useDispatch>(); + + useEffect(() => { + if (servicename !== undefined && loading == false) { + getInitialData({ + end: maxTime, + service: servicename, + start: minTime, + step: 60, + }); + } + + 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: [], + }, + }); + }; + }, [servicename, maxTime, minTime, getInitialData, loading, dispatch]); + + if (error) { + return {errorMessage}; + } + + if (loading) { + return ; + } + + return ; +}; + +interface DispatchProps { + getInitialData: (props: GetInitialDataProps) => void; +} + +interface ServiceProps { + servicename?: string; +} + +const mapDispatchToProps = ( + dispatch: ThunkDispatch, +): DispatchProps => ({ + getInitialData: bindActionCreators(GetInitialData, dispatch), +}); + +type MetricsProps = DispatchProps; + +export default connect(null, mapDispatchToProps)(MetricsApplication); diff --git a/frontend/src/pages/Metrics/index.tsx b/frontend/src/pages/Metrics/index.tsx new file mode 100644 index 0000000000..aeed58319f --- /dev/null +++ b/frontend/src/pages/Metrics/index.tsx @@ -0,0 +1,72 @@ +import Spinner from 'components/Spinner'; +import { SKIP_ONBOARDING } from 'constants/onboarding'; +import MetricTable from 'container/MetricsTable'; +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 { 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 { services } = useSelector( + (state) => state.metrics, + ); + + const isSkipped = localStorage.getItem(SKIP_ONBOARDING) === 'true'; + + useEffect(() => { + if (loading === false) { + getService({ + start: minTime, + end: maxTime, + }); + } + }, [getService, maxTime, minTime, loading]); + + useEffect(() => { + let timeInterval: NodeJS.Timeout; + + if (loading === false && !isSkipped && services.length === 0) { + timeInterval = setInterval(() => { + getService({ + start: minTime, + end: maxTime, + }); + }, 50000); + } + + return (): void => { + clearInterval(timeInterval); + }; + }, [getService, isSkipped, loading, maxTime, minTime, services]); + + if (loading) { + return ; + } + + return ; +}; + +interface DispatchProps { + getService: ({ + end, + start, + }: GetServiceProps) => (dispatch: Dispatch) => void; +} + +const mapDispatchToProps = ( + dispatch: ThunkDispatch, +): DispatchProps => ({ + getService: bindActionCreators(GetService, dispatch), +}); + +type MetricsProps = DispatchProps; + +export default connect(null, mapDispatchToProps)(Metrics); diff --git a/frontend/src/pages/Settings/index.tsx b/frontend/src/pages/Settings/index.tsx index 64123991e8..cfaf996138 100644 --- a/frontend/src/pages/Settings/index.tsx +++ b/frontend/src/pages/Settings/index.tsx @@ -13,26 +13,18 @@ const SettingsPage = (): JSX.Element => { return ( <> - + - + - + { +const Signup = ({ globalLoading }: SignupProps): JSX.Element => { const [state, setState] = useState({ submitted: false }); const [formState, setFormState] = useState({ firstName: { value: '' }, @@ -50,6 +55,7 @@ const Signup = (): JSX.Element => { if (response.statusCode === 200) { localStorage.setItem(IS_LOGGED_IN, 'yes'); history.push(ROUTES.APPLICATION); + globalLoading(); } else { // @TODO throw a error notification here } @@ -113,4 +119,16 @@ const Signup = (): JSX.Element => { ); }; -export default Signup; +interface DispatchProps { + globalLoading: () => void; +} + +const mapDispatchToProps = ( + dispatch: ThunkDispatch, +): DispatchProps => ({ + globalLoading: bindActionCreators(GlobalTimeLoading, dispatch), +}); + +type SignupProps = DispatchProps; + +export default connect(null, mapDispatchToProps)(Signup); diff --git a/frontend/src/store/actions/MetricsActions/metricsActions.ts b/frontend/src/store/actions/MetricsActions/metricsActions.ts index 5a01c4e0de..4090468b13 100644 --- a/frontend/src/store/actions/MetricsActions/metricsActions.ts +++ b/frontend/src/store/actions/MetricsActions/metricsActions.ts @@ -1,6 +1,6 @@ import api from 'api'; import { Dispatch } from 'redux'; -import { GlobalTime } from 'store/actions/global'; +import { GlobalTime } from 'types/actions/globalTime'; import { toUTCEpoch } from 'utils/timeUtils'; import { MetricsActionTypes } from './metricsActionTypes'; diff --git a/frontend/src/store/actions/dashboard/getQueryResults.ts b/frontend/src/store/actions/dashboard/getQueryResults.ts index 0912d424aa..90a74b84f1 100644 --- a/frontend/src/store/actions/dashboard/getQueryResults.ts +++ b/frontend/src/store/actions/dashboard/getQueryResults.ts @@ -35,7 +35,7 @@ export const GetQueryResults = ( end, query: encodeURIComponent(query.query), start: start, - step: '30', + step: '60', }); return { query: query.query, diff --git a/frontend/src/store/actions/global.ts b/frontend/src/store/actions/global.ts index fe8874297a..1b5e8e11d8 100644 --- a/frontend/src/store/actions/global.ts +++ b/frontend/src/store/actions/global.ts @@ -1,81 +1,63 @@ -import { Moment } from 'moment'; +import { Time } from 'container/Header/DateTimeSelection/config'; +import getMinAgo from 'lib/getStartAndEndTime/getMinAgo'; +import { Dispatch } from 'redux'; +import AppActions from 'types/actions'; -import { ActionTypes } from './types'; +export const UpdateTimeInterval = ( + interval: Time, + dateTimeRange: [number, number] = [0, 0], +): ((dispatch: Dispatch) => void) => { + return (dispatch: Dispatch): void => { + let maxTime = new Date().getTime(); + let minTime = 0; -export type DateTimeRangeType = [Moment | null, Moment | null] | null; + 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]; + } -export interface GlobalTime { - maxTime: number; - minTime: number; -} - -export interface updateTimeIntervalAction { - type: ActionTypes.updateTimeInterval; - payload: GlobalTime; -} - -export const updateTimeInterval = ( - interval: string, - datetimeRange?: [number, number], -) => { - let maxTime = 0; - let minTime = 0; - // if interval string is custom, then datetimRange should be present and max & min time should be - // set directly based on that. Assuming datetimeRange values are in ms, and minTime is 0th element - - switch (interval) { - case '1min': - maxTime = Date.now() * 1000000; // in nano sec - minTime = (Date.now() - 1 * 60 * 1000) * 1000000; - break; - case '5min': - maxTime = Date.now() * 1000000; // in nano sec - minTime = (Date.now() - 5 * 60 * 1000) * 1000000; - break; - - case '15min': - maxTime = Date.now() * 1000000; // in nano sec - minTime = (Date.now() - 15 * 60 * 1000) * 1000000; - break; - - case '30min': - maxTime = Date.now() * 1000000; // in nano sec - minTime = (Date.now() - 30 * 60 * 1000) * 1000000; - break; - - case '1hr': - maxTime = Date.now() * 1000000; // in nano sec - minTime = (Date.now() - 1 * 60 * 60 * 1000) * 1000000; - break; - - case '6hr': - maxTime = Date.now() * 1000000; // in nano sec - minTime = (Date.now() - 6 * 60 * 60 * 1000) * 1000000; - break; - - case '1day': - maxTime = Date.now() * 1000000; // in nano sec - minTime = (Date.now() - 24 * 60 * 60 * 1000) * 1000000; - break; - - case '1week': - maxTime = Date.now() * 1000000; // in nano sec - minTime = (Date.now() - 7 * 24 * 60 * 60 * 1000) * 1000000; - break; - - case 'custom': - if (datetimeRange !== undefined) { - maxTime = datetimeRange[1] * 1000000; // in nano sec - minTime = datetimeRange[0] * 1000000; // in nano sec - } - break; - - default: - // console.log('not found matching case'); - } - - return { - type: ActionTypes.updateTimeInterval, - payload: { maxTime: maxTime, minTime: minTime }, + dispatch({ + type: 'UPDATE_TIME_INTERVAL', + payload: { + maxTime: maxTime * 1000000, // in nano sec, + minTime: minTime * 1000000, + }, + }); + }; +}; + +export const GlobalTimeLoading = (): (( + dispatch: Dispatch, +) => void) => { + return (dispatch: Dispatch): void => { + dispatch({ + type: 'GLOBAL_TIME_LOADING_START', + }); }; }; diff --git a/frontend/src/store/actions/index.ts b/frontend/src/store/actions/index.ts index 09f21a1948..22d4fd9178 100644 --- a/frontend/src/store/actions/index.ts +++ b/frontend/src/store/actions/index.ts @@ -1,6 +1,7 @@ export * from './app'; export * from './dashboard'; export * from './global'; +export * from './metrics'; export * from './MetricsActions'; export * from './serviceMap'; export * from './traceFilters'; diff --git a/frontend/src/store/actions/metrics/getInitialData.ts b/frontend/src/store/actions/metrics/getInitialData.ts new file mode 100644 index 0000000000..41c798a88b --- /dev/null +++ b/frontend/src/store/actions/metrics/getInitialData.ts @@ -0,0 +1,94 @@ +// import getDBOverView from 'api/metrics/getDBOverView'; +// import getExternalAverageDuration from 'api/metrics/getExternalAverageDuration'; +// import getExternalError from 'api/metrics/getExternalError'; +// import getExternalService from 'api/metrics/getExternalService'; +import getServiceOverview from 'api/metrics/getServiceOverview'; +import getTopEndPoints from 'api/metrics/getTopEndPoints'; +import { AxiosError } from 'axios'; +import { Dispatch } from 'redux'; +import AppActions from 'types/actions'; +import { Props } from 'types/api/metrics/getDBOverview'; + +export const GetInitialData = ( + props: GetInitialDataProps, +): ((dispatch: Dispatch) => void) => { + return async (dispatch: Dispatch): Promise => { + try { + dispatch({ + type: 'GET_INITIAL_APPLICATION_LOADING', + }); + + const [ + // getDBOverViewResponse, + // getExternalAverageDurationResponse, + // getExternalErrorResponse, + // getExternalServiceResponse, + getServiceOverviewResponse, + getTopEndPointsResponse, + ] = await Promise.all([ + // getDBOverView({ + // ...props, + // }), + // getExternalAverageDuration({ + // ...props, + // }), + // getExternalError({ + // ...props, + // }), + // getExternalService({ + // ...props, + // }), + getServiceOverview({ + ...props, + }), + getTopEndPoints({ + ...props, + }), + ]); + + if ( + // getDBOverViewResponse.statusCode === 200 && + // getExternalAverageDurationResponse.statusCode === 200 && + // getExternalErrorResponse.statusCode === 200 && + // getExternalServiceResponse.statusCode === 200 && + getServiceOverviewResponse.statusCode === 200 && + getTopEndPointsResponse.statusCode === 200 + ) { + dispatch({ + type: 'GET_INTIAL_APPLICATION_DATA', + payload: { + // dbOverView: getDBOverViewResponse.payload, + // externalAverageDuration: getExternalAverageDurationResponse.payload, + // externalError: getExternalErrorResponse.payload, + // externalService: getExternalServiceResponse.payload, + serviceOverview: getServiceOverviewResponse.payload, + topEndPoints: getTopEndPointsResponse.payload, + }, + }); + } else { + dispatch({ + type: 'GET_INITIAL_APPLICATION_ERROR', + payload: { + errorMessage: + getTopEndPointsResponse.error || + getServiceOverviewResponse.error || + // getExternalServiceResponse.error || + // getExternalErrorResponse.error || + // getExternalAverageDurationResponse.error || + // getDBOverViewResponse.error || + 'Something went wrong', + }, + }); + } + } catch (error) { + dispatch({ + type: 'GET_INITIAL_APPLICATION_ERROR', + payload: { + errorMessage: (error as AxiosError).toString() || 'Something went wrong', + }, + }); + } + }; +}; + +export type GetInitialDataProps = Props; diff --git a/frontend/src/store/actions/metrics/getService.ts b/frontend/src/store/actions/metrics/getService.ts new file mode 100644 index 0000000000..bb839cf2c6 --- /dev/null +++ b/frontend/src/store/actions/metrics/getService.ts @@ -0,0 +1,43 @@ +import getService from 'api/metrics/getService'; +import { AxiosError } from 'axios'; +import { Dispatch } from 'redux'; +import AppActions from 'types/actions'; +import { Props } from 'types/api/metrics/getService'; + +export const GetService = ({ + end, + start, +}: GetServiceProps): ((dispatch: Dispatch) => void) => { + return async (dispatch: Dispatch): Promise => { + try { + dispatch({ + type: 'GET_SERVICE_LIST_LOADING_START', + }); + + const response = await getService({ end, start }); + + if (response.statusCode === 200) { + dispatch({ + type: 'GET_SERVICE_LIST_SUCCESS', + payload: response.payload, + }); + } else { + dispatch({ + type: 'GET_SERVICE_LIST_ERROR', + payload: { + errorMessage: response.error || 'Something went wrong', + }, + }); + } + } catch (error) { + dispatch({ + type: 'GET_SERVICE_LIST_ERROR', + payload: { + errorMessage: (error as AxiosError).toString() || 'Something went wrong', + }, + }); + } + }; +}; + +export type GetServiceProps = Props; diff --git a/frontend/src/store/actions/metrics/index.ts b/frontend/src/store/actions/metrics/index.ts new file mode 100644 index 0000000000..b253899322 --- /dev/null +++ b/frontend/src/store/actions/metrics/index.ts @@ -0,0 +1 @@ +export * from './getService'; diff --git a/frontend/src/store/actions/serviceMap.ts b/frontend/src/store/actions/serviceMap.ts index 617a91d225..78811c1327 100644 --- a/frontend/src/store/actions/serviceMap.ts +++ b/frontend/src/store/actions/serviceMap.ts @@ -1,7 +1,7 @@ import api from 'api'; import { Dispatch } from 'redux'; +import { GlobalTime } from 'types/actions/globalTime'; -import { GlobalTime } from './global'; import { ActionTypes } from './types'; export interface serviceMapStore { diff --git a/frontend/src/store/actions/traces.ts b/frontend/src/store/actions/traces.ts index c2ffef8f6e..d312865e24 100644 --- a/frontend/src/store/actions/traces.ts +++ b/frontend/src/store/actions/traces.ts @@ -1,9 +1,9 @@ import api from 'api'; import ROUTES from 'constants/routes'; import { Dispatch } from 'redux'; +import { GlobalTime } from 'types/actions/globalTime'; import { toUTCEpoch } from 'utils/timeUtils'; -import { GlobalTime } from './global'; import { ActionTypes } from './types'; // PNOTE diff --git a/frontend/src/store/actions/types.ts b/frontend/src/store/actions/types.ts index 639b66b060..6c2902c1b3 100644 --- a/frontend/src/store/actions/types.ts +++ b/frontend/src/store/actions/types.ts @@ -1,4 +1,3 @@ -import { updateTimeIntervalAction } from './global'; import { serviceMapItemAction, servicesAction } from './serviceMap'; import { updateTraceFiltersAction } from './traceFilters'; import { FetchTraceItemAction, FetchTracesAction } from './traces'; @@ -19,6 +18,5 @@ export type Action = | FetchTracesAction | updateTraceFiltersAction | getUsageDataAction - | updateTimeIntervalAction | servicesAction | serviceMapItemAction; diff --git a/frontend/src/store/reducers/global.ts b/frontend/src/store/reducers/global.ts index ebbb946295..104d3c6ba6 100644 --- a/frontend/src/store/reducers/global.ts +++ b/frontend/src/store/reducers/global.ts @@ -1,17 +1,39 @@ -import { Action, ActionTypes, GlobalTime } from 'store/actions'; +import { + GLOBAL_TIME_LOADING_START, + GlobalTimeAction, + UPDATE_TIME_INTERVAL, +} from 'types/actions/globalTime'; +import { GlobalReducer } from 'types/reducer/globalTime'; -export const updateGlobalTimeReducer = ( - state: GlobalTime = { - maxTime: Date.now() * 1000000, - minTime: (Date.now() - 15 * 60 * 1000) * 1000000, - }, - action: Action, -): GlobalTime => { - // Initial global state is time now and 15 minute interval +const intitalState: GlobalReducer = { + maxTime: Date.now() * 1000000, + minTime: (Date.now() - 15 * 60 * 1000) * 1000000, + loading: true, +}; + +const globalTimeReducer = ( + state = intitalState, + action: GlobalTimeAction, +): GlobalReducer => { switch (action.type) { - case ActionTypes.updateTimeInterval: - return action.payload; + case UPDATE_TIME_INTERVAL: { + return { + ...state, + ...action.payload, + loading: false, + }; + } + + case GLOBAL_TIME_LOADING_START: { + return { + ...state, + loading: true, + }; + } + default: return state; } }; + +export default globalTimeReducer; diff --git a/frontend/src/store/reducers/index.ts b/frontend/src/store/reducers/index.ts index ed22f6c638..adbb190968 100644 --- a/frontend/src/store/reducers/index.ts +++ b/frontend/src/store/reducers/index.ts @@ -2,7 +2,8 @@ import { combineReducers } from 'redux'; import appReducer from './app'; import dashboardReducer from './dashboard'; -import { updateGlobalTimeReducer } from './global'; +import globalTimeReducer from './global'; +import metricsReducers from './metric'; import { metricsReducer } from './metrics'; import { ServiceMapReducer } from './serviceMap'; import TraceFilterReducer from './traceFilters'; @@ -14,11 +15,12 @@ const reducers = combineReducers({ traces: tracesReducer, traceItem: traceItemReducer, usageDate: usageDataReducer, - globalTime: updateGlobalTimeReducer, + globalTime: globalTimeReducer, metricsData: metricsReducer, serviceMap: ServiceMapReducer, dashboards: dashboardReducer, app: appReducer, + metrics: metricsReducers, }); export type AppState = ReturnType; diff --git a/frontend/src/store/reducers/metric.ts b/frontend/src/store/reducers/metric.ts new file mode 100644 index 0000000000..dbe0178ade --- /dev/null +++ b/frontend/src/store/reducers/metric.ts @@ -0,0 +1,97 @@ +import { + GET_INITIAL_APPLICATION_ERROR, + GET_INITIAL_APPLICATION_LOADING, + GET_INTIAL_APPLICATION_DATA, + GET_SERVICE_LIST_ERROR, + GET_SERVICE_LIST_LOADING_START, + GET_SERVICE_LIST_SUCCESS, + MetricsActions, +} from 'types/actions/metrics'; +import InitialValueTypes from 'types/reducer/metrics'; + +const InitialValue: InitialValueTypes = { + error: false, + errorMessage: '', + loading: false, + services: [], + dbOverView: [], + externalService: [], + topEndPoints: [], + externalAverageDuration: [], + externalError: [], + serviceOverview: [], +}; + +const metrics = ( + state = InitialValue, + action: MetricsActions, +): InitialValueTypes => { + switch (action.type) { + case GET_SERVICE_LIST_ERROR: { + const { errorMessage } = action.payload; + + return { + ...state, + error: true, + errorMessage: errorMessage, + loading: false, + }; + } + + case GET_SERVICE_LIST_LOADING_START: { + return { + ...state, + loading: true, + }; + } + + case GET_SERVICE_LIST_SUCCESS: { + return { + ...state, + loading: false, + services: action.payload, + }; + } + + case GET_INITIAL_APPLICATION_LOADING: { + return { + ...state, + loading: true, + }; + } + case GET_INITIAL_APPLICATION_ERROR: { + return { + ...state, + loading: false, + errorMessage: action.payload.errorMessage, + error: true, + }; + } + + case GET_INTIAL_APPLICATION_DATA: { + const { + // dbOverView, + topEndPoints, + serviceOverview, + // externalService, + // externalAverageDuration, + // externalError, + } = action.payload; + + return { + ...state, + loading: false, + // dbOverView, + topEndPoints, + serviceOverview, + // externalService, + // externalAverageDuration, + // externalError, + }; + } + default: + return state; + } +}; + +export default metrics; diff --git a/frontend/src/types/actions/globalTime.ts b/frontend/src/types/actions/globalTime.ts new file mode 100644 index 0000000000..31102f0139 --- /dev/null +++ b/frontend/src/types/actions/globalTime.ts @@ -0,0 +1,18 @@ +export const UPDATE_TIME_INTERVAL = 'UPDATE_TIME_INTERVAL'; +export const GLOBAL_TIME_LOADING_START = 'GLOBAL_TIME_LOADING_START'; + +export type GlobalTime = { + maxTime: number; + minTime: number; +}; + +interface UpdateTimeInterval { + type: typeof UPDATE_TIME_INTERVAL; + payload: GlobalTime; +} + +interface GlobalTimeLoading { + type: typeof GLOBAL_TIME_LOADING_START; +} + +export type GlobalTimeAction = UpdateTimeInterval | GlobalTimeLoading; diff --git a/frontend/src/types/actions/index.ts b/frontend/src/types/actions/index.ts index afcda6efef..618e2564b6 100644 --- a/frontend/src/types/actions/index.ts +++ b/frontend/src/types/actions/index.ts @@ -1,6 +1,12 @@ import { AppAction } from './app'; import { DashboardActions } from './dashboard'; +import { GlobalTimeAction } from './globalTime'; +import { MetricsActions } from './metrics'; -type AppActions = DashboardActions | AppAction; +type AppActions = + | DashboardActions + | AppAction + | GlobalTimeAction + | MetricsActions; export default AppActions; diff --git a/frontend/src/types/actions/metrics.ts b/frontend/src/types/actions/metrics.ts new file mode 100644 index 0000000000..a2454be066 --- /dev/null +++ b/frontend/src/types/actions/metrics.ts @@ -0,0 +1,51 @@ +// import { DBOverView } from 'types/api/metrics/getDBOverview'; +// import { ExternalAverageDuration } from 'types/api/metrics/getExternalAverageDuration'; +// import { ExternalError } from 'types/api/metrics/getExternalError'; +// import { ExternalService } from 'types/api/metrics/getExternalService'; +import { ServicesList } from 'types/api/metrics/getService'; +import { ServiceOverview } from 'types/api/metrics/getServiceOverview'; +import { TopEndPoints } from 'types/api/metrics/getTopEndPoints'; + +export const GET_SERVICE_LIST_SUCCESS = 'GET_SERVICE_LIST_SUCCESS'; +export const GET_SERVICE_LIST_LOADING_START = 'GET_SERVICE_LIST_LOADING_START'; +export const GET_SERVICE_LIST_ERROR = 'GET_SERVICE_LIST_ERROR'; +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 interface GetServiceList { + type: typeof GET_SERVICE_LIST_SUCCESS; + payload: ServicesList[]; +} + +export interface GetServiceListLoading { + type: + | typeof GET_SERVICE_LIST_LOADING_START + | typeof GET_INITIAL_APPLICATION_LOADING; +} + +export interface GetServiceListError { + type: typeof GET_SERVICE_LIST_ERROR | typeof GET_INITIAL_APPLICATION_ERROR; + payload: { + errorMessage: string; + }; +} + +export interface GetInitialApplicationData { + type: typeof GET_INTIAL_APPLICATION_DATA; + payload: { + topEndPoints: TopEndPoints[]; + // dbOverView: DBOverView[]; + // externalService: ExternalService[]; + // externalAverageDuration: ExternalAverageDuration[]; + // externalError: ExternalError[]; + serviceOverview: ServiceOverview[]; + }; +} + +export type MetricsActions = + | GetServiceListError + | GetServiceListLoading + | GetServiceList + | GetInitialApplicationData; diff --git a/frontend/src/types/api/metrics/getDBOverview.ts b/frontend/src/types/api/metrics/getDBOverview.ts new file mode 100644 index 0000000000..1e99b9688e --- /dev/null +++ b/frontend/src/types/api/metrics/getDBOverview.ts @@ -0,0 +1,16 @@ +export interface Props { + service: string; + start: number; + end: number; + step: number; +} + +export interface DBOverView { + avgDuration: number; + callRate: number; + externalHttpUrl: string; + numCalls: number; + timestamp: number; +} + +export type PayloadProps = DBOverView[]; diff --git a/frontend/src/types/api/metrics/getExternalAverageDuration.ts b/frontend/src/types/api/metrics/getExternalAverageDuration.ts new file mode 100644 index 0000000000..f19a980e58 --- /dev/null +++ b/frontend/src/types/api/metrics/getExternalAverageDuration.ts @@ -0,0 +1,12 @@ +import { Props as GetDBOverViewProps } from './getDBOverview'; + +export type Props = GetDBOverViewProps; + +export interface ExternalAverageDuration { + avgDuration: number; + errorRate: number; + numErrors: number; + timestamp: number; +} + +export type PayloadProps = ExternalAverageDuration[]; diff --git a/frontend/src/types/api/metrics/getExternalError.ts b/frontend/src/types/api/metrics/getExternalError.ts new file mode 100644 index 0000000000..cc63e51486 --- /dev/null +++ b/frontend/src/types/api/metrics/getExternalError.ts @@ -0,0 +1,13 @@ +import { Props as GetDBOverViewProps } from './getDBOverview'; + +export type Props = GetDBOverViewProps; + +export interface ExternalError { + avgDuration: number; + errorRate: number; + externalHttpUrl: string; + numErrors: number; + timestamp: number; +} + +export type PayloadProps = ExternalError[]; diff --git a/frontend/src/types/api/metrics/getExternalService.ts b/frontend/src/types/api/metrics/getExternalService.ts new file mode 100644 index 0000000000..91f620d392 --- /dev/null +++ b/frontend/src/types/api/metrics/getExternalService.ts @@ -0,0 +1,15 @@ +import { Props as GetDBOverViewProps } from './getDBOverview'; + +export type Props = GetDBOverViewProps; + +export interface ExternalService { + avgDuration: number; + callRate: number; + errorRate: number; + externalHttpUrl: string; + numCalls: number; + numErrors: number; + timestamp: number; +} + +export type PayloadProps = ExternalService[]; diff --git a/frontend/src/types/api/metrics/getService.ts b/frontend/src/types/api/metrics/getService.ts new file mode 100644 index 0000000000..869f56e2cb --- /dev/null +++ b/frontend/src/types/api/metrics/getService.ts @@ -0,0 +1,16 @@ +export interface Props { + start: number; + end: number; +} + +export interface ServicesList { + serviceName: string; + p99: number; + avgDuration: number; + numCalls: number; + callRate: number; + numErrors: number; + errorRate: number; +} + +export type PayloadProps = ServicesList[]; diff --git a/frontend/src/types/api/metrics/getServiceOverview.ts b/frontend/src/types/api/metrics/getServiceOverview.ts new file mode 100644 index 0000000000..9ac0c26011 --- /dev/null +++ b/frontend/src/types/api/metrics/getServiceOverview.ts @@ -0,0 +1,16 @@ +import { Props as GetDBOverViewProps } from './getDBOverview'; + +export type Props = GetDBOverViewProps; + +export interface ServiceOverview { + callRate: number; + errorRate: number; + numCalls: number; + numErrors: number; + p50: number; + p95: number; + p99: number; + timestamp: number; +} + +export type PayloadProps = ServiceOverview[]; diff --git a/frontend/src/types/api/metrics/getTopEndPoints.ts b/frontend/src/types/api/metrics/getTopEndPoints.ts new file mode 100644 index 0000000000..cdc6cb3746 --- /dev/null +++ b/frontend/src/types/api/metrics/getTopEndPoints.ts @@ -0,0 +1,15 @@ +export interface TopEndPoints { + name: string; + numCalls: number; + p50: number; + p95: number; + p99: number; +} + +export interface Props { + service: string; + start: number; + end: number; +} + +export type PayloadProps = TopEndPoints[]; diff --git a/frontend/src/types/reducer/globalTime.ts b/frontend/src/types/reducer/globalTime.ts new file mode 100644 index 0000000000..d24bf26a6c --- /dev/null +++ b/frontend/src/types/reducer/globalTime.ts @@ -0,0 +1,7 @@ +import { GlobalTime } from 'types/actions/globalTime'; + +export interface GlobalReducer { + maxTime: GlobalTime['maxTime']; + minTime: GlobalTime['minTime']; + loading: boolean; +} diff --git a/frontend/src/types/reducer/metrics.ts b/frontend/src/types/reducer/metrics.ts new file mode 100644 index 0000000000..1fd1e7fe9f --- /dev/null +++ b/frontend/src/types/reducer/metrics.ts @@ -0,0 +1,22 @@ +import { DBOverView } from 'types/api/metrics/getDBOverview'; +import { ExternalAverageDuration } from 'types/api/metrics/getExternalAverageDuration'; +import { ExternalError } from 'types/api/metrics/getExternalError'; +import { ExternalService } from 'types/api/metrics/getExternalService'; +import { ServicesList } from 'types/api/metrics/getService'; +import { ServiceOverview } from 'types/api/metrics/getServiceOverview'; +import { TopEndPoints } from 'types/api/metrics/getTopEndPoints'; + +interface MetricReducer { + services: ServicesList[]; + loading: boolean; + error: boolean; + errorMessage: string; + dbOverView: DBOverView[]; + externalService: ExternalService[]; + topEndPoints: TopEndPoints[]; + externalAverageDuration: ExternalAverageDuration[]; + externalError: ExternalError[]; + serviceOverview: ServiceOverview[]; +} + +export default MetricReducer; diff --git a/frontend/src/typings/chartjs-adapter-date-fns.d.ts b/frontend/src/typings/chartjs-adapter-date-fns.d.ts new file mode 100644 index 0000000000..dc726f27c0 --- /dev/null +++ b/frontend/src/typings/chartjs-adapter-date-fns.d.ts @@ -0,0 +1 @@ +declare module 'chartjs-adapter-date-fns';