mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-11 04:39:59 +08:00
Fix(FE): global time (#332)
* chore: Router provider is removed * update: localstorage set get is added * update: AppLayout is updated * fix: adapter type is fixed * fix: Metric and metric application is now fixed * fix: Metrics page application is updated * fix: Tracepage is made fix * fix: app layout is updated * fix: global Time reducer is updated * refactor: getService api is added * update: metrics reducer is added * update: service list is fixed * fix: Metrics page is updated * fix: api for the metrics application are done * fix: metrics reducer is updated * fix: metrics application is updated * fix: content layout shift is removed * fix: Metric application is updated * fix: metrics application is updated * fix: Metrics application is updated * fix: Application tab is updated * chore: graph is updated * chore: Metrics application is updated * fix: chart x-axis is label is now fixed * fix: application tab is updated * fix: Top end points is added and re-redering in stopped * fix: fixed the edge case when user changes the global time then updated data is fetched * fix: Settings page is updated * chore: AppLayout is updated * chore: AppLayout is updated * chore: applayout is updated * chore: changed default loading is true in the global time reducer * chore: Global Time option is fixed * chore: Signup and Applayout is updated * chore: Button text is updated * chore: Button in the metrics application is updated * chore: dashboard menu item position in the side nav is updated * fix: Logo is now redirecting to the Application page * fix: Application page is updated * fix: AppLayout is updated * fix: starting and ending time is fixed * fix: Metrics Application is updated to the previous chart data * update: getDateArrayFromStartAndEnd function is added * update: Empty graph data is added * fix: External Call and DB Call Tabs graph are updated when there is no data a empty data is rendered * fix: onboarding modal condition is fixed and new calling api every 50000 ms to fetch the data * fix: onBoarding condition modal is updated * fix: onBoarding condition modal is updated * fix: onBoarding condition modal is updated * fix: Application chart re rendering issue is fixed * fix: Application page is changed when we change the global time * chore: step size is increased from 30 to 60 * chore: build is now fixed * chore: metrics application page is updated * fix: empty graph is now fixed * fix: application metrics graph is now fixed * fix: Time selection for custom is fixed * fix: usage graph is fixed * fix: global time selector is fixed and empty graph on click handler is added * fix: metrics application is now fixed
This commit is contained in:
parent
1ebf1a3675
commit
d2b107ec7f
@ -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 => (
|
||||
<Router history={history}>
|
||||
<RouteProvider>
|
||||
<AppLayout>
|
||||
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
||||
<Switch>
|
||||
{routes.map(({ path, component, exact }, index) => {
|
||||
return (
|
||||
<Route key={index} exact={exact} path={path} component={component} />
|
||||
);
|
||||
})}
|
||||
<Route path="*" exact component={NotFound} />
|
||||
</Switch>
|
||||
</Suspense>
|
||||
</AppLayout>
|
||||
</RouteProvider>
|
||||
<AppLayout>
|
||||
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
||||
<Switch>
|
||||
{routes.map(({ path, component, exact }, index) => {
|
||||
return (
|
||||
<Route key={index} exact={exact} path={path} component={component} />
|
||||
);
|
||||
})}
|
||||
<Route path="*" exact component={NotFound} />
|
||||
</Switch>
|
||||
</Suspense>
|
||||
</AppLayout>
|
||||
</Router>
|
||||
);
|
||||
|
||||
|
@ -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'),
|
||||
);
|
||||
|
5
frontend/src/api/browser/localstorage/get.ts
Normal file
5
frontend/src/api/browser/localstorage/get.ts
Normal file
@ -0,0 +1,5 @@
|
||||
const get = (key: string): string | null => {
|
||||
return localStorage.getItem(key);
|
||||
};
|
||||
|
||||
export default get;
|
5
frontend/src/api/browser/localstorage/remove.ts
Normal file
5
frontend/src/api/browser/localstorage/remove.ts
Normal file
@ -0,0 +1,5 @@
|
||||
const remove = (key: string): void => {
|
||||
window.localStorage.removeItem(key);
|
||||
};
|
||||
|
||||
export default remove;
|
5
frontend/src/api/browser/localstorage/set.ts
Normal file
5
frontend/src/api/browser/localstorage/set.ts
Normal file
@ -0,0 +1,5 @@
|
||||
const set = (key: string, value: string): void => {
|
||||
localStorage.setItem(key, value);
|
||||
};
|
||||
|
||||
export default set;
|
26
frontend/src/api/metrics/getDBOverView.ts
Normal file
26
frontend/src/api/metrics/getDBOverView.ts
Normal file
@ -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<SuccessResponse<PayloadProps> | 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;
|
29
frontend/src/api/metrics/getExternalAverageDuration.ts
Normal file
29
frontend/src/api/metrics/getExternalAverageDuration.ts
Normal file
@ -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<SuccessResponse<PayloadProps> | 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;
|
26
frontend/src/api/metrics/getExternalError.ts
Normal file
26
frontend/src/api/metrics/getExternalError.ts
Normal file
@ -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<SuccessResponse<PayloadProps> | 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;
|
26
frontend/src/api/metrics/getExternalService.ts
Normal file
26
frontend/src/api/metrics/getExternalService.ts
Normal file
@ -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<SuccessResponse<PayloadProps> | 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;
|
26
frontend/src/api/metrics/getService.ts
Normal file
26
frontend/src/api/metrics/getService.ts
Normal file
@ -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<SuccessResponse<PayloadProps> | 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;
|
26
frontend/src/api/metrics/getServiceOverview.ts
Normal file
26
frontend/src/api/metrics/getServiceOverview.ts
Normal file
@ -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<SuccessResponse<PayloadProps> | 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;
|
26
frontend/src/api/metrics/getTopEndPoints.ts
Normal file
26
frontend/src/api/metrics/getTopEndPoints.ts
Normal file
@ -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<SuccessResponse<PayloadProps> | 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;
|
@ -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<AppState, AppReducer>((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 (
|
||||
<>
|
||||
<canvas ref={chartRef} />
|
||||
{/* <LegendsContainer id="htmlLegend" /> */}
|
||||
</>
|
||||
);
|
||||
return <canvas ref={chartRef} />;
|
||||
};
|
||||
|
||||
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;
|
||||
|
@ -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<BaseLayoutProps> = ({ children }) => {
|
||||
const location = useLocation();
|
||||
const { dispatch } = useRoute();
|
||||
const currentYear = new Date().getFullYear();
|
||||
const { isLoggedIn } = useSelector<AppState, AppReducer>((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 (
|
||||
<Layout style={{ minHeight: '100vh' }}>
|
26
frontend/src/container/AppLayout/styles.ts
Normal file
26
frontend/src/container/AppLayout/styles.ts
Normal file
@ -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;
|
||||
`;
|
@ -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;
|
||||
|
@ -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<AppState, GlobalReducer>(
|
||||
(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 (
|
||||
<Graph
|
||||
{...{
|
||||
type: 'line',
|
||||
onClickHandler: onClickHandler,
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
data: new Array(date?.length).fill(0),
|
||||
borderColor: colors[0],
|
||||
showLine: true,
|
||||
borderWidth: 1.5,
|
||||
spanGaps: true,
|
||||
pointRadius: 0,
|
||||
},
|
||||
],
|
||||
labels: date,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface EmptyGraphProps {
|
||||
selectedTime: timePreferance;
|
||||
widget: Widgets;
|
||||
onClickHandler: graphOnClickHandler | undefined;
|
||||
}
|
||||
|
||||
export default EmptyGraph;
|
@ -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<AppState, GlobalTime>(
|
||||
(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 (
|
||||
<NotFoundContainer>
|
||||
<Typography>{state.errorMessage}</Typography>
|
||||
</NotFoundContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (state.loading || state.payload === undefined) {
|
||||
return <Spinner height="80vh" size="large" tip="Loading..." />;
|
||||
return (
|
||||
<div>
|
||||
<Spinner height="80vh" size="large" tip="Loading..." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (state.loading === false && state.payload.datasets.length === 0) {
|
||||
return (
|
||||
<>
|
||||
<TimePreference
|
||||
{...{
|
||||
selectedTime,
|
||||
setSelectedTime,
|
||||
}}
|
||||
/>
|
||||
<NotFoundContainer>
|
||||
<Typography>No Data</Typography>
|
||||
</NotFoundContainer>
|
||||
{fullViewOptions && (
|
||||
<TimeContainer>
|
||||
<TimePreference
|
||||
{...{
|
||||
selectedTime,
|
||||
setSelectedTime,
|
||||
}}
|
||||
/>
|
||||
<Button onClick={onFetchDataHandler} type="primary">
|
||||
Refresh
|
||||
</Button>
|
||||
</TimeContainer>
|
||||
)}
|
||||
|
||||
{noDataGraph ? (
|
||||
<EmptyGraph
|
||||
onClickHandler={onClickHandler}
|
||||
widget={widget}
|
||||
selectedTime={selectedTime}
|
||||
/>
|
||||
) : (
|
||||
<NotFoundContainer>
|
||||
<Typography>No Data</Typography>
|
||||
</NotFoundContainer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TimeContainer>
|
||||
<TimePreference
|
||||
{...{
|
||||
selectedTime,
|
||||
setSelectedTime,
|
||||
}}
|
||||
/>
|
||||
<Button onClick={onFetchDataHandler} type="primary">
|
||||
Refresh
|
||||
</Button>
|
||||
</TimeContainer>
|
||||
{fullViewOptions && (
|
||||
<TimeContainer>
|
||||
<TimePreference
|
||||
{...{
|
||||
selectedTime,
|
||||
setSelectedTime,
|
||||
}}
|
||||
/>
|
||||
<Button onClick={onFetchDataHandler} type="primary">
|
||||
Refresh
|
||||
</Button>
|
||||
</TimeContainer>
|
||||
)}
|
||||
|
||||
<GraphContainer>
|
||||
<GridGraphComponent
|
||||
@ -160,6 +197,7 @@ const FullView = ({ widget }: FullViewProps): JSX.Element => {
|
||||
isStacked: widget.isStacked,
|
||||
opacity: widget.opacity,
|
||||
title: widget.title,
|
||||
onClickHandler: onClickHandler,
|
||||
}}
|
||||
/>
|
||||
</GraphContainer>
|
||||
@ -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;
|
||||
});
|
||||
|
@ -6,6 +6,7 @@ export const GraphContainer = styled.div`
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const NotFoundContainer = styled.div`
|
||||
|
@ -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 {
|
||||
|
48
frontend/src/container/Header/Breadcrumbs/index.tsx
Normal file
48
frontend/src/container/Header/Breadcrumbs/index.tsx
Normal file
@ -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 (
|
||||
<Breadcrumb.Item key={url}>
|
||||
<Link to={url}>{url.split('/').slice(-1)[0]}</Link>
|
||||
</Breadcrumb.Item>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Breadcrumb.Item key={url}>
|
||||
<Link to={url}>{breadcrumbNameMap[url]}</Link>
|
||||
</Breadcrumb.Item>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const breadcrumbItems = [
|
||||
<Breadcrumb.Item key="home">
|
||||
<Link to="/">Home</Link>
|
||||
</Breadcrumb.Item>,
|
||||
].concat(extraBreadcrumbItems);
|
||||
|
||||
return <Breadcrumb>{breadcrumbItems}</Breadcrumb>;
|
||||
};
|
||||
|
||||
export default withRouter(ShowBreadcrumbs);
|
@ -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<CustomDateTimeModalProps> = ({
|
||||
//destructuring props
|
||||
const CustomDateTimeModal = ({
|
||||
visible,
|
||||
onCreate,
|
||||
onCancel,
|
||||
}) => {
|
||||
}: CustomDateTimeModalProps): JSX.Element => {
|
||||
const [
|
||||
customDateTimeRange,
|
||||
setCustomDateTimeRange,
|
||||
@ -26,6 +19,7 @@ const CustomDateTimeModal: React.FC<CustomDateTimeModalProps> = ({
|
||||
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<CustomDateTimeModalProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
interface CustomDateTimeModalProps {
|
||||
visible: boolean;
|
||||
onCreate: (dateTimeRange: DateTimeRangeType) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export default CustomDateTimeModal;
|
60
frontend/src/container/Header/DateTimeSelection/config.ts
Normal file
60
frontend/src/container/Header/DateTimeSelection/config.ts
Normal file
@ -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;
|
||||
};
|
328
frontend/src/container/Header/DateTimeSelection/index.tsx
Normal file
328
frontend/src/container/Header/DateTimeSelection/index.tsx
Normal file
@ -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<moment.Moment>();
|
||||
const [endTime, setEndTime] = useState<moment.Moment>();
|
||||
const [options, setOptions] = useState(getOptions(location.pathname));
|
||||
const [refreshButtonHidden, setRefreshButtonHidden] = useState<boolean>(false);
|
||||
const [refreshText, setRefreshText] = useState<string>('');
|
||||
const [customDateTimeVisible, setCustomDTPickerVisible] = useState<boolean>(
|
||||
false,
|
||||
);
|
||||
const isOnSelectHandler = useRef<boolean>(false);
|
||||
|
||||
const { maxTime, loading, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(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<Time>(
|
||||
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 (
|
||||
<Container>
|
||||
<Form
|
||||
form={form_dtselector}
|
||||
layout="inline"
|
||||
initialValues={{ interval: selectedTimeInterval }}
|
||||
>
|
||||
<DefaultSelect
|
||||
onSelect={(value): void => onSelectHandler(value as Time)}
|
||||
value={getInputLabel(startTime, endTime, selectedTimeInterval)}
|
||||
>
|
||||
{options.map(({ value, label }) => (
|
||||
<Option key={value + label} value={value}>
|
||||
{label}
|
||||
</Option>
|
||||
))}
|
||||
</DefaultSelect>
|
||||
|
||||
<FormItem hidden={refreshButtonHidden}>
|
||||
<Button type="primary" onClick={onRefreshHandler}>
|
||||
Refresh
|
||||
</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
|
||||
<RefreshTextContainer>
|
||||
<Typography>{refreshText}</Typography>
|
||||
</RefreshTextContainer>
|
||||
|
||||
<CustomDateTimeModal
|
||||
visible={customDateTimeVisible}
|
||||
onCreate={onCustomDateHandler}
|
||||
onCancel={(): void => {
|
||||
setCustomDTPickerVisible(false);
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
interface DispatchProps {
|
||||
updateTimeInterval: (
|
||||
interval: Time,
|
||||
dateTimeRange?: [number, number],
|
||||
) => (dispatch: Dispatch<AppActions>) => void;
|
||||
// globalTimeLoading: () => void;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
updateTimeInterval: bindActionCreators(UpdateTimeInterval, dispatch),
|
||||
// globalTimeLoading: bindActionCreators(GlobalTimeLoading, dispatch),
|
||||
});
|
||||
|
||||
type Props = DispatchProps & RouteComponentProps;
|
||||
|
||||
export default connect(null, mapDispatchToProps)(withRouter(DateTimeSelection));
|
28
frontend/src/container/Header/DateTimeSelection/styles.ts
Normal file
28
frontend/src/container/Header/DateTimeSelection/styles.ts
Normal file
@ -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;
|
||||
`;
|
@ -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 (
|
||||
<Row>
|
||||
<Container>
|
||||
<Col span={16}>
|
||||
<ShowBreadcrumbs />
|
||||
</Col>
|
||||
@ -19,7 +21,7 @@ const TopNav = (): JSX.Element | null => {
|
||||
<Col span={8}>
|
||||
<DateTimeSelector />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
8
frontend/src/container/Header/styles.ts
Normal file
8
frontend/src/container/Header/styles.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Row } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled(Row)`
|
||||
&&& {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
`;
|
255
frontend/src/container/MetricsApplication/Tabs/Application.tsx
Normal file
255
frontend/src/container/MetricsApplication/Tabs/Application.tsx
Normal file
@ -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<AppState, MetricReducer>(
|
||||
(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<void> => {
|
||||
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 (
|
||||
<>
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
id="Application_button"
|
||||
onClick={(): void => {
|
||||
onTracePopupClick(selectedTimeStamp.current);
|
||||
}}
|
||||
>
|
||||
View Traces
|
||||
</Button>
|
||||
<Card>
|
||||
<GraphTitle>Application latency in ms</GraphTitle>
|
||||
<GraphContainer>
|
||||
<Graph
|
||||
onClickHandler={(ChartEvent, activeElements, chart, data): void => {
|
||||
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);
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</GraphContainer>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col span={12}>
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
id="Request_button"
|
||||
onClick={(): void => {
|
||||
onTracePopupClick(selectedTimeStamp.current);
|
||||
}}
|
||||
>
|
||||
View Traces
|
||||
</Button>
|
||||
<Card>
|
||||
<GraphTitle>Request per sec</GraphTitle>
|
||||
<GraphContainer>
|
||||
<FullView
|
||||
noDataGraph
|
||||
fullViewOptions={false}
|
||||
onClickHandler={(event, element, chart, data): void => {
|
||||
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',
|
||||
},
|
||||
])}
|
||||
/>
|
||||
</GraphContainer>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
id="Error_button"
|
||||
onClick={(): void => {
|
||||
onErrorTrackHandler(selectedTimeStamp.current);
|
||||
}}
|
||||
>
|
||||
View Traces
|
||||
</Button>
|
||||
|
||||
<Card>
|
||||
<Card>
|
||||
<GraphTitle>Error Percentage (%)</GraphTitle>
|
||||
<GraphContainer>
|
||||
<FullView
|
||||
noDataGraph
|
||||
fullViewOptions={false}
|
||||
onClickHandler={(ChartEvent, activeElements, chart, data): void => {
|
||||
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 (%)',
|
||||
},
|
||||
])}
|
||||
/>
|
||||
</GraphContainer>
|
||||
</Card>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col span={12}>
|
||||
<Card>
|
||||
<TopEndpointsTable data={topEndPoints} />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface DispatchProps {
|
||||
globalLoading: () => void;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
globalLoading: bindActionCreators(GlobalTimeLoading, dispatch),
|
||||
});
|
||||
|
||||
interface DashboardProps extends DispatchProps {
|
||||
getWidget: (query: Widgets['query']) => Widgets;
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(Application);
|
59
frontend/src/container/MetricsApplication/Tabs/DBCall.tsx
Normal file
59
frontend/src/container/MetricsApplication/Tabs/DBCall.tsx
Normal file
@ -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 (
|
||||
<>
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Card>
|
||||
<GraphTitle>Database Calls RPS</GraphTitle>
|
||||
<GraphContainer>
|
||||
<FullView
|
||||
noDataGraph
|
||||
fullViewOptions={false}
|
||||
widget={getWidget([
|
||||
{
|
||||
query: `sum(rate(signoz_db_latency_count{service_name="${servicename}"}[1m])) by (db_system)`,
|
||||
legend: '{{db_system}}',
|
||||
},
|
||||
])}
|
||||
/>
|
||||
</GraphContainer>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col span={12}>
|
||||
<Card>
|
||||
<GraphTitle>Database Calls Avg Duration (in ms)</GraphTitle>
|
||||
<GraphContainer>
|
||||
<FullView
|
||||
noDataGraph
|
||||
fullViewOptions={false}
|
||||
widget={getWidget([
|
||||
{
|
||||
query: `sum(rate(signoz_db_latency_sum{service_name="${servicename}"}[5m]))/sum(rate(signoz_db_latency_count{service_name="${servicename}"}[5m]))`,
|
||||
legend: '',
|
||||
},
|
||||
])}
|
||||
/>
|
||||
</GraphContainer>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface DBCallProps {
|
||||
getWidget: (query: Widgets['query']) => Widgets;
|
||||
}
|
||||
|
||||
export default DBCall;
|
97
frontend/src/container/MetricsApplication/Tabs/External.tsx
Normal file
97
frontend/src/container/MetricsApplication/Tabs/External.tsx
Normal file
@ -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 (
|
||||
<>
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Card>
|
||||
<GraphTitle>External Call Error Percentage (%)</GraphTitle>
|
||||
<GraphContainer>
|
||||
<FullView
|
||||
fullViewOptions={false}
|
||||
noDataGraph
|
||||
widget={getWidget([
|
||||
{
|
||||
query: `(sum(rate(signoz_external_call_latency_count{service_name="${servicename}", status_code="STATUS_CODE_ERROR"}[1m]) OR rate(signoz_external_call_latency_count{service_name="${servicename}", http_status_code=~"5.."}[1m]) OR vector(0)) by (http_url))*100/sum(rate(signoz_external_call_latency_count{service_name="${servicename}"}[1m])) by (http_url)`,
|
||||
legend: '{{http_url}}',
|
||||
},
|
||||
])}
|
||||
/>
|
||||
</GraphContainer>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col span={12}>
|
||||
<Card>
|
||||
<GraphTitle>External Call duration</GraphTitle>
|
||||
<GraphContainer>
|
||||
<FullView
|
||||
noDataGraph
|
||||
fullViewOptions={false}
|
||||
widget={getWidget([
|
||||
{
|
||||
query: `sum(rate(signoz_external_call_latency_sum{service_name="${servicename}"}[5m]))/sum(rate(signoz_external_call_latency_count{service_name="${servicename}"}[5m]))`,
|
||||
legend: 'Average Duration',
|
||||
},
|
||||
])}
|
||||
/>
|
||||
</GraphContainer>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Card>
|
||||
<GraphTitle>External Call RPS(by Address)</GraphTitle>
|
||||
<GraphContainer>
|
||||
<FullView
|
||||
noDataGraph
|
||||
fullViewOptions={false}
|
||||
widget={getWidget([
|
||||
{
|
||||
query: `sum(rate(signoz_external_call_latency_count{service_name="${servicename}"}[5m])) by (http_url)`,
|
||||
legend: '{{http_url}}',
|
||||
},
|
||||
])}
|
||||
/>
|
||||
</GraphContainer>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col span={12}>
|
||||
<Card>
|
||||
<GraphTitle>External Call duration(by Address)</GraphTitle>
|
||||
<GraphContainer>
|
||||
<FullView
|
||||
noDataGraph
|
||||
fullViewOptions={false}
|
||||
widget={getWidget([
|
||||
{
|
||||
query: `sum(rate(signoz_external_call_latency_sum{service_name="${servicename}"}[5m])/rate(signoz_external_call_latency_count{service_name="${servicename}"}[5m])) by (http_url)`,
|
||||
legend: '{{http_url}}',
|
||||
},
|
||||
])}
|
||||
/>
|
||||
</GraphContainer>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface ExternalProps {
|
||||
getWidget: (query: Widgets['query']) => Widgets;
|
||||
}
|
||||
|
||||
export default External;
|
10
frontend/src/container/MetricsApplication/Tabs/styles.ts
Normal file
10
frontend/src/container/MetricsApplication/Tabs/styles.ts
Normal file
@ -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;
|
||||
}
|
||||
`;
|
122
frontend/src/container/MetricsApplication/TopEndpointsTable.tsx
Normal file
122
frontend/src/container/MetricsApplication/TopEndpointsTable.tsx
Normal file
@ -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<AppState, GlobalReducer>(
|
||||
(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<DataProps> = [
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (text: string): JSX.Element => (
|
||||
<Tooltip placement="topLeft" title={text}>
|
||||
<Button
|
||||
className="topEndpointsButton"
|
||||
type="link"
|
||||
onClick={(): void => handleOnClick(text)}
|
||||
>
|
||||
{text}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
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 (
|
||||
<Table
|
||||
showHeader
|
||||
title={(): string => {
|
||||
return 'Top Endpoints';
|
||||
}}
|
||||
dataSource={props.data}
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
rowKey="name"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type DataProps = topEndpointListItem;
|
||||
|
||||
interface DispatchProps {
|
||||
globalTimeLoading: () => void;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
globalTimeLoading: bindActionCreators(GlobalTimeLoading, dispatch),
|
||||
});
|
||||
|
||||
interface TopEndpointsTableProps extends DispatchProps {
|
||||
data: topEndpointListItem[];
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(TopEndpointsTable);
|
50
frontend/src/container/MetricsApplication/index.tsx
Normal file
50
frontend/src/container/MetricsApplication/index.tsx
Normal file
@ -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 (
|
||||
<Tabs defaultActiveKey="1">
|
||||
<TabPane animated destroyInactiveTabPane tab="Application Metrics" key="1">
|
||||
<Application getWidget={getWidget} />
|
||||
</TabPane>
|
||||
|
||||
<TabPane animated destroyInactiveTabPane tab="External Calls" key="2">
|
||||
<External getWidget={getWidget} />
|
||||
</TabPane>
|
||||
|
||||
<TabPane animated destroyInactiveTabPane tab="Database Calls" key="3">
|
||||
<DBCall getWidget={getWidget} />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServiceMetrics;
|
46
frontend/src/container/MetricsApplication/styles.ts
Normal file
46
frontend/src/container/MetricsApplication/styles.ts
Normal file
@ -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;
|
||||
}
|
||||
`;
|
@ -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 (
|
||||
<Modal
|
||||
title={'Setup instrumentation'}
|
||||
isModalVisible={true}
|
||||
closable={false}
|
||||
footer={[
|
||||
<Button key="submit" type="primary" onClick={onContinueClick}>
|
||||
Continue without instrumentation
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<>
|
||||
<iframe
|
||||
width="100%"
|
||||
height="265"
|
||||
src="https://www.youtube.com/embed/Ly34WBQ2640"
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
<div>
|
||||
<Typography>No instrumentation data.</Typography>
|
||||
<Typography>
|
||||
Please instrument your application as mentioned
|
||||
<a
|
||||
href={'https://signoz.io/docs/instrumentation/overview'}
|
||||
target={'_blank'}
|
||||
rel="noreferrer"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
</Typography>
|
||||
</div>
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
interface Props {
|
||||
onContinueClick: () => void;
|
||||
}
|
||||
|
||||
export default SkipOnBoardingModal;
|
105
frontend/src/container/MetricsTable/index.tsx
Normal file
105
frontend/src/container/MetricsTable/index.tsx
Normal file
@ -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<AppState, MetricReducer>(
|
||||
(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 <SkipBoardModal onContinueClick={onContinueClick} />;
|
||||
}
|
||||
|
||||
const columns: ColumnsType<DataProps> = [
|
||||
{
|
||||
title: 'Application',
|
||||
dataIndex: 'serviceName',
|
||||
key: 'serviceName',
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (text: string): JSX.Element => (
|
||||
<div onClick={(): void => onClickHandler(ROUTES.APPLICATION + '/' + text)}>
|
||||
<Name>{text}</Name>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
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 (
|
||||
<Container>
|
||||
<Table
|
||||
loading={loading}
|
||||
dataSource={services}
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
rowKey="serviceName"
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
type DataProps = servicesListItem;
|
||||
|
||||
interface DispatchProps {
|
||||
globalTimeLoading: () => void;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
globalTimeLoading: bindActionCreators(GlobalTimeLoading, dispatch),
|
||||
});
|
||||
|
||||
type MetricsProps = DispatchProps;
|
||||
|
||||
export default connect(null, mapDispatchToProps)(Metrics);
|
15
frontend/src/container/MetricsTable/styles.ts
Normal file
15
frontend/src/container/MetricsTable/styles.ts
Normal file
@ -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;
|
||||
}
|
||||
`;
|
@ -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';
|
||||
|
@ -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<boolean>(false);
|
||||
const { pathname } = useLocation();
|
||||
const { isDarkMode } = useSelector<AppState, AppReducer>((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 (
|
||||
<Sider collapsible collapsed={collapsed} onCollapse={onCollapse} width={200}>
|
||||
<ThemeSwitcherWrapper>
|
||||
<ToggleButton checked={isDarkMode} onChange={toggleTheme} />
|
||||
</ThemeSwitcherWrapper>
|
||||
<NavLink to="/">
|
||||
<NavLink to={ROUTES.APPLICATION}>
|
||||
<Logo src={'/signoz.svg'} alt="SigNoz" collapsed={collapsed} />
|
||||
</NavLink>
|
||||
|
||||
@ -80,12 +86,14 @@ type mode = 'darkMode' | 'lightMode';
|
||||
|
||||
interface DispatchProps {
|
||||
toggleDarkMode: () => void;
|
||||
globalTimeLoading: () => void;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
toggleDarkMode: bindActionCreators(ToggleDarkMode, dispatch),
|
||||
globalTimeLoading: bindActionCreators(GlobalTimeLoading, dispatch),
|
||||
});
|
||||
|
||||
type Props = DispatchProps;
|
@ -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 {
|
@ -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),
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { GlobalTime } from 'store/actions';
|
||||
import { GlobalTime } from 'types/actions/globalTime';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
|
||||
const GetMaxMinTime = ({
|
||||
|
15
frontend/src/lib/getTimeString.ts
Normal file
15
frontend/src/lib/getTimeString.ts
Normal file
@ -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;
|
@ -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 (
|
||||
<div>
|
||||
{state.showpopUp && (
|
||||
<ChartPopUpUnique
|
||||
xcoordinate={state.xcoordinate}
|
||||
ycoordinate={state.ycoordinate}
|
||||
>
|
||||
<PopUpElements onClick={gotoTracesHandler}>View Traces</PopUpElements>
|
||||
</ChartPopUpUnique>
|
||||
)}
|
||||
<div style={{ textAlign: 'center' }}>Error Percentage (%)</div>
|
||||
|
||||
<GraphContainer>
|
||||
<Graph
|
||||
onClickHandler={onClickhandler}
|
||||
xAxisType="timeseries"
|
||||
type="line"
|
||||
data={data_chartJS}
|
||||
/>
|
||||
</GraphContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ErrorRateChart;
|
@ -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<any> {
|
||||
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<ExternalApiGraphProps> {
|
||||
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 (
|
||||
<>
|
||||
<div style={{ textAlign: 'center' }}>{title}</div>
|
||||
<GraphContainer>
|
||||
<Graph data={data_chartJS()} xAxisType="timeseries" type="line" />
|
||||
</GraphContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(ExternalApiGraph);
|
@ -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',
|
||||
];
|
@ -1 +0,0 @@
|
||||
export { default } from './ExternalApiGraph';
|
@ -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 (
|
||||
<GraphContainer>
|
||||
<Graph
|
||||
onClickHandler={onClickhandler}
|
||||
xAxisType="timeseries"
|
||||
type="line"
|
||||
data={data}
|
||||
/>
|
||||
</GraphContainer>
|
||||
);
|
||||
};
|
||||
|
||||
interface LatencyChartProps {
|
||||
data: ChartData;
|
||||
onClickhandler: ChartOptions['onClick'];
|
||||
}
|
||||
|
||||
export default LatencyChart;
|
@ -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<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,
|
||||
});
|
||||
} 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 (
|
||||
<>
|
||||
<ChartPopUpUnique
|
||||
xcoordinate={state.xcoordinate}
|
||||
ycoordinate={state.ycoordinate}
|
||||
showPopUp={state.showpopUp}
|
||||
>
|
||||
<PopUpElements
|
||||
onClick={(): void => {
|
||||
popupClickHandler(state.firstpoint_ts);
|
||||
}}
|
||||
>
|
||||
View Traces
|
||||
</PopUpElements>
|
||||
</ChartPopUpUnique>
|
||||
|
||||
<GraphTitle>Application latency in ms</GraphTitle>
|
||||
|
||||
<LatencyLine data={chartData} onClickhandler={onClickhandler} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LatencyLineChart;
|
@ -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<void> => {
|
||||
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 (
|
||||
<ChartPopUpUnique
|
||||
xcoordinate={state.xcoordinate}
|
||||
ycoordinate={state.ycoordinate}
|
||||
>
|
||||
<PopUpElements onClick={gotoTracesHandler}>View Traces</PopUpElements>
|
||||
</ChartPopUpUnique>
|
||||
);
|
||||
} 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 (
|
||||
<>
|
||||
<div>
|
||||
{GraphTracePopUp()}
|
||||
<div style={{ textAlign: 'center' }}>Request per sec</div>
|
||||
<GraphContainer>
|
||||
<Graph
|
||||
onClickHandler={onClickHandler}
|
||||
xAxisType="timeseries"
|
||||
type="line"
|
||||
data={data_chartJS}
|
||||
/>
|
||||
</GraphContainer>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RequestRateChart;
|
@ -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 <Spinner tip="Loading..." height="100vh" size="large" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs defaultActiveKey="1">
|
||||
<TabPane tab="Application Metrics" key="1">
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Card>
|
||||
<LatencyLineChart
|
||||
data={props.serviceMetrics}
|
||||
popupClickHandler={onTracePopupClick}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col span={12}>
|
||||
<Card>
|
||||
<RequestRateChart data={props.serviceMetrics} />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Card>
|
||||
<ErrorRateChart
|
||||
onTracePopupClick={onErrTracePopupClick}
|
||||
data={props.serviceMetrics}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col span={12}>
|
||||
<Card>
|
||||
<TopEndpointsTable data={props.topEndpointsList} />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</TabPane>
|
||||
|
||||
<TabPane tab="External Calls" key="2">
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Card>
|
||||
<ExternalApiGraph
|
||||
title="External Call Error Percentage (%)"
|
||||
keyIdentifier="externalHttpUrl"
|
||||
dataIdentifier="errorRate"
|
||||
data={props.externalErrCodeMetrics}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col span={12}>
|
||||
<Card>
|
||||
<ExternalApiGraph
|
||||
label="Average Duration"
|
||||
title="External Call duration"
|
||||
dataIdentifier="avgDuration"
|
||||
fnDataIdentifier={(s) => Number(s) / 1000000}
|
||||
data={props.externalAvgDurationMetrics}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Card>
|
||||
<ExternalApiGraph
|
||||
title="External Call RPS(by Address)"
|
||||
keyIdentifier="externalHttpUrl"
|
||||
dataIdentifier="callRate"
|
||||
data={props.externalMetrics}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col span={12}>
|
||||
<Card>
|
||||
<ExternalApiGraph
|
||||
title="External Call duration(by Address)"
|
||||
keyIdentifier="externalHttpUrl"
|
||||
dataIdentifier="avgDuration"
|
||||
fnDataIdentifier={(s) => Number(s) / 1000000}
|
||||
data={props.externalMetrics}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</TabPane>
|
||||
|
||||
<TabPane tab="Database Calls" key="3">
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Card>
|
||||
<ExternalApiGraph
|
||||
title="Database Calls RPS"
|
||||
keyIdentifier="dbSystem"
|
||||
dataIdentifier="callRate"
|
||||
data={props.dbOverviewMetrics}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col span={12}>
|
||||
<Card>
|
||||
<ExternalApiGraph
|
||||
label="Average Duration"
|
||||
title="Database Calls Avg Duration (in ms)"
|
||||
dataIdentifier="avgDuration"
|
||||
fnDataIdentifier={(s) => Number(s) / 1000000}
|
||||
data={props.dbOverviewMetrics}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
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);
|
@ -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;
|
||||
}
|
||||
`;
|
@ -1 +0,0 @@
|
||||
export { ServiceMetrics as default } from './ServiceMetrics';
|
@ -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 <Spinner height="90vh" size="large" tip="Fetching data..." />;
|
||||
}
|
||||
|
||||
if (refetchFromBackend && !skipOnboarding) {
|
||||
return (
|
||||
<Modal
|
||||
title={'Setup instrumentation'}
|
||||
isModalVisible={true}
|
||||
closable={false}
|
||||
footer={[
|
||||
<Button key="submit" type="primary" onClick={onContinueClick}>
|
||||
Continue without instrumentation
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<div>
|
||||
<iframe
|
||||
width="100%"
|
||||
height="265"
|
||||
src="https://www.youtube.com/embed/Ly34WBQ2640"
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
></iframe>
|
||||
<div>
|
||||
No instrumentation data.
|
||||
<br />
|
||||
Please instrument your application as mentioned{' '}
|
||||
<a
|
||||
href={'https://signoz.io/docs/instrumentation/overview'}
|
||||
target={'_blank'}
|
||||
rel="noreferrer"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const columns: ColumnsType<DataProps> = [
|
||||
{
|
||||
title: 'Application',
|
||||
dataIndex: 'serviceName',
|
||||
key: 'serviceName',
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (text: string): JSX.Element => (
|
||||
<NavLink
|
||||
style={{ textTransform: 'capitalize' }}
|
||||
to={ROUTES.APPLICATION + '/' + text}
|
||||
>
|
||||
<strong>{text}</strong>
|
||||
</NavLink>
|
||||
),
|
||||
},
|
||||
{
|
||||
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 (
|
||||
<Wrapper>
|
||||
<Table
|
||||
dataSource={props.servicesList}
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
/>
|
||||
|
||||
{props.servicesList[0] !== undefined &&
|
||||
props.servicesList[0].numCalls === 0 && (
|
||||
<Space
|
||||
style={{ width: '100%', margin: '40px 0', justifyContent: 'center' }}
|
||||
>
|
||||
No applications present. Please add instrumentation (follow this
|
||||
<a
|
||||
href={'https://signoz.io/docs/instrumentation/overview'}
|
||||
target={'_blank'}
|
||||
style={{ marginLeft: 3 }}
|
||||
rel="noreferrer"
|
||||
>
|
||||
guide
|
||||
</a>
|
||||
)
|
||||
</Space>
|
||||
)}
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
type DataProps = servicesListItem;
|
||||
|
||||
interface ServicesTableProps {
|
||||
servicesList: servicesListItem[];
|
||||
getServicesList: (props: GlobalTime) => Promise<void>;
|
||||
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);
|
@ -1,5 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
padding: 40px;
|
||||
`;
|
@ -1 +0,0 @@
|
||||
export { ServicesTable as default } from './ServiceTable';
|
@ -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;
|
||||
}
|
||||
}
|
@ -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) => (
|
||||
<Tooltip placement="topLeft" title={text}>
|
||||
<Button
|
||||
className="topEndpointsButton"
|
||||
type="link"
|
||||
onClick={() => handleOnClick(text)}
|
||||
>
|
||||
{text}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
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 (
|
||||
<Wrapper>
|
||||
<h6> Top Endpoints</h6>
|
||||
<Table dataSource={props.data} columns={columns} pagination={false} />
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (
|
||||
state: AppState,
|
||||
): {
|
||||
globalTime: GlobalTime;
|
||||
} => {
|
||||
return { globalTime: state.globalTime };
|
||||
};
|
||||
|
||||
export const TopEndpointsTable = connect(
|
||||
mapStateToProps,
|
||||
null,
|
||||
)(_TopEndpointsTable);
|
||||
|
||||
export default TopEndpointsTable;
|
@ -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;
|
||||
}
|
||||
`;
|
@ -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<moment.Moment | null>(null);
|
||||
const [endTime, setEndTime] = useState<moment.Moment | null>(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 (
|
||||
<DateTimeWrapper>
|
||||
<Space style={{ float: 'right', display: 'block' }}>
|
||||
<Space>
|
||||
<Form
|
||||
form={form_dtselector}
|
||||
layout="inline"
|
||||
initialValues={{ interval: '15min' }}
|
||||
style={{ marginTop: 10, marginBottom: 10 }}
|
||||
>
|
||||
<DefaultSelect onSelect={handleOnSelect} value={inputLabeLToShow}>
|
||||
{options.map(({ value, label }) => (
|
||||
<Option key={value + label} value={value}>
|
||||
{label}
|
||||
</Option>
|
||||
))}
|
||||
</DefaultSelect>
|
||||
|
||||
<FormItem hidden={refreshButtonHidden} name="refresh_button">
|
||||
<Button type="primary" onClick={handleRefresh}>
|
||||
Refresh
|
||||
</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</Space>
|
||||
<Space
|
||||
style={{
|
||||
float: 'right',
|
||||
display: 'block',
|
||||
marginRight: 20,
|
||||
minHeight: 23,
|
||||
width: 200,
|
||||
textAlign: 'right',
|
||||
}}
|
||||
>
|
||||
{refreshText}
|
||||
</Space>
|
||||
<CustomDateTimeModal
|
||||
visible={customDTPickerVisible}
|
||||
onCreate={handleCustomDate}
|
||||
onCancel={(): void => {
|
||||
setCustomDTPickerVisible(false);
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
</DateTimeWrapper>
|
||||
);
|
||||
}
|
||||
};
|
||||
const mapStateToProps = (state: AppState): { globalTime: GlobalTime } => {
|
||||
return { globalTime: state.globalTime };
|
||||
};
|
||||
|
||||
export const DateTimeSelector = connect(mapStateToProps, {
|
||||
updateTimeInterval: updateTimeInterval,
|
||||
})(_DateTimeSelector);
|
||||
|
||||
export default DateTimeSelector;
|
@ -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 (
|
||||
<Breadcrumb.Item key={url}>
|
||||
<Link to={url}>{url.split('/').slice(-1)[0]}</Link>
|
||||
</Breadcrumb.Item>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Breadcrumb.Item key={url}>
|
||||
<Link to={url}>{breadcrumbNameMap[url]}</Link>
|
||||
</Breadcrumb.Item>
|
||||
);
|
||||
}
|
||||
});
|
||||
const breadcrumbItems = [
|
||||
<Breadcrumb.Item key="home">
|
||||
<Link to="/">Home</Link>
|
||||
</Breadcrumb.Item>,
|
||||
].concat(extraBreadcrumbItems);
|
||||
return (
|
||||
<BreadCrumbWrapper>
|
||||
<Breadcrumb>{breadcrumbItems}</Breadcrumb>
|
||||
</BreadCrumbWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShowBreadcrumbs;
|
@ -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,
|
||||
};
|
@ -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<Action>;
|
||||
}
|
||||
|
||||
const RouteContext = createContext<ContextType | null>(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<RouteProviderProps> = ({ children }) => {
|
||||
const [state, dispatch] = React.useReducer(updateLocation, getInitialState());
|
||||
const value = { state, dispatch };
|
||||
return <RouteContext.Provider value={value}>{children}</RouteContext.Provider>;
|
||||
};
|
||||
|
||||
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 };
|
@ -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(() => {
|
||||
|
@ -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<AppState, GlobalReducer>(
|
||||
(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
|
||||
|
@ -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 (
|
||||
<>
|
||||
<TraceFilter />
|
||||
@ -14,4 +25,16 @@ const TraceDetail = (): JSX.Element => {
|
||||
);
|
||||
};
|
||||
|
||||
export default TraceDetail;
|
||||
interface DispatchProps {
|
||||
globalTimeLoading: () => void;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
globalTimeLoading: bindActionCreators(GlobalTimeLoading, dispatch),
|
||||
});
|
||||
|
||||
type Props = DispatchProps;
|
||||
|
||||
export default connect(null, mapDispatchToProps)(TraceDetail);
|
||||
|
@ -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<AppState, GlobalReducer>(
|
||||
(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<string[]>(service_request).then((response) => {
|
||||
// form_basefilter.resetFields(['operation',])
|
||||
setOperationsList(response.data);
|
||||
});
|
||||
|
||||
const tagkeyoptions_request = '/tags?service=' + value;
|
||||
api.get<TagKeyOptionItem[]>(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<string[]>(`/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<string[]>(service_request).then((response) => {
|
||||
// form_basefilter.resetFields(['operation',])
|
||||
setOperationsList(response.data);
|
||||
});
|
||||
|
||||
const tagkeyoptions_request = '/tags?service=' + value;
|
||||
api.get<TagKeyOptionItem[]>(tagkeyoptions_request).then((response) => {
|
||||
setTagKeyOptions(response.data);
|
||||
});
|
||||
}
|
||||
|
||||
const onLatencyButtonClick = (): void => {
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
@ -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<string>('');
|
||||
|
||||
const { state } = useRoute();
|
||||
const { loading } = useSelector<AppState, GlobalReducer>(
|
||||
(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 (
|
||||
<React.Fragment>
|
||||
{/* PNOTE - TODO - Keep it in reponsive row column tab */}
|
||||
|
79
frontend/src/pages/MetricApplication/index.tsx
Normal file
79
frontend/src/pages/MetricApplication/index.tsx
Normal file
@ -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<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
const { error, errorMessage } = useSelector<AppState, MetricReducer>(
|
||||
(state) => state.metrics,
|
||||
);
|
||||
|
||||
const { servicename } = useParams<ServiceProps>();
|
||||
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
|
||||
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 <Typography>{errorMessage}</Typography>;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Spinner tip="Loading..." />;
|
||||
}
|
||||
|
||||
return <MetricsApplicationContainer />;
|
||||
};
|
||||
|
||||
interface DispatchProps {
|
||||
getInitialData: (props: GetInitialDataProps) => void;
|
||||
}
|
||||
|
||||
interface ServiceProps {
|
||||
servicename?: string;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
getInitialData: bindActionCreators(GetInitialData, dispatch),
|
||||
});
|
||||
|
||||
type MetricsProps = DispatchProps;
|
||||
|
||||
export default connect(null, mapDispatchToProps)(MetricsApplication);
|
72
frontend/src/pages/Metrics/index.tsx
Normal file
72
frontend/src/pages/Metrics/index.tsx
Normal file
@ -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<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
const { services } = useSelector<AppState, MetricReducer>(
|
||||
(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 <Spinner tip="Loading..." />;
|
||||
}
|
||||
|
||||
return <MetricTable />;
|
||||
};
|
||||
|
||||
interface DispatchProps {
|
||||
getService: ({
|
||||
end,
|
||||
start,
|
||||
}: GetServiceProps) => (dispatch: Dispatch<AppActions>) => void;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
getService: bindActionCreators(GetService, dispatch),
|
||||
});
|
||||
|
||||
type MetricsProps = DispatchProps;
|
||||
|
||||
export default connect(null, mapDispatchToProps)(Metrics);
|
@ -13,26 +13,18 @@ const SettingsPage = (): JSX.Element => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form
|
||||
labelCol={{
|
||||
span: 3,
|
||||
}}
|
||||
wrapperCol={{ span: 6 }}
|
||||
name="basic"
|
||||
initialValues={{ remember: true }}
|
||||
style={{ marginLeft: 20 }}
|
||||
form={form}
|
||||
>
|
||||
<Form name="basic" initialValues={{ remember: true }} form={form}>
|
||||
<Form.Item
|
||||
label="Retention Period"
|
||||
name="retention_period"
|
||||
rules={[{ required: false }]}
|
||||
style={{ maxWidth: '40%' }}
|
||||
>
|
||||
<Input style={{ marginLeft: 60 }} disabled={true} />
|
||||
<Input disabled={true} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<Space style={{ marginLeft: 60, marginTop: 48 }}>
|
||||
<Space>
|
||||
<Alert
|
||||
message="Mail us at support@signoz.io to get instructions on how to change your retention period"
|
||||
type="info"
|
||||
|
@ -4,10 +4,15 @@ import { IS_LOGGED_IN } from 'constants/auth';
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import React, { useState } 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 { ButtonContainer, Container, FormWrapper, Title } from './styles';
|
||||
|
||||
const Signup = (): JSX.Element => {
|
||||
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<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
globalLoading: bindActionCreators(GlobalTimeLoading, dispatch),
|
||||
});
|
||||
|
||||
type SignupProps = DispatchProps;
|
||||
|
||||
export default connect(null, mapDispatchToProps)(Signup);
|
||||
|
@ -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';
|
||||
|
@ -35,7 +35,7 @@ export const GetQueryResults = (
|
||||
end,
|
||||
query: encodeURIComponent(query.query),
|
||||
start: start,
|
||||
step: '30',
|
||||
step: '60',
|
||||
});
|
||||
return {
|
||||
query: query.query,
|
||||
|
@ -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<AppActions>) => void) => {
|
||||
return (dispatch: Dispatch<AppActions>): 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<AppActions>,
|
||||
) => void) => {
|
||||
return (dispatch: Dispatch<AppActions>): void => {
|
||||
dispatch({
|
||||
type: 'GLOBAL_TIME_LOADING_START',
|
||||
});
|
||||
};
|
||||
};
|
||||
|
@ -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';
|
||||
|
94
frontend/src/store/actions/metrics/getInitialData.ts
Normal file
94
frontend/src/store/actions/metrics/getInitialData.ts
Normal file
@ -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<AppActions>) => void) => {
|
||||
return async (dispatch: Dispatch<AppActions>): Promise<void> => {
|
||||
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;
|
43
frontend/src/store/actions/metrics/getService.ts
Normal file
43
frontend/src/store/actions/metrics/getService.ts
Normal file
@ -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<AppActions>) => void) => {
|
||||
return async (dispatch: Dispatch<AppActions>): Promise<void> => {
|
||||
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;
|
1
frontend/src/store/actions/metrics/index.ts
Normal file
1
frontend/src/store/actions/metrics/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './getService';
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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<typeof reducers>;
|
||||
|
97
frontend/src/store/reducers/metric.ts
Normal file
97
frontend/src/store/reducers/metric.ts
Normal file
@ -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;
|
18
frontend/src/types/actions/globalTime.ts
Normal file
18
frontend/src/types/actions/globalTime.ts
Normal file
@ -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;
|
@ -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;
|
||||
|
51
frontend/src/types/actions/metrics.ts
Normal file
51
frontend/src/types/actions/metrics.ts
Normal file
@ -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;
|
16
frontend/src/types/api/metrics/getDBOverview.ts
Normal file
16
frontend/src/types/api/metrics/getDBOverview.ts
Normal file
@ -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[];
|
12
frontend/src/types/api/metrics/getExternalAverageDuration.ts
Normal file
12
frontend/src/types/api/metrics/getExternalAverageDuration.ts
Normal file
@ -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[];
|
13
frontend/src/types/api/metrics/getExternalError.ts
Normal file
13
frontend/src/types/api/metrics/getExternalError.ts
Normal file
@ -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[];
|
15
frontend/src/types/api/metrics/getExternalService.ts
Normal file
15
frontend/src/types/api/metrics/getExternalService.ts
Normal file
@ -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[];
|
16
frontend/src/types/api/metrics/getService.ts
Normal file
16
frontend/src/types/api/metrics/getService.ts
Normal file
@ -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[];
|
16
frontend/src/types/api/metrics/getServiceOverview.ts
Normal file
16
frontend/src/types/api/metrics/getServiceOverview.ts
Normal file
@ -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[];
|
15
frontend/src/types/api/metrics/getTopEndPoints.ts
Normal file
15
frontend/src/types/api/metrics/getTopEndPoints.ts
Normal file
@ -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[];
|
7
frontend/src/types/reducer/globalTime.ts
Normal file
7
frontend/src/types/reducer/globalTime.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { GlobalTime } from 'types/actions/globalTime';
|
||||
|
||||
export interface GlobalReducer {
|
||||
maxTime: GlobalTime['maxTime'];
|
||||
minTime: GlobalTime['minTime'];
|
||||
loading: boolean;
|
||||
}
|
22
frontend/src/types/reducer/metrics.ts
Normal file
22
frontend/src/types/reducer/metrics.ts
Normal file
@ -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;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user