mirror of
https://github.com/SigNoz/signoz.git
synced 2026-10-12 05:01:04 +01:00
Compare commits
7 Commits
feat/scatt
...
fix/explor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89666ef2d9 | ||
|
|
41ad49d21d | ||
|
|
9e8b1530fd | ||
|
|
9e2ac4c55f | ||
|
|
a0960fe862 | ||
|
|
cd1a88fd7d | ||
|
|
0b6a6134d2 |
@@ -6,7 +6,6 @@ import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { QueryKey } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
@@ -34,6 +33,8 @@ import { Warning } from 'types/api';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { getListOrderBy, setListOrderBy } from 'utils/explorerUtils';
|
||||
|
||||
import TraceExplorerControls from '../Controls';
|
||||
import { getListViewQuery } from '../explorerUtils';
|
||||
import {
|
||||
@@ -58,12 +59,16 @@ function ListView({
|
||||
setIsLoadingQueries,
|
||||
queryKeyRef,
|
||||
}: ListViewProps): JSX.Element {
|
||||
const { stagedQuery, panelType: panelTypeFromQueryBuilder } =
|
||||
useQueryBuilder();
|
||||
const {
|
||||
stagedQuery,
|
||||
currentQuery,
|
||||
redirectWithQueryBuilderData,
|
||||
panelType: panelTypeFromQueryBuilder,
|
||||
} = useQueryBuilder();
|
||||
|
||||
const panelType = panelTypeFromQueryBuilder || PANEL_TYPES.LIST;
|
||||
|
||||
const [orderBy, setOrderBy] = useState<string>('timestamp:desc');
|
||||
const orderBy = useMemo(() => getListOrderBy(stagedQuery), [stagedQuery]);
|
||||
|
||||
const {
|
||||
selectedTime: globalSelectedTime,
|
||||
@@ -92,7 +97,6 @@ function ListView({
|
||||
stagedQuery,
|
||||
panelType,
|
||||
paginationConfig,
|
||||
orderBy,
|
||||
],
|
||||
[
|
||||
stagedQuery,
|
||||
@@ -101,7 +105,6 @@ function ListView({
|
||||
paginationConfig,
|
||||
maxTime,
|
||||
minTime,
|
||||
orderBy,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -167,9 +170,12 @@ function ListView({
|
||||
[queryTableData],
|
||||
);
|
||||
|
||||
const handleOrderChange = useCallback((value: string) => {
|
||||
setOrderBy(value);
|
||||
}, []);
|
||||
const handleOrderChange = useCallback(
|
||||
(value: string) => {
|
||||
redirectWithQueryBuilderData(setListOrderBy(currentQuery, value));
|
||||
},
|
||||
[currentQuery, redirectWithQueryBuilderData],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !isFetching && !isError && rows.length !== 0) {
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { QueryKey } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
@@ -23,6 +22,7 @@ import { TracesTableRow } from '../TracesTable/getFieldColumn';
|
||||
import TracesTable from '../TracesTable/TracesTable';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { getListOrderBy, setListOrderBy } from 'utils/explorerUtils';
|
||||
import { Pagination } from 'hooks/queryPagination';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { ArrowUp10, Minus } from '@signozhq/icons';
|
||||
@@ -57,9 +57,13 @@ function TracesView({
|
||||
setIsLoadingQueries,
|
||||
queryKeyRef,
|
||||
}: TracesViewProps): JSX.Element {
|
||||
const { stagedQuery, panelType } = useQueryBuilder();
|
||||
const { stagedQuery, currentQuery, redirectWithQueryBuilderData, panelType } =
|
||||
useQueryBuilder();
|
||||
|
||||
const [orderBy, setOrderBy] = useState<string>(TRACE_VIEW_DEFAULT_ORDER_BY);
|
||||
const orderBy = useMemo(
|
||||
() => getListOrderBy(stagedQuery, TRACE_VIEW_DEFAULT_ORDER_BY),
|
||||
[stagedQuery],
|
||||
);
|
||||
|
||||
const {
|
||||
columns,
|
||||
@@ -94,7 +98,6 @@ function TracesView({
|
||||
stagedQuery,
|
||||
panelType,
|
||||
paginationQueryData,
|
||||
orderBy,
|
||||
],
|
||||
[
|
||||
globalSelectedTime,
|
||||
@@ -103,7 +106,6 @@ function TracesView({
|
||||
stagedQuery,
|
||||
panelType,
|
||||
paginationQueryData,
|
||||
orderBy,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -165,9 +167,12 @@ function TracesView({
|
||||
}
|
||||
}, [isLoading, isFetching, isError, rows.length]);
|
||||
|
||||
const handleOrderChange = useCallback((value: string): void => {
|
||||
setOrderBy(value);
|
||||
}, []);
|
||||
const handleOrderChange = useCallback(
|
||||
(value: string): void => {
|
||||
redirectWithQueryBuilderData(setListOrderBy(currentQuery, value));
|
||||
},
|
||||
[currentQuery, redirectWithQueryBuilderData],
|
||||
);
|
||||
|
||||
// Without the full column set there is no pool to pick from, so the control is dropped.
|
||||
const fieldsSelectorConfig = useMemo(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { OrderByPayload, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { parseListOrderBy } from 'utils/explorerUtils';
|
||||
|
||||
export const getListViewQuery = (
|
||||
stagedQuery: Query,
|
||||
@@ -11,12 +12,7 @@ export const getListViewQuery = (
|
||||
: cloneDeep(initialQueriesMap.traces);
|
||||
|
||||
const orderByPayload: OrderByPayload[] = orderBy
|
||||
? [
|
||||
{
|
||||
columnName: orderBy.split(':')[0],
|
||||
order: orderBy.split(':')[1] as 'asc' | 'desc',
|
||||
},
|
||||
]
|
||||
? [parseListOrderBy(orderBy)]
|
||||
: [];
|
||||
|
||||
for (let i = 0; i < query.builder.queryData.length; i++) {
|
||||
|
||||
@@ -16,14 +16,14 @@ function LogsActionsContainer({
|
||||
showFrequencyChart,
|
||||
handleToggleFrequencyChart,
|
||||
orderBy,
|
||||
setOrderBy,
|
||||
onOrderChange,
|
||||
explorerActions,
|
||||
}: {
|
||||
listQuery: any;
|
||||
showFrequencyChart: boolean;
|
||||
handleToggleFrequencyChart: () => void;
|
||||
orderBy: string;
|
||||
setOrderBy: (value: string) => void;
|
||||
onOrderChange: (value: string) => void;
|
||||
explorerActions: ReactNode;
|
||||
}): JSX.Element {
|
||||
const { options, config } = useOptionsMenu({
|
||||
@@ -78,7 +78,7 @@ function LogsActionsContainer({
|
||||
|
||||
<ListViewOrderBy
|
||||
value={orderBy}
|
||||
onChange={(value): void => setOrderBy(value)}
|
||||
onChange={onOrderChange}
|
||||
dataSource={DataSource.LOGS}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -59,6 +59,7 @@ import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { v4 } from 'uuid';
|
||||
import { getListOrderBy, setListOrderBy } from 'utils/explorerUtils';
|
||||
|
||||
import LogsActionsContainer from './LogsActionsContainer';
|
||||
|
||||
@@ -101,7 +102,8 @@ function LogsExplorerViewsContainer({
|
||||
const currentMinTimeRef = useRef<number>(minTime);
|
||||
|
||||
// Context
|
||||
const { stagedQuery, panelType } = useQueryBuilder();
|
||||
const { stagedQuery, currentQuery, redirectWithQueryBuilderData, panelType } =
|
||||
useQueryBuilder();
|
||||
|
||||
const selectedPanelType = panelType || PANEL_TYPES.LIST;
|
||||
|
||||
@@ -111,7 +113,7 @@ function LogsExplorerViewsContainer({
|
||||
const [requestData, setRequestData] = useState<Query | null>(null);
|
||||
const [listChartQuery, setListChartQuery] = useState<Query | null>(null);
|
||||
|
||||
const [orderBy, setOrderBy] = useState<string>('timestamp:desc');
|
||||
const orderBy = useMemo(() => getListOrderBy(stagedQuery), [stagedQuery]);
|
||||
|
||||
const { yAxisUnit, onUnitChange } = useUrlYAxisUnit('');
|
||||
|
||||
@@ -192,6 +194,13 @@ function LogsExplorerViewsContainer({
|
||||
'custom',
|
||||
);
|
||||
|
||||
const handleOrderChange = useCallback(
|
||||
(value: string): void => {
|
||||
redirectWithQueryBuilderData(setListOrderBy(currentQuery, value));
|
||||
},
|
||||
[currentQuery, redirectWithQueryBuilderData],
|
||||
);
|
||||
|
||||
const getRequestData = useCallback(
|
||||
(
|
||||
query: Query | null,
|
||||
@@ -430,7 +439,7 @@ function LogsExplorerViewsContainer({
|
||||
showFrequencyChart={showFrequencyChart}
|
||||
handleToggleFrequencyChart={handleToggleFrequencyChart}
|
||||
orderBy={orderBy}
|
||||
setOrderBy={setOrderBy}
|
||||
onOrderChange={handleOrderChange}
|
||||
explorerActions={explorerActions}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
@@ -34,6 +33,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { Pagination } from 'hooks/queryPagination';
|
||||
import { getDefaultPaginationConfig } from 'hooks/queryPagination/utils';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { getListOrderBy, setListOrderBy } from 'utils/explorerUtils';
|
||||
import { ArrowUp10, Minus } from '@signozhq/icons';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Warning } from 'types/api';
|
||||
@@ -66,12 +66,16 @@ function ListView({
|
||||
queryKeyRef,
|
||||
headerActions,
|
||||
}: ListViewProps): JSX.Element {
|
||||
const { stagedQuery, panelType: panelTypeFromQueryBuilder } =
|
||||
useQueryBuilder();
|
||||
const {
|
||||
stagedQuery,
|
||||
currentQuery,
|
||||
redirectWithQueryBuilderData,
|
||||
panelType: panelTypeFromQueryBuilder,
|
||||
} = useQueryBuilder();
|
||||
|
||||
const panelType = panelTypeFromQueryBuilder || PANEL_TYPES.LIST;
|
||||
|
||||
const [orderBy, setOrderBy] = useState<string>('timestamp:desc');
|
||||
const orderBy = useMemo(() => getListOrderBy(stagedQuery), [stagedQuery]);
|
||||
|
||||
const {
|
||||
selectedTime: globalSelectedTime,
|
||||
@@ -123,7 +127,6 @@ function ListView({
|
||||
panelType,
|
||||
paginationConfig,
|
||||
selectColumnsSignature,
|
||||
orderBy,
|
||||
],
|
||||
[
|
||||
stagedQuery,
|
||||
@@ -133,7 +136,6 @@ function ListView({
|
||||
selectColumnsSignature,
|
||||
maxTime,
|
||||
minTime,
|
||||
orderBy,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -215,9 +217,12 @@ function ListView({
|
||||
[config],
|
||||
);
|
||||
|
||||
const handleOrderChange = useCallback((value: string) => {
|
||||
setOrderBy(value);
|
||||
}, []);
|
||||
const handleOrderChange = useCallback(
|
||||
(value: string) => {
|
||||
redirectWithQueryBuilderData(setListOrderBy(currentQuery, value));
|
||||
},
|
||||
[currentQuery, redirectWithQueryBuilderData],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !isFetching && !isError && rows.length !== 0) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { OptionsQuery } from 'container/OptionsMenu/types';
|
||||
import { cloneDeep, set } from 'lodash-es';
|
||||
import { OrderByPayload, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { parseListOrderBy } from 'utils/explorerUtils';
|
||||
|
||||
export const getListViewQuery = (
|
||||
stagedQuery: Query,
|
||||
@@ -12,12 +13,7 @@ export const getListViewQuery = (
|
||||
: cloneDeep(initialQueriesMap.traces);
|
||||
|
||||
const orderByPayload: OrderByPayload[] = orderBy
|
||||
? [
|
||||
{
|
||||
columnName: orderBy.split(':')[0],
|
||||
order: orderBy.split(':')[1] as 'asc' | 'desc',
|
||||
},
|
||||
]
|
||||
? [parseListOrderBy(orderBy)]
|
||||
: [];
|
||||
|
||||
for (let i = 0; i < query.builder.queryData.length; i++) {
|
||||
|
||||
@@ -66,6 +66,7 @@ import {
|
||||
import { sanitizeOrderByForExplorer } from 'utils/sanitizeOrderBy';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { getUnstableCurrentSearchParams } from 'utils/getUnstableCurrentSearchParams';
|
||||
import { readPanelTypeFromUrl } from 'utils/readPanelTypeFromUrl';
|
||||
|
||||
export const QueryBuilderContext = createContext<QueryBuilderContextType>({
|
||||
currentQuery: initialQueriesMap.metrics,
|
||||
@@ -182,6 +183,11 @@ export function QueryBuilderProvider({
|
||||
})) ?? [],
|
||||
};
|
||||
|
||||
const urlPanelType = readPanelTypeFromUrl(PANEL_TYPES.LIST);
|
||||
|
||||
const isRawPanel =
|
||||
urlPanelType === PANEL_TYPES.LIST || urlPanelType === PANEL_TYPES.TRACE;
|
||||
|
||||
const setupedQueryData = builder.queryData.map((item) => {
|
||||
const currentElement: IBuilderQuery = {
|
||||
...item,
|
||||
@@ -200,8 +206,10 @@ export function QueryBuilderProvider({
|
||||
|
||||
// Explorer pages: sanitize stale orderBy before first query
|
||||
const isExplorer =
|
||||
location.pathname === ROUTES.LOGS_EXPLORER ||
|
||||
location.pathname === ROUTES.TRACES_EXPLORER;
|
||||
(location.pathname === ROUTES.LOGS_EXPLORER ||
|
||||
location.pathname === ROUTES.TRACES_EXPLORER ||
|
||||
location.pathname === ROUTES.AI_OBSERVABILITY_EXPLORER) &&
|
||||
!isRawPanel;
|
||||
if (isExplorer) {
|
||||
const sanitizedOrderBy = sanitizeOrderByForExplorer(currentElement);
|
||||
return calledFromHandleRunQuery
|
||||
@@ -262,6 +270,8 @@ export function QueryBuilderProvider({
|
||||
setStagedQuery(nextQuery);
|
||||
setCurrentQuery(newQueryState);
|
||||
setQueryType(type);
|
||||
|
||||
setCalledFromHandleRunQuery(false);
|
||||
},
|
||||
[prepareQueryBuilderData],
|
||||
);
|
||||
@@ -1038,7 +1048,8 @@ export function QueryBuilderProvider({
|
||||
const handleRunQuery = useCallback(() => {
|
||||
const isExplorer =
|
||||
location.pathname === ROUTES.LOGS_EXPLORER ||
|
||||
location.pathname === ROUTES.TRACES_EXPLORER;
|
||||
location.pathname === ROUTES.TRACES_EXPLORER ||
|
||||
location.pathname === ROUTES.AI_OBSERVABILITY_EXPLORER;
|
||||
if (isExplorer) {
|
||||
setCalledFromHandleRunQuery(true);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { act, AllTheProviders, renderHook } from 'tests/test-utils';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
__resetSearchParamsGetter,
|
||||
__setSearchParamsGetterForTest,
|
||||
} from 'utils/getUnstableCurrentSearchParams';
|
||||
|
||||
const timestampOrderBy = [{ columnName: 'timestamp', order: 'desc' }];
|
||||
|
||||
const queryWithTimestampOrder = (base: Query): Query => ({
|
||||
...base,
|
||||
builder: {
|
||||
...base.builder,
|
||||
queryData: base.builder.queryData.map((item) => ({
|
||||
...item,
|
||||
orderBy: timestampOrderBy,
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
||||
const renderOn = (
|
||||
route: string,
|
||||
panelType: PANEL_TYPES | null,
|
||||
): ReturnType<typeof renderHook> => {
|
||||
const search = new URLSearchParams();
|
||||
if (panelType) {
|
||||
search.set('panelTypes', JSON.stringify(panelType));
|
||||
}
|
||||
__setSearchParamsGetterForTest(() => search);
|
||||
|
||||
return renderHook(() => useQueryBuilder(), {
|
||||
wrapper: ({ children }): JSX.Element => (
|
||||
<AllTheProviders initialRoute={route}>{children}</AllTheProviders>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const initOn = (
|
||||
route: string,
|
||||
panelType: PANEL_TYPES | null,
|
||||
query: Query,
|
||||
): Query | null => {
|
||||
const search = new URLSearchParams();
|
||||
if (panelType) {
|
||||
search.set('panelTypes', JSON.stringify(panelType));
|
||||
}
|
||||
__setSearchParamsGetterForTest(() => search);
|
||||
|
||||
const { result } = renderHook(() => useQueryBuilder(), {
|
||||
wrapper: ({ children }): JSX.Element => (
|
||||
<AllTheProviders initialRoute={route}>{children}</AllTheProviders>
|
||||
),
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.initQueryBuilderData(query);
|
||||
});
|
||||
|
||||
return result.current.stagedQuery;
|
||||
};
|
||||
|
||||
describe('explorer orderBy sanitizing by panel type', () => {
|
||||
afterEach(() => {
|
||||
__resetSearchParamsGetter();
|
||||
});
|
||||
|
||||
it('keeps the order on the logs list panel', () => {
|
||||
const staged = initOn(
|
||||
ROUTES.LOGS_EXPLORER,
|
||||
PANEL_TYPES.LIST,
|
||||
queryWithTimestampOrder(initialQueriesMap.logs),
|
||||
);
|
||||
|
||||
expect(staged?.builder.queryData[0].orderBy).toStrictEqual(timestampOrderBy);
|
||||
});
|
||||
|
||||
it('keeps the order when panelTypes is absent, since the explorers default to list', () => {
|
||||
const staged = initOn(
|
||||
ROUTES.LOGS_EXPLORER,
|
||||
null,
|
||||
queryWithTimestampOrder(initialQueriesMap.logs),
|
||||
);
|
||||
|
||||
expect(staged?.builder.queryData[0].orderBy).toStrictEqual(timestampOrderBy);
|
||||
});
|
||||
|
||||
it('keeps the order on the traces trace panel', () => {
|
||||
const staged = initOn(
|
||||
ROUTES.TRACES_EXPLORER,
|
||||
PANEL_TYPES.TRACE,
|
||||
queryWithTimestampOrder(initialQueriesMap.traces),
|
||||
);
|
||||
|
||||
expect(staged?.builder.queryData[0].orderBy).toStrictEqual(timestampOrderBy);
|
||||
});
|
||||
|
||||
// engineering-pod#2813: a timestamp order carried from the list view into the
|
||||
// time series tab is not a group-by key or an aggregation, and errors the query.
|
||||
it('drops the order on the time series panel', () => {
|
||||
const staged = initOn(
|
||||
ROUTES.LOGS_EXPLORER,
|
||||
PANEL_TYPES.TIME_SERIES,
|
||||
queryWithTimestampOrder(initialQueriesMap.logs),
|
||||
);
|
||||
|
||||
expect(staged?.builder.queryData[0].orderBy).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it('keeps the order on the LLM observability list panel', () => {
|
||||
const staged = initOn(
|
||||
ROUTES.AI_OBSERVABILITY_EXPLORER,
|
||||
PANEL_TYPES.LIST,
|
||||
queryWithTimestampOrder(initialQueriesMap.traces),
|
||||
);
|
||||
|
||||
expect(staged?.builder.queryData[0].orderBy).toStrictEqual(timestampOrderBy);
|
||||
});
|
||||
|
||||
it('drops the order on the LLM observability time series panel', () => {
|
||||
const staged = initOn(
|
||||
ROUTES.AI_OBSERVABILITY_EXPLORER,
|
||||
PANEL_TYPES.TIME_SERIES,
|
||||
queryWithTimestampOrder(initialQueriesMap.traces),
|
||||
);
|
||||
|
||||
expect(staged?.builder.queryData[0].orderBy).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it('drops the order on the table panel', () => {
|
||||
const staged = initOn(
|
||||
ROUTES.TRACES_EXPLORER,
|
||||
PANEL_TYPES.TABLE,
|
||||
queryWithTimestampOrder(initialQueriesMap.traces),
|
||||
);
|
||||
|
||||
expect(staged?.builder.queryData[0].orderBy).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// handleRunQuery means "respect the query I just built" for the init it causes.
|
||||
// It used to stay set for the rest of the visit, so every later init skipped
|
||||
// sanitizing as well.
|
||||
describe('the run bypass is one-shot', () => {
|
||||
afterEach(() => {
|
||||
__resetSearchParamsGetter();
|
||||
});
|
||||
|
||||
it('skips sanitizing for the init after a run, and sanitizes the next one', () => {
|
||||
const { result } = renderOn(ROUTES.LOGS_EXPLORER, PANEL_TYPES.TIME_SERIES);
|
||||
|
||||
act(() => {
|
||||
(result.current as ReturnType<typeof useQueryBuilder>).handleRunQuery();
|
||||
});
|
||||
|
||||
const afterRun = {
|
||||
...queryWithTimestampOrder(initialQueriesMap.logs),
|
||||
id: 'run-1',
|
||||
};
|
||||
act(() => {
|
||||
(result.current as ReturnType<typeof useQueryBuilder>).initQueryBuilderData(
|
||||
afterRun,
|
||||
);
|
||||
});
|
||||
expect(
|
||||
(result.current as ReturnType<typeof useQueryBuilder>).stagedQuery?.builder
|
||||
.queryData[0].orderBy,
|
||||
).toStrictEqual(timestampOrderBy);
|
||||
|
||||
const next = {
|
||||
...queryWithTimestampOrder(initialQueriesMap.logs),
|
||||
id: 'run-2',
|
||||
};
|
||||
act(() => {
|
||||
(result.current as ReturnType<typeof useQueryBuilder>).initQueryBuilderData(
|
||||
next,
|
||||
);
|
||||
});
|
||||
expect(
|
||||
(result.current as ReturnType<typeof useQueryBuilder>).stagedQuery?.builder
|
||||
.queryData[0].orderBy,
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
138
frontend/src/utils/__tests__/explorerListOrderBy.test.ts
Normal file
138
frontend/src/utils/__tests__/explorerListOrderBy.test.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
DEFAULT_LIST_ORDER_BY,
|
||||
getListOrderBy,
|
||||
getRawPanelOrderBy,
|
||||
parseListOrderBy,
|
||||
setListOrderBy,
|
||||
} from 'utils/explorerUtils';
|
||||
|
||||
const withOrderBy = (
|
||||
orderBy: { columnName: string; order: string }[],
|
||||
): Query => ({
|
||||
...initialQueriesMap.logs,
|
||||
builder: {
|
||||
...initialQueriesMap.logs.builder,
|
||||
queryData: initialQueriesMap.logs.builder.queryData.map((item) => ({
|
||||
...item,
|
||||
orderBy,
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
||||
describe('getRawPanelOrderBy', () => {
|
||||
it('keeps row fields', () => {
|
||||
expect(
|
||||
getRawPanelOrderBy(
|
||||
withOrderBy([{ columnName: 'duration_nano', order: 'desc' }]),
|
||||
),
|
||||
).toStrictEqual([{ columnName: 'duration_nano', order: 'desc' }]);
|
||||
});
|
||||
|
||||
it('keeps a group-by key, which is a row field too', () => {
|
||||
expect(
|
||||
getRawPanelOrderBy(
|
||||
withOrderBy([{ columnName: 'service.name', order: 'asc' }]),
|
||||
),
|
||||
).toStrictEqual([{ columnName: 'service.name', order: 'asc' }]);
|
||||
});
|
||||
|
||||
// Repro: set an order on the Time Series tab, then switch to List.
|
||||
it('drops an aggregation carried in from an aggregated panel', () => {
|
||||
expect(
|
||||
getRawPanelOrderBy(withOrderBy([{ columnName: 'count()', order: 'desc' }])),
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it('drops the aggregated value sentinel', () => {
|
||||
expect(
|
||||
getRawPanelOrderBy(
|
||||
withOrderBy([{ columnName: '#SIGNOZ_VALUE', order: 'desc' }]),
|
||||
),
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it('keeps the usable entry and drops the rest', () => {
|
||||
expect(
|
||||
getRawPanelOrderBy(
|
||||
withOrderBy([
|
||||
{ columnName: 'count()', order: 'desc' },
|
||||
{ columnName: 'timestamp', order: 'desc' },
|
||||
]),
|
||||
),
|
||||
).toStrictEqual([{ columnName: 'timestamp', order: 'desc' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getListOrderBy', () => {
|
||||
it('falls back to the default when the query holds only an aggregation', () => {
|
||||
expect(
|
||||
getListOrderBy(withOrderBy([{ columnName: 'count()', order: 'desc' }])),
|
||||
).toBe(DEFAULT_LIST_ORDER_BY);
|
||||
});
|
||||
|
||||
it('reads the first order entry as a select value', () => {
|
||||
expect(
|
||||
getListOrderBy(withOrderBy([{ columnName: 'timestamp', order: 'asc' }])),
|
||||
).toBe('timestamp:asc');
|
||||
});
|
||||
|
||||
it('lower-cases a hand-written direction from a deep link', () => {
|
||||
expect(
|
||||
getListOrderBy(withOrderBy([{ columnName: 'timestamp', order: 'ASC' }])),
|
||||
).toBe('timestamp:asc');
|
||||
});
|
||||
|
||||
it('shows the primary entry when the query also carries the id tiebreaker', () => {
|
||||
const query = withOrderBy([
|
||||
{ columnName: 'timestamp', order: 'desc' },
|
||||
{ columnName: 'id', order: 'desc' },
|
||||
]);
|
||||
|
||||
expect(getListOrderBy(query)).toBe('timestamp:desc');
|
||||
});
|
||||
|
||||
it('falls back to the default when the query carries no order', () => {
|
||||
expect(getListOrderBy(withOrderBy([]))).toBe(DEFAULT_LIST_ORDER_BY);
|
||||
expect(getListOrderBy(null)).toBe(DEFAULT_LIST_ORDER_BY);
|
||||
});
|
||||
|
||||
it('falls back to the given default', () => {
|
||||
expect(getListOrderBy(withOrderBy([]), 'last_activity_time:desc')).toBe(
|
||||
'last_activity_time:desc',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseListOrderBy', () => {
|
||||
it('splits a select value into an order payload', () => {
|
||||
expect(parseListOrderBy('durationNano:asc')).toStrictEqual({
|
||||
columnName: 'durationNano',
|
||||
order: 'asc',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setListOrderBy', () => {
|
||||
it('replaces the order on every queryData entry', () => {
|
||||
const base = withOrderBy([{ columnName: 'timestamp', order: 'desc' }]);
|
||||
const next = setListOrderBy(base, 'severity_text:asc');
|
||||
|
||||
next.builder.queryData.forEach((item) => {
|
||||
expect(item.orderBy).toStrictEqual([
|
||||
{ columnName: 'severity_text', order: 'asc' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not mutate the query it is given', () => {
|
||||
const base = withOrderBy([{ columnName: 'timestamp', order: 'desc' }]);
|
||||
|
||||
setListOrderBy(base, 'timestamp:asc');
|
||||
|
||||
expect(base.builder.queryData[0].orderBy).toStrictEqual([
|
||||
{ columnName: 'timestamp', order: 'desc' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
46
frontend/src/utils/__tests__/readPanelTypeFromUrl.test.ts
Normal file
46
frontend/src/utils/__tests__/readPanelTypeFromUrl.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import {
|
||||
__resetSearchParamsGetter,
|
||||
__setSearchParamsGetterForTest,
|
||||
} from 'utils/getUnstableCurrentSearchParams';
|
||||
import { readPanelTypeFromUrl } from 'utils/readPanelTypeFromUrl';
|
||||
|
||||
const withParam = (value?: string): void => {
|
||||
const search = new URLSearchParams();
|
||||
if (value !== undefined) {
|
||||
search.set('panelTypes', value);
|
||||
}
|
||||
__setSearchParamsGetterForTest(() => search);
|
||||
};
|
||||
|
||||
describe('readPanelTypeFromUrl', () => {
|
||||
afterEach(() => {
|
||||
__resetSearchParamsGetter();
|
||||
});
|
||||
|
||||
it('decodes the JSON-encoded value the app writes', () => {
|
||||
withParam(JSON.stringify(PANEL_TYPES.TIME_SERIES));
|
||||
|
||||
expect(readPanelTypeFromUrl()).toBe(PANEL_TYPES.TIME_SERIES);
|
||||
});
|
||||
|
||||
it('accepts an unencoded value from a hand-written link', () => {
|
||||
withParam('list');
|
||||
|
||||
expect(readPanelTypeFromUrl()).toBe(PANEL_TYPES.LIST);
|
||||
});
|
||||
|
||||
// engineering-pod#6158: an unguarded parse of this param crashed the page.
|
||||
it('returns the raw value instead of throwing on a malformed one', () => {
|
||||
withParam('{list');
|
||||
|
||||
expect(readPanelTypeFromUrl()).toBe('{list');
|
||||
});
|
||||
|
||||
it('returns the fallback when the param is absent', () => {
|
||||
withParam();
|
||||
|
||||
expect(readPanelTypeFromUrl(PANEL_TYPES.LIST)).toBe(PANEL_TYPES.LIST);
|
||||
expect(readPanelTypeFromUrl()).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,72 @@
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { SIGNOZ_VALUE } from 'container/QueryBuilder/filters/OrderByFilter/constants';
|
||||
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { OrderByPayload, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { getParsedAggregationOptionsForOrderBy } from './aggregationConverter';
|
||||
|
||||
export const DEFAULT_LIST_ORDER_BY = 'timestamp:desc';
|
||||
|
||||
// Filters only. The query keeps the order for the panel that can use it.
|
||||
export const getRawPanelOrderBy = (query: Query | null): OrderByPayload[] => {
|
||||
const queryData = query?.builder?.queryData?.[0];
|
||||
|
||||
if (!queryData) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const disallowed = new Set<string>([SIGNOZ_VALUE]);
|
||||
getParsedAggregationOptionsForOrderBy(queryData).forEach((aggregation) => {
|
||||
if (aggregation?.key) {
|
||||
disallowed.add(aggregation.key);
|
||||
}
|
||||
});
|
||||
|
||||
return (queryData.orderBy ?? []).filter(
|
||||
(item) => !disallowed.has(item.columnName),
|
||||
);
|
||||
};
|
||||
|
||||
// Never write the fallback back: a view saved without an order would then read
|
||||
// as unsaved on open.
|
||||
export const getListOrderBy = (
|
||||
query: Query | null,
|
||||
fallback: string = DEFAULT_LIST_ORDER_BY,
|
||||
): string => {
|
||||
const [orderBy] = getRawPanelOrderBy(query);
|
||||
|
||||
if (!orderBy?.columnName || !orderBy?.order) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return `${orderBy.columnName}:${orderBy.order.toLowerCase()}`;
|
||||
};
|
||||
|
||||
export const parseListOrderBy = (orderBy: string): OrderByPayload => {
|
||||
const [columnName, order] = orderBy.split(':');
|
||||
|
||||
return {
|
||||
columnName: columnName || 'timestamp',
|
||||
order: order || 'desc',
|
||||
};
|
||||
};
|
||||
|
||||
export const setListOrderBy = (query: Query, orderBy: string): Query => {
|
||||
const nextOrderBy = [parseListOrderBy(orderBy)];
|
||||
|
||||
return {
|
||||
...query,
|
||||
builder: {
|
||||
...query.builder,
|
||||
queryData: query.builder.queryData.map((item) => ({
|
||||
...item,
|
||||
orderBy: nextOrderBy,
|
||||
})),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// Mapping between panel types and explorer views
|
||||
export const panelTypeToExplorerView: Record<PANEL_TYPES, ExplorerViews> = {
|
||||
|
||||
25
frontend/src/utils/readPanelTypeFromUrl.ts
Normal file
25
frontend/src/utils/readPanelTypeFromUrl.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
|
||||
import { getUnstableCurrentSearchParams } from './getUnstableCurrentSearchParams';
|
||||
|
||||
/**
|
||||
* The param is JSON-encoded, but a hand-written link can carry it unencoded, and
|
||||
* a malformed one must not throw (engineering-pod#6158). Reads the params
|
||||
* directly, not through `useUrlQuery`, so it works outside a hook.
|
||||
*/
|
||||
export const readPanelTypeFromUrl = (
|
||||
fallback: PANEL_TYPES | null = null,
|
||||
): PANEL_TYPES | null => {
|
||||
const raw = getUnstableCurrentSearchParams().get(QueryParams.panelTypes);
|
||||
|
||||
if (!raw) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(raw) as PANEL_TYPES;
|
||||
} catch {
|
||||
return raw as PANEL_TYPES;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user