mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-14 06:26:03 +08:00
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:
parent
2722538e82
commit
5540692500
@ -1,6 +1,7 @@
|
||||
import { RadioChangeEvent } from 'antd';
|
||||
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
|
||||
import { QueryBuilderKeys } from 'constants/queryBuilder';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
@ -28,6 +29,8 @@ const useOptionsMenu = ({
|
||||
aggregateOperator,
|
||||
initialOptions = {},
|
||||
}: UseOptionsMenuProps): UseOptionsMenu => {
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const {
|
||||
query: optionsQuery,
|
||||
queryData: optionsQueryData,
|
||||
@ -91,14 +94,22 @@ const useOptionsMenu = ({
|
||||
|
||||
const handleRemoveSelectedColumn = useCallback(
|
||||
(columnKey: string) => {
|
||||
const newSelectedColumns = optionsQueryData?.selectColumns?.filter(
|
||||
({ id }) => id !== columnKey,
|
||||
);
|
||||
|
||||
if (!newSelectedColumns.length) {
|
||||
notifications.error({
|
||||
message: 'There must be at least one selected column',
|
||||
});
|
||||
} else {
|
||||
redirectWithOptionsData({
|
||||
...defaultOptionsQuery,
|
||||
selectColumns: optionsQueryData?.selectColumns?.filter(
|
||||
({ id }) => id !== columnKey,
|
||||
),
|
||||
selectColumns: newSelectedColumns,
|
||||
});
|
||||
}
|
||||
},
|
||||
[optionsQueryData, redirectWithOptionsData],
|
||||
[optionsQueryData, notifications, redirectWithOptionsData],
|
||||
);
|
||||
|
||||
const handleFormatChange = useCallback(
|
||||
|
@ -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 { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
@ -19,10 +22,15 @@ function TimeSeriesViewContainer({
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const { queryData: panelTypeParam } = useUrlQueryData<GRAPH_TYPES>(
|
||||
PANEL_TYPES_QUERY,
|
||||
PANEL_TYPES.TIME_SERIES,
|
||||
);
|
||||
|
||||
const { data, isLoading, isError } = useGetQueryRange(
|
||||
{
|
||||
query: stagedQuery || initialQueriesMap[dataSource],
|
||||
graphType: 'graph',
|
||||
graphType: panelTypeParam,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
globalSelectedInterval: globalSelectedTime,
|
||||
params: {
|
||||
@ -37,7 +45,7 @@ function TimeSeriesViewContainer({
|
||||
minTime,
|
||||
stagedQuery,
|
||||
],
|
||||
enabled: !!stagedQuery,
|
||||
enabled: !!stagedQuery && panelTypeParam === PANEL_TYPES.TIME_SERIES,
|
||||
},
|
||||
);
|
||||
|
||||
|
11
frontend/src/container/TracesExplorer/ListView/configs.tsx
Normal file
11
frontend/src/container/TracesExplorer/ListView/configs.tsx
Normal 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];
|
133
frontend/src/container/TracesExplorer/ListView/index.tsx
Normal file
133
frontend/src/container/TracesExplorer/ListView/index.tsx
Normal 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);
|
17
frontend/src/container/TracesExplorer/ListView/styles.ts
Normal file
17
frontend/src/container/TracesExplorer/ListView/styles.ts
Normal 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;
|
||||
`;
|
97
frontend/src/container/TracesExplorer/ListView/utils.tsx
Normal file
97
frontend/src/container/TracesExplorer/ListView/utils.tsx
Normal 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,
|
||||
})}`;
|
@ -6,7 +6,7 @@ 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 { useMemo } from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
@ -84,4 +84,4 @@ function TracesView(): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
export default TracesView;
|
||||
export default memo(TracesView);
|
||||
|
@ -23,7 +23,7 @@ type DynamicColumn = {
|
||||
key: keyof RowData;
|
||||
data: (string | number)[];
|
||||
type: 'field' | 'operator';
|
||||
sortable: boolean;
|
||||
// sortable: boolean;
|
||||
};
|
||||
|
||||
type DynamicColumns = DynamicColumn[];
|
||||
@ -91,15 +91,15 @@ const createLabels = <T extends ListItemData | SeriesItemLabels>(
|
||||
): void => {
|
||||
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 = {
|
||||
key: label as string,
|
||||
data: [],
|
||||
type: 'field',
|
||||
sortable: isNumber,
|
||||
// sortable: isNumber,
|
||||
};
|
||||
|
||||
dynamicColumns.push(fieldObj);
|
||||
@ -127,7 +127,7 @@ const getDynamicColumns: GetDynamicColumns = (queryTableData, query) => {
|
||||
key: 'timestamp',
|
||||
data: [],
|
||||
type: 'field',
|
||||
sortable: true,
|
||||
// sortable: true,
|
||||
});
|
||||
}
|
||||
|
||||
@ -148,7 +148,7 @@ const getDynamicColumns: GetDynamicColumns = (queryTableData, query) => {
|
||||
key: operator,
|
||||
data: [],
|
||||
type: 'operator',
|
||||
sortable: true,
|
||||
// sortable: true,
|
||||
};
|
||||
dynamicColumns.push(operatorColumn);
|
||||
}
|
||||
@ -220,8 +220,8 @@ const fillDataFromList = (
|
||||
|
||||
Object.keys(listItem.data).forEach((label) => {
|
||||
if (column.key === label) {
|
||||
if (listItem.data[label as ListItemKey]) {
|
||||
column.data.push(listItem.data[label as ListItemKey] as string | number);
|
||||
if (listItem.data[label as ListItemKey] !== '') {
|
||||
column.data.push(listItem.data[label as ListItemKey].toString());
|
||||
} else {
|
||||
column.data.push('N/A');
|
||||
}
|
||||
@ -291,10 +291,10 @@ const generateTableColumns = (
|
||||
dataIndex: item.key,
|
||||
key: item.key,
|
||||
title: prepareColumnTitle(item.key as string),
|
||||
sorter: item.sortable
|
||||
? (a: RowData, b: RowData): number =>
|
||||
(a[item.key] as number) - (b[item.key] as number)
|
||||
: false,
|
||||
// sorter: item.sortable
|
||||
// ? (a: RowData, b: RowData): number =>
|
||||
// (a[item.key] as number) - (b[item.key] as number)
|
||||
// : false,
|
||||
};
|
||||
|
||||
return [...acc, column];
|
||||
|
@ -43,6 +43,15 @@ function TracesExplorer(): JSX.Element {
|
||||
[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(
|
||||
() =>
|
||||
updateAllQueriesOperators(
|
||||
@ -53,15 +62,6 @@ function TracesExplorer(): JSX.Element {
|
||||
[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({
|
||||
isListViewDisabled: isMultipleQueries || isGroupByExist,
|
||||
});
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { TabsProps } from 'antd';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import TimeSeriesView from 'container/TimeSeriesView';
|
||||
import ListView from 'container/TracesExplorer/ListView';
|
||||
import TracesView from 'container/TracesExplorer/TracesView';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
@ -11,6 +12,12 @@ interface GetTabsItemsProps {
|
||||
export const getTabsItems = ({
|
||||
isListViewDisabled,
|
||||
}: GetTabsItemsProps): TabsProps['items'] => [
|
||||
{
|
||||
label: 'List View',
|
||||
key: PANEL_TYPES.LIST,
|
||||
children: <ListView />,
|
||||
disabled: isListViewDisabled,
|
||||
},
|
||||
{
|
||||
label: 'Traces',
|
||||
key: PANEL_TYPES.TRACE,
|
||||
|
@ -1,4 +1,5 @@
|
||||
export interface ILog {
|
||||
date: string;
|
||||
timestamp: number;
|
||||
id: string;
|
||||
traceId: string;
|
||||
|
Loading…
x
Reference in New Issue
Block a user