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:
Palash 2021-10-20 09:24:55 +05:30 committed by GitHub
parent 1ebf1a3675
commit d2b107ec7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
101 changed files with 2601 additions and 2139 deletions

View File

@ -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>
);

View File

@ -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'),
);

View File

@ -0,0 +1,5 @@
const get = (key: string): string | null => {
return localStorage.getItem(key);
};
export default get;

View File

@ -0,0 +1,5 @@
const remove = (key: string): void => {
window.localStorage.removeItem(key);
};
export default remove;

View File

@ -0,0 +1,5 @@
const set = (key: string, value: string): void => {
localStorage.setItem(key, value);
};
export default set;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View File

@ -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;

View File

@ -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' }}>

View 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;
`;

View File

@ -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;

View File

@ -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;

View File

@ -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;
});

View File

@ -6,6 +6,7 @@ export const GraphContainer = styled.div`
justify-content: center;
display: flex;
flex-direction: column;
width: 100%;
`;
export const NotFoundContainer = styled.div`

View File

@ -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 {

View 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);

View File

@ -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;

View 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;
};

View 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));

View 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;
`;

View File

@ -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>
);
};

View File

@ -0,0 +1,8 @@
import { Row } from 'antd';
import styled from 'styled-components';
export const Container = styled(Row)`
&&& {
margin-top: 2rem;
}
`;

View 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);

View 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;

View 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;

View 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;
}
`;

View 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);

View 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;

View 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;
}
`;

View File

@ -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&nbsp;
<a
href={'https://signoz.io/docs/instrumentation/overview'}
target={'_blank'}
rel="noreferrer"
>
here
</a>
</Typography>
</div>
</>
</Modal>
);
};
interface Props {
onContinueClick: () => void;
}
export default SkipOnBoardingModal;

View 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);

View 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;
}
`;

View File

@ -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';

View File

@ -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;

View File

@ -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 {

View File

@ -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),
};

View File

@ -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;

View File

@ -1,4 +1,4 @@
import { GlobalTime } from 'store/actions';
import { GlobalTime } from 'types/actions/globalTime';
import { Widgets } from 'types/api/dashboard/getAll';
const GetMaxMinTime = ({

View 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;

View File

@ -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;

View File

@ -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);

View File

@ -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',
];

View File

@ -1 +0,0 @@
export { default } from './ExternalApiGraph';

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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;
}
`;

View File

@ -1 +0,0 @@
export { ServiceMetrics as default } from './ServiceMetrics';

View File

@ -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);

View File

@ -1,5 +0,0 @@
import styled from 'styled-components';
export const Wrapper = styled.div`
padding: 40px;
`;

View File

@ -1 +0,0 @@
export { ServicesTable as default } from './ServiceTable';

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;
}
`;

View File

@ -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;

View File

@ -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;

View File

@ -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,
};

View File

@ -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 };

View File

@ -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(() => {

View File

@ -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

View File

@ -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);

View File

@ -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);
};

View File

@ -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 */}

View 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);

View 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);

View File

@ -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"

View File

@ -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);

View File

@ -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';

View File

@ -35,7 +35,7 @@ export const GetQueryResults = (
end,
query: encodeURIComponent(query.query),
start: start,
step: '30',
step: '60',
});
return {
query: query.query,

View File

@ -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',
});
};
};

View File

@ -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';

View 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;

View 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;

View File

@ -0,0 +1 @@
export * from './getService';

View File

@ -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 {

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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>;

View 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;

View 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;

View File

@ -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;

View 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;

View 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[];

View 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[];

View 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[];

View 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[];

View 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[];

View 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[];

View 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[];

View File

@ -0,0 +1,7 @@
import { GlobalTime } from 'types/actions/globalTime';
export interface GlobalReducer {
maxTime: GlobalTime['maxTime'];
minTime: GlobalTime['minTime'];
loading: boolean;
}

View 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