feat: add the list view for the traces explorer (#2947)

* feat: add dynamic table based on query

* feat: add the list view for the traces explorer

* fix: fix the options menu

* feat: update the list view columns config for the traces explorer

* feat: fix columns for the list view for the traces explorer page

* feat: update customization columns for the list view from the traces explorer

* feat: add error msg for the list view, fix creating data for the table

---------

Co-authored-by: Yevhen Shevchenko <y.shevchenko@seedium.io>
Co-authored-by: Nazarenko19 <danil.nazarenko2000@gmail.com>
Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
This commit is contained in:
dnazarenkoo 2023-07-05 10:21:22 +03:00 committed by GitHub
parent 2722538e82
commit 5540692500
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 318 additions and 33 deletions

View File

@ -1,6 +1,7 @@
import { RadioChangeEvent } from 'antd'; import { RadioChangeEvent } from 'antd';
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys'; import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
import { QueryBuilderKeys } from 'constants/queryBuilder'; import { QueryBuilderKeys } from 'constants/queryBuilder';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQueryData from 'hooks/useUrlQueryData'; import useUrlQueryData from 'hooks/useUrlQueryData';
import { useCallback, useEffect, useMemo } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
@ -28,6 +29,8 @@ const useOptionsMenu = ({
aggregateOperator, aggregateOperator,
initialOptions = {}, initialOptions = {},
}: UseOptionsMenuProps): UseOptionsMenu => { }: UseOptionsMenuProps): UseOptionsMenu => {
const { notifications } = useNotifications();
const { const {
query: optionsQuery, query: optionsQuery,
queryData: optionsQueryData, queryData: optionsQueryData,
@ -91,14 +94,22 @@ const useOptionsMenu = ({
const handleRemoveSelectedColumn = useCallback( const handleRemoveSelectedColumn = useCallback(
(columnKey: string) => { (columnKey: string) => {
redirectWithOptionsData({ const newSelectedColumns = optionsQueryData?.selectColumns?.filter(
...defaultOptionsQuery, ({ id }) => id !== columnKey,
selectColumns: optionsQueryData?.selectColumns?.filter( );
({ id }) => id !== columnKey,
), if (!newSelectedColumns.length) {
}); notifications.error({
message: 'There must be at least one selected column',
});
} else {
redirectWithOptionsData({
...defaultOptionsQuery,
selectColumns: newSelectedColumns,
});
}
}, },
[optionsQueryData, redirectWithOptionsData], [optionsQueryData, notifications, redirectWithOptionsData],
); );
const handleFormatChange = useCallback( const handleFormatChange = useCallback(

View File

@ -1,7 +1,10 @@
import { initialQueriesMap } from 'constants/queryBuilder'; import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { PANEL_TYPES_QUERY } from 'constants/queryBuilderQueryNames';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { DataSource } from 'types/common/queryBuilder'; import { DataSource } from 'types/common/queryBuilder';
@ -19,10 +22,15 @@ function TimeSeriesViewContainer({
GlobalReducer GlobalReducer
>((state) => state.globalTime); >((state) => state.globalTime);
const { queryData: panelTypeParam } = useUrlQueryData<GRAPH_TYPES>(
PANEL_TYPES_QUERY,
PANEL_TYPES.TIME_SERIES,
);
const { data, isLoading, isError } = useGetQueryRange( const { data, isLoading, isError } = useGetQueryRange(
{ {
query: stagedQuery || initialQueriesMap[dataSource], query: stagedQuery || initialQueriesMap[dataSource],
graphType: 'graph', graphType: panelTypeParam,
selectedTime: 'GLOBAL_TIME', selectedTime: 'GLOBAL_TIME',
globalSelectedInterval: globalSelectedTime, globalSelectedInterval: globalSelectedTime,
params: { params: {
@ -37,7 +45,7 @@ function TimeSeriesViewContainer({
minTime, minTime,
stagedQuery, stagedQuery,
], ],
enabled: !!stagedQuery, enabled: !!stagedQuery && panelTypeParam === PANEL_TYPES.TIME_SERIES,
}, },
); );

View File

@ -0,0 +1,11 @@
import { DEFAULT_PER_PAGE_OPTIONS } from 'hooks/queryPagination';
export const defaultSelectedColumns: string[] = [
'name',
'serviceName',
'responseStatusCode',
'httpMethod',
'durationNano',
];
export const PER_PAGE_OPTIONS: number[] = [10, ...DEFAULT_PER_PAGE_OPTIONS];

View File

@ -0,0 +1,133 @@
import { ColumnsType } from 'antd/es/table';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useOptionsMenu } from 'container/OptionsMenu';
import { QueryTable } from 'container/QueryTable';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { Pagination, URL_PAGINATION } from 'hooks/queryPagination';
import useUrlQueryData from 'hooks/useUrlQueryData';
import history from 'lib/history';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { HTMLAttributes, memo, useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import TraceExplorerControls from '../Controls';
import { defaultSelectedColumns, PER_PAGE_OPTIONS } from './configs';
import { Container, ErrorText, tableStyles } from './styles';
import { getTraceLink, modifyColumns, transformDataWithDate } from './utils';
function ListView(): JSX.Element {
const { stagedQuery, panelType } = useQueryBuilder();
const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const { options, config } = useOptionsMenu({
dataSource: DataSource.TRACES,
aggregateOperator: 'count',
initialOptions: {
selectColumns: defaultSelectedColumns,
},
});
const { queryData: paginationQueryData } = useUrlQueryData<Pagination>(
URL_PAGINATION,
);
const { data, isFetching, isError } = useGetQueryRange(
{
query: stagedQuery || initialQueriesMap.traces,
graphType: panelType || PANEL_TYPES.LIST,
selectedTime: 'GLOBAL_TIME',
globalSelectedInterval: globalSelectedTime,
params: {
dataSource: 'traces',
},
tableParams: {
pagination: paginationQueryData,
selectColumns: options?.selectColumns,
},
},
{
queryKey: [
REACT_QUERY_KEY.GET_QUERY_RANGE,
globalSelectedTime,
maxTime,
minTime,
stagedQuery,
panelType,
paginationQueryData,
options?.selectColumns,
],
enabled: !!stagedQuery && panelType === PANEL_TYPES.LIST,
},
);
const dataLength =
data?.payload?.data?.newResult?.data?.result[0]?.list?.length;
const totalCount = useMemo(() => dataLength || 0, [dataLength]);
const queryTableDataResult = data?.payload.data.newResult.data.result;
const queryTableData = useMemo(() => queryTableDataResult || [], [
queryTableDataResult,
]);
const transformedQueryTableData = useMemo(
() => transformDataWithDate(queryTableData),
[queryTableData],
);
const handleModifyColumns = useCallback(
(columns: ColumnsType<RowData>) =>
modifyColumns(columns, options?.selectColumns || []),
[options?.selectColumns],
);
const handleRow = useCallback(
(record: RowData): HTMLAttributes<RowData> => ({
onClick: (event): void => {
event.preventDefault();
event.stopPropagation();
if (event.metaKey || event.ctrlKey) {
window.open(getTraceLink(record), '_blank');
} else {
history.push(getTraceLink(record));
}
},
}),
[],
);
return (
<Container>
<TraceExplorerControls
isLoading={isFetching}
totalCount={totalCount}
config={config}
perPageOptions={PER_PAGE_OPTIONS}
/>
{isError && <ErrorText>{data?.error || 'Something went wrong'}</ErrorText>}
{!isError && (
<QueryTable
query={stagedQuery || initialQueriesMap.traces}
queryTableData={transformedQueryTableData}
modifyColumns={handleModifyColumns}
loading={isFetching}
pagination={false}
style={tableStyles}
onRow={handleRow}
/>
)}
</Container>
);
}
export default memo(ListView);

View File

@ -0,0 +1,17 @@
import { Typography } from 'antd';
import { CSSProperties } from 'react';
import styled from 'styled-components';
export const tableStyles: CSSProperties = {
cursor: 'pointer',
};
export const Container = styled.div`
display: flex;
flex-direction: column;
gap: 15px;
`;
export const ErrorText = styled(Typography)`
text-align: center;
`;

View File

@ -0,0 +1,97 @@
import { Tag } from 'antd';
import { ColumnsType } from 'antd/es/table';
import Typography from 'antd/es/typography/Typography';
import ROUTES from 'constants/routes';
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
import { formUrlParams } from 'container/TraceDetail/utils';
import dayjs from 'dayjs';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { QueryDataV3 } from 'types/api/widgets/getQuery';
export const transformDataWithDate = (data: QueryDataV3[]): QueryDataV3[] =>
data.map((query) => ({
...query,
list:
query?.list?.map((listItem) => ({
...listItem,
data: {
...listItem?.data,
date: listItem?.timestamp,
},
})) || null,
}));
export const modifyColumns = (
columns: ColumnsType<RowData>,
selectedColumns: BaseAutocompleteData[],
): ColumnsType<RowData> => {
const initialColumns = columns.filter(({ key }) => {
let isValidColumn = true;
const checkIsExistColumnByKey = (attributeKey: string): boolean =>
!selectedColumns.find(({ key }) => key === attributeKey) &&
attributeKey === key;
const isSelectedSpanId = checkIsExistColumnByKey('spanID');
const isSelectedTraceId = checkIsExistColumnByKey('traceID');
if (isSelectedSpanId || isSelectedTraceId || key === 'date')
isValidColumn = false;
return isValidColumn;
});
const dateColumn = columns.find(({ key }) => key === 'date');
if (dateColumn) {
initialColumns.unshift(dateColumn);
}
return initialColumns.map((column) => {
const key = column.key as string;
const getHttpMethodOrStatus = (value: string): JSX.Element => {
if (value === 'N/A') {
return <Typography>{value}</Typography>;
}
return <Tag color="magenta">{value}</Tag>;
};
if (key === 'durationNano') {
return {
...column,
render: (duration: string): JSX.Element => (
<Typography>{getMs(duration)}ms</Typography>
),
};
}
if (key === 'httpMethod' || key === 'responseStatusCode') {
return {
...column,
render: getHttpMethodOrStatus,
};
}
if (key === 'date') {
return {
...column,
render: (date: string): JSX.Element => {
const day = dayjs(date);
return <Typography>{day.format('YYYY/MM/DD HH:mm:ss')}</Typography>;
},
};
}
return column;
});
};
export const getTraceLink = (record: RowData): string =>
`${ROUTES.TRACE}/${record.traceID}${formUrlParams({
spanId: record.spanID,
levelUp: 0,
levelDown: 0,
})}`;

View File

@ -6,7 +6,7 @@ import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { Pagination, URL_PAGINATION } from 'hooks/queryPagination'; import { Pagination, URL_PAGINATION } from 'hooks/queryPagination';
import useUrlQueryData from 'hooks/useUrlQueryData'; import useUrlQueryData from 'hooks/useUrlQueryData';
import { useMemo } from 'react'; import { memo, useMemo } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
@ -84,4 +84,4 @@ function TracesView(): JSX.Element {
); );
} }
export default TracesView; export default memo(TracesView);

View File

@ -23,7 +23,7 @@ type DynamicColumn = {
key: keyof RowData; key: keyof RowData;
data: (string | number)[]; data: (string | number)[];
type: 'field' | 'operator'; type: 'field' | 'operator';
sortable: boolean; // sortable: boolean;
}; };
type DynamicColumns = DynamicColumn[]; type DynamicColumns = DynamicColumn[];
@ -91,15 +91,15 @@ const createLabels = <T extends ListItemData | SeriesItemLabels>(
): void => { ): void => {
if (isColumnExist(label as string, dynamicColumns)) return; if (isColumnExist(label as string, dynamicColumns)) return;
const labelValue = labels[label]; // const labelValue = labels[label];
const isNumber = !Number.isNaN(parseFloat(String(labelValue))); // const isNumber = !Number.isNaN(parseFloat(String(labelValue)));
const fieldObj: DynamicColumn = { const fieldObj: DynamicColumn = {
key: label as string, key: label as string,
data: [], data: [],
type: 'field', type: 'field',
sortable: isNumber, // sortable: isNumber,
}; };
dynamicColumns.push(fieldObj); dynamicColumns.push(fieldObj);
@ -127,7 +127,7 @@ const getDynamicColumns: GetDynamicColumns = (queryTableData, query) => {
key: 'timestamp', key: 'timestamp',
data: [], data: [],
type: 'field', type: 'field',
sortable: true, // sortable: true,
}); });
} }
@ -148,7 +148,7 @@ const getDynamicColumns: GetDynamicColumns = (queryTableData, query) => {
key: operator, key: operator,
data: [], data: [],
type: 'operator', type: 'operator',
sortable: true, // sortable: true,
}; };
dynamicColumns.push(operatorColumn); dynamicColumns.push(operatorColumn);
} }
@ -220,8 +220,8 @@ const fillDataFromList = (
Object.keys(listItem.data).forEach((label) => { Object.keys(listItem.data).forEach((label) => {
if (column.key === label) { if (column.key === label) {
if (listItem.data[label as ListItemKey]) { if (listItem.data[label as ListItemKey] !== '') {
column.data.push(listItem.data[label as ListItemKey] as string | number); column.data.push(listItem.data[label as ListItemKey].toString());
} else { } else {
column.data.push('N/A'); column.data.push('N/A');
} }
@ -291,10 +291,10 @@ const generateTableColumns = (
dataIndex: item.key, dataIndex: item.key,
key: item.key, key: item.key,
title: prepareColumnTitle(item.key as string), title: prepareColumnTitle(item.key as string),
sorter: item.sortable // sorter: item.sortable
? (a: RowData, b: RowData): number => // ? (a: RowData, b: RowData): number =>
(a[item.key] as number) - (b[item.key] as number) // (a[item.key] as number) - (b[item.key] as number)
: false, // : false,
}; };
return [...acc, column]; return [...acc, column];

View File

@ -43,6 +43,15 @@ function TracesExplorer(): JSX.Element {
[currentQuery], [currentQuery],
); );
const isGroupByExist = useMemo(() => {
const groupByCount: number = currentQuery.builder.queryData.reduce<number>(
(acc, query) => acc + query.groupBy.length,
0,
);
return groupByCount > 0;
}, [currentQuery]);
const defaultQuery = useMemo( const defaultQuery = useMemo(
() => () =>
updateAllQueriesOperators( updateAllQueriesOperators(
@ -53,15 +62,6 @@ function TracesExplorer(): JSX.Element {
[updateAllQueriesOperators], [updateAllQueriesOperators],
); );
const isGroupByExist = useMemo(() => {
const groupByCount: number = currentQuery.builder.queryData.reduce<number>(
(acc, query) => acc + query.groupBy.length,
0,
);
return groupByCount > 0;
}, [currentQuery]);
const tabsItems = getTabsItems({ const tabsItems = getTabsItems({
isListViewDisabled: isMultipleQueries || isGroupByExist, isListViewDisabled: isMultipleQueries || isGroupByExist,
}); });

View File

@ -1,6 +1,7 @@
import { TabsProps } from 'antd'; import { TabsProps } from 'antd';
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
import TimeSeriesView from 'container/TimeSeriesView'; import TimeSeriesView from 'container/TimeSeriesView';
import ListView from 'container/TracesExplorer/ListView';
import TracesView from 'container/TracesExplorer/TracesView'; import TracesView from 'container/TracesExplorer/TracesView';
import { DataSource } from 'types/common/queryBuilder'; import { DataSource } from 'types/common/queryBuilder';
@ -11,6 +12,12 @@ interface GetTabsItemsProps {
export const getTabsItems = ({ export const getTabsItems = ({
isListViewDisabled, isListViewDisabled,
}: GetTabsItemsProps): TabsProps['items'] => [ }: GetTabsItemsProps): TabsProps['items'] => [
{
label: 'List View',
key: PANEL_TYPES.LIST,
children: <ListView />,
disabled: isListViewDisabled,
},
{ {
label: 'Traces', label: 'Traces',
key: PANEL_TYPES.TRACE, key: PANEL_TYPES.TRACE,

View File

@ -1,4 +1,5 @@
export interface ILog { export interface ILog {
date: string;
timestamp: number; timestamp: number;
id: string; id: string;
traceId: string; traceId: string;