Feat (UI) :Trace Filter page is updated (#684)

* dayjs and less loader is added

* webpack config is added

* moment is removed

* useDebounceFunction hook is made

* old components and reducer is removed

* search is updated

* changes are upadted for the trace page as skeleton is ready

* chore: method is change from dayjs

* convertObject into params is updated

* initial filters are updated

* initial and final filter issue is fixed

* selection of the filter is updated

* filters are now able to selected

* checkbox disable when loading is in progress

* chore: getFilter filename is updated

* feat: clear all and exapanded filter is updated

* chore: clearAll and expand panel is updated

* feat: useClickOutSide hook is added

* chore: get filter url becomes encoded

* chore: get tag filters is added

* feat: search tags is wip

* bug: global max,min on change bug is resolved

* chore: getInitial filter is updated

* chore: expand panel is updated

* chore: get filter is updated

* chore: code smells is updated

* feat: loader is added in the panel header to show the loading

* chore: search tags in wip

* chore: button style is updated

* chore: search in wip

* chore: search ui is updated from the global state

* chore: search in wip

* chore: search is updated

* chore: getSpansAggregate section is updated

* useOutside click is updated

* useclickoutside hook is updated

* useclickoutside hook is updated

* parsing is updated

* initial filter is updated

* feat: trace table is updated

* chore: trace table is updated

* chore: useClickout side is updated for the search panel

* feat: unneccesary re-render and code is removed

* chore: trace table is updated

* custom component is removed and used antd search component

* error state is updated over search component

* chore: search bar is updated

* chore: left panel search and table component connection is updated

* chore: trace filter config is updated

* chore: for graph reducer is updated

* chore: graph is updated

* chore: table is updated

* chore: spans is updated

* chore: reducer is updated

* chore: graph component is updated

* chore: number of graph condition is updated

* chore: input and range slider is now sync

* chore: duration is updated

* chore: clearAllFilter is updated

* chore: duration slider is updated

* chore: duration is updated and panel body loading is updated

* chore: slider container is added to add padding from left to right

* chore: Select filter is updated

* chore: duration filter is updated

* chore: Divider is added

* chore: none option is added in both the dropdown

* chore: icon are updated

* chore: added padding in the pages component

* chore: none is updated

* chore: antd notification is added in the redux action

* chore: some of the changes are updated

* chore: display value is updated for the filter panel heading

* chore: calulation is memorised

* chore: utils function are updated in trace reducer

* chore: getFilters are updated

* tracetable is updated

* chore: actions is updated

* chore: metrics application is updated

* chore: search on clear action is updated

* chore: serviceName panel position is updated

* chore: added the label in the duration

* bug: edge case is fixed

* chore: some more changes are updated

* chore: some more changes are updated

* chore: clear all is fixed

* chore: panel heading caret is updated

* chore: checkbox is updated

* chore: isError handler is updated over initial render

* chore: traces is updated

* fix: tag search is updated

* chore: loading is added in the trace table and soring is introduced in the trace table

* bug: multiple render for the key is fixed

* Bug(UI): new suggestion is updated

* feat: isTraceFilterEnum function is made

* bug: new changes are updated

* chore: get Filter is updated

* chore: application metrics params is updated

* chore: error is added in the application metrics

* chore: filters is updated

* chore: expand panel edge case is updated

* chore: expand panel is updated and utls: updateUrl function is updated

* chore: reset trace state when unmounted

* chore: getFilter action is updated

* chore: api duration is updated

* chore: useEffect dependency is updated

* chore: filter is updated with the new arch

* bug: trace table issue is resolved

* chore: application rps url is updated for trace

* chore: duration filter is updated

* chore: search key is updated

* chore: filter is added in the search url

* bug: filter is fixed

* bug: filter is fixed

* bug: filter is fixed

* chore: reset trace data when unmounted

* chore: TopEnd point is added

* chore: getInitialSpanAggregate action is updated

* chore: application url is updated

* chore: no tags placeholder is updated

* chore: flow from customer is now fixed

* chore: search is updated

* chore: select all button is removed

* chore: prev filter is removed to show the result

* chore: config is updated

* chore: checkbox component is updated

* chore: span filter is updated

* chore: graph issue is resolved

* chore: selected is updated

* chore: all filter are selected

* feat: new trace page is updated

* chore: utils is updated

* feat: trace filter page is updated

* chore: duration is now fixed

* chore: duration clear filter is added

* chore: onClickCheck is updated

* chore: trace filter page is updated

* bug: some of bugs are resolved

* chore: duration body is updated

* chore: topEndPoint and application query is updated

* chore: user selection is updated in the duration filter

* chore: panel duration is updated

* chore: panel duration is updated

* chore: duration bug is solved

* chore: function display value is updated
This commit is contained in:
palash-signoz 2022-02-09 11:31:13 +05:30 committed by GitHub
parent 2de6574835
commit be8ec756c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
107 changed files with 4206 additions and 2533 deletions

View File

@ -42,11 +42,14 @@
"d3": "^6.2.0",
"d3-flame-graph": "^3.1.1",
"d3-tip": "^0.9.1",
"dayjs": "^1.10.7",
"dotenv": "8.2.0",
"file-loader": "6.1.1",
"history": "4.10.1",
"html-webpack-plugin": "5.1.0",
"jest": "26.6.0",
"less": "^4.1.2",
"less-loader": "^10.2.0",
"mini-css-extract-plugin": "2.4.5",
"monaco-editor": "^0.30.0",
"react": "17.0.0",

View File

@ -19,7 +19,7 @@ export const ServiceMapPage = Loadable(
);
export const TraceDetailPages = Loadable(
() => import(/* webpackChunkName: "TraceDetailPage" */ 'pages/TraceDetails'),
() => import(/* webpackChunkName: "TraceDetailPage" */ 'pages/Trace'),
);
export const TraceGraphPage = Loadable(

View File

@ -3,14 +3,13 @@ import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/alerts/getGroups';
import convertObjectIntoParams from 'lib/query/convertObjectIntoParams';
const getGroups = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const queryParams = Object.keys(props)
.map((e) => `${e}=${props[e]}`)
.join('&');
const queryParams = convertObjectIntoParams(props);
const response = await AxiosAlertManagerInstance.get(
`/alerts/groups?${queryParams}`,

View File

@ -0,0 +1,48 @@
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/getFilters';
import omitBy from 'lodash-es/omitBy';
const getFilters = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const duration =
omitBy(props.other, (_, key) => !key.startsWith('duration')) || [];
const nonDuration = omitBy(props.other, (_, key) =>
key.startsWith('duration'),
);
const exclude: string[] = [];
props.isFilterExclude.forEach((value, key) => {
if (value) {
exclude.push(key);
}
});
const response = await axios.post<PayloadProps>(`/getSpanFilters`, {
start: props.start,
end: props.end,
getFilters: props.getFilters,
...nonDuration,
maxDuration: String((duration['duration'] || [])[0] || ''),
minDuration: String((duration['duration'] || [])[1] || ''),
exclude: exclude,
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getFilters;

View File

@ -1,24 +0,0 @@
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

@ -1,24 +0,0 @@
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

@ -1,26 +0,0 @@
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

@ -1,26 +0,0 @@
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,59 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import omitBy from 'lodash-es/omitBy';
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 updatedSelectedTags = props.selectedTags.map((e) => ({
Key: e.Key[0],
Operator: e.Operator,
Values: e.Values,
}));
const exclude: string[] = [];
props.isFilterExclude.forEach((value, key) => {
if (value) {
exclude.push(key);
}
});
const other = Object.fromEntries(props.selectedFilter);
const duration = omitBy(other, (_, key) => !key.startsWith('duration')) || [];
const nonDuration = omitBy(other, (_, key) => key.startsWith('duration'));
const response = await axios.post<PayloadProps>(
`/getFilteredSpans/aggregates`,
{
start: String(props.start),
end: String(props.end),
function: props.function,
groupBy: props.groupBy,
step: props.step,
tags: updatedSelectedTags,
...nonDuration,
maxDuration: String((duration['duration'] || [])[0] || ''),
minDuration: String((duration['duration'] || [])[1] || ''),
exclude,
},
);
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,60 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import omitBy from 'lodash-es/omitBy';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/trace/getSpanAggregate';
import { TraceFilterEnum } from 'types/reducer/trace';
const getSpanAggregate = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const preProps = {
start: String(props.start),
end: String(props.end),
limit: props.limit,
offset: props.offset,
};
const exclude: TraceFilterEnum[] = [];
props.isFilterExclude.forEach((value, key) => {
if (value) {
exclude.push(key);
}
});
const updatedSelectedTags = props.selectedTags.map((e) => ({
Key: e.Key[0],
Operator: e.Operator,
Values: e.Values,
}));
const other = Object.fromEntries(props.selectedFilter);
const duration = omitBy(other, (_, key) => !key.startsWith('duration')) || [];
const nonDuration = omitBy(other, (_, key) => key.startsWith('duration'));
const response = await axios.post<PayloadProps>(`/getFilteredSpans`, {
...preProps,
tags: updatedSelectedTags,
...nonDuration,
maxDuration: String((duration['duration'] || [])[0] || ''),
minDuration: String((duration['duration'] || [])[1] || ''),
exclude,
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getSpanAggregate;

View File

@ -0,0 +1,38 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { omitBy } from 'lodash-es';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/trace/getTagFilters';
const getTagFilters = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const duration =
omitBy(props.other, (_, key) => !key.startsWith('duration')) || [];
const nonDuration = omitBy(props.other, (_, key) =>
key.startsWith('duration'),
);
const response = await axios.post<PayloadProps>(`/getTagFilters`, {
start: String(props.start),
end: String(props.end),
...nonDuration,
maxDuration: String((duration['duration'] || [])[0] || ''),
minDuration: String((duration['duration'] || [])[1] || ''),
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getTagFilters;

View File

@ -1,24 +0,0 @@
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

@ -0,0 +1,7 @@
import { Dayjs } from 'dayjs';
import dayjsGenerateConfig from 'rc-picker/lib/generate/dayjs';
import generatePicker from 'antd/es/date-picker/generatePicker';
const DatePicker = generatePicker<Dayjs>(dayjsGenerateConfig);
export default DatePicker;

View File

@ -148,7 +148,7 @@ const Graph = ({
useEffect(() => {
buildChart();
}, [buildChart]);
}, []);
return (
<div style={{ height: '85%' }}>

View File

@ -1,8 +1,8 @@
import { DatePicker, Modal } from 'antd';
import { Moment } from 'moment';
import moment from 'moment';
import { Modal } from 'antd';
import React, { useState } from 'react';
export type DateTimeRangeType = [Moment | null, Moment | null] | null;
export type DateTimeRangeType = [Dayjs | null, Dayjs | null] | null;
import DatePicker from 'components/DatePicker';
import dayjs, { Dayjs } from 'dayjs';
const { RangePicker } = DatePicker;
@ -20,8 +20,8 @@ const CustomDateTimeModal = ({
setCustomDateTimeRange(date_time);
}
function disabledDate(current: Moment): boolean {
if (current > moment()) {
function disabledDate(current: Dayjs): boolean {
if (current > dayjs()) {
return true;
} else {
return false;

View File

@ -8,7 +8,7 @@ import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
import { LOCAL_STORAGE } from 'constants/localStorage';
import getTimeString from 'lib/getTimeString';
import moment from 'moment';
import dayjs, { Dayjs } from 'dayjs';
import { connect, useSelector } from 'react-redux';
import { RouteComponentProps, withRouter } from 'react-router';
import { bindActionCreators, Dispatch } from 'redux';
@ -37,26 +37,18 @@ const DateTimeSelection = ({
const getTime = useCallback((): [number, number] | undefined => {
if (searchEndTime && searchStartTime) {
const startMoment = moment(
const startDate = dayjs(
new Date(parseInt(getTimeString(searchStartTime), 10)),
);
const endMoment = moment(
new Date(parseInt(getTimeString(searchEndTime), 10)),
);
const endDate = dayjs(new Date(parseInt(getTimeString(searchEndTime), 10)));
return [
startMoment.toDate().getTime() || 0,
endMoment.toDate().getTime() || 0,
];
return [startDate.toDate().getTime() || 0, endDate.toDate().getTime() || 0];
}
if (localstorageStartTime && localstorageEndTime) {
const startMoment = moment(localstorageStartTime);
const endMoment = moment(localstorageEndTime);
const startDate = dayjs(localstorageStartTime);
const endDate = dayjs(localstorageEndTime);
return [
startMoment.toDate().getTime() || 0,
endMoment.toDate().getTime() || 0,
];
return [startDate.toDate().getTime() || 0, endDate.toDate().getTime() || 0];
}
return undefined;
}, [
@ -66,8 +58,8 @@ const DateTimeSelection = ({
searchStartTime,
]);
const [startTime, setStartTime] = useState<moment.Moment>();
const [endTime, setEndTime] = useState<moment.Moment>();
const [startTime, setStartTime] = useState<Dayjs>();
const [endTime, setEndTime] = useState<Dayjs>();
const [options, setOptions] = useState(getOptions(location.pathname));
const [refreshButtonHidden, setRefreshButtonHidden] = useState<boolean>(false);
@ -136,8 +128,8 @@ const DateTimeSelection = ({
};
const getInputLabel = (
startTime?: moment.Moment,
endTime?: moment.Moment,
startTime?: Dayjs,
endTime?: Dayjs,
timeInterval: Time = '15min',
): string | Time => {
if (startTime && endTime && timeInterval === 'custom') {
@ -153,18 +145,18 @@ const DateTimeSelection = ({
};
const onLastRefreshHandler = useCallback(() => {
const currentTime = moment();
const currentTime = dayjs();
const lastRefresh = moment(
const lastRefresh = dayjs(
selectedTimeInterval === 'custom' ? minTime / 1000000 : maxTime / 1000000,
);
const duration = moment.duration(currentTime.diff(lastRefresh));
const secondsDiff = Math.floor(duration.asSeconds());
const minutedDiff = Math.floor(duration.asMinutes());
const hoursDiff = Math.floor(duration.asHours());
const daysDiff = Math.floor(duration.asDays());
const monthsDiff = Math.floor(duration.asMonths());
const secondsDiff = currentTime.diff(lastRefresh, 'seconds');
const minutedDiff = currentTime.diff(lastRefresh, 'minutes');
const hoursDiff = currentTime.diff(lastRefresh, 'hours');
const daysDiff = currentTime.diff(lastRefresh, 'days');
const monthsDiff = currentTime.diff(lastRefresh, 'months');
if (monthsDiff > 0) {
return `Last refresh -${monthsDiff} months ago`;
@ -242,8 +234,8 @@ const DateTimeSelection = ({
const [preStartTime = 0, preEndTime = 0] = getTime() || [];
setStartTime(moment(preStartTime));
setEndTime(moment(preEndTime));
setStartTime(dayjs(preStartTime));
setEndTime(dayjs(preEndTime));
updateTimeInterval(updatedTime, [preStartTime, preEndTime]);
}, [
@ -318,8 +310,3 @@ const mapDispatchToProps = (
type Props = DispatchProps & RouteComponentProps;
export default connect(null, mapDispatchToProps)(withRouter(DateTimeSelection));
// DateTimeSelection.whyDidYouRender = {
// logOnDifferentValues: true,
// customName: 'DateTimeSelection',
// };

View File

@ -32,11 +32,12 @@ const Application = ({ getWidget }: DashboardProps): JSX.Element => {
const urlParams = new URLSearchParams();
urlParams.set(METRICS_PAGE_QUERY_PARAM.startTime, currentTime.toString());
urlParams.set(METRICS_PAGE_QUERY_PARAM.endTime, tPlusOne.toString());
if (servicename) {
urlParams.set(METRICS_PAGE_QUERY_PARAM.service, servicename);
}
history.push(`${ROUTES.TRACE}?${urlParams.toString()}`);
history.replace(
`${
ROUTES.TRACE
}?${urlParams.toString()}&selected={"serviceName":["${servicename}"],"status":["ok","error"]}&filterToFetchData=["duration","status","serviceName"]&userSelectedFilter={"status":["error","ok"],"serviceName":["${servicename}"]}&isSelectedFilterSkipped=true`,
);
};
const onClickhandler = async (
@ -85,12 +86,12 @@ const Application = ({ getWidget }: DashboardProps): JSX.Element => {
const urlParams = new URLSearchParams();
urlParams.set(METRICS_PAGE_QUERY_PARAM.startTime, currentTime.toString());
urlParams.set(METRICS_PAGE_QUERY_PARAM.endTime, tPlusOne.toString());
if (servicename) {
urlParams.set(METRICS_PAGE_QUERY_PARAM.service, servicename);
}
urlParams.set(METRICS_PAGE_QUERY_PARAM.error, 'true');
history.push(`${ROUTES.TRACE}?${urlParams.toString()}`);
history.replace(
`${
ROUTES.TRACE
}?${urlParams.toString()}&selected={"serviceName":["${servicename}"],"status":["error"]}&filterToFetchData=["duration","status","serviceName"]&userSelectedFilter={"status":["error"],"serviceName":["${servicename}"]}&isSelectedFilterSkipped=true`,
);
};
return (

View File

@ -28,12 +28,12 @@ const TopEndpointsTable = (props: TopEndpointsTableProps): JSX.Element => {
METRICS_PAGE_QUERY_PARAM.endTime,
(maxTime / 1000000).toString(),
);
if (servicename) {
urlParams.set(METRICS_PAGE_QUERY_PARAM.service, servicename);
}
urlParams.set(METRICS_PAGE_QUERY_PARAM.operation, operation);
history.push(`${ROUTES.TRACE}?${urlParams.toString()}`);
history.push(
`${
ROUTES.TRACE
}?${urlParams.toString()}&selected={"status":["error","ok"],"serviceName":["${servicename}"],"operation":["${operation}"]}&filterToFetchData=["duration","status","serviceName","operation"]&isSelectedFilterSkipped=true&userSelectedFilter={"status":["error","ok"],"serviceName":["${servicename}"],"operation":["${operation}"]}&isSelectedFilterSkipped=true`,
);
};
const columns: ColumnsType<DataProps> = [

View File

@ -0,0 +1,191 @@
import React, { useState } from 'react';
import { CheckBoxContainer } from './styles';
import { Checkbox, notification, Typography } from 'antd';
import { connect, useDispatch, useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace';
import { SelectedTraceFilter } from 'store/actions/trace/selectTraceFilter';
import AppActions from 'types/actions';
import { ThunkDispatch } from 'redux-thunk';
import { bindActionCreators, Dispatch } from 'redux';
import { getFilter, updateURL } from 'store/actions/trace/util';
import getFilters from 'api/trace/getFilters';
import { AxiosError } from 'axios';
import { GlobalReducer } from 'types/reducer/globalTime';
import { UPDATE_ALL_FILTERS } from 'types/actions/trace';
const CheckBoxComponent = (props: CheckBoxProps): JSX.Element => {
const {
selectedFilter,
filterLoading,
filterToFetchData,
spansAggregate,
selectedTags,
filter,
userSelectedFilter,
isFilterExclude,
} = useSelector<AppState, TraceReducer>((state) => state.traces);
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const dispatch = useDispatch<Dispatch<AppActions>>();
const [isLoading, setIsLoading] = useState<boolean>(false);
const isUserSelected =
(userSelectedFilter.get(props.name) || []).find(
(e) => e === props.keyValue,
) !== undefined;
const onCheckHandler = async () => {
try {
setIsLoading(true);
const newSelectedMap = new Map(selectedFilter);
const preUserSelectedMap = new Map(userSelectedFilter);
const preIsFilterExclude = new Map(isFilterExclude);
const isTopicPresent = preUserSelectedMap.get(props.name);
// append the value
if (!isTopicPresent) {
preUserSelectedMap.set(props.name, [props.keyValue]);
} else {
const isValuePresent =
isTopicPresent.find((e) => e === props.keyValue) !== undefined;
// check the value if present then remove the value or isChecked
if (isValuePresent) {
preUserSelectedMap.set(
props.name,
isTopicPresent.filter((e) => e !== props.keyValue),
);
} else {
// if not present add into the array of string
preUserSelectedMap.set(props.name, [...isTopicPresent, props.keyValue]);
}
}
if (newSelectedMap.get(props.name)?.find((e) => e === props.keyValue)) {
newSelectedMap.set(props.name, [
...(newSelectedMap.get(props.name) || []).filter(
(e) => e !== props.keyValue,
),
]);
} else {
newSelectedMap.set(props.name, [
...new Set([...(newSelectedMap.get(props.name) || []), props.keyValue]),
]);
}
if (preIsFilterExclude.get(props.name) !== false) {
preIsFilterExclude.set(props.name, true);
}
const response = await getFilters({
other: Object.fromEntries(newSelectedMap),
end: String(globalTime.maxTime),
start: String(globalTime.minTime),
getFilters: filterToFetchData.filter((e) => e !== props.name),
isFilterExclude: preIsFilterExclude,
});
if (response.statusCode === 200) {
const updatedFilter = getFilter(response.payload);
updatedFilter.forEach((value, key) => {
if (key !== 'duration' && props.name !== key) {
preUserSelectedMap.set(key, Object.keys(value));
}
});
updatedFilter.set(props.name, {
[`${props.keyValue}`]: '-1',
...(filter.get(props.name) || {}),
...(updatedFilter.get(props.name) || {}),
});
dispatch({
type: UPDATE_ALL_FILTERS,
payload: {
selectedTags,
current: spansAggregate.currentPage,
filter: updatedFilter,
filterToFetchData,
selectedFilter: newSelectedMap,
userSelected: preUserSelectedMap,
isFilterExclude: preIsFilterExclude,
},
});
setIsLoading(false);
updateURL(
newSelectedMap,
filterToFetchData,
spansAggregate.currentPage,
selectedTags,
updatedFilter,
preIsFilterExclude,
preUserSelectedMap,
);
} else {
setIsLoading(false);
notification.error({
message: response.error || 'Something went wrong',
});
}
} catch (error) {
notification.error({
message: (error as AxiosError).toString() || 'Something went wrong',
});
setIsLoading(false);
}
};
const isCheckBoxSelected = isUserSelected;
return (
<CheckBoxContainer>
<Checkbox
disabled={isLoading || filterLoading}
onClick={onCheckHandler}
checked={isCheckBoxSelected}
defaultChecked
key={props.keyValue}
>
{props.keyValue}
</Checkbox>
{isCheckBoxSelected ? (
<Typography>{props.value}</Typography>
) : (
<Typography>-</Typography>
)}
</CheckBoxContainer>
);
};
interface DispatchProps {
selectedTraceFilter: (props: {
topic: TraceFilterEnum;
value: string;
}) => void;
}
interface CheckBoxProps extends DispatchProps {
keyValue: string;
value: string;
name: TraceFilterEnum;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
selectedTraceFilter: bindActionCreators(SelectedTraceFilter, dispatch),
});
export default connect(null, mapDispatchToProps)(CheckBoxComponent);

View File

@ -0,0 +1,11 @@
import styled from 'styled-components';
export const CheckBoxContainer = styled.div`
display: flex;
justify-content: space-between;
margin-left: 1rem;
margin-right: 1rem;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
`;

View File

@ -0,0 +1,36 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace';
import CheckBoxComponent from '../Common/Checkbox';
const CommonCheckBox = (props: CommonCheckBoxProps): JSX.Element => {
const { filter } = useSelector<AppState, TraceReducer>(
(state) => state.traces,
);
const status = filter.get(props.name) || {};
const statusObj = Object.keys(status);
return (
<>
{statusObj.map((e) => (
<CheckBoxComponent
key={e}
{...{
name: props.name,
keyValue: e,
value: status[e],
}}
/>
))}
</>
);
};
interface CommonCheckBoxProps {
name: TraceFilterEnum;
}
export default CommonCheckBox;

View File

@ -0,0 +1,212 @@
import React, { useState } from 'react';
import { Input, Slider } from 'antd';
import { Container, InputContainer, Text } from './styles';
import { useDispatch, useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { TraceReducer } from 'types/reducer/trace';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { getFilter, updateURL } from 'store/actions/trace/util';
import dayjs from 'dayjs';
import durationPlugin from 'dayjs/plugin/duration';
import { Dispatch } from 'redux';
import AppActions from 'types/actions';
import { UPDATE_ALL_FILTERS } from 'types/actions/trace';
import getFilters from 'api/trace/getFilters';
import { GlobalReducer } from 'types/reducer/globalTime';
import { SliderRangeProps } from 'antd/lib/slider';
dayjs.extend(durationPlugin);
const getMs = (value: string) => {
return dayjs
.duration({
milliseconds: parseInt(value, 10) / 1000000,
})
.format('SSS');
};
const Duration = (): JSX.Element => {
const {
filter,
selectedFilter,
filterToFetchData,
spansAggregate,
selectedTags,
userSelectedFilter,
isFilterExclude,
} = useSelector<AppState, TraceReducer>((state) => state.traces);
const dispatch = useDispatch<Dispatch<AppActions>>();
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const getDuration = () => {
const selectedDuration = selectedFilter.get('duration');
if (selectedDuration) {
return {
maxDuration: selectedDuration[0],
minDuration: selectedDuration[1],
};
}
return filter.get('duration') || {};
};
const duration = getDuration();
const maxDuration = duration['maxDuration'] || '0';
const minDuration = duration['minDuration'] || '0';
const [localMax, setLocalMax] = useState<string>(maxDuration);
const [localMin, setLocalMin] = useState<string>(minDuration);
const defaultValue = [parseFloat(minDuration), parseFloat(maxDuration)];
const updatedUrl = async (min: number, max: number) => {
const preSelectedFilter = new Map(selectedFilter);
const preUserSelected = new Map(userSelectedFilter);
preSelectedFilter.set('duration', [String(max), String(min)]);
console.log('on the update Url');
const response = await getFilters({
end: String(globalTime.maxTime),
getFilters: filterToFetchData,
other: Object.fromEntries(preSelectedFilter),
start: String(globalTime.minTime),
isFilterExclude,
});
if (response.statusCode === 200) {
const preFilter = getFilter(response.payload);
preFilter.forEach((value, key) => {
if (key !== 'duration') {
preUserSelected.set(key, Object.keys(value));
}
});
dispatch({
type: UPDATE_ALL_FILTERS,
payload: {
current: spansAggregate.currentPage,
filter: preFilter,
filterToFetchData,
selectedFilter: preSelectedFilter,
selectedTags,
userSelected: preUserSelected,
isFilterExclude,
},
});
updateURL(
preSelectedFilter,
filterToFetchData,
spansAggregate.currentPage,
selectedTags,
preFilter,
isFilterExclude,
userSelectedFilter,
);
}
};
const onRangeSliderHandler = (number: [number, number]) => {
const [min, max] = number;
setLocalMin(min.toString());
setLocalMax(max.toString());
};
const debouncedFunction = useDebouncedFn(
(min, max) => {
console.log('debounce function');
updatedUrl(min, max);
},
500,
undefined,
[],
);
const onChangeMaxHandler: React.ChangeEventHandler<HTMLInputElement> = (
event,
) => {
const value = event.target.value;
const min = parseFloat(localMin);
const max = parseFloat(value) * 1000000;
console.log('on change in max');
onRangeSliderHandler([min, max]);
debouncedFunction(min, max);
};
const onChangeMinHandler: React.ChangeEventHandler<HTMLInputElement> = (
event,
) => {
const value = event.target.value;
const min = parseFloat(value) * 1000000;
const max = parseFloat(localMax);
onRangeSliderHandler([min, max]);
console.log('on change in min');
debouncedFunction(min, max);
};
const onRangeHandler: SliderRangeProps['onChange'] = ([min, max]) => {
updatedUrl(min, max);
};
return (
<div>
<Container>
<InputContainer>
<Text>Min</Text>
</InputContainer>
<Input
addonAfter="ms"
onChange={onChangeMinHandler}
value={getMs(localMin)}
/>
<InputContainer>
<Text>Max</Text>
</InputContainer>
<Input
addonAfter="ms"
onChange={onChangeMaxHandler}
value={getMs(localMax)}
/>
</Container>
<Container>
<Slider
defaultValue={[defaultValue[0], defaultValue[1]]}
min={parseFloat((filter.get('duration') || {})['minDuration'])}
max={parseFloat((filter.get('duration') || {})['maxDuration'])}
range
tipFormatter={(value) => {
if (value === undefined) {
return '';
}
return <div>{`${getMs(value.toString())}ms`}</div>;
}}
onChange={([min, max]) => {
onRangeSliderHandler([min, max]);
}}
onAfterChange={onRangeHandler}
// onAfterChange={([min, max]) => {
// const returnFunction = debounce((min, max) => updatedUrl(min, max));
// returnFunction(min, max);
// }}
value={[parseFloat(localMin), parseFloat(localMax)]}
/>
</Container>
</div>
);
};
export default Duration;

View File

@ -0,0 +1,27 @@
import styled from 'styled-components';
import { Typography } from 'antd';
export const DurationText = styled.div`
display: flex;
align-items: center;
justify-content: space-around;
min-height: 8vh;
flex-direction: column;
`;
export const InputContainer = styled.div`
width: 100%;
margin-top: 0.5rem;
margin-bottom: 0.2rem;
`;
export const Text = styled(Typography)`
&&& {
text-align: left;
}
`;
export const Container = styled.div`
padding-left: 1rem;
padding-right: 1rem;
`;

View File

@ -0,0 +1,37 @@
import React from 'react';
import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace';
import { Card } from 'antd';
import Duration from './Duration';
import CommonCheckBox from './CommonCheckBox';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import Spinner from 'components/Spinner';
const PanelBody = (props: PanelBodyProps): JSX.Element => {
const { type } = props;
const { filterLoading } = useSelector<AppState, TraceReducer>(
(state) => state.traces,
);
if (filterLoading) {
return (
<Card bordered={false}>
<Spinner height="10vh" tip="Loading.." />
</Card>
);
}
return (
<Card bordered={false}>
{type === 'duration' ? <Duration /> : <CommonCheckBox name={type} />}
</Card>
);
};
interface PanelBodyProps {
type: TraceFilterEnum;
}
export default PanelBody;

View File

@ -0,0 +1,3 @@
import styled from 'styled-components';
export const Container = styled.div``;

View File

@ -0,0 +1,317 @@
import React, { useState } from 'react';
import { DownOutlined, RightOutlined } from '@ant-design/icons';
import { Card, Typography, Divider, notification } from 'antd';
import {
ButtonComponent,
ButtonContainer,
Container,
IconContainer,
TextCotainer,
} from './styles';
import { useDispatch, useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace';
const { Text } = Typography;
import { AllPanelHeading } from 'types/reducer/trace';
import getFilters from 'api/trace/getFilters';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getFilter, updateURL } from 'store/actions/trace/util';
import AppActions from 'types/actions';
import { Dispatch } from 'redux';
import { UPDATE_ALL_FILTERS } from 'types/actions/trace';
import { AxiosError } from 'axios';
const PanelHeading = (props: PanelHeadingProps): JSX.Element => {
const {
filterLoading,
filterToFetchData,
selectedFilter,
spansAggregate,
selectedTags,
filter,
isFilterExclude,
userSelectedFilter,
} = useSelector<AppState, TraceReducer>((state) => state.traces);
const isDefaultOpen =
filterToFetchData.find((e) => e === props.name) !== undefined;
const [isLoading, setIsLoading] = useState<boolean>(false);
const global = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const dispatch = useDispatch<Dispatch<AppActions>>();
const onExpandHandler: React.MouseEventHandler<HTMLDivElement> = async (e) => {
try {
e.preventDefault();
e.stopPropagation();
setIsLoading(true);
let updatedFilterData: TraceReducer['filterToFetchData'] = [];
const getprepdatedSelectedFilter = new Map(selectedFilter);
const getPreUserSelected = new Map(userSelectedFilter);
if (!isDefaultOpen) {
updatedFilterData = [props.name];
} else {
// removing the selected filter
updatedFilterData = [
...filterToFetchData.filter((name) => name !== props.name),
];
getprepdatedSelectedFilter.delete(props.name);
getPreUserSelected.delete(props.name);
}
const response = await getFilters({
end: String(global.maxTime),
start: String(global.minTime),
getFilters: updatedFilterData,
other: Object.fromEntries(getprepdatedSelectedFilter),
isFilterExclude,
});
if (response.statusCode === 200) {
const updatedFilter = getFilter(response.payload);
// is closed
if (!isDefaultOpen) {
// getprepdatedSelectedFilter.set(
// props.name,
// Object.keys(updatedFilter.get(props.name) || {}),
// );
getPreUserSelected.set(
props.name,
Object.keys(updatedFilter.get(props.name) || {}),
);
updatedFilterData = [...filterToFetchData, props.name];
}
// now append the non prop.name trace filter enum over the list
// selectedFilter.forEach((value, key) => {
// if (key !== props.name) {
// getprepdatedSelectedFilter.set(key, value);
// }
// });
getPreUserSelected.forEach((value, key) => {
if (key !== props.name) {
getPreUserSelected.set(key, value);
}
});
filter.forEach((value, key) => {
if (key !== props.name) {
updatedFilter.set(key, value);
}
});
dispatch({
type: UPDATE_ALL_FILTERS,
payload: {
current: spansAggregate.currentPage,
filter: updatedFilter,
filterToFetchData: updatedFilterData,
selectedFilter: getprepdatedSelectedFilter,
selectedTags,
userSelected: getPreUserSelected,
isFilterExclude,
},
});
updateURL(
getprepdatedSelectedFilter,
updatedFilterData,
spansAggregate.currentPage,
selectedTags,
updatedFilter,
isFilterExclude,
getPreUserSelected,
);
} else {
notification.error({
message: response.error || 'Something went wrong',
});
}
setIsLoading(false);
} catch (error) {
notification.error({
message: (error as AxiosError).toString() || 'Something went wrong',
});
}
};
const onClearAllHandler = async () => {
try {
setIsLoading(true);
const updatedFilter = new Map(selectedFilter);
const preUserSelected = new Map(userSelectedFilter);
updatedFilter.delete(props.name);
preUserSelected.delete(props.name);
const postIsFilterExclude = new Map(isFilterExclude);
postIsFilterExclude.set(props.name, false);
const response = await getFilters({
end: String(global.maxTime),
start: String(global.minTime),
getFilters: filterToFetchData,
other: Object.fromEntries(preUserSelected),
isFilterExclude: postIsFilterExclude,
});
if (response.statusCode === 200 && response.payload) {
const getUpatedFilter = getFilter(response.payload);
dispatch({
type: UPDATE_ALL_FILTERS,
payload: {
current: spansAggregate.currentPage,
filter: getUpatedFilter,
filterToFetchData,
selectedFilter: updatedFilter,
selectedTags,
userSelected: preUserSelected,
isFilterExclude: postIsFilterExclude,
},
});
updateURL(
updatedFilter,
filterToFetchData,
spansAggregate.currentPage,
selectedTags,
getUpatedFilter,
postIsFilterExclude,
preUserSelected,
);
} else {
notification.error({
message: response.error || 'Something went wrong',
});
}
setIsLoading(false);
} catch (error) {
notification.error({
message: (error as AxiosError).toString(),
});
setIsLoading(false);
}
};
// const onSelectAllHandler = async () => {
// try {
// setIsLoading(true);
// const preFilter = new Map(filter);
// const preSelectedFilter = new Map(selectedFilter);
// preSelectedFilter.set(
// props.name,
// Object.keys(preFilter.get(props.name) || {}),
// );
// const response = await getFilters({
// end: String(global.maxTime),
// start: String(global.minTime),
// getFilters: filterToFetchData,
// other: Object.fromEntries(preSelectedFilter),
// });
// if (response.statusCode === 200 && response.payload) {
// const getUpatedFilter = getFilter(response.payload);
// preSelectedFilter.set(
// props.name,
// Object.keys(getUpatedFilter.get(props.name) || {}),
// );
// dispatch({
// type: UPDATE_ALL_FILTERS,
// payload: {
// current: spansAggregate.currentPage,
// filter: preFilter,
// filterToFetchData,
// selectedFilter: preSelectedFilter,
// selectedTags,
// },
// });
// updateURL(
// preSelectedFilter,
// filterToFetchData,
// spansAggregate.currentPage,
// selectedTags,
// preFilter,
// );
// }
// setIsLoading(false);
// } catch (error) {
// setIsLoading(false);
// notification.error({
// message: (error as AxiosError).toString(),
// });
// }
// };
return (
<>
{props.name !== 'duration' && <Divider plain style={{ margin: 0 }} />}
<Card bordered={false}>
<Container
disabled={filterLoading || isLoading}
aria-disabled={filterLoading || isLoading}
aria-expanded={props.isOpen}
>
<TextCotainer onClick={onExpandHandler}>
<IconContainer>
{!props.isOpen ? <RightOutlined /> : <DownOutlined />}
</IconContainer>
<Text style={{ textTransform: 'capitalize' }} ellipsis>
{AllPanelHeading.find((e) => e.key === props.name)?.displayValue || ''}
</Text>
</TextCotainer>
{props.name !== 'duration' && (
<ButtonContainer>
{/* <ButtonComponent
aria-disabled={isLoading || filterLoading}
disabled={isLoading || filterLoading}
onClick={onSelectAllHandler}
type="link"
>
Select All
</ButtonComponent> */}
<ButtonComponent
aria-disabled={isLoading || filterLoading}
disabled={isLoading || filterLoading}
onClick={onClearAllHandler}
type="link"
>
Clear All
</ButtonComponent>
</ButtonContainer>
)}
</Container>
</Card>
</>
);
};
interface PanelHeadingProps {
name: TraceFilterEnum;
isOpen: boolean;
}
export default PanelHeading;

View File

@ -0,0 +1,49 @@
import { Button } from 'antd';
import styled, { css } from 'styled-components';
interface Props {
disabled: boolean;
}
export const Container = styled.div<Props>`
&&& {
display: flex;
justify-content: space-between;
align-items: center;
padding-left: 0.5rem;
min-height: 5vh;
cursor: ${({ disabled }) => disabled && 'not-allowed'};
${({ disabled }) =>
disabled &&
css`
opacity: 0.5;
`}
}
`;
export const IconContainer = styled.div`
&&& {
margin-right: 0.5rem;
}
`;
export const TextCotainer = styled.div`
&&& {
display: flex;
cursor: pointer;
}
`;
export const ButtonComponent = styled(Button)`
&&& {
font-size: 0.75rem;
}
`;
export const ButtonContainer = styled.div`
&&& {
display: flex;
}
`;

View File

@ -0,0 +1,28 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace';
import PanelBody from './PanelBody';
import PanelHeading from './PanelHeading';
const Panel = (props: PanelProps): JSX.Element => {
const traces = useSelector<AppState, TraceReducer>((state) => state.traces);
const isDefaultOpen =
traces.filterToFetchData.find((e) => e === props.name) !== undefined;
return (
<>
<PanelHeading name={props.name} isOpen={isDefaultOpen} />
{isDefaultOpen && <PanelBody type={props.name} />}
</>
);
};
interface PanelProps {
name: TraceFilterEnum;
}
export default Panel;

View File

@ -0,0 +1,27 @@
import React from 'react';
import { TraceFilterEnum } from 'types/reducer/trace';
import Panel from './Panel';
export const AllTraceFilterEnum: TraceFilterEnum[] = [
'duration',
'status',
'serviceName',
'operation',
'component',
'httpCode',
'httpHost',
'httpMethod',
'httpRoute',
'httpUrl',
];
const Filters = (): JSX.Element => (
<React.Fragment>
{AllTraceFilterEnum.map((panelName) => (
<Panel key={panelName} name={panelName} />
))}
</React.Fragment>
);
export default Filters;

View File

@ -0,0 +1,20 @@
import { Button, Input } from 'antd';
import styled from 'styled-components';
export const DurationContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
`;
export const InputComponent = styled(Input)`
&&& {
margin-left: 0.5rem;
margin-right: 0.5rem;
}
`;
export const CheckBoxContainer = styled.div`
display: flex;
flex-direction: column;
`;

View File

@ -0,0 +1,123 @@
import { ChartData, ChartDataset, ChartDatasetProperties } from 'chart.js';
import { TraceReducer } from 'types/reducer/trace';
import dayjs from 'dayjs';
import { colors } from 'lib/getRandomColor';
function transposeArray(array: number[][], arrayLength: number) {
let newArray: number[][] = [];
for (let i = 0; i < array.length; i++) {
newArray.push([]);
}
for (let i = 0; i < array.length; i++) {
for (let j = 0; j < arrayLength; j++) {
newArray[j]?.push(array[i][j]);
}
}
return newArray;
}
export const getChartData = (
data: TraceReducer['spansGraph']['payload'],
): ChartData<'line'> => {
const allDataPoints = data.items;
const chartDataset: ChartDatasetProperties<'line', number[]> = {
data: [],
type: 'line',
};
const chartLabels: ChartData<'line'>['labels'] = [];
Object.keys(allDataPoints).forEach((timestamp) => {
const key = allDataPoints[timestamp];
if (key.value) {
chartDataset.data.push(key.value);
const date = dayjs(key.timestamp / 1000000);
chartLabels.push(date.toDate().getTime());
}
});
return {
datasets: [
{
...chartDataset,
borderWidth: 1.5,
spanGaps: true,
borderColor: colors[0] || 'red',
showLine: true,
pointRadius: 0,
},
],
labels: chartLabels,
};
};
export const getChartDataforGroupBy = (
props: TraceReducer['spansGraph']['payload'],
): ChartData => {
const items = props.items;
const chartData: ChartData = {
datasets: [],
labels: [],
};
let max = 0;
const allGroupBy = Object.keys(items).map((e) => items[e].groupBy);
Object.keys(allGroupBy).map((e) => {
const length = Object.keys(allGroupBy[e]).length;
if (length >= max) {
max = length;
}
});
const numberOfGraphs = max;
const spansGraph: number[][] = [];
const names: string[] = [];
// number of data points
Object.keys(items).forEach((item) => {
const spanData = items[item];
const date = dayjs(Number(item) / 1000000)
.toDate()
.getTime();
chartData.labels?.push(date);
const groupBy = spanData.groupBy;
const preData: number[] = [];
if (groupBy) {
Object.keys(groupBy).forEach((key) => {
const value = groupBy[key];
preData.push(value);
names.push(key);
});
spansGraph.push(preData);
}
});
const updatedName = [...new Set(names)];
transposeArray(spansGraph, numberOfGraphs).forEach((values, index) => {
chartData.datasets.push({
data: values.map((e) => e || 0),
borderWidth: 1.5,
spanGaps: true,
borderColor: colors[index] || 'red',
showLine: true,
pointRadius: 0,
label: updatedName[index],
});
});
return chartData;
};

View File

@ -0,0 +1,48 @@
import React, { useMemo } from 'react';
import Graph from 'components/Graph';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { TraceReducer } from 'types/reducer/trace';
import Spinner from 'components/Spinner';
import { Container } from './styles';
import { Typography } from 'antd';
import { getChartData, getChartDataforGroupBy } from './config';
const TraceGraph = () => {
const { spansGraph, selectedGroupBy } = useSelector<AppState, TraceReducer>(
(state) => state.traces,
);
const { loading, error, errorMessage, payload } = spansGraph;
const ChartData = useMemo(() => {
return selectedGroupBy.length === 0
? getChartData(payload)
: getChartDataforGroupBy(payload);
}, [payload]);
if (error) {
return (
<Container center>
<Typography>{errorMessage || 'Something went wrong'}</Typography>
</Container>
);
}
if (loading || payload === undefined) {
return (
<Container>
<Spinner height={'20vh'} size="small" tip="Loading..." />
</Container>
);
}
return (
<Container>
<Graph data={ChartData} name="traceGraphph" type="line" />
</Container>
);
};
export default TraceGraph;

View File

@ -0,0 +1,19 @@
import styled, { css } from 'styled-components';
interface Props {
center?: boolean;
}
export const Container = styled.div<Props>`
height: 25vh;
margin-top: 1rem;
margin-bottom: 1rem;
${({ center }) =>
center &&
css`
display: flex;
justify-content: center;
align-items: center;
`}
`;

View File

@ -0,0 +1,102 @@
import { AutoComplete, AutoCompleteProps, Input, notification } from 'antd';
import getTagFilters from 'api/trace/getTagFilter';
import React, { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { TraceReducer } from 'types/reducer/trace';
const TagsKey = (props: TagsKeysProps): JSX.Element => {
const [selectLoading, setSelectLoading] = useState<boolean>(false);
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const [selectedKey, setSelectedKey] = useState<string>(props.tag.Key[0] || '');
const traces = useSelector<AppState, TraceReducer>((state) => state.traces);
const [options, setOptions] = useState<AutoCompleteProps['options']>([]);
const onSearchHandler = async () => {
try {
setSelectLoading(true);
const response = await getTagFilters({
start: globalTime.minTime,
end: globalTime.maxTime,
other: Object.fromEntries(traces.selectedFilter),
});
if (response.statusCode === 200) {
if (response.payload === null) {
setOptions([
{
value: '',
label: 'No tags available',
},
]);
} else {
setOptions(
response.payload.map((e) => ({
value: e.tagKeys,
label: e.tagKeys,
})),
);
}
} else {
notification.error({
message: response.error || 'Something went wrong',
});
}
setSelectLoading(false);
} catch (error) {
notification.error({
message: 'Something went wrong',
});
setSelectLoading(false);
}
};
useEffect(() => {
onSearchHandler();
}, []);
return (
<AutoComplete
dropdownClassName="certain-category-search-dropdown"
dropdownMatchSelectWidth={500}
style={{ width: 300 }}
options={options}
value={selectedKey}
onChange={(value) => {
if (options && options.find((option) => option.value === value)) {
setSelectedKey(value);
props.setLocalSelectedTags((tags) => [
...tags.slice(0, props.index),
{
Key: [value],
Operator: props.tag.Operator,
Values: props.tag.Values,
},
...tags.slice(props.index + 1, tags.length),
]);
} else {
setSelectedKey('');
}
}}
>
<Input disabled={selectLoading} placeholder="Please select" />
</AutoComplete>
);
};
interface TagsKeysProps {
index: number;
tag: FlatArray<TraceReducer['selectedTags'], 1>;
setLocalSelectedTags: React.Dispatch<
React.SetStateAction<TraceReducer['selectedTags']>
>;
}
export default TagsKey;

View File

@ -0,0 +1,130 @@
import React from 'react';
import { Select } from 'antd';
import {
Container,
IconContainer,
SelectComponent,
ValueSelect,
} from './styles';
import { connect, useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { TraceReducer } from 'types/reducer/trace';
import { CloseOutlined } from '@ant-design/icons';
import { SelectValue } from 'antd/lib/select';
import { ThunkDispatch } from 'redux-thunk';
import AppActions from 'types/actions';
import { bindActionCreators } from 'redux';
import { UpdateSelectedTags } from 'store/actions/trace/updateTagsSelected';
import TagsKey from './TagKey';
const { Option } = Select;
type Tags = FlatArray<TraceReducer['selectedTags'], 1>['Operator'];
const AllMenu: AllMenu[] = [
{
key: 'in',
value: 'IN',
},
{
key: 'not in',
value: 'NOT IN',
},
];
interface AllMenu {
key: Tags | '';
value: string;
}
const SingleTags = (props: AllTagsProps): JSX.Element => {
const traces = useSelector<AppState, TraceReducer>((state) => state.traces);
const {
Key: selectedKey,
Operator: selectedOperator,
Values: selectedValues,
} = props.tag;
const onDeleteTagHandler = (index: number) => {
props.onCloseHandler(index);
};
const onChangeOperatorHandler = (key: SelectValue) => {
props.setLocalSelectedTags([
...traces.selectedTags.slice(0, props.index),
{
Key: selectedKey,
Values: selectedValues,
Operator: key as Tags,
},
...traces.selectedTags.slice(props.index + 1, traces.selectedTags.length),
]);
};
return (
<>
<Container>
<TagsKey
index={props.index}
tag={props.tag}
setLocalSelectedTags={props.setLocalSelectedTags}
/>
<SelectComponent
onChange={onChangeOperatorHandler}
value={AllMenu.find((e) => e.key === selectedOperator)?.value || ''}
>
{AllMenu.map((e) => (
<Option key={e.key} value={e.value}>
{e.value}
</Option>
))}
</SelectComponent>
<ValueSelect
value={selectedValues}
onChange={(value) => {
props.setLocalSelectedTags((tags) => [
...tags.slice(0, props.index),
{
Key: selectedKey,
Operator: selectedOperator,
Values: value as string[],
},
...tags.slice(props.index + 1, tags.length),
]);
}}
mode="tags"
/>
<IconContainer
role={'button'}
onClick={() => onDeleteTagHandler(props.index)}
>
<CloseOutlined />
</IconContainer>
</Container>
</>
);
};
interface DispatchProps {
updateSelectedTags: (props: TraceReducer['selectedTags']) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
updateSelectedTags: bindActionCreators(UpdateSelectedTags, dispatch),
});
interface AllTagsProps extends DispatchProps {
onCloseHandler: (index: number) => void;
index: number;
tag: FlatArray<TraceReducer['selectedTags'], 1>;
setLocalSelectedTags: React.Dispatch<
React.SetStateAction<TraceReducer['selectedTags']>
>;
}
export default connect(null, mapDispatchToProps)(SingleTags);

View File

@ -0,0 +1,39 @@
import styled from 'styled-components';
import { Button, Select, Space } from 'antd';
export const SpaceComponent = styled(Space)`
&&& {
width: 100%;
}
`;
export const SelectComponent = styled(Select)`
&&& {
min-width: 170px;
margin-right: 21.91px;
margin-left: 21.92px;
}
`;
export const ValueSelect = styled(Select)`
&&& {
width: 100%;
}
`;
export const Container = styled.div`
&&& {
display: flex;
margin-top: 1rem;
margin-bottom: 1rem;
}
`;
export const IconContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
margin-left: 1.125rem;
`;

View File

@ -0,0 +1,158 @@
import React, { useEffect, useState } from 'react';
import { Button, Space, Typography } from 'antd';
import { CaretRightFilled } from '@ant-design/icons';
import {
Container,
ButtonContainer,
CurrentTagsContainer,
Wrapper,
ErrorContainer,
} from './styles';
import Tags from './Tag';
const { Text } = Typography;
import { PlusOutlined } from '@ant-design/icons';
import { connect, useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { TraceReducer } from 'types/reducer/trace';
import { bindActionCreators } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import AppActions from 'types/actions';
import { UpdateTagIsError } from 'store/actions/trace/updateIsTagsError';
import { parseTagsToQuery } from '../util';
import { isEqual } from 'lodash-es';
import { UpdateTagVisiblity } from 'store/actions/trace/updateTagPanelVisiblity';
const { Paragraph } = Typography;
const AllTags = ({
updateTagIsError,
onChangeHandler,
updateTagVisiblity,
updateFilters,
}: AllTagsProps): JSX.Element => {
const traces = useSelector<AppState, TraceReducer>((state) => state.traces);
const [localSelectedTags, setLocalSelectedTags] = useState<
TraceReducer['selectedTags']
>(traces.selectedTags);
const onTagAddHandler = () => {
setLocalSelectedTags((tags) => [
...tags,
{
Key: [],
Operator: 'in',
Values: [],
},
]);
};
useEffect(() => {
if (!isEqual(traces.selectedTags, localSelectedTags)) {
setLocalSelectedTags(traces.selectedTags);
}
}, [traces.selectedTags]);
const onCloseHandler = (index: number) => {
setLocalSelectedTags([
...localSelectedTags.slice(0, index),
...localSelectedTags.slice(index + 1, localSelectedTags.length),
]);
};
const onRunQueryHandler = () => {
const parsedQuery = parseTagsToQuery(localSelectedTags);
if (parsedQuery.isError) {
updateTagIsError(true);
} else {
onChangeHandler(parsedQuery.payload);
updateFilters(localSelectedTags);
updateTagIsError(false);
updateTagVisiblity(false);
}
};
const onResetHandler = () => {
setLocalSelectedTags([]);
};
if (traces.isTagModalError) {
return (
<ErrorContainer>
<Paragraph style={{ color: '#E89A3C' }}>
Unrecognised query format. Please reset your query by clicking `X` in the
search bar above.
</Paragraph>
<Paragraph style={{ color: '#E89A3C' }}>
Please click on the search bar to get a drop down to select relevant tags
</Paragraph>
</ErrorContainer>
);
}
return (
<>
<Container>
<Wrapper>
<Typography>Tags</Typography>
<CurrentTagsContainer>
{localSelectedTags.map((tags, index) => (
<Tags
key={index}
tag={tags}
index={index}
onCloseHandler={() => onCloseHandler(index)}
setLocalSelectedTags={setLocalSelectedTags}
/>
))}
</CurrentTagsContainer>
<Space wrap direction="horizontal">
<Button type="primary" onClick={onTagAddHandler} icon={<PlusOutlined />}>
Add Tags Filter
</Button>
<Text ellipsis>
Results will include spans with ALL the specified tags ( Rows are `anded`
)
</Text>
</Space>
</Wrapper>
<ButtonContainer>
<Button onClick={onResetHandler}>Reset</Button>
<Button
type="primary"
onClick={onRunQueryHandler}
icon={<CaretRightFilled />}
>
Run Query
</Button>
</ButtonContainer>
</Container>
</>
);
};
interface DispatchProps {
updateTagIsError: (value: boolean) => void;
updateTagVisiblity: (value: boolean) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
updateTagIsError: bindActionCreators(UpdateTagIsError, dispatch),
updateTagVisiblity: bindActionCreators(UpdateTagVisiblity, dispatch),
});
interface AllTagsProps extends DispatchProps {
updateFilters: (tags: TraceReducer['selectedTags']) => void;
onChangeHandler: (search: string) => void;
}
export default connect(null, mapDispatchToProps)(AllTags);

View File

@ -0,0 +1,56 @@
import styled from 'styled-components';
import { Card } from 'antd';
export const Container = styled(Card)`
top: 120%;
min-height: 20vh;
width: 100%;
z-index: 2;
position: absolute;
.ant-card-body {
padding-bottom: 0;
padding-right: 0;
padding-left: 0;
}
`;
export const ErrorContainer = styled(Card)`
top: 120%;
min-height: 20vh;
width: 100%;
z-index: 2;
position: absolute;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
`;
export const Wrapper = styled.div`
&&& {
padding-right: 2rem;
padding-left: 2rem;
}
`;
export const ButtonContainer = styled.div`
display: flex;
justify-content: flex-end;
align-items: center;
background-color: #303030;
padding-top: 11px;
padding-bottom: 11px;
padding-right: 38.01px;
margin-top: 1rem;
> button:nth-child(1) {
margin-right: 1rem;
}
`;
export const CurrentTagsContainer = styled.div`
margin-bottom: 1rem;
`;

View File

@ -0,0 +1,163 @@
import React, { useEffect, useRef, useState } from 'react';
import { Space } from 'antd';
import { Container, SearchComponent } from './styles';
import useClickOutside from 'hooks/useClickOutside';
import Tags from './AllTags';
import { connect, useDispatch, useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { TraceReducer } from 'types/reducer/trace';
import { ThunkDispatch } from 'redux-thunk';
import AppActions from 'types/actions';
import { bindActionCreators, Dispatch } from 'redux';
import { UpdateTagVisiblity } from 'store/actions/trace/updateTagPanelVisiblity';
import { parseQueryToTags, parseTagsToQuery } from './util';
import { UpdateTagIsError } from 'store/actions/trace/updateIsTagsError';
import { CaretRightFilled } from '@ant-design/icons';
import { updateURL } from 'store/actions/trace/util';
import { UPDATE_ALL_FILTERS } from 'types/actions/trace';
const Search = ({
updateTagVisiblity,
updateTagIsError,
}: SearchProps): JSX.Element => {
const traces = useSelector<AppState, TraceReducer>((state) => state.traces);
const [value, setValue] = useState<string>('');
const dispatch = useDispatch<Dispatch<AppActions>>();
useEffect(() => {
if (traces.filterLoading) {
const initialTags = parseTagsToQuery(traces.selectedTags);
if (!initialTags.isError) {
setValue(initialTags.payload);
}
}
}, [traces.selectedTags, traces.filterLoading]);
useEffect(() => {
if (value.length === 0 && traces.isTagModalError) {
updateTagIsError(false);
}
}, [traces.isTagModalError, value]);
const tagRef = useRef<HTMLDivElement>(null);
useClickOutside(tagRef, (e: HTMLElement) => {
// using this hack as overlay span is voilating this condition
if (
e.nodeName === 'svg' ||
e.nodeName === 'path' ||
e.nodeName === 'span' ||
e.nodeName === 'button'
) {
return;
}
if (
e.nodeName === 'DIV' &&
![
'ant-select-item-option-content',
'ant-empty-image',
'ant-select-item',
'ant-col',
'ant-select-item-option-active',
].find((p) => p.indexOf(e.className) !== -1) &&
!(e.ariaSelected === 'true') &&
traces.isTagModalOpen
) {
updateTagVisiblity(false);
}
});
const onChangeHandler = (search: string) => {
setValue(search);
};
const setIsTagsModalHandler = (value: boolean) => {
updateTagVisiblity(value);
};
const onFocusHandler: React.FocusEventHandler<HTMLInputElement> = (e) => {
e.preventDefault();
setIsTagsModalHandler(true);
};
const updateFilters = async (selectedTags: TraceReducer['selectedTags']) => {
dispatch({
type: UPDATE_ALL_FILTERS,
payload: {
selectedTags,
current: traces.spansAggregate.currentPage,
filter: traces.filter,
filterToFetchData: traces.filterToFetchData,
selectedFilter: traces.selectedFilter,
userSelected: traces.userSelectedFilter,
isFilterExclude: traces.isFilterExclude,
},
});
updateURL(
traces.selectedFilter,
traces.filterToFetchData,
traces.spansAggregate.currentPage,
selectedTags,
traces.filter,
traces.isFilterExclude,
traces.userSelectedFilter,
);
};
return (
<Space direction="vertical" style={{ width: '100%' }}>
<Container ref={tagRef}>
<SearchComponent
onChange={(event) => onChangeHandler(event.target.value)}
value={value}
allowClear
disabled={traces.filterLoading}
onFocus={onFocusHandler}
placeholder="Click to filter by tags"
type={'search'}
enterButton={<CaretRightFilled />}
onSearch={(string) => {
if (string.length === 0) {
updateTagVisiblity(false);
updateFilters([]);
return;
}
const { isError, payload } = parseQueryToTags(string);
if (isError) {
updateTagIsError(true);
} else {
updateTagIsError(false);
updateTagVisiblity(false);
updateFilters(payload);
}
}}
/>
{traces.isTagModalOpen && (
<Tags updateFilters={updateFilters} onChangeHandler={onChangeHandler} />
)}
</Container>
</Space>
);
};
interface DispatchProps {
updateTagVisiblity: (value: boolean) => void;
updateTagIsError: (value: boolean) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
updateTagVisiblity: bindActionCreators(UpdateTagVisiblity, dispatch),
updateTagIsError: bindActionCreators(UpdateTagIsError, dispatch),
});
type SearchProps = DispatchProps;
export default connect(null, mapDispatchToProps)(Search);

View File

@ -0,0 +1,17 @@
import styled from 'styled-components';
import { Input } from 'antd';
const { Search } = Input;
export const Container = styled.div`
display: flex;
position: relative;
`;
export const SearchComponent = styled(Search)`
.ant-btn-primary {
svg {
transform: scale(1.5);
}
}
`;

View File

@ -0,0 +1,84 @@
import { TraceReducer } from 'types/reducer/trace';
type Tags = TraceReducer['selectedTags'];
interface PayloadProps<T> {
isError: boolean;
payload: T;
}
export const parseQueryToTags = (query: string): PayloadProps<Tags> => {
let isError = false;
const noOfTags = query.split(' AND ');
const tags: Tags = noOfTags.map((filter) => {
const isInPresent = filter.includes('IN');
const isNotInPresent = filter.includes('NOT_IN');
if (!isNotInPresent || !isInPresent) {
isError = true;
}
const splitBy = isNotInPresent ? 'NOT_IN' : isInPresent ? 'IN' : '';
if (splitBy.length === 0) {
isError = true;
}
const filteredtags = filter.split(splitBy).map((e) => e.trim());
if (filteredtags.length !== 2) {
isError = true;
}
const filterForTags = filteredtags[1];
if (!filterForTags) {
isError = true;
}
const removingFirstAndLastBrackets = `${filterForTags?.slice(1, -1)}`;
const noofFilters = removingFirstAndLastBrackets.split(',');
noofFilters.forEach((e) => {
const firstChar = e.charAt(0);
const lastChar = e.charAt(e.length - 1);
if (!(firstChar === '"' && lastChar === '"')) {
isError = true;
}
});
return {
Key: [filteredtags[0]],
Values: noofFilters,
Operator: splitBy as FlatArray<Tags, 1>['Operator'],
};
});
return {
isError,
payload: tags,
};
};
export const parseTagsToQuery = (tags: Tags): PayloadProps<string> => {
let isError = false;
const payload = tags
.map(({ Values, Key, Operator }) => {
if (Key[0] === undefined) {
isError = true;
}
return `${Key[0]} ${Operator} (${Values.map((e) => `"${e}"`).join(',')})`;
})
.join(' AND ');
return {
isError,
payload,
};
};

View File

@ -0,0 +1,91 @@
interface Dropdown {
key: string;
displayValue: string;
}
export const groupBy: Dropdown[] = [
{
key: '',
displayValue: 'None',
},
{
key: 'serviceName',
displayValue: 'Service Name',
},
{
displayValue: 'Operation',
key: 'operation',
},
{
displayValue: 'HTTP url',
key: 'httpUrl',
},
{
displayValue: 'HTTP method',
key: 'httpMethod',
},
{
displayValue: 'HTTP host',
key: 'httpHost',
},
{
displayValue: 'HTTP route',
key: 'httpRoute',
},
{
displayValue: 'HTTP status code',
key: 'httpCode',
},
{
displayValue: 'Database name',
key: 'dbName',
},
{
displayValue: 'Database operation',
key: 'dbSystem',
},
{
displayValue: 'Messaging System',
key: 'msgSystem',
},
{
displayValue: 'Messaging Operation',
key: 'msgOperation',
},
{
displayValue: 'Component',
key: 'component',
},
];
export const functions: Dropdown[] = [
{ displayValue: 'Count', key: 'count' },
{ displayValue: 'Rate per sec', key: 'ratePerSec' },
{ displayValue: 'Sum(duration in ns)', key: 'sum' },
{ displayValue: 'Avg(duration in ns)', key: 'avg' },
{
displayValue: 'Max(duration in ns)',
key: 'max',
},
{
displayValue: 'Min(duration in ns)',
key: 'min',
},
{
displayValue: '50th percentile(duration in ns)',
key: 'p50',
},
{
displayValue: '90th percentile(duration in ns',
key: 'p90',
},
{
displayValue: '95th percentile(duration in ns)',
key: 'p95',
},
{
displayValue: '99th percentile(duration in ns)',
key: 'p99',
},
];

View File

@ -0,0 +1,85 @@
import React from 'react';
import { Space, SelectProps } from 'antd';
import { functions, groupBy } from './config';
import { useDispatch, useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { TraceReducer } from 'types/reducer/trace';
import AppActions from 'types/actions';
import {
UPDATE_SELECTED_FUNCTION,
UPDATE_SELECTED_GROUP_BY,
} from 'types/actions/trace';
import { Dispatch } from 'redux';
import { SelectComponent } from './styles';
import { SelectValue } from 'antd/lib/select';
const { Option } = SelectComponent;
const TraceGraphFilter = () => {
const { selectedFunction, selectedGroupBy } = useSelector<
AppState,
TraceReducer
>((state) => state.traces);
const dispatch = useDispatch<Dispatch<AppActions>>();
const onClickSelectedFunctionHandler: SelectProps<SelectValue>['onChange'] = (
ev,
) => {
const selected = functions.find((e) => e.key === ev);
if (selected) {
dispatch({
type: UPDATE_SELECTED_FUNCTION,
payload: {
selectedFunction: selected.key,
},
});
}
};
const onClickSelectedGroupByHandler: SelectProps<SelectValue>['onChange'] = (
ev,
) => {
const selected = groupBy.find((e) => e.key === ev);
if (selected) {
dispatch({
type: UPDATE_SELECTED_GROUP_BY,
payload: {
selectedGroupBy: selected.key,
},
});
}
};
return (
<Space>
<label>Function</label>
<SelectComponent
dropdownMatchSelectWidth
value={functions.find((e) => selectedFunction === e.key)?.displayValue}
onChange={onClickSelectedFunctionHandler}
>
{functions.map((value) => (
<Option value={value.key} key={value.key}>
{value.displayValue}
</Option>
))}
</SelectComponent>
<label>Group By</label>
<SelectComponent
dropdownMatchSelectWidth
value={groupBy.find((e) => selectedGroupBy === e.key)?.displayValue}
onChange={onClickSelectedGroupByHandler}
>
{groupBy.map((value) => (
<Option value={value.key} key={value.key}>
{value.displayValue}
</Option>
))}
</SelectComponent>
</Space>
);
};
export default TraceGraphFilter;

View File

@ -0,0 +1,9 @@
import { Select } from 'antd';
import styled from 'styled-components';
export const SelectComponent = styled(Select)`
&&& {
min-width: 10rem;
}
`;

View File

@ -0,0 +1,155 @@
import React from 'react';
import Table, { ColumnsType } from 'antd/lib/table';
import { TableProps, Tag } from 'antd';
import { connect, useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { TraceReducer } from 'types/reducer/trace';
import { bindActionCreators } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import AppActions from 'types/actions';
import {
GetSpansAggregate,
GetSpansAggregateProps,
} from 'store/actions/trace/getInitialSpansAggregate';
import { GlobalReducer } from 'types/reducer/globalTime';
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
import history from 'lib/history';
import ROUTES from 'constants/routes';
dayjs.extend(duration);
const TraceTable = ({ getSpansAggregate }: TraceProps) => {
const {
spansAggregate,
selectedFilter,
selectedTags,
filterLoading,
} = useSelector<AppState, TraceReducer>((state) => state.traces);
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { loading, total } = spansAggregate;
type TableType = FlatArray<TraceReducer['spansAggregate']['data'], 1>;
const columns: ColumnsType<TableType> = [
{
title: 'Date',
dataIndex: 'timestamp',
key: 'timestamp',
render: (value: TableType['timestamp']) => {
const day = dayjs(value);
return <div>{day.format('DD/MM/YYYY HH:MM:ss A')}</div>;
},
sorter: (a, b) => dayjs(a.timestamp).diff(dayjs(b.timestamp)),
},
{
title: 'Service',
dataIndex: 'serviceName',
key: 'serviceName',
sorter: (a, b) => a.serviceName.length - b.serviceName.length,
},
{
title: 'Operation',
dataIndex: 'operation',
key: 'operation',
},
{
title: 'Duration',
dataIndex: 'durationNano',
key: 'durationNano',
sorter: (a, b) => a.durationNano - b.durationNano,
render: (value: TableType['durationNano']) => {
return (
<div>
{`${dayjs
.duration({ milliseconds: value / 1000000 })
.asMilliseconds()} ms`}
</div>
);
},
},
{
title: 'Method',
dataIndex: 'httpMethod',
key: 'httpMethod',
render: (value: TableType['httpMethod']) => {
if (value.length === 0) {
return <div>-</div>;
}
return <Tag color="magenta">{value}</Tag>;
},
},
{
title: 'Status Code',
dataIndex: 'httpCode',
key: 'httpCode',
sorter: (a, b) => a.httpCode.length - b.httpCode.length,
render: (value: TableType['httpCode']) => {
if (value.length === 0) {
return <div>-</div>;
}
return <Tag color="magenta">{value}</Tag>;
},
},
];
const onChangeHandler: TableProps<TableType>['onChange'] = (props) => {
if (props.current && props.pageSize) {
getSpansAggregate({
maxTime: globalTime.maxTime,
minTime: globalTime.minTime,
selectedFilter,
current: props.current,
pageSize: props.pageSize,
selectedTags,
});
}
};
return (
<Table
onChange={onChangeHandler}
dataSource={spansAggregate.data}
loading={loading || filterLoading}
columns={columns}
onRow={(record) => ({
onClick: () => {
history.push({
pathname: ROUTES.TRACE + '/' + record.traceID,
state: {
spanId: record.spanID,
},
});
},
})}
size="middle"
rowKey={'timestamp'}
pagination={{
current: spansAggregate.currentPage,
pageSize: spansAggregate.pageSize,
responsive: true,
position: ['bottomLeft'],
total: total,
}}
/>
);
};
interface DispatchProps {
getSpansAggregate: (props: GetSpansAggregateProps) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
getSpansAggregate: bindActionCreators(GetSpansAggregate, dispatch),
});
type TraceProps = DispatchProps;
export default connect(null, mapDispatchToProps)(TraceTable);

View File

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

View File

@ -1,56 +0,0 @@
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

@ -1,127 +0,0 @@
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 { Store } from 'rc-field-form/lib/interface';
import { bindActionCreators } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import {
GetTraceVisualAggregates,
GetTraceVisualAggregatesProps,
} from 'store/actions/trace/getTraceVisualAgrregates';
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';
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: Store): 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

@ -1,34 +0,0 @@
import {
Card as CardComponent,
Form,
Space as SpaceComponent,
Typography,
} from 'antd';
import styled from 'styled-components';
export const CustomGraphContainer = styled.div`
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

@ -1,182 +0,0 @@
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 { 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

@ -1,160 +0,0 @@
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

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

View File

@ -1,390 +0,0 @@
import { Button, Input, notification, Typography } 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,
form_basefilter,
]);
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

@ -1,34 +0,0 @@
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

@ -1,141 +0,0 @@
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.TRACE + '/' + 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

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

View File

@ -0,0 +1,26 @@
import React, { useEffect } from 'react';
const useClickOutside = (
ref: React.RefObject<HTMLElement>,
callback: (e: HTMLElement) => void | null,
) => {
const listener = (e: Event) => {
const node = e?.target as HTMLElement;
if (ref.current && !ref.current.contains(node)) {
if (callback) {
callback(node);
}
}
};
useEffect(() => {
document.addEventListener('click', listener);
return () => {
document.removeEventListener('click', listener);
};
}, [ref, callback]);
};
export default useClickOutside;

View File

@ -0,0 +1,34 @@
import { useCallback } from 'react';
import debounce from 'lodash-es/debounce';
export interface DebouncedFunc<T extends (...args: any[]) => any> {
(...args: Parameters<T>): ReturnType<T> | undefined;
cancel(): void;
flush(): ReturnType<T> | undefined;
}
export type DebounceOptions = {
leading?: boolean;
maxWait?: number;
trailing?: boolean;
};
const defaultOptions: DebounceOptions = {
leading: false,
trailing: true,
};
const useDebouncedFn = <T extends (...args: any) => any>(
fn: T,
wait: number = 100,
options: DebounceOptions = defaultOptions,
dependencies?: ReadonlyArray<any>,
): DebouncedFunc<T> => {
const debounced = debounce(fn, wait, options);
return useCallback(debounced, dependencies || []);
};
export default useDebouncedFn;

View File

@ -0,0 +1,15 @@
const convertObjectIntoParams = (
props: Record<any, any>,
stringify = false,
) => {
return Object.keys(props)
.map(
(e) =>
`${e}=${
stringify ? encodeURIComponent(JSON.stringify(props[e])) : props[e]
}`,
)
.join('&');
};
export default convertObjectIntoParams;

View File

@ -0,0 +1,157 @@
import { Card } from 'antd';
import ROUTES from 'constants/routes';
import Filters from 'container/Trace/Filters';
import TraceGraph from 'container/Trace/Graph';
import Search from 'container/Trace/Search';
import TraceGraphFilter from 'container/Trace/TraceGraphFilter';
import TraceTable from 'container/Trace/TraceTable';
import history from 'lib/history';
import React, { useCallback, useEffect, useState } from 'react';
import { connect, useDispatch, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { GetInitialTraceFilter } from 'store/actions/trace/getInitialFilter';
import {
GetSpansAggregate,
GetSpansAggregateProps,
} from 'store/actions/trace/getInitialSpansAggregate';
import { GetSpans, GetSpansProps } from 'store/actions/trace/getSpans';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { RESET_TRACE_FILTER } from 'types/actions/trace';
import { GlobalReducer } from 'types/reducer/globalTime';
import { TraceReducer } from 'types/reducer/trace';
import {
Container,
LeftContainer,
RightContainer,
ClearAllFilter,
} from './styles';
const Trace = ({
getSpansAggregate,
getSpans,
getInitialFilter,
}: Props): JSX.Element => {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const dispatch = useDispatch<Dispatch<AppActions>>();
const [isChanged, setIsChanged] = useState<boolean>(true);
const {
selectedFilter,
spansAggregate,
selectedTags,
selectedFunction,
selectedGroupBy,
isFilterExclude,
} = useSelector<AppState, TraceReducer>((state) => state.traces);
useEffect(() => {
getInitialFilter(minTime, maxTime);
}, [maxTime, minTime, getInitialFilter, isChanged]);
useEffect(() => {
getSpansAggregate({
maxTime: maxTime,
minTime: minTime,
selectedFilter,
current: spansAggregate.currentPage,
pageSize: spansAggregate.pageSize,
selectedTags,
});
}, [selectedTags, selectedFilter, maxTime, minTime]);
useEffect(() => {
getSpans({
end: maxTime,
function: selectedFunction,
groupBy: selectedGroupBy,
selectedFilter,
selectedTags,
start: minTime,
step: 60,
isFilterExclude,
});
}, [
selectedFunction,
selectedGroupBy,
selectedFilter,
selectedTags,
maxTime,
minTime,
]);
useEffect(() => {
return () => {
dispatch({
type: RESET_TRACE_FILTER,
});
};
}, []);
const onClickHandler = useCallback((e) => {
e.preventDefault();
e.stopPropagation();
history.replace(ROUTES.TRACE);
dispatch({
type: RESET_TRACE_FILTER,
});
setIsChanged((state) => !state);
}, []);
return (
<>
<Search />
<Container>
<div>
<ClearAllFilter onClick={onClickHandler} type="primary">
Clear all filters
</ClearAllFilter>
<LeftContainer>
<Filters />
</LeftContainer>
</div>
<RightContainer>
<Card>
<TraceGraphFilter />
<TraceGraph />
</Card>
<Card style={{ marginTop: '2rem' }}>
<TraceTable />
</Card>
</RightContainer>
</Container>
</>
);
};
interface DispatchProps {
getSpansAggregate: (props: GetSpansAggregateProps) => void;
getSpans: (props: GetSpansProps) => void;
getInitialFilter: (
minTime: GlobalReducer['minTime'],
maxTime: GlobalReducer['maxTime'],
) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
getInitialFilter: bindActionCreators(GetInitialTraceFilter, dispatch),
getSpansAggregate: bindActionCreators(GetSpansAggregate, dispatch),
getSpans: bindActionCreators(GetSpans, dispatch),
});
type Props = DispatchProps;
export default connect(null, mapDispatchToProps)(Trace);

View File

@ -0,0 +1,37 @@
import styled from 'styled-components';
import { Button, Card } from 'antd';
export const Container = styled.div`
display: flex;
flex: 1;
min-height: 80vh;
margin-top: 1rem;
`;
export const LeftContainer = styled(Card)`
flex: 0.5;
width: 95%;
padding-right: 0.5rem;
.ant-card-body {
padding: 0;
}
`;
export const RightContainer = styled(Card)`
&&& {
flex: 2;
}
.ant-card-body {
padding: 0.5rem;
}
`;
export const ClearAllFilter = styled(Button)`
&&& {
width: 95%;
margin-bottom: 0.5rem;
}
`;

View File

@ -1,76 +0,0 @@
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,
GetInitialTraceDataProps,
ResetRaceData,
} 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, resetTraceData]);
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,201 +0,0 @@
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,173 @@
import { Dispatch, Store } from 'redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { GlobalReducer } from 'types/reducer/globalTime';
import getFiltersApi from 'api/trace/getFilters';
import {
parseSelectedFilter,
parseFilterToFetchData,
parseQueryIntoCurrent,
parseQueryIntoSelectedTags,
isTraceFilterEnum,
parseQueryIntoFilter,
parseIsSkippedSelection,
parseFilterExclude,
} from './util';
import {
UPDATE_ALL_FILTERS,
UPDATE_TRACE_FILTER_LOADING,
} from 'types/actions/trace';
import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace';
import { notification } from 'antd';
import xor from 'lodash-es/xor';
export const GetInitialTraceFilter = (
minTime: GlobalReducer['minTime'],
maxTime: GlobalReducer['maxTime'],
): ((
dispatch: Dispatch<AppActions>,
getState: Store<AppState>['getState'],
) => void) => {
return async (dispatch, getState): Promise<void> => {
try {
const query = location.search;
const { traces, globalTime } = getState();
if (globalTime.maxTime !== maxTime && globalTime.minTime !== minTime) {
return;
}
const getSelectedFilter = parseSelectedFilter(
query,
traces.selectedFilter,
true,
);
const getFilterToFetchData = parseFilterToFetchData(
query,
traces.filterToFetchData,
);
const getUserSelected = parseSelectedFilter(
query,
traces.userSelectedFilter,
);
const getIsFilterExcluded = parseFilterExclude(
query,
traces.isFilterExclude,
);
const parsedQueryCurrent = parseQueryIntoCurrent(
query,
traces.spansAggregate.currentPage,
);
const isSelectionSkipped = parseIsSkippedSelection(query);
const parsedSelectedTags = parseQueryIntoSelectedTags(
query,
traces.selectedTags,
);
const parsedFilter = parseQueryIntoFilter(query, traces.filter);
// now filter are not matching we need to fetch the data and make in sync
dispatch({
type: UPDATE_TRACE_FILTER_LOADING,
payload: {
filterLoading: true,
},
});
const response = await getFiltersApi({
end: String(maxTime),
getFilters: getFilterToFetchData.currentValue,
start: String(minTime),
other: Object.fromEntries(getSelectedFilter.currentValue),
isFilterExclude: getIsFilterExcluded.currentValue,
});
let preSelectedFilter: Map<TraceFilterEnum, string[]> = new Map(
getSelectedFilter.currentValue,
);
if (response.payload && !isSelectionSkipped.currentValue) {
const diff =
query.length === 0
? traces.filterToFetchData
: xor(traces.filterToFetchData, getFilterToFetchData.currentValue);
Object.keys(response.payload).map((key) => {
const value = response.payload[key];
Object.keys(value)
// remove maxDuration and minDuration filter from initial selection logic
.filter((e) => !['maxDuration', 'minDuration'].includes(e))
.map((preKey) => {
if (isTraceFilterEnum(key) && diff.find((v) => v === key)) {
// const preValue = preSelectedFilter?.get(key) || [];
const preValue = getUserSelected.currentValue?.get(key) || [];
// preSelectedFilter?.set(key, [...new Set([...preValue, preKey])]);
getUserSelected.currentValue.set(key, [
...new Set([...preValue, preKey]),
]);
}
});
});
}
if (response.statusCode === 200) {
const preResponseSelected: TraceReducer['filterResponseSelected'] = new Set();
const initialFilter = new Map<TraceFilterEnum, Record<string, string>>(
parsedFilter.currentValue,
);
Object.keys(response.payload).forEach((key) => {
const value = response.payload[key];
if (isTraceFilterEnum(key)) {
Object.keys(value).forEach((e) => preResponseSelected.add(e));
initialFilter.set(key, {
...initialFilter.get(key),
...value,
});
}
});
dispatch({
type: UPDATE_ALL_FILTERS,
payload: {
filter: initialFilter,
selectedFilter: preSelectedFilter,
filterToFetchData: getFilterToFetchData.currentValue,
current: parsedQueryCurrent.currentValue,
selectedTags: parsedSelectedTags.currentValue,
userSelected: getUserSelected.currentValue,
isFilterExclude: getIsFilterExcluded.currentValue,
},
});
} else {
notification.error({
message: response.error || 'Something went wrong',
});
}
dispatch({
type: UPDATE_TRACE_FILTER_LOADING,
payload: {
filterLoading: false,
},
});
} catch (error) {
console.log(error);
dispatch({
type: UPDATE_TRACE_FILTER_LOADING,
payload: {
filterLoading: false,
},
});
}
};
};

View File

@ -0,0 +1,115 @@
import { Dispatch, Store } from 'redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { UPDATE_SPANS_AGGREEGATE } from 'types/actions/trace';
import getSpansAggregate from 'api/trace/getSpansAggregate';
import { GlobalReducer } from 'types/reducer/globalTime';
import { TraceReducer } from 'types/reducer/trace';
import { notification } from 'antd';
export const GetSpansAggregate = (
props: GetSpansAggregateProps,
): ((
dispatch: Dispatch<AppActions>,
getState: Store<AppState>['getState'],
) => void) => {
return async (dispatch, getState): Promise<void> => {
const { traces, globalTime } = getState();
const { spansAggregate } = traces;
if (
globalTime.maxTime !== props.maxTime &&
globalTime.minTime !== props.minTime
) {
return;
}
if (traces.filterLoading) {
return;
}
try {
// triggering loading
dispatch({
type: UPDATE_SPANS_AGGREEGATE,
payload: {
spansAggregate: {
currentPage: props.current,
loading: true,
data: spansAggregate.data,
error: false,
total: spansAggregate.total,
pageSize: props.pageSize,
},
},
});
const response = await getSpansAggregate({
end: props.maxTime,
start: props.minTime,
selectedFilter: props.selectedFilter,
limit: props.pageSize,
offset: props.current * props.pageSize - props.pageSize,
selectedTags: props.selectedTags,
isFilterExclude: traces.isFilterExclude,
});
if (response.statusCode === 200) {
dispatch({
type: UPDATE_SPANS_AGGREEGATE,
payload: {
spansAggregate: {
currentPage: props.current,
loading: false,
data: response.payload.spans,
error: false,
total: response.payload.totalSpans,
pageSize: props.pageSize,
},
},
});
} else {
notification.error({
message: response.error || 'Something went wrong',
});
dispatch({
type: UPDATE_SPANS_AGGREEGATE,
payload: {
spansAggregate: {
currentPage: props.current,
loading: false,
data: spansAggregate.data,
error: true,
total: spansAggregate.total,
pageSize: props.pageSize,
},
},
});
}
} catch (error) {
dispatch({
type: UPDATE_SPANS_AGGREEGATE,
payload: {
spansAggregate: {
currentPage: props.current,
loading: false,
data: spansAggregate.data,
error: true,
total: spansAggregate.total,
pageSize: props.pageSize,
},
},
});
}
};
};
export interface GetSpansAggregateProps {
maxTime: GlobalReducer['maxTime'];
minTime: GlobalReducer['minTime'];
selectedFilter: TraceReducer['selectedFilter'];
current: TraceReducer['spansAggregate']['currentPage'];
pageSize: TraceReducer['spansAggregate']['pageSize'];
selectedTags: TraceReducer['selectedTags'];
}

View File

@ -0,0 +1,96 @@
import { Dispatch, Store } from 'redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import {
UPDATE_TRACE_GRAPH_ERROR,
UPDATE_TRACE_GRAPH_LOADING,
UPDATE_TRACE_GRAPH_SUCCESS,
} from 'types/actions/trace';
import getSpans from 'api/trace/getSpans';
import { Props } from 'types/api/trace/getSpans';
import { notification } from 'antd';
export const GetSpans = (
props: GetSpansProps,
): ((
dispatch: Dispatch<AppActions>,
getState: Store<AppState>['getState'],
) => void) => {
return async (dispatch, getState): Promise<void> => {
try {
const { traces, globalTime } = getState();
const { spansGraph } = traces;
if (globalTime.maxTime !== props.end && globalTime.minTime !== props.start) {
return;
}
const { selectedTime } = globalTime;
if (traces.filterLoading) {
return;
}
// @TODO refactor this logic when share url functionlity is updated
const isCustomSelected = selectedTime === 'custom';
const end = isCustomSelected
? globalTime.maxTime + 15 * 60 * 1000000000
: props.end;
const start = isCustomSelected
? globalTime.minTime - 15 * 60 * 1000000000
: props.start;
if (!spansGraph.loading) {
dispatch({
type: UPDATE_TRACE_GRAPH_LOADING,
payload: {
loading: true,
},
});
}
const response = await getSpans({
end: end,
function: props.function,
groupBy: props.groupBy,
selectedFilter: props.selectedFilter,
selectedTags: props.selectedTags,
start: start,
step: props.step,
isFilterExclude: props.isFilterExclude,
});
if (response.statusCode === 200) {
dispatch({
type: UPDATE_TRACE_GRAPH_SUCCESS,
payload: {
data: response.payload,
},
});
} else {
notification.error({
message: response.error || 'Something went wrong',
});
dispatch({
type: UPDATE_TRACE_GRAPH_ERROR,
payload: {
error: true,
errorMessage: response.error || 'Something went wrong',
},
});
}
} catch (error) {
dispatch({
type: UPDATE_TRACE_GRAPH_ERROR,
payload: {
error: true,
errorMessage: (error as Error)?.toString() || 'Something went wrong',
},
});
}
};
};
export type GetSpansProps = Props;

View File

@ -1,92 +0,0 @@
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

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

View File

@ -1,12 +0,0 @@
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,36 @@
import { TraceReducer } from 'types/reducer/trace';
import { ParsedUrl } from '../util';
export const parseQueryIntoCurrent = (
query: string,
stateCurrent: TraceReducer['spansAggregate']['currentPage'],
): ParsedUrl<TraceReducer['spansAggregate']['currentPage']> => {
const url = new URLSearchParams(query);
let current = 1;
const selected = url.get('current');
if (selected) {
try {
const parsedValue = JSON.parse(decodeURIComponent(selected));
if (Number.isInteger(parsedValue)) {
current = parseInt(parsedValue, 10);
}
} catch (error) {
console.log('error while parsing json');
}
}
if (selected) {
return {
currentValue: parseInt(selected, 10),
urlValue: current,
};
}
return {
currentValue: stateCurrent,
urlValue: current,
};
};

View File

@ -0,0 +1,43 @@
import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace';
import { isTraceFilterEnum, ParsedUrl } from '../util';
export const parseQueryIntoFilter = (
query: string,
stateFilter: TraceReducer['filter'],
): ParsedUrl<TraceReducer['filter']> => {
const urlFilter = new Map<TraceFilterEnum, Record<string, string>>();
const url = new URLSearchParams(query);
const selected = url.get('filter');
if (selected) {
try {
const parsedValue = JSON.parse(selected);
if (typeof parsedValue === 'object') {
Object.keys(parsedValue).forEach((key) => {
if (isTraceFilterEnum(key)) {
const value = parsedValue[key];
if (typeof value === 'object') {
urlFilter.set(key, value);
}
}
});
}
} catch (error) {
console.log(error);
}
}
if (selected) {
return {
currentValue: urlFilter,
urlValue: urlFilter,
};
}
return {
currentValue: stateFilter,
urlValue: urlFilter,
};
};

View File

@ -0,0 +1,37 @@
import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace';
import { ParsedUrl } from '../util';
export const parseFilterToFetchData = (
query: string,
stateTraceFilterData: TraceReducer['filterToFetchData'],
): ParsedUrl<TraceFilterEnum[]> => {
const url = new URLSearchParams(query);
let filterToFetchData: TraceFilterEnum[] = [];
const selected = url.get('filterToFetchData');
if (selected) {
try {
const parsedValue = JSON.parse(decodeURIComponent(selected));
if (Array.isArray(parsedValue)) {
filterToFetchData.push(...parsedValue);
}
} catch (error) {
//error while parsing json
}
}
if (selected) {
return {
currentValue: filterToFetchData,
urlValue: filterToFetchData,
};
}
return {
currentValue: stateTraceFilterData,
urlValue: filterToFetchData,
};
};

View File

@ -0,0 +1,8 @@
export * from './minMaxTime';
export * from './selectedFilter';
export * from './filterToFetchData';
export * from './selectedTags';
export * from './filter';
export * from './skippedSelected';
export * from './current';
export * from './isFilterExclude';

View File

@ -0,0 +1,44 @@
import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace';
import { isTraceFilterEnum, ParsedUrl } from '../util';
export const parseFilterExclude = (
query: string,
stateFilterExclude: TraceReducer['isFilterExclude'],
): ParsedUrl<TraceReducer['isFilterExclude']> => {
const currentFilter = new Map<TraceFilterEnum, boolean>();
const url = new URLSearchParams(query);
const isPresent = url.get('isFilterExclude');
if (isPresent) {
try {
const parsedValue = JSON.parse(isPresent);
if (typeof parsedValue === 'object') {
Object.keys(parsedValue).forEach((key) => {
if (isTraceFilterEnum(key)) {
const keyValue = parsedValue[key];
if (typeof keyValue === 'boolean') {
currentFilter.set(key, keyValue);
}
}
});
}
} catch (error) {
// parsing the value
}
}
if (isPresent) {
return {
currentValue: currentFilter,
urlValue: currentFilter,
};
}
return {
currentValue: stateFilterExclude,
urlValue: currentFilter,
};
};

View File

@ -0,0 +1,20 @@
import { GlobalTime } from 'types/actions/globalTime';
export const parseMinMaxTime = (query: string): GlobalTime => {
const url = new URLSearchParams(query);
let maxTime = 0;
let minTime = 0;
const urlMaxTime = url.get('minTime');
const urlMinTime = url.get('maxTime');
if (urlMaxTime && urlMinTime) {
maxTime = parseInt(urlMaxTime);
minTime = parseInt(urlMinTime);
}
return {
maxTime,
minTime,
};
};

View File

@ -0,0 +1,43 @@
import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace';
import { isTraceFilterEnum, ParsedUrl } from '../util';
export const parseSelectedFilter = (
query: string,
selectedFilter: TraceReducer['selectedFilter'],
isUserSelected = false,
): ParsedUrl<Map<TraceFilterEnum, string[]>> => {
const url = new URLSearchParams(query);
const filters = new Map<TraceFilterEnum, string[]>();
const title = isUserSelected ? 'selected' : 'userSelectedFilter';
const selected = url.get(title);
if (selected) {
try {
const parsedValue = JSON.parse(decodeURIComponent(selected));
if (typeof parsedValue === 'object') {
Object.keys(parsedValue).forEach((e) => {
if (isTraceFilterEnum(e)) {
filters.set(e, parsedValue[e]);
}
});
}
} catch (error) {
// if the parsing error happens
}
}
if (selected) {
return {
urlValue: filters,
currentValue: filters,
};
}
return {
urlValue: filters,
currentValue: selectedFilter,
};
};

View File

@ -0,0 +1,37 @@
import { TraceReducer } from 'types/reducer/trace';
import { ParsedUrl } from '../util';
export const parseQueryIntoSelectedTags = (
query: string,
stateSelectedTags: TraceReducer['selectedTags'],
): ParsedUrl<TraceReducer['selectedTags']> => {
const url = new URLSearchParams(query);
let selectedTags: TraceReducer['selectedTags'] = [];
const querySelectedTags = url.get('selectedTags');
if (querySelectedTags) {
try {
const parsedQuerySelectedTags = JSON.parse(querySelectedTags);
if (Array.isArray(parsedQuerySelectedTags)) {
selectedTags = parsedQuerySelectedTags;
}
} catch (error) {
//error while parsing
}
}
if (querySelectedTags) {
return {
currentValue: selectedTags,
urlValue: selectedTags,
};
}
return {
currentValue: stateSelectedTags,
urlValue: selectedTags,
};
};

View File

@ -0,0 +1,33 @@
import { ParsedUrl } from '../util';
export const parseIsSkippedSelection = (query: string): ParsedUrl<boolean> => {
const url = new URLSearchParams(query);
let current = false;
const isSkippedSelected = url.get('isSelectedFilterSkipped');
if (isSkippedSelected) {
try {
const parsedValue = JSON.parse(isSkippedSelected);
if (typeof parsedValue === 'boolean') {
current = parsedValue;
}
} catch (error) {
current = false;
}
}
if (isSkippedSelected) {
return {
currentValue: current,
urlValue: current,
};
}
return {
currentValue: current,
urlValue: current,
};
};

View File

@ -1,10 +0,0 @@
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,50 @@
import { Dispatch, Store } from 'redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { TraceFilterEnum } from 'types/reducer/trace';
import { updateURL } from './util';
export const SelectedTraceFilter = (props: {
topic: TraceFilterEnum;
value: string;
}): ((
dispatch: Dispatch<AppActions>,
getState: Store<AppState>['getState'],
) => void) => {
return (_, getState): void => {
const { topic, value } = props;
const { traces } = getState();
const filter = traces.selectedFilter;
const isTopicPresent = filter.get(topic);
// append the value
if (!isTopicPresent) {
filter.set(props.topic, [props.value]);
} else {
const isValuePresent =
isTopicPresent.find((e) => e === props.value) !== undefined;
// check the value if present then remove the value
if (isValuePresent) {
filter.set(
props.topic,
isTopicPresent.filter((e) => e !== value),
);
} else {
// if not present add into the array of string
filter.set(props.topic, [...isTopicPresent, props.value]);
}
}
updateURL(
filter,
traces.filterToFetchData,
traces.spansAggregate.currentPage,
traces.selectedTags,
traces.filter,
traces.isFilterExclude,
);
};
};

View File

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

View File

@ -1,16 +0,0 @@
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

@ -1,164 +0,0 @@
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

@ -1,16 +0,0 @@
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

@ -1,16 +0,0 @@
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

@ -1,16 +0,0 @@
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

@ -1,16 +0,0 @@
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

@ -1,16 +0,0 @@
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

@ -1,94 +0,0 @@
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

@ -1,16 +0,0 @@
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

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

View File

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

View File

@ -0,0 +1,79 @@
import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace';
import history from 'lib/history';
import { AllTraceFilterEnum } from 'container/Trace/Filters';
import { PayloadProps as GetFilterPayload } from 'types/api/trace/getFilters';
export * from './parseFilter';
export interface ParsedUrl<T> {
currentValue: T;
urlValue: T;
}
export function isTraceFilterEnum(
value: TraceFilterEnum | string,
): value is TraceFilterEnum {
if (AllTraceFilterEnum.find((enums) => enums === value)) {
return true;
}
return false;
}
export const updateURL = (
selectedFilter: TraceReducer['selectedFilter'],
filterToFetchData: TraceReducer['filterToFetchData'],
current: TraceReducer['spansAggregate']['total'],
selectedTags: TraceReducer['selectedTags'],
filter: TraceReducer['filter'],
isFilterExclude: TraceReducer['isFilterExclude'],
userSelectedFilter: TraceReducer['userSelectedFilter'],
) => {
const search = new URLSearchParams(location.search);
const preResult: { key: string; value: string }[] = [];
const keyToSkip = [
'selected',
'filterToFetchData',
'current',
'selectedTags',
'filter',
'isFilterExclude',
'userSelectedFilter',
];
search.forEach((value, key) => {
if (!keyToSkip.includes(key)) {
preResult.push({
key,
value,
});
}
});
history.replace(
`${history.location.pathname}?selected=${JSON.stringify(
Object.fromEntries(selectedFilter),
)}&filterToFetchData=${JSON.stringify(
filterToFetchData,
)}&current=${current}&selectedTags=${JSON.stringify(
selectedTags,
)}&filter=${JSON.stringify(Object.fromEntries(filter))}&${preResult
.map((e) => `${e.key}=${e.value}`)
.join('&')}&isFilterExclude=${JSON.stringify(
Object.fromEntries(isFilterExclude),
)}&userSelectedFilter=${JSON.stringify(
Object.fromEntries(userSelectedFilter),
)}`,
);
};
export const getFilter = (data: GetFilterPayload): TraceReducer['filter'] => {
const filter = new Map<TraceFilterEnum, Record<string, string>>();
Object.keys(data).forEach((key) => {
const value = data[key];
if (isTraceFilterEnum(key)) {
filter.set(key, value);
}
});
return filter;
};

View File

@ -5,16 +5,15 @@ import dashboardReducer from './dashboard';
import globalTimeReducer from './global';
import metricsReducers from './metric';
import { ServiceMapReducer } from './serviceMap';
import { traceReducer } from './trace';
import traceReducer from './trace';
import TraceFilterReducer from './traceFilters';
import { traceItemReducer, tracesReducer } from './traces';
import { traceItemReducer } from './traces';
import { usageDataReducer } from './usage';
const reducers = combineReducers({
traceFilters: TraceFilterReducer,
traces: tracesReducer,
traces: traceReducer,
traceItem: traceItemReducer,
trace: traceReducer,
usageDate: usageDataReducer,
globalTime: globalTimeReducer,
serviceMap: ServiceMapReducer,

View File

@ -1,200 +1,199 @@
import {
GET_TRACE_INITIAL_DATA_ERROR,
GET_TRACE_INITIAL_DATA_SUCCESS,
GET_TRACE_LOADING_END,
GET_TRACE_LOADING_START,
RESET_TRACE_DATA,
SELECT_TRACE_FILTER,
TraceActions,
UPDATE_AGGREGATES,
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,
UPDATE_TRACE_FILTER,
UPDATE_TRACE_FILTER_LOADING,
UPDATE_ALL_FILTERS,
UPDATE_SELECTED_TAGS,
UPDATE_SPANS_AGGREEGATE,
UPDATE_TAG_MODAL_VISIBLITY,
UPDATE_IS_TAG_ERROR,
UPDATE_SELECTED_FUNCTION,
UPDATE_SELECTED_GROUP_BY,
UPDATE_TRACE_GRAPH_LOADING,
UPDATE_TRACE_GRAPH_ERROR,
UPDATE_TRACE_GRAPH_SUCCESS,
RESET_TRACE_FILTER,
UPDATE_FILTER_RESPONSE_SELECTED,
UPDATE_FILTER_EXCLUDE,
} from 'types/actions/trace';
import { TraceReducer } from 'types/reducer/trace';
import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace';
const intitalState: TraceReducer = {
error: false,
errorMessage: '',
loading: true,
operationsList: [],
selectedKind: '',
selectedLatency: {
max: '',
min: '',
},
selectedOperation: '',
selectedService: '',
const initialValue: TraceReducer = {
filter: new Map(),
filterToFetchData: ['duration', 'status', 'serviceName'],
filterLoading: true,
filterResponseSelected: new Set(),
selectedFilter: new Map(),
selectedTags: [],
serviceList: [],
spanList: [],
tagsSuggestions: [],
selectedAggOption: 'count',
selectedEntity: 'calls',
spansAggregate: [],
spansLoading: false,
isTagModalOpen: false,
isTagModalError: false,
isFilterExclude: new Map<TraceFilterEnum, boolean>([]),
userSelectedFilter: new Map(),
spansAggregate: {
currentPage: 1,
loading: false,
data: [],
error: false,
total: 0,
pageSize: 10,
},
selectedGroupBy: '',
selectedFunction: 'count',
spansGraph: {
error: false,
errorMessage: '',
loading: true,
payload: { items: {} },
},
};
export const traceReducer = (
state = intitalState,
const traceReducer = (
state = initialValue,
action: TraceActions,
): TraceReducer => {
switch (action.type) {
case GET_TRACE_INITIAL_DATA_ERROR: {
case UPDATE_TRACE_FILTER: {
return {
...state,
errorMessage: action.payload.errorMessage,
loading: false,
error: true,
filter: action.payload.filter,
};
}
case GET_TRACE_LOADING_START: {
return {
...state,
loading: true,
spansLoading: true,
};
}
case GET_TRACE_INITIAL_DATA_SUCCESS: {
case UPDATE_ALL_FILTERS: {
const { payload } = action;
const {
serviceList,
operationList,
tagsSuggestions,
selectedOperation,
selectedService,
filter,
filterToFetchData,
selectedFilter,
current,
selectedTags,
spansList,
selectedKind,
selectedLatency,
spansAggregate,
} = action.payload;
userSelected,
isFilterExclude,
} = payload;
return {
...state,
serviceList: serviceList,
tagsSuggestions,
selectedOperation,
selectedService,
filter,
filterToFetchData,
selectedFilter,
selectedTags,
spanList: spansList,
operationsList: operationList,
error: false,
selectedKind,
selectedLatency,
spansAggregate,
spansLoading: false,
userSelectedFilter: userSelected,
isFilterExclude,
spansAggregate: {
...state.spansAggregate,
currentPage: current,
},
};
}
case UPDATE_TRACE_SELECTED_KIND: {
case UPDATE_TRACE_FILTER_LOADING: {
return {
...state,
selectedKind: action.payload.selectedKind,
filterLoading: action.payload.filterLoading,
};
}
case UPDATE_TRACE_SELECTED_LATENCY_VALUE: {
case SELECT_TRACE_FILTER: {
return {
...state,
selectedLatency: action.payload.selectedLatency,
selectedFilter: action.payload.selectedFilter,
};
}
case UPDATE_TRACE_SELECTED_OPERATION: {
case RESET_TRACE_FILTER: {
return {
...state,
selectedOperation: action.payload.selectedOperation,
...initialValue,
};
}
case UPDATE_TRACE_SELECTED_SERVICE: {
return {
...state,
selectedService: action.payload.selectedService,
};
}
case UPDATE_TRACE_SELECTED_TAGS: {
case UPDATE_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: {
case UPDATE_SPANS_AGGREEGATE: {
return {
...state,
spansAggregate: action.payload.spansAggregate,
selectedAggOption: action.payload.selectedAggOption,
selectedEntity: action.payload.selectedEntity,
};
}
case UPDATE_TAG_MODAL_VISIBLITY: {
return {
...state,
isTagModalOpen: action.payload.isTagModalOpen,
};
}
case UPDATE_IS_TAG_ERROR: {
return {
...state,
isTagModalError: action.payload.isTagModalError,
};
}
case UPDATE_SELECTED_FUNCTION: {
return {
...state,
selectedFunction: action.payload.selectedFunction,
};
}
case UPDATE_SELECTED_GROUP_BY: {
return {
...state,
selectedGroupBy: action.payload.selectedGroupBy,
};
}
case UPDATE_TRACE_GRAPH_LOADING: {
return {
...state,
spansGraph: {
...state.spansGraph,
loading: action.payload.loading,
},
};
}
case UPDATE_TRACE_GRAPH_ERROR: {
return {
...state,
spansGraph: {
...state.spansGraph,
error: action.payload.error,
errorMessage: action.payload.errorMessage,
loading: false,
},
};
}
case UPDATE_TRACE_GRAPH_SUCCESS: {
return {
...state,
spansGraph: {
...state.spansGraph,
payload: action.payload.data,
loading: false,
error: false,
},
};
}
case UPDATE_FILTER_RESPONSE_SELECTED: {
return {
...state,
filterResponseSelected: action.payload.filterResponseSelected,
};
}
case UPDATE_FILTER_EXCLUDE: {
return {
...state,
isFilterExclude: action.payload.isFilterExclude,
};
}
@ -202,3 +201,5 @@ export const traceReducer = (
return state;
}
};
export default traceReducer;

View File

@ -1,151 +1,172 @@
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;
}
export const UPDATE_TRACE_FILTER = 'UPDATE_TRACE_FILTER';
export const GET_TRACE_FILTER = 'GET_TRACE_FILTER';
export const UPDATE_TRACE_FILTER_LOADING = 'UPDATE_TRACE_FILTER_LOADING';
interface UpdateSpansLoading {
type: typeof UPDATE_SPANS_LOADING;
export const SELECT_TRACE_FILTER = 'SELECT_TRACE_FILTER';
export const UPDATE_ALL_FILTERS = 'UPDATE_ALL_FILTERS';
export const UPDATE_SELECTED_TAGS = 'UPDATE_SELECTED_TAGS';
export const UPDATE_TAG_MODAL_VISIBLITY = 'UPDATE_TAG_MODAL_VISIBLITY';
export const UPDATE_SPANS_AGGREEGATE = 'UPDATE_SPANS_AGGREEGATE';
export const UPDATE_IS_TAG_ERROR = 'UPDATE_IS_TAG_ERROR';
export const UPDATE_SELECTED_FUNCTION = 'UPDATE_SELECTED_FUNCTION';
export const UPDATE_SELECTED_GROUP_BY = 'UPDATE_SELECTED_GROUP_BY';
export const UPDATE_TRACE_GRAPH_LOADING = 'UPDATE_TRACE_GRAPH_LOADING';
export const UPDATE_TRACE_GRAPH_ERROR = 'UPDATE_TRACE_GRAPH_ERROR';
export const UPDATE_TRACE_GRAPH_SUCCESS = 'UPDATE_TRACE_GRAPH_SUCCESS';
export const RESET_TRACE_FILTER = 'RESET_TRACE_FILTER';
export const UPDATE_FILTER_RESPONSE_SELECTED =
'UPDATE_FILTER_RESPONSE_SELECTED';
export const UPDATE_FILTER_EXCLUDE = 'UPDATE_FILTER_EXCLUDE';
export interface UpdateFilter {
type: typeof UPDATE_TRACE_FILTER;
payload: {
loading: boolean;
filter: TraceReducer['filter'];
};
}
interface GetTraceInitialData {
type: typeof GET_TRACE_INITIAL_DATA_SUCCESS;
export interface UpdateSpansAggregate {
type: typeof UPDATE_SPANS_AGGREEGATE;
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;
export interface UpdateTagVisiblity {
type: typeof UPDATE_TAG_MODAL_VISIBLITY;
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'];
isTagModalOpen: TraceReducer['isTagModalOpen'];
};
}
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;
export interface UpdateSelectedTags {
type: typeof UPDATE_SELECTED_TAGS;
payload: {
selectedTags: TraceReducer['selectedTags'];
spansList: TraceReducer['spanList'];
spansAggregate: TraceReducer['spansAggregate'];
};
}
interface UpdateSelectedAggOption {
type: typeof UPDATE_SELECTED_AGG_OPTION;
export interface UpdateSelected {
type: typeof UPDATE_FILTER_RESPONSE_SELECTED;
payload: {
selectedAggOption: TraceReducer['selectedAggOption'];
filterResponseSelected: TraceReducer['filterResponseSelected'];
};
}
interface UpdateSelectedEntity {
type: typeof UPDATE_SELECTED_ENTITY;
export interface UpdateAllFilters {
type: typeof UPDATE_ALL_FILTERS;
payload: {
selectedEntity: TraceReducer['selectedEntity'];
filter: TraceReducer['filter'];
selectedFilter: TraceReducer['selectedFilter'];
filterToFetchData: TraceReducer['filterToFetchData'];
current: TraceReducer['spansAggregate']['currentPage'];
selectedTags: TraceReducer['selectedTags'];
userSelected: TraceReducer['userSelectedFilter'];
isFilterExclude: TraceReducer['isFilterExclude'];
};
}
interface UpdateAggregates {
type: typeof UPDATE_AGGREGATES;
export interface UpdateFilterLoading {
type: typeof UPDATE_TRACE_FILTER_LOADING;
payload: {
spansAggregate: TraceReducer['spansAggregate'];
selectedEntity: TraceReducer['selectedEntity'];
selectedAggOption: TraceReducer['selectedAggOption'];
filterLoading: TraceReducer['filterLoading'];
};
}
interface ResetTraceData {
type: typeof RESET_TRACE_DATA;
export interface SelectTraceFilter {
type: typeof SELECT_TRACE_FILTER;
payload: {
selectedFilter: TraceReducer['selectedFilter'];
};
}
export interface ResetTraceFilter {
type: typeof RESET_TRACE_FILTER;
}
export interface GetTraceFilter {
type: typeof GET_TRACE_FILTER;
payload: {
filter: TraceReducer['filter'];
};
}
export interface UpdateIsTagError {
type: typeof UPDATE_IS_TAG_ERROR;
payload: {
isTagModalError: TraceReducer['isTagModalError'];
};
}
export interface UpdateSelectedGroupBy {
type: typeof UPDATE_SELECTED_GROUP_BY;
payload: {
selectedGroupBy: TraceReducer['selectedGroupBy'];
};
}
export interface UpdateSelectedFunction {
type: typeof UPDATE_SELECTED_FUNCTION;
payload: {
selectedFunction: TraceReducer['selectedFunction'];
};
}
export interface UpdateSpanLoading {
type: typeof UPDATE_TRACE_GRAPH_LOADING;
payload: {
loading: TraceReducer['spansGraph']['loading'];
};
}
export interface UpdateSpansError {
type: typeof UPDATE_TRACE_GRAPH_ERROR;
payload: {
error: TraceReducer['spansGraph']['error'];
errorMessage: TraceReducer['spansGraph']['errorMessage'];
};
}
export interface UpdateFilterExclude {
type: typeof UPDATE_FILTER_EXCLUDE;
payload: {
isFilterExclude: TraceReducer['isFilterExclude'];
};
}
export interface UpdateSpans {
type: typeof UPDATE_TRACE_GRAPH_SUCCESS;
payload: {
data: TraceReducer['spansGraph']['payload'];
};
}
export interface ResetTraceFilter {
type: typeof RESET_TRACE_FILTER;
}
export type TraceActions =
| GetTraceLoading
| GetTraceInitialData
| GetTraceInitialDataError
| UpdateTraceSelectedService
| UpdateTraceSelectedLatencyValue
| UpdateTraceSelectedKind
| UpdateTraceSelectedOperation
| UpdateTraceSelectedTags
| UpdateSelectedDate
| UpdateSelectedAggOption
| UpdateSelectedEntity
| UpdateSpansLoading
| ResetTraceData
| UpdateAggregates;
| UpdateFilter
| GetTraceFilter
| UpdateFilterLoading
| SelectTraceFilter
| UpdateAllFilters
| UpdateSelectedTags
| UpdateTagVisiblity
| UpdateSpansAggregate
| UpdateIsTagError
| UpdateSelectedGroupBy
| UpdateSelectedFunction
| UpdateSpanLoading
| UpdateSpansError
| UpdateSpans
| ResetTraceFilter
| UpdateSelected
| UpdateFilterExclude;

View File

@ -0,0 +1,15 @@
import { TraceReducer } from 'types/reducer/trace';
export interface Props {
start: string;
end: string;
getFilters: string[];
other: {
[k: string]: string[];
};
isFilterExclude: TraceReducer['isFilterExclude'];
}
export interface PayloadProps {
[key: string]: Record<string, string>;
}

View File

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

View File

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

View File

@ -1,20 +1,16 @@
import { TraceReducer } from 'types/reducer/trace';
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;
selectedFilter: TraceReducer['selectedFilter'];
limit: number;
offset: number;
selectedTags: TraceReducer['selectedTags'];
isFilterExclude: TraceReducer['isFilterExclude'];
}
interface Timestamp {
timestamp: number;
value: number;
}
export type PayloadProps = Timestamp[];
export type PayloadProps = {
spans: TraceReducer['spansAggregate']['data'];
totalSpans: number;
};

Some files were not shown because too many files have changed in this diff Show More