diff --git a/frontend/src/container/OptionsMenu/useOptionsMenu.ts b/frontend/src/container/OptionsMenu/useOptionsMenu.ts index ffec17e388..13db2516dc 100644 --- a/frontend/src/container/OptionsMenu/useOptionsMenu.ts +++ b/frontend/src/container/OptionsMenu/useOptionsMenu.ts @@ -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) => { - redirectWithOptionsData({ - ...defaultOptionsQuery, - selectColumns: optionsQueryData?.selectColumns?.filter( - ({ id }) => id !== columnKey, - ), - }); + 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: newSelectedColumns, + }); + } }, - [optionsQueryData, redirectWithOptionsData], + [optionsQueryData, notifications, redirectWithOptionsData], ); const handleFormatChange = useCallback( diff --git a/frontend/src/container/TimeSeriesView/index.tsx b/frontend/src/container/TimeSeriesView/index.tsx index 1e47226b01..b5a15e7751 100644 --- a/frontend/src/container/TimeSeriesView/index.tsx +++ b/frontend/src/container/TimeSeriesView/index.tsx @@ -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( + 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, }, ); diff --git a/frontend/src/container/TracesExplorer/ListView/configs.tsx b/frontend/src/container/TracesExplorer/ListView/configs.tsx new file mode 100644 index 0000000000..3b05ed8169 --- /dev/null +++ b/frontend/src/container/TracesExplorer/ListView/configs.tsx @@ -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]; diff --git a/frontend/src/container/TracesExplorer/ListView/index.tsx b/frontend/src/container/TracesExplorer/ListView/index.tsx new file mode 100644 index 0000000000..674184f749 --- /dev/null +++ b/frontend/src/container/TracesExplorer/ListView/index.tsx @@ -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( + 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) => + modifyColumns(columns, options?.selectColumns || []), + [options?.selectColumns], + ); + + const handleRow = useCallback( + (record: RowData): HTMLAttributes => ({ + onClick: (event): void => { + event.preventDefault(); + event.stopPropagation(); + if (event.metaKey || event.ctrlKey) { + window.open(getTraceLink(record), '_blank'); + } else { + history.push(getTraceLink(record)); + } + }, + }), + [], + ); + + return ( + + + + {isError && {data?.error || 'Something went wrong'}} + + {!isError && ( + + )} + + ); +} + +export default memo(ListView); diff --git a/frontend/src/container/TracesExplorer/ListView/styles.ts b/frontend/src/container/TracesExplorer/ListView/styles.ts new file mode 100644 index 0000000000..834dd5209e --- /dev/null +++ b/frontend/src/container/TracesExplorer/ListView/styles.ts @@ -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; +`; diff --git a/frontend/src/container/TracesExplorer/ListView/utils.tsx b/frontend/src/container/TracesExplorer/ListView/utils.tsx new file mode 100644 index 0000000000..59f36a88ab --- /dev/null +++ b/frontend/src/container/TracesExplorer/ListView/utils.tsx @@ -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, + selectedColumns: BaseAutocompleteData[], +): ColumnsType => { + 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 {value}; + } + + return {value}; + }; + + if (key === 'durationNano') { + return { + ...column, + render: (duration: string): JSX.Element => ( + {getMs(duration)}ms + ), + }; + } + + if (key === 'httpMethod' || key === 'responseStatusCode') { + return { + ...column, + render: getHttpMethodOrStatus, + }; + } + + if (key === 'date') { + return { + ...column, + render: (date: string): JSX.Element => { + const day = dayjs(date); + return {day.format('YYYY/MM/DD HH:mm:ss')}; + }, + }; + } + + return column; + }); +}; + +export const getTraceLink = (record: RowData): string => + `${ROUTES.TRACE}/${record.traceID}${formUrlParams({ + spanId: record.spanID, + levelUp: 0, + levelDown: 0, + })}`; diff --git a/frontend/src/container/TracesExplorer/TracesView/index.tsx b/frontend/src/container/TracesExplorer/TracesView/index.tsx index e4ef1e5ec5..0d1eae9d02 100644 --- a/frontend/src/container/TracesExplorer/TracesView/index.tsx +++ b/frontend/src/container/TracesExplorer/TracesView/index.tsx @@ -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); diff --git a/frontend/src/lib/query/createTableColumnsFromQuery.ts b/frontend/src/lib/query/createTableColumnsFromQuery.ts index 168261238f..653a859ba8 100644 --- a/frontend/src/lib/query/createTableColumnsFromQuery.ts +++ b/frontend/src/lib/query/createTableColumnsFromQuery.ts @@ -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 = ( ): 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]; diff --git a/frontend/src/pages/TracesExplorer/index.tsx b/frontend/src/pages/TracesExplorer/index.tsx index d6073fc466..c70f318dc0 100644 --- a/frontend/src/pages/TracesExplorer/index.tsx +++ b/frontend/src/pages/TracesExplorer/index.tsx @@ -43,6 +43,15 @@ function TracesExplorer(): JSX.Element { [currentQuery], ); + const isGroupByExist = useMemo(() => { + const groupByCount: number = currentQuery.builder.queryData.reduce( + (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( - (acc, query) => acc + query.groupBy.length, - 0, - ); - - return groupByCount > 0; - }, [currentQuery]); - const tabsItems = getTabsItems({ isListViewDisabled: isMultipleQueries || isGroupByExist, }); diff --git a/frontend/src/pages/TracesExplorer/utils.tsx b/frontend/src/pages/TracesExplorer/utils.tsx index 116a125c83..2c0143dadc 100644 --- a/frontend/src/pages/TracesExplorer/utils.tsx +++ b/frontend/src/pages/TracesExplorer/utils.tsx @@ -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: , + disabled: isListViewDisabled, + }, { label: 'Traces', key: PANEL_TYPES.TRACE, diff --git a/frontend/src/types/api/logs/log.ts b/frontend/src/types/api/logs/log.ts index ef61ba9871..eb862daa9c 100644 --- a/frontend/src/types/api/logs/log.ts +++ b/frontend/src/types/api/logs/log.ts @@ -1,4 +1,5 @@ export interface ILog { + date: string; timestamp: number; id: string; traceId: string;