Fix(FE):trace page (#356)

* chore: Router provider is removed

* update: localstorage set get is added

* update: AppLayout is updated

* fix: adapter type is fixed

* fix: Metric and metric application is now fixed

* fix: Metrics page application is updated

* fix: Tracepage is made fix

* fix: app layout is updated

* fix: global Time reducer is updated

* refactor: getService api is added

* update: metrics reducer is added

* update: service list is fixed

* fix: Metrics page is updated

* fix: api for the metrics application are done

* fix: metrics reducer is updated

* fix: metrics application is updated

* fix: content layout shift is removed

* fix: Metric application is updated

* fix: metrics application is updated

* fix: Metrics application is updated

* fix: Application tab is updated

* chore: graph is updated

* chore: Metrics application is updated

* fix: chart x-axis is label is now fixed

* fix: application tab is updated

* fix: Top end points is added and re-redering in stopped

* fix: fixed the edge case when user changes the global time then updated data is fetched

* fix: Settings page is updated

* chore: AppLayout is updated

* chore: AppLayout is updated

* chore: applayout is updated

* chore: changed default loading is true in the global time reducer

* chore: Global Time option is fixed

* chore: Signup and Applayout is updated

* chore: Button text is updated

* chore: Button in the metrics application is updated

* chore: dashboard menu item position in the side nav is updated

* fix: Logo is now redirecting to the Application page

* fix: Application page is updated

* fix: AppLayout is updated

* fix: starting and ending time is fixed

* fix: Metrics Application is updated to the previous chart data

* update: getDateArrayFromStartAndEnd function is added

* update: Empty graph data is added

* fix: External Call and DB Call Tabs graph are updated when there is no data a empty data is rendered

* fix: onboarding modal condition is fixed and new calling api every 50000 ms to fetch the data

* fix: onBoarding condition modal is updated

* fix: onBoarding condition modal is updated

* fix: onBoarding condition modal is updated

* fix: Application chart re rendering issue is fixed

* fix: Application page is changed when we change the global time

* chore: step size is increased from 30 to 60

* chore: build is now fixed

* chore: metrics application page is updated

* fix: empty graph is now fixed

* fix: application metrics graph is now fixed

* update: seperate api for trace page are made

* fix: /trace page is updated

* chore: Filter of the Trace page is updated

* chore: initial trace page is updated

* fix: changing the filters,fetches the updated values from the backend

* chore: Trace page is updated

* update: trace page is updated

* fix: trace page is updated

* Refresh Text is updated

* update: Trace page is updated

* update:header is updated

* update: Trace page is updated

* update: Trace page is updated

* update: Trace page is updated

* update: Trace page is updated

* update: why did you re render is added

* update: trace page is updated

* update: trace page is updated

* update: Loading is updated

* update: start and end time is updated

* fix: metrics and metrics page redudant calls is reduced

* fix: Metrics Application page reducer is reset on the unmount

* fix: Trace page reducer is reset when the page is unmounted

* fix: Custom Visualizations is now fetching only one api to get the details

* fix: Trace page is updated

* fix: composeEnhancers is updated

* fix: metrics application is updated

* chore: webpack eslint fixes are updated

* chore: some of the type definition is added

* fix(UI): Trace page bug is resolved

* chore(UI): if length of the selected tags is zero updated the value over the form

* chore(UI): check for the no spans filter is updated
This commit is contained in:
pal-sig 2021-11-16 21:13:20 +05:30 committed by GitHub
parent 510815655f
commit 28c8df5e63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
84 changed files with 2991 additions and 377 deletions

View File

@ -145,6 +145,7 @@
"@types/webpack-dev-server": "^4.3.0",
"@typescript-eslint/eslint-plugin": "^4.28.2",
"@typescript-eslint/parser": "^4.28.2",
"@welldone-software/why-did-you-render": "^6.2.1",
"autoprefixer": "^9.0.0",
"babel-plugin-styled-components": "^1.12.0",
"compression-webpack-plugin": "^9.0.0",

View File

@ -25,6 +25,10 @@ export const TraceDetailPage = Loadable(
),
);
export const TraceDetailPages = Loadable(
() => import(/* webpackChunkName: "TraceDetailPage" */ 'pages/TraceDetails'),
);
export const TraceGraphPage = Loadable(
() =>
import(

View File

@ -12,6 +12,7 @@ import {
SettingsPage,
SignupPage,
TraceDetailPage,
TraceDetailPages,
TraceGraphPage,
UsageExplorerPage,
} from './pageComponents';
@ -77,6 +78,11 @@ const routes: AppRoutes[] = [
exact: true,
component: DashboardWidget,
},
{
path: ROUTES.TRACE,
exact: true,
component: TraceDetailPages,
},
];
interface AppRoutes {

View File

@ -0,0 +1,24 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps } from 'types/api/trace/getServiceList';
const getServiceList = async (): Promise<
SuccessResponse<PayloadProps> | ErrorResponse
> => {
try {
const response = await axios.get('/services/list');
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getServiceList;

View File

@ -0,0 +1,24 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/trace/getServiceOperation';
const getServiceOperation = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(`/service/${props.service}/operations`);
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getServiceOperation;

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/trace/getSpans';
const getSpans = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(
`/spans?&start=${props.start}&end=${props.end}&kind=${props.kind}&lookback=${props.lookback}&maxDuration=${props.maxDuration}&minDuration=${props.minDuration}&operation=${props.operation}&service=${props.service}&limit=${props.limit}&tags=${props.tags}`,
);
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getSpans;

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/trace/getSpanAggregate';
const getSpansAggregate = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(
`/spans/aggregates?start=${props.start}&end=${props.end}&aggregation_option=${props.aggregation_option}&dimension=${props.dimension}&kind=${props.kind}&maxDuration=${props.maxDuration}&minDuration=${props.minDuration}&operation=${props.operation}&service=${props.service}&step=${props.step}&tags=${props.tags}`,
);
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getSpansAggregate;

View File

@ -0,0 +1,24 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/trace/getTags';
const getTags = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(`/tags?service=${props.service}`);
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getTags;

View File

@ -6,4 +6,9 @@ export enum METRICS_PAGE_QUERY_PARAM {
error = 'error',
operation = 'operation',
kind = 'kind',
latencyMax = 'latencyMax',
latencyMin = 'latencyMin',
selectedTags = 'selectedTags',
aggregationOption = 'aggregationOption',
entity = 'entity',
}

View File

@ -3,6 +3,7 @@ const ROUTES = {
SERVICE_METRICS: '/application/:servicename',
SERVICE_MAP: '/service-map',
TRACES: '/traces',
TRACE: '/trace',
TRACE_GRAPH: '/traces/:id',
SETTINGS: '/settings',
INSTRUMENTATION: '/add-instrumentation',

View File

@ -13,7 +13,7 @@ import {
import getChartData from 'lib/getChartData';
import GetMaxMinTime from 'lib/getMaxMinTime';
import getStartAndEndTime from 'lib/getStartAndEndTime';
import React, { memo, useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalTime } from 'types/actions/globalTime';
@ -219,15 +219,4 @@ interface FullViewProps {
noDataGraph?: boolean;
}
export default memo(FullView, (prev, next) => {
if (
next.widget.query.length !== prev.widget.query.length &&
next.widget.query.every((value, index) => {
return value === prev.widget.query[index];
})
) {
return false;
}
return true;
});
export default FullView;

View File

@ -0,0 +1,34 @@
import React, { useEffect, useState } from 'react';
import { RefreshTextContainer, Typography } from './styles';
const RefreshText = ({
onLastRefreshHandler,
}: RefreshTextProps): JSX.Element => {
const [refreshText, setRefreshText] = useState<string>('');
// this is to update the refresh text
useEffect(() => {
const interval = setInterval(() => {
const text = onLastRefreshHandler();
if (refreshText !== text) {
setRefreshText(text);
}
}, 2000);
return (): void => {
clearInterval(interval);
};
}, [onLastRefreshHandler, refreshText]);
return (
<RefreshTextContainer>
<Typography>{refreshText}</Typography>
</RefreshTextContainer>
);
};
interface RefreshTextProps {
onLastRefreshHandler: () => string;
}
export default RefreshText;

View File

@ -2,13 +2,7 @@ import { Button, Select as DefaultSelect } from 'antd';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { getDefaultOption, getOptions, Time } from './config';
import {
Container,
Form,
FormItem,
RefreshTextContainer,
Typography,
} from './styles';
import { Container, Form, FormItem } from './styles';
const { Option } = DefaultSelect;
import get from 'api/browser/localstorage/get';
import set from 'api/browser/localstorage/set';
@ -19,31 +13,72 @@ import { connect, useSelector } from 'react-redux';
import { RouteComponentProps, withRouter } from 'react-router';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { UpdateTimeInterval } from 'store/actions';
import { GlobalTimeLoading, UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { GlobalReducer } from 'types/reducer/globalTime';
import CustomDateTimeModal, { DateTimeRangeType } from '../CustomDateTimeModal';
import RefreshText from './Refresh';
const DateTimeSelection = ({
location,
updateTimeInterval,
globalTimeLoading,
}: Props): JSX.Element => {
const [form_dtselector] = Form.useForm();
const params = new URLSearchParams(location.search);
const searchStartTime = params.get('startTime');
const searchEndTime = params.get('endTime');
const localstorageStartTime = get('startTime');
const localstorageEndTime = get('endTime');
const getTime = useCallback((): [number, number] | undefined => {
if (searchEndTime && searchStartTime) {
const startMoment = moment(
new Date(parseInt(getTimeString(searchStartTime), 10)),
);
const endMoment = moment(
new Date(parseInt(getTimeString(searchEndTime), 10)),
);
return [
startMoment.toDate().getTime() || 0,
endMoment.toDate().getTime() || 0,
];
}
if (localstorageStartTime && localstorageEndTime) {
const startMoment = moment(localstorageStartTime);
const endMoment = moment(localstorageEndTime);
return [
startMoment.toDate().getTime() || 0,
endMoment.toDate().getTime() || 0,
];
}
return undefined;
}, [
localstorageEndTime,
localstorageStartTime,
searchEndTime,
searchStartTime,
]);
const [startTime, setStartTime] = useState<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 { maxTime, minTime, selectedTime, loading } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const getDefaultTime = (pathName: string): Time => {
const defaultSelectedOption = getDefaultOption(pathName);
@ -81,8 +116,6 @@ const DateTimeSelection = ({
};
const onSelectHandler = (value: Time): void => {
isOnSelectHandler.current = true;
if (value !== 'custom') {
updateTimeInterval(value);
const selectedLabel = getInputLabel(undefined, undefined, value);
@ -168,17 +201,6 @@ const DateTimeSelection = ({
}
};
// this is to update the refresh text
useEffect(() => {
const interval = setInterval(() => {
const text = onLastRefreshHandler();
setRefreshText(text);
}, 2000);
return (): void => {
clearInterval(interval);
};
}, [onLastRefreshHandler]);
// this is triggred when we change the routes and based on that we are changing the default options
useEffect(() => {
const metricsTimeDuration = get(LOCAL_STORAGE.METRICS_TIME_IN_DURATION);
@ -187,85 +209,44 @@ const DateTimeSelection = ({
set(LOCAL_STORAGE.METRICS_TIME_IN_DURATION, JSON.stringify({}));
}
if (isOnSelectHandler.current === false) {
const currentRoute = location.pathname;
const params = new URLSearchParams(location.search);
const time = getDefaultTime(currentRoute);
const currentRoute = location.pathname;
const time = getDefaultTime(currentRoute);
const currentOptions = getOptions(currentRoute);
setOptions(currentOptions);
const currentOptions = getOptions(currentRoute);
setOptions(currentOptions);
const searchStartTime = params.get('startTime');
const searchEndTime = params.get('endTime');
const localstorageStartTime = get('startTime');
const localstorageEndTime = get('endTime');
const getUpdatedTime = (time: Time): Time => {
if (searchEndTime !== null && searchStartTime !== null) {
return 'custom';
}
if (
(localstorageEndTime === null || localstorageStartTime === null) &&
time === 'custom'
) {
return getDefaultOption(location.pathname);
}
return time;
};
const updatedTime = getUpdatedTime(time);
setSelectedTimeInterval(updatedTime);
const getTime = (): [number, number] | undefined => {
if (searchEndTime && searchStartTime) {
const startMoment = moment(
new Date(parseInt(getTimeString(searchStartTime), 10)),
);
const endMoment = moment(
new Date(parseInt(getTimeString(searchEndTime), 10)),
);
setStartTime(startMoment);
setEndTime(endMoment);
return [
startMoment.toDate().getTime() || 0,
endMoment.toDate().getTime() || 0,
];
}
if (localstorageStartTime && localstorageEndTime) {
const startMoment = moment(localstorageStartTime);
const endMoment = moment(localstorageEndTime);
setStartTime(startMoment);
setEndTime(endMoment);
return [
startMoment.toDate().getTime() || 0,
endMoment.toDate().getTime() || 0,
];
}
return undefined;
};
if (loading === true) {
updateTimeInterval(updatedTime, getTime());
const getCustomOrIntervalTime = (time: Time): Time => {
if (searchEndTime !== null && searchStartTime !== null) {
return 'custom';
}
} else {
isOnSelectHandler.current = false;
}
if (
(localstorageEndTime === null || localstorageStartTime === null) &&
time === 'custom'
) {
return getDefaultOption(currentRoute);
}
return time;
};
const updatedTime = getCustomOrIntervalTime(time);
const [preStartTime = 0, preEndTime = 0] = getTime() || [];
setStartTime(moment(preStartTime));
setEndTime(moment(preEndTime));
updateTimeInterval(updatedTime, [preStartTime, preEndTime]);
}, [
location.pathname,
location.search,
startTime,
endTime,
getTime,
localstorageEndTime,
localstorageStartTime,
searchEndTime,
searchStartTime,
updateTimeInterval,
selectedTimeInterval,
loading,
globalTimeLoading,
]);
return (
@ -273,11 +254,11 @@ const DateTimeSelection = ({
<Form
form={form_dtselector}
layout="inline"
initialValues={{ interval: selectedTimeInterval }}
initialValues={{ interval: selectedTime }}
>
<DefaultSelect
onSelect={(value): void => onSelectHandler(value as Time)}
value={getInputLabel(startTime, endTime, selectedTimeInterval)}
value={getInputLabel(startTime, endTime, selectedTime)}
data-testid="dropDown"
>
{options.map(({ value, label }) => (
@ -294,9 +275,11 @@ const DateTimeSelection = ({
</FormItem>
</Form>
<RefreshTextContainer>
<Typography>{refreshText}</Typography>
</RefreshTextContainer>
<RefreshText
{...{
onLastRefreshHandler,
}}
/>
<CustomDateTimeModal
visible={customDateTimeVisible}
@ -314,16 +297,21 @@ interface DispatchProps {
interval: Time,
dateTimeRange?: [number, number],
) => (dispatch: Dispatch<AppActions>) => void;
// globalTimeLoading: () => void;
globalTimeLoading: () => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
updateTimeInterval: bindActionCreators(UpdateTimeInterval, dispatch),
// globalTimeLoading: bindActionCreators(GlobalTimeLoading, dispatch),
globalTimeLoading: bindActionCreators(GlobalTimeLoading, dispatch),
});
type Props = DispatchProps & RouteComponentProps;
export default connect(null, mapDispatchToProps)(withRouter(DateTimeSelection));
// DateTimeSelection.whyDidYouRender = {
// logOnDifferentValues: true,
// customName: 'DateTimeSelection',
// };

View File

@ -6,13 +6,9 @@ import FullView from 'container/GridGraphLayout/Graph/FullView';
import { colors } from 'lib/getRandomColor';
import history from 'lib/history';
import React, { useRef } from 'react';
import { connect, useSelector } from 'react-redux';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { bindActionCreators } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { GlobalTimeLoading } from 'store/actions';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { Widgets } from 'types/api/dashboard/getAll';
import MetricReducer from 'types/reducer/metrics';
@ -20,10 +16,7 @@ import { Card, Col, GraphContainer, GraphTitle, Row } from '../styles';
import TopEndpointsTable from '../TopEndpointsTable';
import { Button } from './styles';
const Application = ({
globalLoading,
getWidget,
}: DashboardProps): JSX.Element => {
const Application = ({ getWidget }: DashboardProps): JSX.Element => {
const { servicename } = useParams<{ servicename?: string }>();
const selectedTimeStamp = useRef(0);
@ -42,8 +35,7 @@ const Application = ({
urlParams.set(METRICS_PAGE_QUERY_PARAM.service, servicename);
}
globalLoading();
history.push(`${ROUTES.TRACES}?${urlParams.toString()}`);
history.push(`${ROUTES.TRACE}?${urlParams.toString()}`);
};
const onClickhandler = async (
@ -74,7 +66,7 @@ const Application = ({
buttonElement.style.display = 'block';
buttonElement.style.left = `${firstPoint.element.x}px`;
buttonElement.style.top = `${firstPoint.element.y}px`;
selectedTimeStamp.current = new Date(time).getTime();
selectedTimeStamp.current = time.getTime();
}
}
} else {
@ -97,8 +89,7 @@ const Application = ({
}
urlParams.set(METRICS_PAGE_QUERY_PARAM.error, 'true');
globalLoading();
history.push(`${ROUTES.TRACES}?${urlParams.toString()}`);
history.push(`${ROUTES.TRACE}?${urlParams.toString()}`);
};
return (
@ -238,18 +229,8 @@ const Application = ({
);
};
interface DispatchProps {
globalLoading: () => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
globalLoading: bindActionCreators(GlobalTimeLoading, dispatch),
});
interface DashboardProps extends DispatchProps {
interface DashboardProps {
getWidget: (query: Widgets['query']) => Widgets;
}
export default connect(null, mapDispatchToProps)(Application);
export default Application;

View File

@ -1,15 +1,12 @@
import { Button, Table, Tooltip } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { METRICS_PAGE_QUERY_PARAM } from 'constants/query';
import ROUTES from 'constants/routes';
import React from 'react';
import { connect, useSelector } from 'react-redux';
import { useSelector } from 'react-redux';
import { useHistory, useParams } from 'react-router-dom';
import { bindActionCreators } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { GlobalTimeLoading } from 'store/actions';
import { topEndpointListItem } from 'store/actions/MetricsActions';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { GlobalReducer } from 'types/reducer/globalTime';
const TopEndpointsTable = (props: TopEndpointsTableProps): JSX.Element => {
@ -25,19 +22,18 @@ const TopEndpointsTable = (props: TopEndpointsTableProps): JSX.Element => {
const { servicename } = params;
urlParams.set(
METRICS_PAGE_QUERY_PARAM.startTime,
String(Number(minTime) / 1000000),
(minTime / 1000000).toString(),
);
urlParams.set(
METRICS_PAGE_QUERY_PARAM.endTime,
String(Number(maxTime) / 1000000),
(maxTime / 1000000).toString(),
);
if (servicename) {
urlParams.set(METRICS_PAGE_QUERY_PARAM.service, servicename);
}
urlParams.set(METRICS_PAGE_QUERY_PARAM.operation, operation);
props.globalTimeLoading();
history.push(`/traces?${urlParams.toString()}`);
history.push(`${ROUTES.TRACE}?${urlParams.toString()}`);
};
const columns: ColumnsType<DataProps> = [
@ -105,18 +101,8 @@ const TopEndpointsTable = (props: TopEndpointsTableProps): JSX.Element => {
type DataProps = topEndpointListItem;
interface DispatchProps {
globalTimeLoading: () => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
globalTimeLoading: bindActionCreators(GlobalTimeLoading, dispatch),
});
interface TopEndpointsTableProps extends DispatchProps {
interface TopEndpointsTableProps {
data: topEndpointListItem[];
}
export default connect(null, mapDispatchToProps)(TopEndpointsTable);
export default TopEndpointsTable;

View File

@ -3,18 +3,15 @@ import { SKIP_ONBOARDING } from 'constants/onboarding';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import React, { useState } from 'react';
import { connect, useSelector } from 'react-redux';
import { bindActionCreators } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { GlobalTimeLoading, servicesListItem } from 'store/actions';
import { useSelector } from 'react-redux';
import { servicesListItem } from 'store/actions/MetricsActions/metricsInterfaces';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import MetricReducer from 'types/reducer/metrics';
import SkipBoardModal from './SkipOnBoardModal';
import { Container, Name } from './styles';
const Metrics = ({ globalTimeLoading }: MetricsProps): JSX.Element => {
const Metrics = (): JSX.Element => {
const [skipOnboarding, setSkipOnboarding] = useState(
localStorage.getItem(SKIP_ONBOARDING) === 'true',
);
@ -30,7 +27,6 @@ const Metrics = ({ globalTimeLoading }: MetricsProps): JSX.Element => {
const onClickHandler = (to: string): void => {
history.push(to);
globalTimeLoading();
};
if (
@ -90,16 +86,4 @@ const Metrics = ({ globalTimeLoading }: MetricsProps): JSX.Element => {
type DataProps = servicesListItem;
interface DispatchProps {
globalTimeLoading: () => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
globalTimeLoading: bindActionCreators(GlobalTimeLoading, dispatch),
});
type MetricsProps = DispatchProps;
export default connect(null, mapDispatchToProps)(Metrics);
export default Metrics;

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 { GlobalTimeLoading, ToggleDarkMode } from 'store/actions';
import { ToggleDarkMode } from 'store/actions';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import AppReducer from 'types/reducer/app';
@ -15,7 +15,7 @@ import AppReducer from 'types/reducer/app';
import menus from './menuItems';
import { Logo, Sider, ThemeSwitcherWrapper } from './styles';
const SideNav = ({ toggleDarkMode, globalTimeLoading }: Props): JSX.Element => {
const SideNav = ({ toggleDarkMode }: Props): JSX.Element => {
const [collapsed, setCollapsed] = useState<boolean>(false);
const { pathname } = useLocation();
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
@ -49,10 +49,9 @@ const SideNav = ({ toggleDarkMode, globalTimeLoading }: Props): JSX.Element => {
(to: string) => {
if (pathname !== to) {
history.push(to);
globalTimeLoading();
}
},
[pathname, globalTimeLoading],
[pathname],
);
return (
@ -86,14 +85,12 @@ type mode = 'darkMode' | 'lightMode';
interface DispatchProps {
toggleDarkMode: () => void;
globalTimeLoading: () => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
toggleDarkMode: bindActionCreators(ToggleDarkMode, dispatch),
globalTimeLoading: bindActionCreators(GlobalTimeLoading, dispatch),
});
type Props = DispatchProps;

View File

@ -17,7 +17,7 @@ const menus: SidebarMenu[] = [
},
{
Icon: AlignLeftOutlined,
to: ROUTES.TRACES,
to: ROUTES.TRACE,
name: 'Traces',
},
{

View File

@ -0,0 +1,33 @@
import Graph from 'components/Graph';
import { colors } from 'lib/getRandomColor';
import React, { memo } from 'react';
import { TraceReducer } from 'types/reducer/trace';
import { CustomGraphContainer } from './styles';
const TraceCustomGraph = ({
spansAggregate,
}: TraceCustomGraphProps): JSX.Element => {
return (
<CustomGraphContainer>
<Graph
type="line"
data={{
labels: spansAggregate.map((s) => new Date(s.timestamp / 1000000)),
datasets: [
{
data: spansAggregate.map((e) => e.value),
borderColor: colors[0],
},
],
}}
/>
</CustomGraphContainer>
);
};
interface TraceCustomGraphProps {
spansAggregate: TraceReducer['spansAggregate'];
}
export default memo(TraceCustomGraph);

View File

@ -0,0 +1,56 @@
export const entity = [
{
title: 'Calls',
key: 'calls',
dataindex: 'calls',
},
{
title: 'Duration',
key: 'duration',
dataindex: 'duration',
},
{
title: 'Error',
key: 'error',
dataindex: 'error',
},
{
title: 'Status Code',
key: 'status_code',
dataindex: 'status_code',
},
];
export const aggregation_options = [
{
linked_entity: 'calls',
default_selected: { title: 'count', dataindex: 'count' },
options_available: [
{ title: 'Count', dataindex: 'count' },
{ title: 'Rate (per sec)', dataindex: 'rate_per_sec' },
],
},
{
linked_entity: 'duration',
default_selected: { title: 'p99', dataindex: 'p99' },
// options_available: [ {title:'Avg', dataindex:'avg'}, {title:'Max', dataindex:'max'},{title:'Min', dataindex:'min'}, {title:'p50', dataindex:'p50'},{title:'p95', dataindex:'p95'}, {title:'p95', dataindex:'p95'}]
options_available: [
{ title: 'p50', dataindex: 'p50' },
{ title: 'p95', dataindex: 'p95' },
{ title: 'p99', dataindex: 'p99' },
],
},
{
linked_entity: 'error',
default_selected: { title: 'count', dataindex: 'count' },
options_available: [
{ title: 'count', dataindex: 'count' },
{ title: 'Rate (per sec)', dataindex: 'rate_per_sec' },
],
},
{
linked_entity: 'status_code',
default_selected: { title: 'count', dataindex: 'count' },
options_available: [{ title: 'count', dataindex: 'count' }],
},
];

View File

@ -0,0 +1,127 @@
import { Form, Select } from 'antd';
import Spinner from 'components/Spinner';
import React from 'react';
import { connect, useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
const { Option } = Select;
import { bindActionCreators } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import AppActions from 'types/actions';
import { TraceReducer } from 'types/reducer/trace';
import { aggregation_options, entity } from './config';
import { Card, CustomVisualizationsTitle, FormItem, Space } from './styles';
import TraceCustomGraph from './TraceCustomGraph';
import {
GetTraceVisualAggregates,
GetTraceVisualAggregatesProps,
} from 'store/actions/trace/getTraceVisualAgrregates';
const TraceCustomVisualisation = ({
getTraceVisualAggregates,
}: TraceCustomVisualisationProps): JSX.Element => {
const {
selectedEntity,
spansLoading,
selectedAggOption,
spansAggregate,
} = useSelector<AppState, TraceReducer>((state) => state.trace);
const [form] = Form.useForm();
if (spansLoading) {
return <Spinner tip="Loading..." height="40vh" />;
}
const handleFormValuesChange = (changedValues: any): void => {
const formFieldName = Object.keys(changedValues)[0];
if (formFieldName === 'entity') {
const temp_entity = aggregation_options.filter(
(item) => item.linked_entity === changedValues[formFieldName],
)[0];
form.setFieldsValue({
agg_options: temp_entity.default_selected.title,
});
const values = form.getFieldsValue(['agg_options', 'entity']);
getTraceVisualAggregates({
selectedAggOption: values.agg_options,
selectedEntity: values.entity,
});
}
if (formFieldName === 'agg_options') {
getTraceVisualAggregates({
selectedAggOption: changedValues[formFieldName],
selectedEntity,
});
}
};
return (
<Card>
<CustomVisualizationsTitle>Custom Visualizations</CustomVisualizationsTitle>
<Form
form={form}
onValuesChange={handleFormValuesChange}
initialValues={{
entity: selectedEntity,
agg_options: selectedAggOption,
chart_style: 'line',
interval: '5m',
group_by: 'none',
}}
>
<Space>
<FormItem name="entity">
<Select style={{ width: 120 }} allowClear>
{entity.map((item) => (
<Option key={item.key} value={item.dataindex}>
{item.title}
</Option>
))}
</Select>
</FormItem>
<FormItem name="agg_options">
<Select style={{ width: 120 }} allowClear>
{aggregation_options
.filter((item) => item.linked_entity === selectedEntity)[0]
.options_available.map((item) => (
<Option key={item.dataindex} value={item.dataindex}>
{item.title}
</Option>
))}
</Select>
</FormItem>
</Space>
</Form>
<TraceCustomGraph
{...{
spansAggregate,
}}
/>
</Card>
);
};
interface DispatchProps {
getTraceVisualAggregates: (props: GetTraceVisualAggregatesProps) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
getTraceVisualAggregates: bindActionCreators(
GetTraceVisualAggregates,
dispatch,
),
});
type TraceCustomVisualisationProps = DispatchProps;
export default connect(null, mapDispatchToProps)(TraceCustomVisualisation);

View File

@ -0,0 +1,34 @@
import {
Card as CardComponent,
Form,
Space as SpaceComponent,
Typography,
} from 'antd';
import styled from 'styled-components';
export const CustomGraphContainer = styled.div`
min-height: 30vh;
`;
export const Card = styled(CardComponent)`
.ant-card-body {
padding-bottom: 0;
}
`;
export const CustomVisualizationsTitle = styled(Typography)`
margin-bottom: 1rem;
`;
export const FormItem = styled(Form.Item)`
&&& {
margin: 0;
}
`;
export const Space = styled(SpaceComponent)`
&&& {
display: flex;
flex-wrap: wrap;
}
`;

View File

@ -0,0 +1,187 @@
import { Tag } from 'antd';
import { METRICS_PAGE_QUERY_PARAM } from 'constants/query';
import React from 'react';
import { connect, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { TagItem } from 'store/actions';
import {
UpdateSelectedLatency,
UpdateSelectedOperation,
UpdateSelectedService,
UpdateSelectedTags,
} from 'store/actions/trace';
import {
UpdateSelectedData,
UpdateSelectedDataProps,
} from 'store/actions/trace/updateSelectedData';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { TraceReducer } from 'types/reducer/trace';
import { Card } from './styles';
const Filter = ({
updatedQueryParams,
updateSelectedData,
updateSelectedTags,
}: FilterProps): JSX.Element => {
const {
selectedService,
selectedOperation,
selectedLatency,
selectedTags,
selectedKind,
selectedEntity,
selectedAggOption,
} = useSelector<AppState, TraceReducer>((state) => state.trace);
function handleCloseTag(value: string): void {
if (value === 'service') {
updatedQueryParams([''], [METRICS_PAGE_QUERY_PARAM.service]);
updateSelectedData({
selectedAggOption,
selectedEntity,
selectedKind,
selectedLatency,
selectedOperation,
selectedService: '',
});
}
if (value === 'operation') {
updatedQueryParams([''], [METRICS_PAGE_QUERY_PARAM.operation]);
updateSelectedData({
selectedAggOption,
selectedEntity,
selectedKind,
selectedLatency,
selectedOperation: '',
selectedService,
});
}
if (value === 'maxLatency') {
updatedQueryParams([''], [METRICS_PAGE_QUERY_PARAM.latencyMax]);
updateSelectedData({
selectedAggOption,
selectedEntity,
selectedKind,
selectedLatency: {
min: selectedLatency.min,
max: '',
},
selectedOperation,
selectedService,
});
}
if (value === 'minLatency') {
updatedQueryParams([''], [METRICS_PAGE_QUERY_PARAM.latencyMin]);
updateSelectedData({
selectedAggOption,
selectedEntity,
selectedKind,
selectedLatency: {
min: '',
max: selectedLatency.max,
},
selectedOperation,
selectedService,
});
}
}
function handleCloseTagElement(item: TagItem): void {
const updatedSelectedtags = selectedTags.filter((e) => e.key !== item.key);
updatedQueryParams(
[updatedSelectedtags],
[METRICS_PAGE_QUERY_PARAM.selectedTags],
);
updateSelectedTags(updatedSelectedtags);
}
return (
<Card>
{selectedService.length !== 0 && (
<Tag
closable
onClose={(e): void => {
e.preventDefault();
handleCloseTag('service');
}}
>
service:{selectedService}
</Tag>
)}
{selectedOperation.length !== 0 && (
<Tag
closable
onClose={(e): void => {
e.preventDefault();
handleCloseTag('operation');
}}
>
operation:{selectedOperation}
</Tag>
)}
{selectedLatency?.min.length !== 0 && (
<Tag
closable
onClose={(e): void => {
e.preventDefault();
handleCloseTag('minLatency');
}}
>
minLatency:
{(parseInt(selectedLatency?.min || '0') / 1000000).toString()}ms
</Tag>
)}
{selectedLatency?.max.length !== 0 && (
<Tag
closable
onClose={(e): void => {
e.preventDefault();
handleCloseTag('maxLatency');
}}
>
maxLatency:
{(parseInt(selectedLatency?.max || '0') / 1000000).toString()}ms
</Tag>
)}
{selectedTags.map((item) => (
<Tag
closable
key={`${item.key}-${item.operator}-${item.value}`}
onClose={(e): void => {
e.preventDefault();
handleCloseTagElement(item);
}}
>
{item.key} {item.operator} {item.value}
</Tag>
))}
</Card>
);
};
interface DispatchProps {
updateSelectedTags: (
selectedTags: TraceReducer['selectedTags'],
) => (dispatch: Dispatch<AppActions>) => void;
updateSelectedData: (props: UpdateSelectedDataProps) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
updateSelectedTags: bindActionCreators(UpdateSelectedTags, dispatch),
updateSelectedData: bindActionCreators(UpdateSelectedData, dispatch),
});
interface FilterProps extends DispatchProps {
updatedQueryParams: (updatedValue: string[], key: string[]) => void;
}
export default connect(null, mapDispatchToProps)(Filter);

View File

@ -0,0 +1,160 @@
import { Col, Form, InputNumber, Modal, notification, Row } from 'antd';
import { METRICS_PAGE_QUERY_PARAM } from 'constants/query';
import { FormInstance, RuleObject } from 'rc-field-form/lib/interface';
import React from 'react';
import { connect, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { UpdateSelectedLatency } from 'store/actions/trace';
import {
UpdateSelectedData,
UpdateSelectedDataProps,
} from 'store/actions/trace/updateSelectedData';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { TraceReducer } from 'types/reducer/trace';
const LatencyForm = ({
onCancel,
visible,
updateSelectedLatency,
onLatencyButtonClick,
updatedQueryParams,
updateSelectedData,
}: LatencyModalFormProps): JSX.Element => {
const [form] = Form.useForm();
const [notifications, Element] = notification.useNotification();
const {
selectedLatency,
selectedKind,
selectedOperation,
selectedService,
selectedAggOption,
selectedEntity,
} = useSelector<AppState, TraceReducer>((state) => state.trace);
const validateMinValue = (form: FormInstance): RuleObject => ({
validator(_: RuleObject, value): Promise<void> {
const { getFieldValue } = form;
const minValue = getFieldValue('min');
const maxValue = getFieldValue('max');
if (value <= maxValue && value >= minValue) {
return Promise.resolve();
}
return Promise.reject(new Error('Min value should be less than Max value'));
},
});
const validateMaxValue = (form: FormInstance): RuleObject => ({
validator(_, value): Promise<void> {
const { getFieldValue } = form;
const minValue = getFieldValue('min');
const maxValue = getFieldValue('max');
if (value >= minValue && value <= maxValue) {
return Promise.resolve();
}
return Promise.reject(
new Error('Max value should be greater than Min value'),
);
},
});
const onOkHandler = (): void => {
form
.validateFields()
.then((values) => {
const maxValue = (values.max * 1000000).toString();
const minValue = (values.min * 1000000).toString();
onLatencyButtonClick();
updatedQueryParams(
[maxValue, minValue],
[METRICS_PAGE_QUERY_PARAM.latencyMax, METRICS_PAGE_QUERY_PARAM.latencyMin],
);
updateSelectedLatency({
max: maxValue,
min: minValue,
});
updateSelectedData({
selectedKind,
selectedLatency: {
max: maxValue,
min: minValue,
},
selectedOperation,
selectedService,
selectedAggOption,
selectedEntity,
});
})
.catch((info) => {
notifications.error({
message: info.toString(),
});
});
};
return (
<>
{Element}
<Modal
title="Chose min and max values of Latency"
okText="Apply"
cancelText="Cancel"
visible={visible}
onCancel={onCancel}
onOk={onOkHandler}
>
<Form
form={form}
layout="horizontal"
name="form_in_modal"
initialValues={{
min: parseInt(selectedLatency.min, 10) / 1000000,
max: parseInt(selectedLatency.max, 10) / 1000000,
}}
>
<Row>
<Col span={12}>
<Form.Item name="min" label="Min (in ms)" rules={[validateMinValue]}>
<InputNumber />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="max" label="Max (in ms)" rules={[validateMaxValue]}>
<InputNumber />
</Form.Item>
</Col>
</Row>
</Form>
</Modal>
</>
);
};
interface DispatchProps {
updateSelectedLatency: (
selectedLatency: TraceReducer['selectedLatency'],
) => (dispatch: Dispatch<AppActions>) => void;
updateSelectedData: (props: UpdateSelectedDataProps) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
updateSelectedLatency: bindActionCreators(UpdateSelectedLatency, dispatch),
updateSelectedData: bindActionCreators(UpdateSelectedData, dispatch),
});
interface LatencyModalFormProps extends DispatchProps {
onCancel: () => void;
visible: boolean;
onLatencyButtonClick: () => void;
updatedQueryParams: (updatedValue: string[], value: string[]) => void;
}
export default connect(null, mapDispatchToProps)(LatencyForm);

View File

@ -0,0 +1,15 @@
interface SpanKindList {
label: 'SERVER' | 'CLIENT';
value: string;
}
export const spanKindList: SpanKindList[] = [
{
label: 'SERVER',
value: '2',
},
{
label: 'CLIENT',
value: '3',
},
];

View File

@ -0,0 +1,384 @@
import { Button, Input, Typography, notification } from 'antd';
import { SelectValue } from 'antd/lib/select';
import React, { useCallback, useEffect, useState } from 'react';
import { connect, useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { TagItem, TraceReducer } from 'types/reducer/trace';
import { spanKindList } from './config';
import Filter from './Filter';
import LatencyForm from './LatencyForm';
import { AutoComplete, Form, InfoWrapper, Select } from './styles';
const { Option } = Select;
import { METRICS_PAGE_QUERY_PARAM } from 'constants/query';
import ROUTES from 'constants/routes';
import createQueryParams from 'lib/createQueryParams';
import history from 'lib/history';
import { useLocation } from 'react-router';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { UpdateSelectedTags } from 'store/actions/trace';
import {
UpdateSelectedData,
UpdateSelectedDataProps,
} from 'store/actions/trace/updateSelectedData';
import AppActions from 'types/actions';
const FormItem = Form.Item;
const TraceList = ({
updateSelectedTags,
updateSelectedData,
}: TraceListProps): JSX.Element => {
const [
notificationInstance,
NotificationElement,
] = notification.useNotification();
const [visible, setVisible] = useState<boolean>(false);
const [form] = Form.useForm();
const [form_basefilter] = Form.useForm();
const { search } = useLocation();
const params = new URLSearchParams(search);
const onLatencyButtonClick = useCallback(() => {
setVisible((visible) => !visible);
}, []);
const {
operationsList,
serviceList,
tagsSuggestions,
selectedTags,
selectedService,
selectedOperation,
selectedLatency,
selectedKind,
selectedAggOption,
selectedEntity,
} = useSelector<AppState, TraceReducer>((state) => state.trace);
const paramsInObject = (params: URLSearchParams): { [x: string]: string } => {
const updatedParamas: { [x: string]: string } = {};
params.forEach((value, key) => {
updatedParamas[key] = value;
});
return updatedParamas;
};
const updatedQueryParams = (updatedValue: string[], key: string[]): void => {
const updatedParams = paramsInObject(params);
updatedValue.forEach((_, index) => {
updatedParams[key[index]] = updatedValue[index];
});
const queryParams = createQueryParams(updatedParams);
history.push(ROUTES.TRACE + `?${queryParams}`);
};
const getUpdatedSelectedData = (props: UpdateSelectedDataProps): void => {
const {
selectedKind,
selectedLatency,
selectedOperation,
selectedService,
} = props;
updateSelectedData({
selectedKind,
selectedLatency,
selectedOperation,
selectedService,
selectedAggOption,
selectedEntity,
});
};
const onTagSubmitTagHandler = (values: Item): void => {
if (values.tag_key.length === 0 || values.tag_value.length === 0) {
return;
}
// check whether it is pre-existing in the array or not
const isFound = selectedTags.find((tags) => {
return (
tags.key === values.tag_key &&
tags.value === values.tag_value &&
tags.operator === values.operator
);
});
if (!isFound) {
const preSelectedTags = [
...selectedTags,
{
operator: values.operator,
key: values.tag_key,
value: values.tag_value,
},
];
updatedQueryParams(
[JSON.stringify(preSelectedTags)],
[METRICS_PAGE_QUERY_PARAM.selectedTags],
);
updateSelectedTags(preSelectedTags);
} else {
notificationInstance.error({
message: 'Tag Already Present',
});
}
};
const onChangeTagKey = (data: string): void => {
form.setFieldsValue({ tag_key: data });
};
const updateSelectedServiceHandler = (value: string): void => {
updatedQueryParams([value], [METRICS_PAGE_QUERY_PARAM.service]);
getUpdatedSelectedData({
selectedKind,
selectedLatency,
selectedOperation,
selectedService: value,
selectedAggOption,
selectedEntity,
});
};
const updateSelectedOperationHandler = (value: string): void => {
updatedQueryParams([value], [METRICS_PAGE_QUERY_PARAM.operation]);
getUpdatedSelectedData({
selectedKind,
selectedLatency,
selectedOperation: value,
selectedService,
selectedAggOption,
selectedEntity,
});
};
const updateSelectedKindHandler = (value: string): void => {
updatedQueryParams([value], [METRICS_PAGE_QUERY_PARAM.kind]);
getUpdatedSelectedData({
selectedKind: value,
selectedLatency,
selectedOperation,
selectedService,
selectedAggOption,
selectedEntity,
});
};
useEffect(() => {
if (selectedService.length !== 0) {
form_basefilter.setFieldsValue({
service: selectedService,
});
} else {
form_basefilter.setFieldsValue({
service: '',
});
}
if (selectedOperation.length !== 0) {
form_basefilter.setFieldsValue({
operation: selectedOperation,
});
} else {
form_basefilter.setFieldsValue({
operation: '',
});
}
if (selectedKind.length !== 0) {
form_basefilter.setFieldsValue({
spanKind: selectedKind,
});
} else {
form_basefilter.setFieldsValue({
spanKind: '',
});
}
if (selectedLatency.max.length === 0 && selectedLatency.min.length === 0) {
form_basefilter.setFieldsValue({
latency: 'Latency',
});
}
if (selectedLatency.max.length !== 0 && selectedLatency.min.length === 0) {
form_basefilter.setFieldsValue({
latency: `Latency < Max Latency: ${
parseInt(selectedLatency.max, 10) / 1000000
} ms`,
});
}
if (selectedLatency.max.length === 0 && selectedLatency.min.length !== 0) {
form_basefilter.setFieldsValue({
latency: `Min Latency: ${
parseInt(selectedLatency.min, 10) / 1000000
} ms < Latency`,
});
}
if (selectedLatency.max.length !== 0 && selectedLatency.min.length !== 0) {
form_basefilter.setFieldsValue({
latency: `Min Latency: ${
parseInt(selectedLatency.min, 10) / 1000000
} ms < Latency < Max Latency: ${
parseInt(selectedLatency.min, 10) / 1000000
} ms`,
});
}
}, [selectedService, selectedOperation, selectedKind, selectedLatency]);
return (
<>
{NotificationElement}
<Typography>Filter Traces</Typography>
<Form form={form_basefilter} layout="inline">
<FormItem name="service">
<Select
showSearch
onChange={(value: SelectValue): void => {
updateSelectedServiceHandler(value?.toString() || '');
}}
placeholder="Select Service"
allowClear
>
{serviceList.map((s) => (
<Option key={s} value={s}>
{s}
</Option>
))}
</Select>
</FormItem>
<FormItem name="operation">
<Select
showSearch
onChange={(value: SelectValue): void => {
updateSelectedOperationHandler(value?.toString() || '');
}}
placeholder="Select Operation"
allowClear
>
{operationsList.map((item) => (
<Option key={item} value={item}>
{item}
</Option>
))}
</Select>
</FormItem>
<FormItem name="latency">
<Input type="button" onClick={onLatencyButtonClick} />
</FormItem>
<FormItem name="spanKind">
<Select
showSearch
onChange={(value: SelectValue): void => {
updateSelectedKindHandler(value?.toString() || '');
}}
placeholder="Select Span Kind"
allowClear
>
{spanKindList.map((spanKind) => (
<Option value={spanKind.value} key={spanKind.value}>
{spanKind.label}
</Option>
))}
</Select>
</FormItem>
</Form>
{(selectedTags.length !== 0 ||
selectedService.length !== 0 ||
selectedOperation.length !== 0 ||
selectedLatency.max.length !== 0 ||
selectedLatency.min.length !== 0) && (
<Filter updatedQueryParams={updatedQueryParams} />
)}
<InfoWrapper>Select Service to get Tag suggestions</InfoWrapper>
<Form
form={form}
layout="inline"
onFinish={onTagSubmitTagHandler}
initialValues={{ operator: 'equals' }}
>
<FormItem name="tag_key">
<AutoComplete
options={tagsSuggestions.map((s) => {
return { value: s.tagKeys };
})}
onChange={onChangeTagKey}
filterOption={(inputValue, option): boolean =>
option?.value.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1
}
placeholder="Tag Key"
/>
</FormItem>
<FormItem name="operator">
<Select>
<Option value="equals">EQUAL</Option>
<Option value="contains">CONTAINS</Option>
<Option value="regex">REGEX</Option>
</Select>
</FormItem>
<FormItem name="tag_value">
<Input placeholder="Tag Value" />
</FormItem>
<FormItem>
<Button type="primary" htmlType="submit">
Apply Tag Filter
</Button>
</FormItem>
</Form>
<LatencyForm
onCancel={(): void => {
setVisible(false);
}}
updatedQueryParams={updatedQueryParams}
visible={visible}
onLatencyButtonClick={onLatencyButtonClick}
/>
</>
);
};
interface Item {
tag_key: string;
tag_value: string;
operator: TagItem['operator'];
}
interface DispatchProps {
updateSelectedTags: (
selectedTags: TraceReducer['selectedTags'],
) => (dispatch: Dispatch<AppActions>) => void;
updateSelectedData: (props: UpdateSelectedDataProps) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
updateSelectedTags: bindActionCreators(UpdateSelectedTags, dispatch),
updateSelectedData: bindActionCreators(UpdateSelectedData, dispatch),
});
type TraceListProps = DispatchProps;
export default connect(null, mapDispatchToProps)(TraceList);

View File

@ -0,0 +1,34 @@
import {
AutoComplete as AutoCompleteComponent,
Card as CardComponent,
Form as FormComponent,
Select as SelectComponent,
Typography,
} from 'antd';
import styled from 'styled-components';
export const InfoWrapper = styled(Typography)`
padding-top: 1rem;
font-style: italic;
font-size: 0.75rem;
`;
export const Select = styled(SelectComponent)`
min-width: 180px;
`;
export const AutoComplete = styled(AutoCompleteComponent)`
min-width: 180px;
`;
export const Form = styled(FormComponent)`
margin-top: 1rem;
margin-bottom: 1rem;
gap: 0.5rem;
`;
export const Card = styled(CardComponent)`
.ant-card-body {
padding: 0.5rem;
}
`;

View File

@ -0,0 +1,141 @@
import { Space, Table, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table/Table';
import ROUTES from 'constants/routes';
import convertDateToAmAndPm from 'lib/convertDateToAmAndPm';
import getFormattedDate from 'lib/getFormatedDate';
import history from 'lib/history';
import React from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { pushDStree } from 'types/api/trace/getSpans';
import { TraceReducer } from 'types/reducer/trace';
import { isOnboardingSkipped } from 'utils/app';
import { TitleContainer } from './styles';
const TraceDetails = (): JSX.Element => {
const { spanList } = useSelector<AppState, TraceReducer>(
(state) => state.trace,
);
const spans: TableDataSourceItem[] = spanList[0]?.events?.map(
(item: (number | string | string[] | pushDStree[])[], index) => {
if (
typeof item[0] === 'number' &&
typeof item[4] === 'string' &&
typeof item[6] === 'string' &&
typeof item[1] === 'string' &&
typeof item[2] === 'string' &&
typeof item[3] === 'string'
) {
return {
startTime: item[0],
operationName: item[4],
duration: parseInt(item[6]),
spanid: item[1],
traceid: item[2],
key: index.toString(),
service: item[3],
};
}
return {
duration: 0,
key: '',
operationName: '',
service: '',
spanid: '',
startTime: 0,
traceid: '',
};
},
);
const columns: ColumnsType<TableDataSourceItem> = [
{
title: 'Start Time',
dataIndex: 'startTime',
key: 'startTime',
sorter: (a, b): number => a.startTime - b.startTime,
sortDirections: ['descend', 'ascend'],
render: (value: number): string => {
const date = new Date(value);
const result = `${getFormattedDate(date)} ${convertDateToAmAndPm(date)}`;
return result;
},
},
{
title: 'Service',
dataIndex: 'service',
key: 'service',
},
{
title: 'Operation',
dataIndex: 'operationName',
key: 'operationName',
},
{
title: 'Duration (in ms)',
dataIndex: 'duration',
key: 'duration',
sorter: (a, b): number => a.duration - b.duration,
sortDirections: ['descend', 'ascend'],
render: (value: number): string => (value / 1000000).toFixed(2),
},
];
if (isOnboardingSkipped() && spans?.length === 0) {
return (
<Space style={{ width: '100%', margin: '40px 0', justifyContent: 'center' }}>
No spans found. Please add instrumentation (follow this
<a
href={'https://signoz.io/docs/instrumentation/overview'}
target={'_blank'}
rel="noreferrer"
>
guide
</a>
)
</Space>
);
}
if (spans?.length === 0) {
return <Typography> No spans found for given filter!</Typography>;
}
return (
<>
<TitleContainer>List of filtered spans</TitleContainer>
<Table
dataSource={spans}
columns={columns}
size="middle"
onRow={(
record: TableDataSourceItem,
): React.HTMLAttributes<HTMLElement> => ({
onClick: (): void => {
history.push({
pathname: ROUTES.TRACES + '/' + record.traceid,
state: {
spanId: record.spanid,
},
});
},
})}
/>
</>
);
};
export interface TableDataSourceItem {
key: string;
spanid: string;
traceid: string;
operationName: string;
startTime: number;
duration: number;
service: string;
}
export default TraceDetails;

View File

@ -0,0 +1,7 @@
import { Typography } from 'antd';
import styled from 'styled-components';
export const TitleContainer = styled(Typography)`
margin-top: 1rem;
margin-bottom: 1rem;
`;

View File

@ -1,3 +1,4 @@
import './wdyr';
import 'assets/index.css';
import AppRoutes from 'AppRoutes';

View File

@ -2,6 +2,7 @@ const convertDateToAmAndPm = (date: Date): string => {
return date.toLocaleString('en-US', {
hour: '2-digit',
minute: 'numeric',
second: 'numeric',
hour12: true,
});
};

View File

@ -0,0 +1,6 @@
const createQueryParams = (params: { [x: string]: string }): string =>
Object.keys(params)
.map((k) => `${k}=${encodeURI(params[k])}`)
.join('&');
export default createQueryParams;

View File

@ -0,0 +1,57 @@
import { Time } from 'container/Header/DateTimeSelection/config';
import { GlobalReducer } from 'types/reducer/globalTime';
import getMinAgo from './getStartAndEndTime/getMinAgo';
const GetMinMax = (
interval: Time,
dateTimeRange?: [number, number],
): GetMinMaxPayload => {
let maxTime = new Date().getTime();
let minTime = 0;
if (interval === '1min') {
const minTimeAgo = getMinAgo({ minutes: 1 }).getTime();
minTime = minTimeAgo;
} else if (interval === '15min') {
const minTimeAgo = getMinAgo({ minutes: 15 }).getTime();
minTime = minTimeAgo;
} else if (interval === '1hr') {
const minTimeAgo = getMinAgo({ minutes: 60 }).getTime();
minTime = minTimeAgo;
} else if (interval === '30min') {
const minTimeAgo = getMinAgo({ minutes: 30 }).getTime();
minTime = minTimeAgo;
} else if (interval === '5min') {
const minTimeAgo = getMinAgo({ minutes: 5 }).getTime();
minTime = minTimeAgo;
} else if (interval === '1day') {
// one day = 24*60(min)
const minTimeAgo = getMinAgo({ minutes: 26 * 60 }).getTime();
minTime = minTimeAgo;
} else if (interval === '1week') {
// one week = one day * 7
const minTimeAgo = getMinAgo({ minutes: 26 * 60 * 7 }).getTime();
minTime = minTimeAgo;
} else if (interval === '6hr') {
const minTimeAgo = getMinAgo({ minutes: 6 * 60 }).getTime();
minTime = minTimeAgo;
} else if (interval === 'custom') {
maxTime = (dateTimeRange || [])[1] || 0;
minTime = (dateTimeRange || [])[0] || 0;
} else {
throw new Error('invalid time type');
}
return {
minTime: minTime * 1000000,
maxTime: maxTime * 1000000,
};
};
interface GetMinMaxPayload {
minTime: GlobalReducer['minTime'];
maxTime: GlobalReducer['maxTime'];
}
export default GetMinMax;

View File

@ -1,5 +1,5 @@
import React, { useState } from "react";
import { servicesItem } from "Src/store/actions";
import { servicesItem } from "store/actions";
import { InfoCircleOutlined } from "@ant-design/icons";
import { Select } from "antd";
import styled from "styled-components";

View File

@ -69,7 +69,6 @@ const LatencyModalForm: React.FC<LatencyModalFormProps> = ({
initialValues={latencyFilterValues}
>
<Row>
{/* <Input.Group compact> */}
<Col span={12}>
<Form.Item
name="min"

View File

@ -247,7 +247,6 @@ const _TraceCustomVisualizations = (
},
],
}}
xAxisType="timeseries"
/>
</CustomGraphContainer>
</Card>

View File

@ -1,21 +1,10 @@
import React, { useEffect } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { GlobalTimeLoading } from 'store/actions';
import AppActions from 'types/actions';
import React from 'react';
import { TraceCustomVisualizations } from './TraceCustomVisualizations';
import { TraceFilter } from './TraceFilter';
import { TraceList } from './TraceList';
const TraceDetail = ({ globalTimeLoading }: Props): JSX.Element => {
useEffect(() => {
return (): void => {
globalTimeLoading();
};
}, [globalTimeLoading]);
const TraceDetail = (): JSX.Element => {
return (
<>
<TraceFilter />
@ -25,16 +14,4 @@ const TraceDetail = ({ globalTimeLoading }: Props): JSX.Element => {
);
};
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);
export default TraceDetail;

View File

@ -120,15 +120,6 @@ const _TraceFilter = (props: TraceFilterProps): JSX.Element => {
const handleApplyFilterForm = useCallback(
(values: any): void => {
// setTagKeyValueApplied((tagKeyValueApplied) => [
// ...tagKeyValueApplied,
// 'service eq' + values.service,
// 'operation eq ' + values.operation,
// 'maxduration eq ' +
// (parseInt(latencyFilterValues.max) / 1000000).toString(),
// 'minduration eq ' +
// (parseInt(latencyFilterValues.min) / 1000000).toString(),
// ]);
updateTraceFilters({
service: values.service,
operation: values.operation,
@ -272,10 +263,6 @@ const _TraceFilter = (props: TraceFilterProps): JSX.Element => {
'&tags=' +
encodeURIComponent(JSON.stringify(traceFilters.tags));
/*
Call the apis only when the route is loaded.
Check this issue: https://github.com/SigNoz/signoz/issues/110
*/
if (loading === false) {
fetchTraces(globalTime, request_string);
}
@ -305,19 +292,10 @@ const _TraceFilter = (props: TraceFilterProps): JSX.Element => {
'ms';
form_basefilter.setFieldsValue({ latency: latencyButtonText });
}, [traceFilters.latency, form_basefilter]);
useEffect(() => {
form_basefilter.setFieldsValue({ service: traceFilters.service });
}, [traceFilters.service, form_basefilter]);
useEffect(() => {
form_basefilter.setFieldsValue({ operation: traceFilters.operation });
}, [traceFilters.operation, form_basefilter]);
useEffect(() => {
form_basefilter.setFieldsValue({ kind: traceFilters.kind });
}, [traceFilters.kind, form_basefilter]);
}, [traceFilters, form_basefilter]);
const onLatencyButtonClick = (): void => {
setModalVisible(true);
@ -438,8 +416,6 @@ const _TraceFilter = (props: TraceFilterProps): JSX.Element => {
<FilterStateDisplay />
{/* // What will be the empty state of card when there is no Tag , it should show something */}
<InfoWrapper>Select Service to get Tag suggestions </InfoWrapper>
<Form

View File

@ -1,67 +1,62 @@
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 React, { useEffect, useRef } from 'react';
import { connect, useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { bindActionCreators, Dispatch } from 'redux';
import { bindActionCreators } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import {
GetInitialData,
GetInitialDataProps,
} from 'store/actions/metrics/getInitialData';
import { ResetInitialData } from 'store/actions/metrics/resetInitialData';
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>(
const MetricsApplication = ({
getInitialData,
resetInitialData,
}: MetricsProps): JSX.Element => {
const { selectedTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { error, errorMessage } = useSelector<AppState, MetricReducer>(
(state) => state.metrics,
);
const { error, errorMessage, metricsApplicationLoading } = useSelector<
AppState,
MetricReducer
>((state) => state.metrics);
const { servicename } = useParams<ServiceProps>();
const dispatch = useDispatch<Dispatch<AppActions>>();
useEffect(() => {
if (servicename !== undefined && loading == false) {
if (servicename !== undefined) {
getInitialData({
end: maxTime,
service: servicename,
start: minTime,
step: 60,
selectedTimeInterval: selectedTime,
serviceName: servicename,
});
}
return (): void => {
// setting the data to it's initial this will avoid the re-rendering the graph
dispatch({
type: 'GET_INTIAL_APPLICATION_DATA',
payload: {
serviceOverview: [],
topEndPoints: [],
},
});
return () => {
resetInitialData();
};
}, [servicename, maxTime, minTime, getInitialData, loading, dispatch]);
}, [servicename, getInitialData, selectedTime]);
if (metricsApplicationLoading) {
return <Spinner tip="Loading..." />;
}
if (error) {
return <Typography>{errorMessage}</Typography>;
}
if (loading) {
return <Spinner tip="Loading..." />;
}
return <MetricsApplicationContainer />;
};
interface DispatchProps {
getInitialData: (props: GetInitialDataProps) => void;
resetInitialData: () => void;
}
interface ServiceProps {
@ -72,6 +67,7 @@ const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
getInitialData: bindActionCreators(GetInitialData, dispatch),
resetInitialData: bindActionCreators(ResetInitialData, dispatch),
});
type MetricsProps = DispatchProps;

View File

@ -5,16 +5,17 @@ import React, { useEffect } from 'react';
import { connect, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { GetService, GetServiceProps } from 'store/actions';
import { GetService, GetServiceProps } from 'store/actions/metrics';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { GlobalReducer } from 'types/reducer/globalTime';
import MetricReducer from 'types/reducer/metrics';
const Metrics = ({ getService }: MetricsProps): JSX.Element => {
const { minTime, maxTime, loading } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { minTime, maxTime, loading, selectedTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const { services } = useSelector<AppState, MetricReducer>(
(state) => state.metrics,
);
@ -24,11 +25,10 @@ const Metrics = ({ getService }: MetricsProps): JSX.Element => {
useEffect(() => {
if (loading === false) {
getService({
start: minTime,
end: maxTime,
selectedTimeInterval: selectedTime,
});
}
}, [getService, maxTime, minTime, loading]);
}, [getService, loading, selectedTime]);
useEffect(() => {
let timeInterval: NodeJS.Timeout;
@ -36,8 +36,7 @@ const Metrics = ({ getService }: MetricsProps): JSX.Element => {
if (loading === false && !isSkipped && services.length === 0) {
timeInterval = setInterval(() => {
getService({
start: minTime,
end: maxTime,
selectedTimeInterval: selectedTime,
});
}, 50000);
}
@ -45,7 +44,7 @@ const Metrics = ({ getService }: MetricsProps): JSX.Element => {
return (): void => {
clearInterval(timeInterval);
};
}, [getService, isSkipped, loading, maxTime, minTime, services]);
}, [getService, isSkipped, loading, maxTime, minTime, services, selectedTime]);
if (loading) {
return <Spinner tip="Loading..." />;
@ -55,10 +54,9 @@ const Metrics = ({ getService }: MetricsProps): JSX.Element => {
};
interface DispatchProps {
getService: ({
end,
start,
}: GetServiceProps) => (dispatch: Dispatch<AppActions>) => void;
getService: (
props: GetServiceProps,
) => (dispatch: Dispatch<AppActions>, getState: () => AppState) => void;
}
const mapDispatchToProps = (

View File

@ -6,7 +6,7 @@ import React, { useState } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { GlobalTimeLoading, UserLoggedIn } from 'store/actions';
import { UserLoggedIn } from 'store/actions';
import AppActions from 'types/actions';
import {
@ -17,7 +17,7 @@ import {
Title,
} from './styles';
const Signup = ({ globalLoading, loggedIn }: SignupProps): JSX.Element => {
const Signup = ({ loggedIn }: SignupProps): JSX.Element => {
const [state, setState] = useState({ submitted: false });
const [formState, setFormState] = useState({
firstName: { value: '' },
@ -59,7 +59,6 @@ const Signup = ({ globalLoading, loggedIn }: SignupProps): JSX.Element => {
if (response.statusCode === 200) {
loggedIn();
globalLoading();
history.push(ROUTES.APPLICATION);
} else {
// @TODO throw a error notification here
@ -125,14 +124,12 @@ const Signup = ({ globalLoading, loggedIn }: SignupProps): JSX.Element => {
};
interface DispatchProps {
globalLoading: () => void;
loggedIn: () => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
globalLoading: bindActionCreators(GlobalTimeLoading, dispatch),
loggedIn: bindActionCreators(UserLoggedIn, dispatch),
});

View File

@ -0,0 +1,76 @@
import { Typography } from 'antd';
import Spinner from 'components/Spinner';
import TraceCustomVisualisation from 'container/TraceCustomVisualization';
import TraceFilter from 'container/TraceFilter';
import TraceList from 'container/TraceList';
import React, { useEffect } from 'react';
import { connect, useSelector } from 'react-redux';
import { bindActionCreators } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import {
GetInitialTraceData,
ResetRaceData,
GetInitialTraceDataProps,
} from 'store/actions/trace';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { GlobalReducer } from 'types/reducer/globalTime';
import { TraceReducer } from 'types/reducer/trace';
const TraceDetail = ({
getInitialTraceData,
resetTraceData,
}: TraceDetailProps): JSX.Element => {
const { loading, selectedTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { loading: TraceLoading, error, errorMessage } = useSelector<
AppState,
TraceReducer
>((state) => state.trace);
useEffect(() => {
if (!loading) {
getInitialTraceData({
selectedTime,
});
}
return (): void => {
resetTraceData();
};
}, [getInitialTraceData, loading, selectedTime]);
if (error) {
return <Typography>{errorMessage}</Typography>;
}
if (loading || TraceLoading) {
return <Spinner tip="Loading..." />;
}
return (
<>
<TraceFilter />
<TraceCustomVisualisation />
<TraceList />
</>
);
};
interface DispatchProps {
getInitialTraceData: (props: GetInitialTraceDataProps) => void;
resetTraceData: () => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
getInitialTraceData: bindActionCreators(GetInitialTraceData, dispatch),
resetTraceData: bindActionCreators(ResetRaceData, dispatch),
});
type TraceDetailProps = DispatchProps;
export default connect(null, mapDispatchToProps)(TraceDetail);

View File

@ -1,5 +1,5 @@
import { Time } from 'container/Header/DateTimeSelection/config';
import getMinAgo from 'lib/getStartAndEndTime/getMinAgo';
import GetMinMax from 'lib/getMinMax';
import { Dispatch } from 'redux';
import AppActions from 'types/actions';
@ -8,45 +8,14 @@ export const UpdateTimeInterval = (
dateTimeRange: [number, number] = [0, 0],
): ((dispatch: Dispatch<AppActions>) => void) => {
return (dispatch: Dispatch<AppActions>): void => {
let maxTime = new Date().getTime();
let minTime = 0;
if (interval === '1min') {
const minTimeAgo = getMinAgo({ minutes: 1 }).getTime();
minTime = minTimeAgo;
} else if (interval === '15min') {
const minTimeAgo = getMinAgo({ minutes: 15 }).getTime();
minTime = minTimeAgo;
} else if (interval === '1hr') {
const minTimeAgo = getMinAgo({ minutes: 60 }).getTime();
minTime = minTimeAgo;
} else if (interval === '30min') {
const minTimeAgo = getMinAgo({ minutes: 30 }).getTime();
minTime = minTimeAgo;
} else if (interval === '5min') {
const minTimeAgo = getMinAgo({ minutes: 5 }).getTime();
minTime = minTimeAgo;
} else if (interval === '1day') {
// one day = 24*60(min)
const minTimeAgo = getMinAgo({ minutes: 26 * 60 }).getTime();
minTime = minTimeAgo;
} else if (interval === '1week') {
// one week = one day * 7
const minTimeAgo = getMinAgo({ minutes: 26 * 60 * 7 }).getTime();
minTime = minTimeAgo;
} else if (interval === '6hr') {
const minTimeAgo = getMinAgo({ minutes: 6 * 60 }).getTime();
minTime = minTimeAgo;
} else if (interval === 'custom') {
maxTime = dateTimeRange[1];
minTime = dateTimeRange[0];
}
const { maxTime, minTime } = GetMinMax(interval, dateTimeRange);
dispatch({
type: 'UPDATE_TIME_INTERVAL',
payload: {
maxTime: maxTime * 1000000, // in nano sec,
minTime: minTime * 1000000,
maxTime: maxTime,
minTime: minTime,
selectedTime: interval,
},
});
};

View File

@ -5,19 +5,38 @@
import getServiceOverview from 'api/metrics/getServiceOverview';
import getTopEndPoints from 'api/metrics/getTopEndPoints';
import { AxiosError } from 'axios';
import GetMinMax from 'lib/getMinMax';
import { Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { Props } from 'types/api/metrics/getDBOverview';
import { GlobalReducer } from 'types/reducer/globalTime';
export const GetInitialData = (
props: GetInitialDataProps,
): ((dispatch: Dispatch<AppActions>) => void) => {
return async (dispatch: Dispatch<AppActions>): Promise<void> => {
): ((dispatch: Dispatch<AppActions>, getState: () => AppState) => void) => {
return async (dispatch, getState): Promise<void> => {
try {
const { globalTime } = getState();
/**
* @description This is because we keeping the store as source of truth
*/
if (props.selectedTimeInterval !== globalTime.selectedTime) {
return;
}
dispatch({
type: 'GET_INITIAL_APPLICATION_LOADING',
});
const { maxTime, minTime } = GetMinMax(props.selectedTimeInterval, [
globalTime.minTime / 1000000,
globalTime.maxTime / 1000000,
]);
const step = 60;
const [
// getDBOverViewResponse,
// getExternalAverageDurationResponse,
@ -39,10 +58,15 @@ export const GetInitialData = (
// ...props,
// }),
getServiceOverview({
...props,
end: maxTime,
service: props.serviceName,
start: minTime,
step,
}),
getTopEndPoints({
...props,
end: maxTime,
service: props.serviceName,
start: minTime,
}),
]);
@ -91,4 +115,7 @@ export const GetInitialData = (
};
};
export type GetInitialDataProps = Props;
export interface GetInitialDataProps {
serviceName: Props['service'];
selectedTimeInterval: GlobalReducer['selectedTime'];
}

View File

@ -1,20 +1,35 @@
import getService from 'api/metrics/getService';
import { AxiosError } from 'axios';
import GetMinMax from 'lib/getMinMax';
import { Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { Props } from 'types/api/metrics/getService';
import { GlobalReducer } from 'types/reducer/globalTime';
export const GetService = ({
end,
start,
}: GetServiceProps): ((dispatch: Dispatch<AppActions>) => void) => {
return async (dispatch: Dispatch<AppActions>): Promise<void> => {
export const GetService = (
props: GetServiceProps,
): ((dispatch: Dispatch<AppActions>, getState: () => AppState) => void) => {
return async (dispatch, getState): Promise<void> => {
try {
const { globalTime } = getState();
if (props.selectedTimeInterval !== globalTime.selectedTime) {
return;
}
const { maxTime, minTime } = GetMinMax(props.selectedTimeInterval, [
globalTime.minTime / 1000000,
globalTime.maxTime / 1000000,
]);
dispatch({
type: 'GET_SERVICE_LIST_LOADING_START',
});
const response = await getService({ end, start });
const response = await getService({
end: maxTime,
start: minTime,
});
if (response.statusCode === 200) {
dispatch({
@ -40,4 +55,6 @@ export const GetService = ({
};
};
export type GetServiceProps = Props;
export type GetServiceProps = {
selectedTimeInterval: GlobalReducer['selectedTime'];
};

View File

@ -0,0 +1,14 @@
import { Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
export const ResetInitialData = (): ((
dispatch: Dispatch<AppActions>,
getState: () => AppState,
) => void) => {
return (dispatch, getState): void => {
dispatch({
type: 'RESET_INITIAL_APPLICATION_DATA',
});
};
};

View File

@ -0,0 +1,201 @@
import getServiceList from 'api/trace/getServiceList';
import getServiceOperation from 'api/trace/getServiceOperation';
import getSpan from 'api/trace/getSpan';
import getSpansAggregate from 'api/trace/getSpanAggregate';
import getTags from 'api/trace/getTags';
import { AxiosError } from 'axios';
import { METRICS_PAGE_QUERY_PARAM } from 'constants/query';
import history from 'lib/history';
import { Dispatch } from 'redux';
import store from 'store';
import AppActions from 'types/actions';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps as ServiceOperationPayloadProps } from 'types/api/trace/getServiceOperation';
import { PayloadProps as TagPayloadProps } from 'types/api/trace/getTags';
import { GlobalReducer } from 'types/reducer/globalTime';
import { TraceReducer } from 'types/reducer/trace';
export const GetInitialTraceData = ({
selectedTime,
}: GetInitialTraceDataProps): ((dispatch: Dispatch<AppActions>) => void) => {
return async (dispatch: Dispatch<AppActions>): Promise<void> => {
try {
const { globalTime, trace } = store.getState();
const { minTime, maxTime, selectedTime: globalSelectedTime } = globalTime;
const { selectedAggOption, selectedEntity } = trace;
// keeping the redux as source of truth
if (selectedTime !== globalSelectedTime) {
return;
}
dispatch({
type: 'UPDATE_SPANS_LOADING',
payload: {
loading: true,
},
});
const urlParams = new URLSearchParams(history.location.search.split('?')[1]);
const operationName = urlParams.get(METRICS_PAGE_QUERY_PARAM.operation);
const serviceName = urlParams.get(METRICS_PAGE_QUERY_PARAM.service);
const errorTag = urlParams.get(METRICS_PAGE_QUERY_PARAM.error);
const kindTag = urlParams.get(METRICS_PAGE_QUERY_PARAM.kind);
const latencyMin = urlParams.get(METRICS_PAGE_QUERY_PARAM.latencyMin);
const latencyMax = urlParams.get(METRICS_PAGE_QUERY_PARAM.latencyMax);
const selectedTags = urlParams.get(METRICS_PAGE_QUERY_PARAM.selectedTags);
const aggregationOption = urlParams.get(
METRICS_PAGE_QUERY_PARAM.aggregationOption,
);
const selectedEntityOption = urlParams.get(METRICS_PAGE_QUERY_PARAM.entity);
const isCustomSelected = selectedTime === 'custom';
const end = isCustomSelected
? globalTime.maxTime + 15 * 60 * 1000000000
: maxTime;
const start = isCustomSelected
? globalTime.minTime - 15 * 60 * 1000000000
: minTime;
const [
serviceListResponse,
spanResponse,
spanAggregateResponse,
] = await Promise.all([
getServiceList(),
getSpan({
start,
end,
kind: kindTag || '',
limit: '100',
lookback: '2d',
maxDuration: latencyMax || '',
minDuration: latencyMin || '',
operation: operationName || '',
service: serviceName || '',
tags: selectedTags || '[]',
}),
getSpansAggregate({
aggregation_option: aggregationOption || selectedAggOption,
dimension: selectedEntityOption || selectedEntity,
end,
start,
kind: kindTag || '',
maxDuration: latencyMax || '',
minDuration: latencyMin || '',
operation: operationName || '',
service: serviceName || '',
step: '60',
tags: selectedTags || '[]',
}),
]);
let tagResponse:
| SuccessResponse<TagPayloadProps>
| ErrorResponse
| undefined;
let serviceOperationResponse:
| SuccessResponse<ServiceOperationPayloadProps>
| ErrorResponse
| undefined;
if (serviceName !== null && serviceName.length !== 0) {
[tagResponse, serviceOperationResponse] = await Promise.all([
getTags({
service: serviceName,
}),
getServiceOperation({
service: serviceName,
}),
]);
}
const getSelectedTags = (): TraceReducer['selectedTags'] => {
const selectedTag = JSON.parse(selectedTags || '[]');
if (typeof selectedTag !== 'object' && Array.isArray(selectedTag)) {
return [];
}
if (errorTag) {
return [
...selectedTag,
{
key: METRICS_PAGE_QUERY_PARAM.error,
operator: 'equals',
value: errorTag,
},
];
}
return [...selectedTag];
};
const getCondition = (): boolean => {
const basicCondition =
serviceListResponse.statusCode === 200 &&
spanResponse.statusCode === 200 &&
(spanAggregateResponse.statusCode === 200 ||
spanAggregateResponse.statusCode === 400);
if (serviceName === null || serviceName.length === 0) {
return basicCondition;
}
return (
basicCondition &&
tagResponse?.statusCode === 200 &&
serviceOperationResponse?.statusCode === 200
);
};
const condition = getCondition();
if (condition) {
dispatch({
type: 'GET_TRACE_INITIAL_DATA_SUCCESS',
payload: {
serviceList: serviceListResponse.payload || [],
operationList: serviceOperationResponse?.payload || [],
tagsSuggestions: tagResponse?.payload || [],
spansList: spanResponse?.payload || [],
selectedService: serviceName || '',
selectedOperation: operationName || '',
selectedTags: getSelectedTags(),
selectedKind: kindTag || '',
selectedLatency: {
max: latencyMax || '',
min: latencyMin || '',
},
spansAggregate: spanAggregateResponse.payload || [],
},
});
dispatch({
type: 'GET_TRACE_LOADING_END',
});
} else {
dispatch({
type: 'GET_TRACE_INITIAL_DATA_ERROR',
payload: {
errorMessage: serviceListResponse?.error || 'Something went wrong',
},
});
}
} catch (error) {
dispatch({
type: 'GET_TRACE_INITIAL_DATA_ERROR',
payload: {
errorMessage: (error as AxiosError).toString() || 'Something went wrong',
},
});
}
};
};
export interface GetInitialTraceDataProps {
selectedTime: GlobalReducer['selectedTime'];
}

View File

@ -0,0 +1,92 @@
import getSpansAggregate from 'api/trace/getSpanAggregate';
import { AxiosError } from 'axios';
import { Dispatch } from 'redux';
import store from 'store';
import AppActions from 'types/actions';
import { TraceReducer } from 'types/reducer/trace';
export const GetTraceVisualAggregates = ({
selectedEntity,
selectedAggOption,
}: GetTraceVisualAggregatesProps): ((
dispatch: Dispatch<AppActions>,
) => void) => {
return async (dispatch: Dispatch<AppActions>): Promise<void> => {
try {
dispatch({
type: 'UPDATE_SPANS_LOADING',
payload: {
loading: true,
},
});
const { trace, globalTime } = store.getState();
const {
selectedKind,
selectedLatency,
selectedOperation,
selectedService,
selectedTags,
} = trace;
const { selectedTime, maxTime, minTime } = globalTime;
const isCustomSelected = selectedTime === 'custom';
const end = isCustomSelected
? globalTime.maxTime + 15 * 60 * 1000000000
: maxTime;
const start = isCustomSelected
? globalTime.minTime - 15 * 60 * 1000000000
: minTime;
const [spanAggregateResponse] = await Promise.all([
getSpansAggregate({
aggregation_option: selectedAggOption,
dimension: selectedEntity,
end,
start,
kind: selectedKind || '',
maxDuration: selectedLatency.max || '',
minDuration: selectedLatency.min || '',
operation: selectedOperation || '',
service: selectedService || '',
step: '60',
tags: JSON.stringify(selectedTags) || '[]',
}),
]);
if (spanAggregateResponse.statusCode === 200) {
dispatch({
type: 'UPDATE_AGGREGATES',
payload: {
spansAggregate: spanAggregateResponse.payload,
selectedAggOption,
selectedEntity,
},
});
}
dispatch({
type: 'UPDATE_SPANS_LOADING',
payload: {
loading: false,
},
});
} catch (error) {
dispatch({
type: 'GET_TRACE_INITIAL_DATA_ERROR',
payload: {
errorMessage: (error as AxiosError).toString() || 'Something went wrong',
},
});
}
};
};
export interface GetTraceVisualAggregatesProps {
selectedAggOption: TraceReducer['selectedAggOption'];
selectedEntity: TraceReducer['selectedEntity'];
}

View File

@ -0,0 +1,9 @@
export * from './getInitialData';
export * from './updateSelectedAggOption';
export * from './updateSelectedEntity';
export * from './updateSelectedKind';
export * from './updateSelectedLatency';
export * from './updateSelectedOperation';
export * from './updateSelectedService';
export * from './updateSelectedTags';
export * from './resetTraceDetails';

View File

@ -0,0 +1,12 @@
import { Dispatch } from 'redux';
import AppActions from 'types/actions';
export const LoadingCompleted = (): ((
dispatch: Dispatch<AppActions>,
) => void) => {
return (dispatch: Dispatch<AppActions>): void => {
dispatch({
type: 'GET_TRACE_LOADING_END',
});
};
};

View File

@ -0,0 +1,10 @@
import { Dispatch } from 'redux';
import AppActions from 'types/actions';
export const ResetRaceData = (): ((dispatch: Dispatch<AppActions>) => void) => {
return (dispatch: Dispatch<AppActions>): void => {
dispatch({
type: 'RESET_TRACE_DATA',
});
};
};

View File

@ -0,0 +1,16 @@
import { Dispatch } from 'redux';
import AppActions from 'types/actions';
import { TraceReducer } from 'types/reducer/trace';
export const UpdateSelectedAggOption = (
selectedAggOption: TraceReducer['selectedAggOption'],
): ((dispatch: Dispatch<AppActions>) => void) => {
return (dispatch: Dispatch<AppActions>): void => {
dispatch({
type: 'UPDATE_SELECTED_AGG_OPTION',
payload: {
selectedAggOption,
},
});
};
};

View File

@ -0,0 +1,164 @@
import getServiceOperation from 'api/trace/getServiceOperation';
import getSpan from 'api/trace/getSpan';
import getSpansAggregate from 'api/trace/getSpanAggregate';
import getTags from 'api/trace/getTags';
import { AxiosError } from 'axios';
import { Dispatch } from 'redux';
import store from 'store';
import AppActions from 'types/actions';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps as ServiceOperationPayloadProps } from 'types/api/trace/getServiceOperation';
import { PayloadProps as TagPayloadProps } from 'types/api/trace/getTags';
import { TraceReducer } from 'types/reducer/trace';
export const UpdateSelectedData = ({
selectedKind,
selectedService,
selectedLatency,
selectedOperation,
selectedAggOption,
selectedEntity,
}: UpdateSelectedDataProps): ((dispatch: Dispatch<AppActions>) => void) => {
return async (dispatch: Dispatch<AppActions>): Promise<void> => {
try {
dispatch({
type: 'UPDATE_SPANS_LOADING',
payload: {
loading: true,
},
});
const { trace, globalTime } = store.getState();
const { minTime, maxTime, selectedTime } = globalTime;
const { selectedTags } = trace;
const isCustomSelected = selectedTime === 'custom';
const end = isCustomSelected
? globalTime.maxTime + 15 * 60 * 1000000000
: maxTime;
const start = isCustomSelected
? globalTime.minTime - 15 * 60 * 1000000000
: minTime;
const [spanResponse, getSpanAggregateResponse] = await Promise.all([
getSpan({
start,
end,
kind: selectedKind || '',
limit: '100',
lookback: '2d',
maxDuration: selectedLatency.max || '',
minDuration: selectedLatency.min || '',
operation: selectedOperation || '',
service: selectedService || '',
tags: JSON.stringify(selectedTags),
}),
getSpansAggregate({
aggregation_option: selectedAggOption || '',
dimension: selectedEntity || '',
end,
kind: selectedKind || '',
maxDuration: selectedLatency.max || '',
minDuration: selectedLatency.min || '',
operation: selectedOperation || '',
service: selectedService || '',
start,
step: '60',
tags: JSON.stringify(selectedTags),
}),
]);
let tagResponse:
| SuccessResponse<TagPayloadProps>
| ErrorResponse
| undefined;
let serviceOperationResponse:
| SuccessResponse<ServiceOperationPayloadProps>
| ErrorResponse
| undefined;
if (selectedService !== null && selectedService.length !== 0) {
[tagResponse, serviceOperationResponse] = await Promise.all([
getTags({
service: selectedService,
}),
getServiceOperation({
service: selectedService,
}),
]);
}
const spanAggregateCondition =
getSpanAggregateResponse.statusCode === 200 ||
getSpanAggregateResponse.statusCode === 400;
const getCondition = (): boolean => {
const basicCondition =
spanResponse.statusCode === 200 && spanAggregateCondition;
if (selectedService === null || selectedService.length === 0) {
return basicCondition;
}
return (
basicCondition &&
tagResponse?.statusCode === 200 &&
serviceOperationResponse?.statusCode === 200
);
};
const condition = getCondition();
if (condition) {
dispatch({
type: 'UPDATE_SELECTED_TRACE_DATA',
payload: {
operationList: serviceOperationResponse?.payload || [],
spansList: spanResponse.payload || [],
tagsSuggestions: tagResponse?.payload || [],
selectedKind,
selectedService,
selectedLatency,
selectedOperation,
spansAggregate: spanAggregateCondition
? getSpanAggregateResponse.payload || []
: [],
},
});
} else {
dispatch({
type: 'GET_TRACE_INITIAL_DATA_ERROR',
payload: {
errorMessage: 'Something went wrong',
},
});
}
dispatch({
type: 'UPDATE_SPANS_LOADING',
payload: {
loading: false,
},
});
} catch (error) {
dispatch({
type: 'GET_TRACE_INITIAL_DATA_ERROR',
payload: {
errorMessage: (error as AxiosError).toString() || 'Something went wrong',
},
});
}
};
};
export interface UpdateSelectedDataProps {
selectedKind: TraceReducer['selectedKind'];
selectedService: TraceReducer['selectedService'];
selectedLatency: TraceReducer['selectedLatency'];
selectedOperation: TraceReducer['selectedOperation'];
selectedEntity: TraceReducer['selectedEntity'];
selectedAggOption: TraceReducer['selectedAggOption'];
}

View File

@ -0,0 +1,16 @@
import { Dispatch } from 'redux';
import AppActions from 'types/actions';
import { TraceReducer } from 'types/reducer/trace';
export const UpdateSelectedEntity = (
selectedEntity: TraceReducer['selectedEntity'],
): ((dispatch: Dispatch<AppActions>) => void) => {
return (dispatch: Dispatch<AppActions>): void => {
dispatch({
type: 'UPDATE_SELECTED_ENTITY',
payload: {
selectedEntity,
},
});
};
};

View File

@ -0,0 +1,16 @@
import { Dispatch } from 'redux';
import AppActions from 'types/actions';
import { TraceReducer } from 'types/reducer/trace';
export const UpdateSelectedKind = (
selectedKind: TraceReducer['selectedKind'],
): ((dispatch: Dispatch<AppActions>) => void) => {
return (dispatch: Dispatch<AppActions>): void => {
dispatch({
type: 'UPDATE_TRACE_SELECTED_KIND',
payload: {
selectedKind,
},
});
};
};

View File

@ -0,0 +1,16 @@
import { Dispatch } from 'redux';
import AppActions from 'types/actions';
import { TraceReducer } from 'types/reducer/trace';
export const UpdateSelectedLatency = (
selectedLatency: TraceReducer['selectedLatency'],
): ((dispatch: Dispatch<AppActions>) => void) => {
return (dispatch: Dispatch<AppActions>): void => {
dispatch({
type: 'UPDATE_TRACE_SELECTED_LATENCY_VALUE',
payload: {
selectedLatency,
},
});
};
};

View File

@ -0,0 +1,16 @@
import { Dispatch } from 'redux';
import AppActions from 'types/actions';
import { TraceReducer } from 'types/reducer/trace';
export const UpdateSelectedOperation = (
selectedOperation: TraceReducer['selectedOperation'],
): ((dispatch: Dispatch<AppActions>) => void) => {
return (dispatch: Dispatch<AppActions>): void => {
dispatch({
type: 'UPDATE_TRACE_SELECTED_OPERATION',
payload: {
selectedOperation,
},
});
};
};

View File

@ -0,0 +1,16 @@
import { Dispatch } from 'redux';
import AppActions from 'types/actions';
import { TraceReducer } from 'types/reducer/trace';
export const UpdateSelectedService = (
selectedService: TraceReducer['selectedService'],
): ((dispatch: Dispatch<AppActions>) => void) => {
return (dispatch: Dispatch<AppActions>): void => {
dispatch({
type: 'UPDATE_TRACE_SELECTED_SERVICE',
payload: {
selectedService,
},
});
};
};

View File

@ -0,0 +1,94 @@
import getSpan from 'api/trace/getSpan';
import getSpansAggregate from 'api/trace/getSpanAggregate';
import { AxiosError } from 'axios';
import { Dispatch } from 'redux';
import store from 'store';
import AppActions from 'types/actions';
import { TraceReducer } from 'types/reducer/trace';
export const UpdateSelectedTags = (
selectedTags: TraceReducer['selectedTags'],
): ((dispatch: Dispatch<AppActions>) => void) => {
return async (dispatch: Dispatch<AppActions>): Promise<void> => {
try {
dispatch({
type: 'UPDATE_SPANS_LOADING',
payload: {
loading: true,
},
});
const { trace, globalTime } = store.getState();
const {
selectedKind,
selectedLatency,
selectedOperation,
selectedService,
selectedAggOption,
selectedEntity,
spansAggregate,
} = trace;
const { maxTime, minTime } = globalTime;
const [spanResponse, spansAggregateResponse] = await Promise.all([
getSpan({
start: minTime,
end: maxTime,
kind: selectedKind || '',
limit: '100',
lookback: '2d',
maxDuration: selectedLatency.max || '',
minDuration: selectedLatency.min || '',
operation: selectedOperation || '',
service: selectedService || '',
tags: JSON.stringify(selectedTags),
}),
getSpansAggregate({
aggregation_option: selectedAggOption,
dimension: selectedEntity,
end: maxTime,
kind: selectedKind || '',
maxDuration: selectedLatency.max || '',
minDuration: selectedLatency.min || '',
operation: selectedOperation || '',
service: selectedService || '',
start: minTime,
step: '60',
tags: JSON.stringify(selectedTags),
}),
]);
const condition =
spansAggregateResponse.statusCode === 200 ||
spansAggregateResponse.statusCode === 400;
if (spanResponse.statusCode === 200 && condition) {
dispatch({
type: 'UPDATE_TRACE_SELECTED_TAGS',
payload: {
selectedTags,
spansList: spanResponse.payload,
spansAggregate:
spansAggregateResponse.statusCode === 400
? spansAggregate
: spansAggregateResponse.payload || [],
},
});
}
dispatch({
type: 'UPDATE_SPANS_LOADING',
payload: {
loading: false,
},
});
} catch (error) {
dispatch({
type: 'GET_TRACE_INITIAL_DATA_ERROR',
payload: {
errorMessage: (error as AxiosError).toString() || 'Something went wrong',
},
});
}
};
};

View File

@ -0,0 +1,16 @@
import { Dispatch } from 'redux';
import AppActions from 'types/actions';
import { TraceReducer } from 'types/reducer/trace';
export const UpdateSpanLoading = (
spansLoading: TraceReducer['spansLoading'],
): ((dispatch: Dispatch<AppActions>) => void) => {
return (dispatch: Dispatch<AppActions>): void => {
dispatch({
type: 'UPDATE_SPANS_LOADING',
payload: {
loading: spansLoading,
},
});
};
};

View File

@ -1,3 +1,4 @@
import { getDefaultOption } from 'container/Header/DateTimeSelection/config';
import {
GLOBAL_TIME_LOADING_START,
GlobalTimeAction,
@ -9,6 +10,7 @@ const intitalState: GlobalReducer = {
maxTime: Date.now() * 1000000,
minTime: (Date.now() - 15 * 60 * 1000) * 1000000,
loading: true,
selectedTime: getDefaultOption(location.pathname),
};
const globalTimeReducer = (

View File

@ -6,6 +6,7 @@ import globalTimeReducer from './global';
import metricsReducers from './metric';
import { metricsReducer } from './metrics';
import { ServiceMapReducer } from './serviceMap';
import { traceReducer } from './trace';
import TraceFilterReducer from './traceFilters';
import { traceItemReducer, tracesReducer } from './traces';
import { usageDataReducer } from './usage';
@ -14,6 +15,7 @@ const reducers = combineReducers({
traceFilters: TraceFilterReducer,
traces: tracesReducer,
traceItem: traceItemReducer,
trace: traceReducer,
usageDate: usageDataReducer,
globalTime: globalTimeReducer,
metricsData: metricsReducer,

View File

@ -5,6 +5,7 @@ import {
GET_SERVICE_LIST_ERROR,
GET_SERVICE_LIST_LOADING_START,
GET_SERVICE_LIST_SUCCESS,
RESET_INITIAL_APPLICATION_DATA,
MetricsActions,
} from 'types/actions/metrics';
import InitialValueTypes from 'types/reducer/metrics';
@ -12,7 +13,8 @@ import InitialValueTypes from 'types/reducer/metrics';
const InitialValue: InitialValueTypes = {
error: false,
errorMessage: '',
loading: false,
loading: true,
metricsApplicationLoading: true,
services: [],
dbOverView: [],
externalService: [],
@ -56,18 +58,24 @@ const metrics = (
case GET_INITIAL_APPLICATION_LOADING: {
return {
...state,
loading: true,
metricsApplicationLoading: true,
};
}
case GET_INITIAL_APPLICATION_ERROR: {
return {
...state,
loading: false,
metricsApplicationLoading: false,
errorMessage: action.payload.errorMessage,
error: true,
};
}
case RESET_INITIAL_APPLICATION_DATA: {
return {
...InitialValue,
};
}
case GET_INTIAL_APPLICATION_DATA: {
const {
// dbOverView,
@ -80,13 +88,13 @@ const metrics = (
return {
...state,
loading: false,
// dbOverView,
topEndPoints,
serviceOverview,
// externalService,
// externalAverageDuration,
// externalError,
metricsApplicationLoading: false,
};
}
default:

View File

@ -0,0 +1,204 @@
import {
GET_TRACE_INITIAL_DATA_ERROR,
GET_TRACE_INITIAL_DATA_SUCCESS,
GET_TRACE_LOADING_END,
GET_TRACE_LOADING_START,
TraceActions,
UPDATE_SELECTED_AGG_OPTION,
UPDATE_SELECTED_ENTITY,
UPDATE_SELECTED_TRACE_DATA,
UPDATE_SPANS_LOADING,
UPDATE_TRACE_SELECTED_KIND,
UPDATE_TRACE_SELECTED_LATENCY_VALUE,
UPDATE_TRACE_SELECTED_OPERATION,
UPDATE_TRACE_SELECTED_SERVICE,
UPDATE_TRACE_SELECTED_TAGS,
RESET_TRACE_DATA,
UPDATE_AGGREGATES,
} from 'types/actions/trace';
import { TraceReducer } from 'types/reducer/trace';
const intitalState: TraceReducer = {
error: false,
errorMessage: '',
loading: true,
operationsList: [],
selectedKind: '',
selectedLatency: {
max: '',
min: '',
},
selectedOperation: '',
selectedService: '',
selectedTags: [],
serviceList: [],
spanList: [],
tagsSuggestions: [],
selectedAggOption: 'count',
selectedEntity: 'calls',
spansAggregate: [],
spansLoading: false,
};
export const traceReducer = (
state = intitalState,
action: TraceActions,
): TraceReducer => {
switch (action.type) {
case GET_TRACE_INITIAL_DATA_ERROR: {
return {
...state,
errorMessage: action.payload.errorMessage,
loading: false,
error: true,
};
}
case GET_TRACE_LOADING_START: {
return {
...state,
loading: true,
spansLoading: true,
};
}
case GET_TRACE_INITIAL_DATA_SUCCESS: {
const {
serviceList,
operationList,
tagsSuggestions,
selectedOperation,
selectedService,
selectedTags,
spansList,
selectedKind,
selectedLatency,
spansAggregate,
} = action.payload;
return {
...state,
serviceList: serviceList,
tagsSuggestions,
selectedOperation,
selectedService,
selectedTags,
spanList: spansList,
operationsList: operationList,
error: false,
selectedKind,
selectedLatency,
spansAggregate,
spansLoading: false,
};
}
case UPDATE_TRACE_SELECTED_KIND: {
return {
...state,
selectedKind: action.payload.selectedKind,
};
}
case UPDATE_TRACE_SELECTED_LATENCY_VALUE: {
return {
...state,
selectedLatency: action.payload.selectedLatency,
};
}
case UPDATE_TRACE_SELECTED_OPERATION: {
return {
...state,
selectedOperation: action.payload.selectedOperation,
};
}
case UPDATE_TRACE_SELECTED_SERVICE: {
return {
...state,
selectedService: action.payload.selectedService,
};
}
case UPDATE_TRACE_SELECTED_TAGS: {
return {
...state,
selectedTags: action.payload.selectedTags,
spanList: action.payload.spansList,
spansAggregate: action.payload.spansAggregate,
};
}
case UPDATE_SELECTED_TRACE_DATA: {
const {
spansList,
tagsSuggestions,
operationList,
selectedOperation,
selectedLatency,
selectedService,
selectedKind,
spansAggregate,
} = action.payload;
return {
...state,
spanList: spansList,
tagsSuggestions,
operationsList: operationList,
selectedOperation,
selectedLatency,
selectedService,
selectedKind,
spansAggregate,
};
}
case GET_TRACE_LOADING_END: {
return {
...state,
loading: false,
};
}
case UPDATE_SELECTED_AGG_OPTION: {
return {
...state,
selectedAggOption: action.payload.selectedAggOption,
};
}
case UPDATE_SELECTED_ENTITY: {
return {
...state,
selectedEntity: action.payload.selectedEntity,
};
}
case UPDATE_SPANS_LOADING: {
return {
...state,
spansLoading: action.payload.loading,
};
}
case RESET_TRACE_DATA: {
return {
...intitalState,
};
}
case UPDATE_AGGREGATES: {
return {
...state,
spansAggregate: action.payload.spansAggregate,
selectedAggOption: action.payload.selectedAggOption,
selectedEntity: action.payload.selectedEntity,
};
}
default:
return state;
}
};

View File

@ -1,9 +1,11 @@
import { ActionTypes, TraceFilters } from 'store/actions';
import { TraceFilters } from 'store/actions/traceFilters';
import { ActionTypes } from 'store/actions/types';
type ACTION = {
type: ActionTypes;
payload: TraceFilters;
};
const initialState: TraceFilters = {
service: '',
tags: [],

View File

@ -1,3 +1,5 @@
import { Time } from 'container/Header/DateTimeSelection/config';
export const UPDATE_TIME_INTERVAL = 'UPDATE_TIME_INTERVAL';
export const GLOBAL_TIME_LOADING_START = 'GLOBAL_TIME_LOADING_START';
@ -6,9 +8,13 @@ export type GlobalTime = {
minTime: number;
};
interface UpdateTime extends GlobalTime {
selectedTime: Time;
}
interface UpdateTimeInterval {
type: typeof UPDATE_TIME_INTERVAL;
payload: GlobalTime;
payload: UpdateTime;
}
interface GlobalTimeLoading {

View File

@ -2,11 +2,13 @@ import { AppAction } from './app';
import { DashboardActions } from './dashboard';
import { GlobalTimeAction } from './globalTime';
import { MetricsActions } from './metrics';
import { TraceActions } from './trace';
type AppActions =
| DashboardActions
| AppAction
| GlobalTimeAction
| MetricsActions;
| MetricsActions
| TraceActions;
export default AppActions;

View File

@ -13,7 +13,7 @@ export const GET_INITIAL_APPLICATION_LOADING =
'GET_INITIAL_APPLICATION_LOADING';
export const GET_INITIAL_APPLICATION_ERROR = 'GET_INITIAL_APPLICATION_ERROR';
export const GET_INTIAL_APPLICATION_DATA = 'GET_INTIAL_APPLICATION_DATA';
export const RESET_INITIAL_APPLICATION_DATA = 'RESET_INITIAL_APPLICATION_DATA';
export interface GetServiceList {
type: typeof GET_SERVICE_LIST_SUCCESS;
payload: ServicesList[];
@ -44,8 +44,13 @@ export interface GetInitialApplicationData {
};
}
export interface ResetInitialApplicationData {
type: typeof RESET_INITIAL_APPLICATION_DATA;
}
export type MetricsActions =
| GetServiceListError
| GetServiceListLoading
| GetServiceList
| GetInitialApplicationData;
| GetInitialApplicationData
| ResetInitialApplicationData;

View File

@ -0,0 +1,151 @@
export const GET_TRACE_INITIAL_DATA_SUCCESS = 'GET_TRACE_INITIAL_DATA_SUCCESS';
export const GET_TRACE_INITIAL_DATA_ERROR = 'GET_TRACE_INITIAL_DATA_ERROR';
export const GET_TRACE_LOADING_START = 'GET_TRACE_LOADING_START';
export const GET_TRACE_LOADING_END = 'GET_TRACE_LOADING_END';
export const UPDATE_TRACE_SELECTED_SERVICE = 'UPDATE_TRACE_SELECTED_SERVICE';
export const UPDATE_TRACE_SELECTED_OPERATION =
'UPDATE_TRACE_SELECTED_OPERATION';
export const UPDATE_TRACE_SELECTED_LATENCY_VALUE =
'UPDATE_TRACE_SELECTED_LATENCY_VALUE';
export const UPDATE_TRACE_SELECTED_KIND = 'UPDATE_TRACE_SELECTED_KIND';
export const UPDATE_TRACE_SELECTED_TAGS = 'UPDATE_TRACE_SELECTED_TAGS';
export const UPDATE_SELECTED_AGG_OPTION = 'UPDATE_SELECTED_AGG_OPTION';
export const UPDATE_SELECTED_ENTITY = 'UPDATE_SELECTED_ENTITY';
export const UPDATE_SPANS_LOADING = 'UPDATE_SPANS_LOADING';
export const UPDATE_SELECTED_TRACE_DATA = 'UPDATE_SELECTED_TRACE_DATA';
export const UPDATE_AGGREGATES = 'UPDATE_AGGREGATES';
export const RESET_TRACE_DATA = 'RESET_TRACE_DATA';
import { TraceReducer } from 'types/reducer/trace';
interface GetTraceLoading {
type: typeof GET_TRACE_LOADING_START | typeof GET_TRACE_LOADING_END;
}
interface UpdateSpansLoading {
type: typeof UPDATE_SPANS_LOADING;
payload: {
loading: boolean;
};
}
interface GetTraceInitialData {
type: typeof GET_TRACE_INITIAL_DATA_SUCCESS;
payload: {
serviceList: TraceReducer['serviceList'];
selectedTags: TraceReducer['selectedTags'];
operationList: TraceReducer['operationsList'];
tagsSuggestions: TraceReducer['tagsSuggestions'];
spansList: TraceReducer['spanList'];
selectedService: TraceReducer['selectedService'];
selectedOperation: TraceReducer['selectedOperation'];
selectedLatency: TraceReducer['selectedLatency'];
selectedKind: TraceReducer['selectedKind'];
spansAggregate: TraceReducer['spansAggregate'];
};
}
interface UpdateSelectedDate {
type: typeof UPDATE_SELECTED_TRACE_DATA;
payload: {
operationList: TraceReducer['operationsList'];
tagsSuggestions: TraceReducer['tagsSuggestions'];
spansList: TraceReducer['spanList'];
selectedKind: TraceReducer['selectedKind'];
selectedService: TraceReducer['selectedService'];
selectedLatency: TraceReducer['selectedLatency'];
selectedOperation: TraceReducer['selectedOperation'];
spansAggregate: TraceReducer['spansAggregate'];
};
}
export interface GetTraceInitialDataError {
type: typeof GET_TRACE_INITIAL_DATA_ERROR;
payload: {
errorMessage: string;
};
}
interface UpdateTraceSelectedService {
type: typeof UPDATE_TRACE_SELECTED_SERVICE;
payload: {
selectedService: TraceReducer['selectedService'];
};
}
interface UpdateTraceSelectedOperation {
type: typeof UPDATE_TRACE_SELECTED_OPERATION;
payload: {
selectedOperation: TraceReducer['selectedOperation'];
};
}
interface UpdateTraceSelectedKind {
type: typeof UPDATE_TRACE_SELECTED_KIND;
payload: {
selectedKind: TraceReducer['selectedKind'];
};
}
interface UpdateTraceSelectedLatencyValue {
type: typeof UPDATE_TRACE_SELECTED_LATENCY_VALUE;
payload: {
selectedLatency: TraceReducer['selectedLatency'];
};
}
interface UpdateTraceSelectedTags {
type: typeof UPDATE_TRACE_SELECTED_TAGS;
payload: {
selectedTags: TraceReducer['selectedTags'];
spansList: TraceReducer['spanList'];
spansAggregate: TraceReducer['spansAggregate'];
};
}
interface UpdateSelectedAggOption {
type: typeof UPDATE_SELECTED_AGG_OPTION;
payload: {
selectedAggOption: TraceReducer['selectedAggOption'];
};
}
interface UpdateSelectedEntity {
type: typeof UPDATE_SELECTED_ENTITY;
payload: {
selectedEntity: TraceReducer['selectedEntity'];
};
}
interface UpdateAggregates {
type: typeof UPDATE_AGGREGATES;
payload: {
spansAggregate: TraceReducer['spansAggregate'];
selectedEntity: TraceReducer['selectedEntity'];
selectedAggOption: TraceReducer['selectedAggOption'];
};
}
interface ResetTraceData {
type: typeof RESET_TRACE_DATA;
}
export type TraceActions =
| GetTraceLoading
| GetTraceInitialData
| GetTraceInitialDataError
| UpdateTraceSelectedService
| UpdateTraceSelectedLatencyValue
| UpdateTraceSelectedKind
| UpdateTraceSelectedOperation
| UpdateTraceSelectedTags
| UpdateSelectedDate
| UpdateSelectedAggOption
| UpdateSelectedEntity
| UpdateSpansLoading
| ResetTraceData
| UpdateAggregates;

View File

@ -0,0 +1 @@
export type PayloadProps = string[];

View File

@ -0,0 +1,5 @@
export type PayloadProps = string[];
export interface Props {
service: string;
}

View File

@ -0,0 +1,20 @@
export interface Props {
start: number;
end: number;
service: string;
operation: string;
maxDuration: string;
minDuration: string;
kind: string;
tags: string;
dimension: string;
aggregation_option: string;
step: string;
}
interface Timestamp {
timestamp: number;
value: number;
}
export type PayloadProps = Timestamp[];

View File

@ -0,0 +1,51 @@
import { GlobalTime } from 'types/actions/globalTime';
export interface TraceTagItem {
key: string;
value: string;
}
export interface pushDStree {
id: string;
name: string;
value: number;
time: number;
startTime: number;
tags: TraceTagItem[];
children: pushDStree[];
}
export type span = [
number,
string,
string,
string,
string,
string,
string,
string | string[],
string | string[],
string | string[],
pushDStree[],
];
export interface SpanList {
events: span[];
segmentID: string;
columns: string[];
}
export type PayloadProps = SpanList[];
export interface Props {
start: GlobalTime['minTime'];
end: GlobalTime['maxTime'];
lookback: string;
service: string;
operation: string;
maxDuration: string;
minDuration: string;
kind: string;
limit: string;
tags: string;
}

View File

@ -0,0 +1,10 @@
import { Props as Prop } from './getServiceOperation';
interface TagKeys {
tagCount: number;
tagKeys: string;
}
export type PayloadProps = TagKeys[];
export type Props = Prop;

View File

@ -4,6 +4,8 @@ export type Success = 200;
export type Forbidden = 403;
export type BadRequest = 400;
export type Unauthorized = 401;
export type NotFound = 404;
@ -17,6 +19,7 @@ export type ErrorStatusCode =
| Forbidden
| Unauthorized
| NotFound
| ServerError;
| ServerError
| BadRequest;
export type StatusCode = SuccessStatusCode | ErrorStatusCode;

View File

@ -1,7 +1,9 @@
import { Time } from 'container/Header/DateTimeSelection/config';
import { GlobalTime } from 'types/actions/globalTime';
export interface GlobalReducer {
maxTime: GlobalTime['maxTime'];
minTime: GlobalTime['minTime'];
loading: boolean;
selectedTime: Time;
}

View File

@ -9,6 +9,7 @@ import { TopEndPoints } from 'types/api/metrics/getTopEndPoints';
interface MetricReducer {
services: ServicesList[];
loading: boolean;
metricsApplicationLoading: boolean;
error: boolean;
errorMessage: string;
dbOverView: DBOverView[];

View File

@ -0,0 +1,37 @@
import { PayloadProps as ServicePayload } from 'types/api/trace/getServiceList';
import { PayloadProps as OperationsPayload } from 'types/api/trace/getServiceOperation';
import { PayloadProps as GetSpansAggregatePayload } from 'types/api/trace/getSpanAggregate';
import { PayloadProps as GetSpansPayloadProps } from 'types/api/trace/getSpans';
import { PayloadProps as TagsPayload } from 'types/api/trace/getTags';
type TagItemOperator = 'equals' | 'contains' | 'regex';
export interface TagItem {
key: string;
value: string;
operator: TagItemOperator;
}
export interface LatencyValue {
min: string;
max: string;
}
export interface TraceReducer {
selectedService: string;
selectedLatency: LatencyValue;
selectedOperation: string;
selectedKind: '' | '2' | '3' | string;
selectedTags: TagItem[];
tagsSuggestions: TagsPayload;
errorMessage: string;
serviceList: ServicePayload;
spanList: GetSpansPayloadProps;
operationsList: OperationsPayload;
error: boolean;
loading: boolean;
selectedAggOption: string;
selectedEntity: string;
spansAggregate: GetSpansAggregatePayload;
spansLoading: boolean;
}

View File

@ -6,7 +6,7 @@ declare module 'react-graph-vis' {
export { Network, NetworkEvents, Options, Node, Edge, DataSet } from 'vis';
export interface graphEvents {
[event: NetworkEvents]: (params?: any) => void;
[event: NetworkEvents]: (params) => void;
}
//Doesn't appear that this module supports passing in a vis.DataSet directly. Once it does graph can just use the Data object from vis.

View File

@ -1,5 +1,5 @@
import { SKIP_ONBOARDING } from 'constants/onboarding';
export const isOnboardingSkipped = () => {
export const isOnboardingSkipped = (): boolean => {
return localStorage.getItem(SKIP_ONBOARDING) === 'true';
};

15
frontend/src/wdyr.ts Normal file
View File

@ -0,0 +1,15 @@
/// <reference types="@welldone-software/why-did-you-render" />
// ^ https://github.com/welldone-software/why-did-you-render/issues/161
import React from 'react';
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React, {
trackAllPureComponents: false,
trackExtraHooks: [[require('react-redux/lib'), 'useSelector']],
include: [/^ConnectFunction/],
logOnDifferentValues: true,
});
}
export default '';

View File

@ -4,8 +4,7 @@ import CopyPlugin from 'copy-webpack-plugin';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import { resolve } from 'path';
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin';
import webpack from 'webpack';
import { WebpackPluginInstance } from 'webpack-dev-middleware/node_modules/webpack';
import webpack, { WebpackPluginInstance } from 'webpack';
const __dirname = resolve();
@ -14,7 +13,7 @@ const config: webpack.Configuration = {
devtool: 'source-map',
entry: resolve(__dirname, './src/index.tsx'),
output: {
filename: ({ chunk }: any): string => {
filename: ({ chunk }): string => {
const hash = chunk?.hash;
const name = chunk?.name;
return `js/${name}-${hash}.js`;
@ -53,12 +52,12 @@ const config: webpack.Configuration = {
},
plugins: [
new HtmlWebpackPlugin({ template: 'src/index.html.ejs' }),
new CompressionPlugin({
(new CompressionPlugin({
exclude: /.map$/,
}) as any,
new CopyPlugin({
}) as unknown) as WebpackPluginInstance,
(new CopyPlugin({
patterns: [{ from: resolve(__dirname, 'public/'), to: '.' }],
}) as any,
}) as unknown) as WebpackPluginInstance,
new webpack.ProvidePlugin({
process: 'process/browser',
}),
@ -71,4 +70,4 @@ const config: webpack.Configuration = {
},
};
export default config;
export default config;

View File

@ -2,13 +2,12 @@
import dotenv from 'dotenv';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import { resolve } from 'path';
//@ts-ignore
import portFinderSync from 'portfinder-sync';
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin';
import webpack from 'webpack';
import { Configuration as WebpackDevServerConfiguration } from 'webpack-dev-server';
// @ts-ignore
import portFinderSync from 'portfinder-sync';
dotenv.config();
const __dirname = resolve();
@ -29,10 +28,10 @@ const config: Configuration = {
liveReload: true,
port: portFinderSync.getPort(3000),
static: {
directory: resolve(__dirname, "public"),
publicPath: "/",
directory: resolve(__dirname, 'public'),
publicPath: '/',
watch: true,
}
},
},
target: 'web',
output: {
@ -86,4 +85,4 @@ const config: Configuration = {
},
};
export default config;
export default config;

View File

@ -3118,6 +3118,13 @@
resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.5.1.tgz#b5fde2f0f79c1e120307c415a4c1d5eb15a6f278"
integrity sha512-4vSVUiOPJLmr45S8rMGy7WDvpWxfFxfP/Qx/cxZFCfvoypTYpPPL1X8VIZMe0WTA+Jr7blUxwUSEZNkjoMTgSw==
"@welldone-software/why-did-you-render@^6.2.1":
version "6.2.1"
resolved "https://registry.yarnpkg.com/@welldone-software/why-did-you-render/-/why-did-you-render-6.2.1.tgz#6a87926cc8386b748dc07341cf495caa5be1db28"
integrity sha512-eIVKeK6ueS3tuzCqMVTaaNrPYvb9cA8NHiNgLA7Op8SD4TiT31zqNjxmhzLEK+y3sBxcwr6YhsiQGX9EThrvaw==
dependencies:
lodash "^4"
"@xtuc/ieee754@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
@ -5373,10 +5380,10 @@ custom-event-polyfill@^1.0.6:
resolved "https://registry.yarnpkg.com/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz#9bc993ddda937c1a30ccd335614c6c58c4f87aee"
integrity sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==
cypress@8.6.0:
version "8.6.0"
resolved "https://registry.yarnpkg.com/cypress/-/cypress-8.6.0.tgz#8d02fa58878b37cfc45bbfce393aa974fa8a8e22"
integrity sha512-F7qEK/6Go5FsqTueR+0wEw2vOVKNgk5847Mys8vsWkzPoEKdxs+7N9Y1dit+zhaZCLtMPyrMwjfA53ZFy+lSww==
cypress@^8.3.0:
version "8.7.0"
resolved "https://registry.yarnpkg.com/cypress/-/cypress-8.7.0.tgz#2ee371f383d8f233d3425b6cc26ddeec2668b6da"
integrity sha512-b1bMC3VQydC6sXzBMFnSqcvwc9dTZMgcaOzT0vpSD+Gq1yFc+72JDWi55sfUK5eIeNLAtWOGy1NNb6UlhMvB+Q==
dependencies:
"@cypress/request" "^2.88.6"
"@cypress/xvfb" "^1.2.4"
@ -9929,7 +9936,7 @@ lodash.uniq@^4.3.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
"lodash@>=3.5 <5", lodash@>=4.17.21, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0:
"lodash@>=3.5 <5", lodash@>=4.17.21, lodash@^4, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@ -14658,7 +14665,7 @@ uuid@^2.0.1:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a"
integrity sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=
uuid@^3.3.2, uuid@^3.4.0:
uuid@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==