mirror of
https://github.com/SigNoz/signoz.git
synced 2026-10-22 01:50:41 +01:00
Compare commits
3 Commits
backup/thr
...
ai-o11y-sa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d53e80f29 | ||
|
|
7a9c4d4a0a | ||
|
|
e54ac6d6f7 |
@@ -12,8 +12,10 @@ export enum LOCALSTORAGE {
|
||||
GRAPH_VISIBILITY_STATES = 'GRAPH_VISIBILITY_STATES',
|
||||
TRACES_LIST_COLUMNS = 'TRACES_LIST_COLUMNS',
|
||||
TRACES_VIEW_COLUMNS = 'TRACES_VIEW_COLUMNS',
|
||||
AI_OBSERVABILITY_TRACE_VIEW_COLUMNS = 'AI_OBSERVABILITY_TRACE_VIEW_COLUMNS',
|
||||
AI_OBSERVABILITY_LIST_COLUMNS = 'AI_OBSERVABILITY_LIST_COLUMNS',
|
||||
AI_OBSERVABILITY_LIST_OPTIONS = 'AI_OBSERVABILITY_LIST_OPTIONS',
|
||||
AI_OBSERVABILITY_TRACE_VIEW_OPTIONS = 'AI_OBSERVABILITY_TRACE_VIEW_OPTIONS',
|
||||
AI_OBSERVABILITY_TRACE_VIEW_COLUMN_SIZING = 'AI_OBSERVABILITY_TRACE_VIEW_COLUMN_SIZING',
|
||||
LOGS_LIST_COLUMNS = 'LOGS_LIST_COLUMNS',
|
||||
LOGS_LIST_COLUMN_SIZING = 'LOGS_LIST_COLUMN_SIZING',
|
||||
LOGGED_IN_USER_NAME = 'LOGGED_IN_USER_NAME',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
|
||||
import QueryCancelledPlaceholder from 'components/QueryCancelledPlaceholder';
|
||||
@@ -189,6 +190,7 @@ function Explorer(): JSX.Element {
|
||||
<QuickFiltersLayout
|
||||
className="trace-explorer-page"
|
||||
testId="llm-observability-explorer"
|
||||
savedViewProps={{ source: SavedviewtypesSourceDTO.ai_observability }}
|
||||
showFilters={isOpen}
|
||||
onToggleFilters={(): void => setOpen(!isOpen)}
|
||||
quickFilterProps={{
|
||||
|
||||
@@ -12,7 +12,6 @@ import { QueryKey } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ListViewOrderBy from 'components/OrderBy/ListViewOrderBy';
|
||||
import type { TableColumnDef } from 'components/TanStackTableView/types';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { QueryParams } from 'constants/query';
|
||||
@@ -20,7 +19,6 @@ import { initialQueryAIWithType, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { getTraceLink, transformSpanRows } from './utils';
|
||||
import { getFieldColumn, TracesTableRow } from '../TracesTable/getFieldColumn';
|
||||
import TracesTable from '../TracesTable/TracesTable';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
@@ -37,11 +35,8 @@ import { getListOrderBy, setListOrderBy } from 'utils/explorerUtils';
|
||||
|
||||
import TraceExplorerControls from '../Controls';
|
||||
import { getListViewQuery } from '../explorerUtils';
|
||||
import {
|
||||
defaultSelectedColumns,
|
||||
PER_PAGE_OPTIONS,
|
||||
TIMESTAMP_FIELD,
|
||||
} from './configs';
|
||||
import { PER_PAGE_OPTIONS } from './configs';
|
||||
import { useListViewColumns } from './useListViewColumns';
|
||||
import './ListView.styles.scss';
|
||||
|
||||
import styles from './ListView.module.scss';
|
||||
@@ -83,6 +78,15 @@ function ListView({
|
||||
const paginationConfig =
|
||||
paginationQueryData ?? getDefaultPaginationConfig(PER_PAGE_OPTIONS);
|
||||
|
||||
const {
|
||||
selectedColumns,
|
||||
selectColumnsSignature,
|
||||
columns,
|
||||
config,
|
||||
onColumnOrderChange,
|
||||
onColumnRemove,
|
||||
} = useListViewColumns();
|
||||
|
||||
const requestQuery = useMemo(
|
||||
() => getListViewQuery(stagedQuery || initialQueryAIWithType, orderBy),
|
||||
[stagedQuery, orderBy],
|
||||
@@ -97,12 +101,14 @@ function ListView({
|
||||
stagedQuery,
|
||||
panelType,
|
||||
paginationConfig,
|
||||
selectColumnsSignature,
|
||||
],
|
||||
[
|
||||
stagedQuery,
|
||||
panelType,
|
||||
globalSelectedTime,
|
||||
paginationConfig,
|
||||
selectColumnsSignature,
|
||||
maxTime,
|
||||
minTime,
|
||||
],
|
||||
@@ -123,7 +129,7 @@ function ListView({
|
||||
},
|
||||
tableParams: {
|
||||
pagination: paginationConfig,
|
||||
selectColumns: defaultSelectedColumns,
|
||||
selectColumns: selectedColumns,
|
||||
},
|
||||
},
|
||||
ENTITY_VERSION_V5,
|
||||
@@ -156,15 +162,6 @@ function ListView({
|
||||
[queryTableDataResult],
|
||||
);
|
||||
|
||||
// TODO(ai-explorer): static columns until the preferences framework lands.
|
||||
const columns = useMemo<TableColumnDef<TracesTableRow>[]>(
|
||||
() =>
|
||||
[TIMESTAMP_FIELD, ...defaultSelectedColumns].map((field) =>
|
||||
getFieldColumn(field),
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const rows = useMemo(
|
||||
() => transformSpanRows(queryTableData),
|
||||
[queryTableData],
|
||||
@@ -204,6 +201,7 @@ function ListView({
|
||||
isLoading={isFetching}
|
||||
totalCount={rows.length}
|
||||
perPageOptions={PER_PAGE_OPTIONS}
|
||||
config={config}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -211,7 +209,7 @@ function ListView({
|
||||
data={rows}
|
||||
columns={columns}
|
||||
columnStorageKey={LOCALSTORAGE.AI_OBSERVABILITY_LIST_COLUMNS}
|
||||
respectColumnOrder
|
||||
respectColumnOrder={false}
|
||||
panelType="LIST"
|
||||
getRowHref={getTraceLink}
|
||||
isLoading={isLoading}
|
||||
@@ -219,6 +217,8 @@ function ListView({
|
||||
isError={isError}
|
||||
error={error}
|
||||
isFilterApplied={isFilterApplied}
|
||||
onColumnOrderChange={onColumnOrderChange}
|
||||
onColumnRemove={onColumnRemove}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import type { TableColumnDef } from 'components/TanStackTableView/types';
|
||||
import { OptionsMenuConfig } from 'container/OptionsMenu/types';
|
||||
import { buildCompositeKey } from 'container/OptionsMenu/utils';
|
||||
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
|
||||
import { getFieldColumn, TracesTableRow } from '../TracesTable/getFieldColumn';
|
||||
import { defaultSelectedColumns, TIMESTAMP_FIELD } from './configs';
|
||||
|
||||
const columnIdOf = (field: TelemetryFieldKey): string =>
|
||||
buildCompositeKey(field.name, field.fieldContext, field.fieldDataType);
|
||||
|
||||
interface UseListViewColumns {
|
||||
selectedColumns: TelemetryFieldKey[];
|
||||
/** Changes on add/remove only, so a reorder doesn't refetch. */
|
||||
selectColumnsSignature: string;
|
||||
columns: TableColumnDef<TracesTableRow>[];
|
||||
config: OptionsMenuConfig;
|
||||
onColumnOrderChange: (cols: TableColumnDef<TracesTableRow>[]) => void;
|
||||
onColumnRemove: (columnId: string) => void;
|
||||
}
|
||||
|
||||
export function useListViewColumns(): UseListViewColumns {
|
||||
const { preferences, updateColumns } = usePreferenceContext().aiObservability;
|
||||
|
||||
const selectedColumns = useMemo(
|
||||
() =>
|
||||
(preferences?.columns ?? defaultSelectedColumns).filter(
|
||||
(field) => field.name !== TIMESTAMP_FIELD.name,
|
||||
),
|
||||
[preferences?.columns],
|
||||
);
|
||||
|
||||
const selectColumnsSignature = useMemo(
|
||||
() =>
|
||||
selectedColumns
|
||||
.map((field) => field.name)
|
||||
.sort()
|
||||
.join(','),
|
||||
[selectedColumns],
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [TIMESTAMP_FIELD, ...selectedColumns].map(getFieldColumn),
|
||||
[selectedColumns],
|
||||
);
|
||||
|
||||
const config = useMemo<OptionsMenuConfig>(
|
||||
() => ({
|
||||
fieldsSelector: { value: selectedColumns, onFieldsChange: updateColumns },
|
||||
}),
|
||||
[selectedColumns, updateColumns],
|
||||
);
|
||||
|
||||
const onColumnOrderChange = useCallback(
|
||||
(cols: TableColumnDef<TracesTableRow>[]): void => {
|
||||
const byId = new Map(selectedColumns.map((f) => [columnIdOf(f), f]));
|
||||
updateColumns(
|
||||
cols
|
||||
.map((col) => byId.get(col.id))
|
||||
.filter((f): f is TelemetryFieldKey => !!f),
|
||||
);
|
||||
},
|
||||
[selectedColumns, updateColumns],
|
||||
);
|
||||
|
||||
const onColumnRemove = useCallback(
|
||||
(columnId: string): void => {
|
||||
updateColumns(selectedColumns.filter((f) => columnIdOf(f) !== columnId));
|
||||
},
|
||||
[selectedColumns, updateColumns],
|
||||
);
|
||||
|
||||
return {
|
||||
selectedColumns,
|
||||
selectColumnsSignature,
|
||||
columns,
|
||||
config,
|
||||
onColumnOrderChange,
|
||||
onColumnRemove,
|
||||
};
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { render, screen, userEvent } from 'tests/test-utils';
|
||||
import { buildTraceViewColumns } from '../../TracesView/configs';
|
||||
import TracesTable from '../TracesTable';
|
||||
|
||||
const STORAGE_KEY = LOCALSTORAGE.AI_OBSERVABILITY_TRACE_VIEW_COLUMNS;
|
||||
const STORAGE_KEY = LOCALSTORAGE.AI_OBSERVABILITY_TRACE_VIEW_COLUMN_SIZING;
|
||||
const PERSISTED_KEY = `@signoz/table-columns/${STORAGE_KEY}`;
|
||||
|
||||
const ROWS = [{ id: 't1', trace_id: 'abc', 'service.name': 'checkout' }];
|
||||
@@ -17,7 +17,11 @@ const COLUMNS = buildTraceViewColumns([
|
||||
{ name: 'trace_id' },
|
||||
{ name: 'service.name', fieldContext: 'resource' },
|
||||
{ name: 'unlisted_aggregate' },
|
||||
]);
|
||||
]).map((column) =>
|
||||
column.id === 'unlisted_aggregate'
|
||||
? { ...column, defaultVisibility: false }
|
||||
: column,
|
||||
);
|
||||
|
||||
function RaceHarness(): JSX.Element {
|
||||
const [columnsReady, setColumnsReady] = useState(false);
|
||||
|
||||
@@ -69,6 +69,8 @@ function TracesView({
|
||||
columns,
|
||||
selectedFields,
|
||||
onFieldsChange,
|
||||
onColumnOrderChange,
|
||||
onColumnRemove,
|
||||
requiredFields,
|
||||
isLoading: isColumnsLoading,
|
||||
canPersistColumns,
|
||||
@@ -183,15 +185,6 @@ function TracesView({
|
||||
[canPersistColumns, selectedFields, onFieldsChange],
|
||||
);
|
||||
|
||||
// Rendering the pool unfiltered would surface columns the defaults keep hidden.
|
||||
const tableColumns = useMemo(
|
||||
() =>
|
||||
canPersistColumns
|
||||
? columns
|
||||
: columns.filter((column) => column.defaultVisibility !== false),
|
||||
[canPersistColumns, columns],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.actionsContainer}>
|
||||
@@ -226,13 +219,13 @@ function TracesView({
|
||||
|
||||
<TracesTable
|
||||
data={rows}
|
||||
columns={tableColumns}
|
||||
columns={columns}
|
||||
columnStorageKey={
|
||||
canPersistColumns
|
||||
? LOCALSTORAGE.AI_OBSERVABILITY_TRACE_VIEW_COLUMNS
|
||||
? LOCALSTORAGE.AI_OBSERVABILITY_TRACE_VIEW_COLUMN_SIZING
|
||||
: undefined
|
||||
}
|
||||
respectColumnOrder
|
||||
respectColumnOrder={false}
|
||||
panelType="TRACE"
|
||||
getRowHref={getTraceLink}
|
||||
isLoading={isLoading || isColumnsLoading}
|
||||
@@ -240,6 +233,8 @@ function TracesView({
|
||||
isError={isError}
|
||||
error={error}
|
||||
isFilterApplied={isFilterApplied}
|
||||
onColumnOrderChange={onColumnOrderChange}
|
||||
onColumnRemove={onColumnRemove}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,14 +6,12 @@ import {
|
||||
TelemetrytypesFieldContextDTO,
|
||||
TelemetrytypesFieldDataTypeDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { useColumnStore } from 'components/TanStackTableView/useColumnStore';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { initialQueryAIWithType, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
|
||||
import TracesView from '../TracesView';
|
||||
|
||||
const STORAGE_KEY = LOCALSTORAGE.AI_OBSERVABILITY_TRACE_VIEW_COLUMNS;
|
||||
const PERSISTED_KEY = `@signoz/table-columns/${STORAGE_KEY}`;
|
||||
const STORAGE_KEY = LOCALSTORAGE.AI_OBSERVABILITY_TRACE_VIEW_OPTIONS;
|
||||
const QUERY_RANGE_URL = `${ENVIRONMENT.baseURL}/api/v5/query_range`;
|
||||
const FIELD_KEYS_URL = `${ENVIRONMENT.baseURL}/api/v1/ai_observability/fields/keys`;
|
||||
|
||||
@@ -84,11 +82,6 @@ const mockFieldKeysFailure = (): void => {
|
||||
);
|
||||
};
|
||||
|
||||
const persistedState = (): { hiddenColumnIds: string[] } | null => {
|
||||
const raw = localStorage.getItem(PERSISTED_KEY);
|
||||
return raw ? (JSON.parse(raw) as { hiddenColumnIds: string[] }) : null;
|
||||
};
|
||||
|
||||
const renderTracesView = (): ReturnType<typeof render> =>
|
||||
render(
|
||||
<TracesView
|
||||
@@ -109,7 +102,6 @@ const renderTracesView = (): ReturnType<typeof render> =>
|
||||
|
||||
describe('TracesView column persistence', () => {
|
||||
beforeEach(() => {
|
||||
useColumnStore.setState({ tables: {} });
|
||||
localStorage.clear();
|
||||
mockRows();
|
||||
});
|
||||
@@ -121,16 +113,15 @@ describe('TracesView column persistence', () => {
|
||||
// Rows are virtualised, so a mounted table stands in for "rows arrived".
|
||||
const findTable = (): Promise<HTMLElement> => screen.findByRole('table');
|
||||
|
||||
it('seeds the persisted defaults once the field keys arrive', async () => {
|
||||
it('shows the default columns once the field keys arrive', async () => {
|
||||
mockFieldKeys(['llm_call_count', 'tool_call_count']);
|
||||
renderTracesView();
|
||||
|
||||
await findTable();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(persistedState()?.hiddenColumnIds).toStrictEqual([]);
|
||||
expect(screen.getByText(OPTIONS_TRIGGER)).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText(OPTIONS_TRIGGER)).toBeInTheDocument();
|
||||
expect(screen.getByText('llm_call_count')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -140,8 +131,7 @@ describe('TracesView column persistence', () => {
|
||||
|
||||
await findTable();
|
||||
|
||||
expect(useColumnStore.getState().tables[STORAGE_KEY]).toBeUndefined();
|
||||
expect(persistedState()).toBeNull();
|
||||
expect(localStorage.getItem(STORAGE_KEY)).toBeNull();
|
||||
});
|
||||
|
||||
it('drops the column picker when the field keys fail', async () => {
|
||||
@@ -166,18 +156,23 @@ describe('TracesView column persistence', () => {
|
||||
});
|
||||
|
||||
it('leaves an existing selection untouched while the field keys fail', async () => {
|
||||
const existing = {
|
||||
hiddenColumnIds: ['trace:tool_call_count:float64', 'input', 'output'],
|
||||
columnOrder: ['trace_id', 'resource:service.name'],
|
||||
columnSizing: {},
|
||||
};
|
||||
localStorage.setItem(PERSISTED_KEY, JSON.stringify(existing));
|
||||
const existing = JSON.stringify({
|
||||
selectColumns: [
|
||||
{ name: 'trace_id' },
|
||||
{
|
||||
name: 'tool_call_count',
|
||||
fieldContext: 'trace',
|
||||
fieldDataType: 'float64',
|
||||
},
|
||||
],
|
||||
});
|
||||
localStorage.setItem(STORAGE_KEY, existing);
|
||||
|
||||
mockFieldKeysFailure();
|
||||
renderTracesView();
|
||||
|
||||
await findTable();
|
||||
|
||||
expect(persistedState()).toStrictEqual(existing);
|
||||
expect(localStorage.getItem(STORAGE_KEY)).toBe(existing);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import { ReactNode } from 'react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { server } from 'mocks-server/server';
|
||||
@@ -9,13 +10,28 @@ import {
|
||||
TelemetrytypesFieldContextDTO,
|
||||
TelemetrytypesFieldDataTypeDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { useColumnStore } from 'components/TanStackTableView/useColumnStore';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import { AI_OBSERVABILITY_TRACE_VIEW_URL_OPTIONS } from 'providers/preferences/types';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
|
||||
import { useTraceViewColumns } from '../useTraceViewColumns';
|
||||
|
||||
const STORAGE_KEY = LOCALSTORAGE.AI_OBSERVABILITY_TRACE_VIEW_COLUMNS;
|
||||
// The loader reads window.location, which MemoryRouter never moves.
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: (): { safeNavigate: (url: string) => void } => ({
|
||||
safeNavigate: (url: string): void => window.history.replaceState({}, '', url),
|
||||
}),
|
||||
}));
|
||||
|
||||
const STORAGE_KEY = LOCALSTORAGE.AI_OBSERVABILITY_TRACE_VIEW_OPTIONS;
|
||||
|
||||
const storedColumnNames = (): string[] | undefined =>
|
||||
(
|
||||
JSON.parse(localStorage.getItem(STORAGE_KEY) ?? 'null') as {
|
||||
selectColumns: TelemetryFieldKey[];
|
||||
} | null
|
||||
)?.selectColumns.map((field) => field.name);
|
||||
|
||||
const AGGREGATE_KEYS = [
|
||||
'llm_call_count',
|
||||
@@ -39,7 +55,11 @@ function wrapper({ children }: { children: ReactNode }): JSX.Element {
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
<MemoryRouter>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<PreferenceContextProvider>{children}</PreferenceContextProvider>
|
||||
</QueryClientProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -90,8 +110,8 @@ const renderColumns = async (): Promise<
|
||||
describe('useTraceViewColumns', () => {
|
||||
beforeEach(() => {
|
||||
seenAI.length = 0;
|
||||
useColumnStore.getState().tables = {};
|
||||
localStorage.clear();
|
||||
window.history.replaceState({}, '', '/');
|
||||
mockAggregateKeys(AGGREGATE_KEYS);
|
||||
});
|
||||
|
||||
@@ -160,10 +180,22 @@ describe('useTraceViewColumns', () => {
|
||||
|
||||
const { result } = await renderColumns();
|
||||
|
||||
expect(columnNames(result.current.columns)).toContain('brand_new_aggregate');
|
||||
expect(fieldNames(result.current.selectedFields)).not.toContain(
|
||||
'brand_new_aggregate',
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.onFieldsChange([
|
||||
...result.current.selectedFields,
|
||||
{
|
||||
name: 'brand_new_aggregate',
|
||||
fieldContext: 'trace',
|
||||
fieldDataType: 'float64',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
expect(columnNames(result.current.columns)).toContain('brand_new_aggregate');
|
||||
});
|
||||
|
||||
describe('when the keys fetch fails', () => {
|
||||
@@ -179,10 +211,7 @@ describe('useTraceViewColumns', () => {
|
||||
it('does not persist defaults', async () => {
|
||||
await renderColumns();
|
||||
|
||||
expect(useColumnStore.getState().tables[STORAGE_KEY]).toBeUndefined();
|
||||
expect(
|
||||
localStorage.getItem(`@signoz/table-columns/${STORAGE_KEY}`),
|
||||
).toBeNull();
|
||||
expect(localStorage.getItem(STORAGE_KEY)).toBeNull();
|
||||
});
|
||||
|
||||
it('reports the column state as not persistable', async () => {
|
||||
@@ -198,7 +227,7 @@ describe('useTraceViewColumns', () => {
|
||||
result.current.onFieldsChange([{ name: 'trace_id' }]);
|
||||
});
|
||||
|
||||
expect(useColumnStore.getState().tables[STORAGE_KEY]).toBeUndefined();
|
||||
expect(localStorage.getItem(STORAGE_KEY)).toBeNull();
|
||||
});
|
||||
|
||||
it('seeds the defaults once a later fetch succeeds', async () => {
|
||||
@@ -291,12 +320,72 @@ describe('useTraceViewColumns', () => {
|
||||
'trace_id',
|
||||
'service.name',
|
||||
]);
|
||||
expect(
|
||||
useColumnStore.getState().tables[STORAGE_KEY].columnOrder,
|
||||
).toStrictEqual([
|
||||
'trace:total_tokens:float64',
|
||||
expect(storedColumnNames()).toStrictEqual([
|
||||
'total_tokens',
|
||||
'trace_id',
|
||||
'resource:service.name',
|
||||
'service.name',
|
||||
]);
|
||||
});
|
||||
|
||||
it('writes the selection to its own URL param, not the list view one', async () => {
|
||||
const { result } = await renderColumns();
|
||||
|
||||
act(() => {
|
||||
result.current.onFieldsChange([{ name: 'trace_id' }, { name: 'input' }]);
|
||||
});
|
||||
|
||||
expect(window.location.search).not.toContain('options=');
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
expect(
|
||||
JSON.parse(params.get(AI_OBSERVABILITY_TRACE_VIEW_URL_OPTIONS) ?? '{}')
|
||||
.selectColumns,
|
||||
).toStrictEqual([{ name: 'trace_id' }, { name: 'input' }]);
|
||||
});
|
||||
|
||||
it('takes the URL columns over the stored ones', async () => {
|
||||
localStorage.setItem(
|
||||
STORAGE_KEY,
|
||||
JSON.stringify({ selectColumns: [{ name: 'trace_id' }, { name: 'input' }] }),
|
||||
);
|
||||
window.history.replaceState(
|
||||
{},
|
||||
'',
|
||||
`/?${AI_OBSERVABILITY_TRACE_VIEW_URL_OPTIONS}=${encodeURIComponent(
|
||||
JSON.stringify({
|
||||
selectColumns: [{ name: 'output' }, { name: 'trace_id' }],
|
||||
}),
|
||||
)}`,
|
||||
);
|
||||
|
||||
const { result } = await renderColumns();
|
||||
|
||||
expect(fieldNames(result.current.selectedFields)).toStrictEqual([
|
||||
'output',
|
||||
'trace_id',
|
||||
]);
|
||||
});
|
||||
|
||||
it('drops stored fields the keys endpoint no longer reports', async () => {
|
||||
localStorage.setItem(
|
||||
STORAGE_KEY,
|
||||
JSON.stringify({
|
||||
selectColumns: [{ name: 'trace_id' }, { name: 'removed_aggregate' }],
|
||||
}),
|
||||
);
|
||||
|
||||
const { result } = await renderColumns();
|
||||
|
||||
expect(fieldNames(result.current.selectedFields)).toStrictEqual(['trace_id']);
|
||||
});
|
||||
|
||||
it('removes a column dropped from the table header', async () => {
|
||||
const { result } = await renderColumns();
|
||||
|
||||
act(() => {
|
||||
result.current.onColumnRemove('input');
|
||||
});
|
||||
|
||||
expect(fieldNames(result.current.selectedFields)).not.toContain('input');
|
||||
expect(storedColumnNames()).not.toContain('input');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,15 +40,17 @@ export const sortByDefaultOrder = (
|
||||
): TelemetryFieldKey[] =>
|
||||
[...fields].sort((a, b) => orderRank(a) - orderRank(b));
|
||||
|
||||
/** Anything the keys endpoint adds beyond the ordered set starts hidden; only applied at first init, since the store persists hidden ids. */
|
||||
/** Anything the keys endpoint adds beyond the ordered set starts hidden; only applied until the user picks columns. */
|
||||
const DEFAULT_VISIBLE_FIELDS = new Set(DEFAULT_COLUMN_ORDER);
|
||||
|
||||
export const isDefaultVisible = (field: TelemetryFieldKey): boolean =>
|
||||
DEFAULT_VISIBLE_FIELDS.has(field.name);
|
||||
|
||||
export const buildTraceViewColumns = (
|
||||
fields: TelemetryFieldKey[],
|
||||
): TableColumnDef<TracesTableRow>[] =>
|
||||
fields.map((field) => ({
|
||||
...getFieldColumn(field),
|
||||
defaultVisibility: DEFAULT_VISIBLE_FIELDS.has(field.name),
|
||||
// The shared column builder pins anything in TIMESTAMP_FIELD_NAMES; these stay movable.
|
||||
enableMove: true,
|
||||
enableRemove: field.name !== TRACE_ID_COLUMN_ID,
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useFieldKeysSuggestion } from 'hooks/querySuggestions/useFieldKeysSuggestion';
|
||||
import { mergeExtraFields } from 'utils/extraFields';
|
||||
import type { TableColumnDef } from 'components/TanStackTableView/types';
|
||||
import {
|
||||
hideColumn,
|
||||
initializeFromDefaults,
|
||||
setColumnOrder,
|
||||
showColumn,
|
||||
useColumnOrder,
|
||||
useHiddenColumnIds,
|
||||
} from 'components/TanStackTableView/useColumnStore';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { buildCompositeKey } from 'container/OptionsMenu/utils';
|
||||
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import { TracesTableRow } from '../TracesTable/getFieldColumn';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
import { DATA_SOURCE_TO_SIGNAL, DataSource } from 'types/common/queryBuilder';
|
||||
@@ -23,28 +15,35 @@ import {
|
||||
} from '../constants';
|
||||
import {
|
||||
buildTraceViewColumns,
|
||||
isDefaultVisible,
|
||||
sortByDefaultOrder,
|
||||
TRACE_ID_COLUMN_ID,
|
||||
} from './configs';
|
||||
|
||||
const STORAGE_KEY = LOCALSTORAGE.AI_OBSERVABILITY_TRACE_VIEW_COLUMNS;
|
||||
|
||||
/** Matches the id getFieldColumn derives, so fields and columns address alike. */
|
||||
const columnIdOf = (field: TelemetryFieldKey): string =>
|
||||
buildCompositeKey(field.name, field.fieldContext, field.fieldDataType);
|
||||
|
||||
const isTraceId = (field: TelemetryFieldKey): boolean =>
|
||||
columnIdOf(field) === TRACE_ID_COLUMN_ID;
|
||||
|
||||
interface UseTraceViewColumns {
|
||||
/** Visible columns, in the user's order. */
|
||||
columns: TableColumnDef<TracesTableRow>[];
|
||||
selectedFields: TelemetryFieldKey[];
|
||||
onFieldsChange: (next: TelemetryFieldKey[]) => void;
|
||||
onColumnOrderChange: (cols: TableColumnDef<TracesTableRow>[]) => void;
|
||||
onColumnRemove: (columnId: string) => void;
|
||||
requiredFields: readonly string[];
|
||||
isLoading: boolean;
|
||||
/** False until the keys fetch lands; a partial set must not reach the persisted store. */
|
||||
canPersistColumns: boolean;
|
||||
}
|
||||
|
||||
// TODO(ai-explorer): browser-local only, unlike the list views' `?options=` columns.
|
||||
export function useTraceViewColumns(): UseTraceViewColumns {
|
||||
const { preferences, updateColumns } =
|
||||
usePreferenceContext().aiObservabilityTraceView;
|
||||
|
||||
const {
|
||||
data: fetchedFields = [],
|
||||
isFetched,
|
||||
@@ -66,60 +65,69 @@ export function useTraceViewColumns(): UseTraceViewColumns {
|
||||
[fetchedFields],
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() => buildTraceViewColumns(availableFields),
|
||||
[availableFields],
|
||||
);
|
||||
|
||||
// Defaults from a partial column set would persist as the user's own choice.
|
||||
useEffect(() => {
|
||||
if (isSuccess) {
|
||||
initializeFromDefaults(STORAGE_KEY, columns);
|
||||
}
|
||||
}, [isSuccess, columns]);
|
||||
|
||||
const hiddenColumnIds = useHiddenColumnIds(STORAGE_KEY);
|
||||
const columnOrder = useColumnOrder(STORAGE_KEY);
|
||||
|
||||
// Stored fields resolve against the pool, so a field that is gone drops out; none left means the defaults.
|
||||
const selectedFields = useMemo(() => {
|
||||
const hidden = new Set(hiddenColumnIds);
|
||||
const orderIndex = new Map(columnOrder.map((id, index) => [id, index]));
|
||||
const byId = new Map(
|
||||
availableFields.map((field) => [columnIdOf(field), field]),
|
||||
);
|
||||
const stored = (preferences?.columns ?? [])
|
||||
.map((field) => byId.get(columnIdOf(field)))
|
||||
.filter((field): field is TelemetryFieldKey => !!field);
|
||||
const selected = stored.length
|
||||
? stored
|
||||
: availableFields.filter(isDefaultVisible);
|
||||
const traceId = byId.get(TRACE_ID_COLUMN_ID);
|
||||
|
||||
return availableFields
|
||||
.filter((field) => !hidden.has(columnIdOf(field)))
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(orderIndex.get(columnIdOf(a)) ?? Infinity) -
|
||||
(orderIndex.get(columnIdOf(b)) ?? Infinity),
|
||||
);
|
||||
}, [availableFields, hiddenColumnIds, columnOrder]);
|
||||
return selected.some(isTraceId) || !traceId
|
||||
? selected
|
||||
: [traceId, ...selected];
|
||||
}, [availableFields, preferences?.columns]);
|
||||
|
||||
const columns = useMemo(
|
||||
() => buildTraceViewColumns(selectedFields),
|
||||
[selectedFields],
|
||||
);
|
||||
|
||||
const onFieldsChange = useCallback(
|
||||
(next: TelemetryFieldKey[]): void => {
|
||||
if (!isSuccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
const keptIds = new Set(next.map(columnIdOf));
|
||||
|
||||
columns.forEach((column) => {
|
||||
if (keptIds.has(column.id) || column.id === TRACE_ID_COLUMN_ID) {
|
||||
showColumn(STORAGE_KEY, column.id);
|
||||
} else {
|
||||
hideColumn(STORAGE_KEY, column.id);
|
||||
}
|
||||
});
|
||||
|
||||
// Columns missing from the order sort last, so the visible ones suffice.
|
||||
setColumnOrder(STORAGE_KEY, next.map(columnIdOf));
|
||||
const traceId = selectedFields.find(isTraceId);
|
||||
updateColumns(next.some(isTraceId) || !traceId ? next : [traceId, ...next]);
|
||||
},
|
||||
[columns, isSuccess],
|
||||
[isSuccess, selectedFields, updateColumns],
|
||||
);
|
||||
|
||||
const onColumnOrderChange = useCallback(
|
||||
(cols: TableColumnDef<TracesTableRow>[]): void => {
|
||||
const byId = new Map(
|
||||
selectedFields.map((field) => [columnIdOf(field), field]),
|
||||
);
|
||||
onFieldsChange(
|
||||
cols
|
||||
.map((col) => byId.get(col.id))
|
||||
.filter((field): field is TelemetryFieldKey => !!field),
|
||||
);
|
||||
},
|
||||
[selectedFields, onFieldsChange],
|
||||
);
|
||||
|
||||
const onColumnRemove = useCallback(
|
||||
(columnId: string): void => {
|
||||
onFieldsChange(
|
||||
selectedFields.filter((field) => columnIdOf(field) !== columnId),
|
||||
);
|
||||
},
|
||||
[selectedFields, onFieldsChange],
|
||||
);
|
||||
|
||||
return {
|
||||
columns,
|
||||
selectedFields,
|
||||
onFieldsChange,
|
||||
onColumnOrderChange,
|
||||
onColumnRemove,
|
||||
requiredFields: [TRACE_ID_COLUMN_ID],
|
||||
isLoading: !isFetched,
|
||||
canPersistColumns: isSuccess,
|
||||
|
||||
@@ -1,27 +1,15 @@
|
||||
import { Copy, Link, PenLine, Trash2 } from '@signozhq/icons';
|
||||
import {
|
||||
SavedviewtypesSchemaVersionDTO,
|
||||
SavedviewtypesSourceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { SavedviewtypesSchemaVersionDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import {
|
||||
buildSavedViewDeletePermission,
|
||||
buildSavedViewUpdatePermission,
|
||||
} from 'lib/authz/hooks/useAuthZ/permissions/saved-view.permissions';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { SavedViewRowActionConfig, SaveViewModalMode } from './types';
|
||||
|
||||
export const SAVED_VIEW_SCHEMA_VERSION = SavedviewtypesSchemaVersionDTO.v2;
|
||||
|
||||
// Only the logs and traces explorers have columns and formatting to save.
|
||||
export const SAVED_VIEW_OPTIONS_DATA_SOURCE: Partial<
|
||||
Record<SavedviewtypesSourceDTO, DataSource>
|
||||
> = {
|
||||
[SavedviewtypesSourceDTO.logs]: DataSource.LOGS,
|
||||
[SavedviewtypesSourceDTO.traces]: DataSource.TRACES,
|
||||
};
|
||||
|
||||
// What selecting a view writes to the url, so clearing it removes exactly these.
|
||||
export const SAVED_VIEW_URL_PARAMS = [
|
||||
QueryParams.viewKey,
|
||||
|
||||
@@ -2,18 +2,16 @@ import { useEffect, useMemo, useRef } from 'react';
|
||||
import { useGetSavedView } from 'api/generated/services/saved-view';
|
||||
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import useOptionsMenu from 'container/OptionsMenu/useOptionsMenu';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useGetSavedViewParams } from 'hooks/saveViews/useGetSavedViewParams';
|
||||
import { buildSavedViewReadPermission } from 'lib/authz/hooks/useAuthZ/permissions/saved-view.permissions';
|
||||
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { toAPIError } from 'utils/errorUtils';
|
||||
|
||||
import { SAVED_VIEW_OPTIONS_DATA_SOURCE } from '../constants';
|
||||
import { UseActiveSavedViewResult } from '../types';
|
||||
import { hasUnsavedViewChanges } from '../utils/hasUnsavedViewChanges';
|
||||
import { useSavedViewOptions } from './useSavedViewOptions';
|
||||
|
||||
export function useActiveSavedView(
|
||||
source: SavedviewtypesSourceDTO,
|
||||
@@ -21,10 +19,7 @@ export function useActiveSavedView(
|
||||
const { viewKey } = useGetSavedViewParams();
|
||||
const { stagedQuery, panelType } = useQueryBuilder();
|
||||
|
||||
const optionsDataSource = SAVED_VIEW_OPTIONS_DATA_SOURCE[source];
|
||||
const { options } = useOptionsMenu({
|
||||
dataSource: optionsDataSource ?? DataSource.LOGS,
|
||||
});
|
||||
const { preferenceSource, options } = useSavedViewOptions(source, panelType);
|
||||
|
||||
const { isLoading: isCheckingRead, deniedPermissions } = useAuthZ(
|
||||
[buildSavedViewReadPermission(viewKey)],
|
||||
@@ -63,7 +58,8 @@ export function useActiveSavedView(
|
||||
view,
|
||||
stagedQuery,
|
||||
panelType: panelType ?? PANEL_TYPES.LIST,
|
||||
options: optionsDataSource ? options : undefined,
|
||||
preferenceSource,
|
||||
options,
|
||||
});
|
||||
}, [
|
||||
view,
|
||||
@@ -71,7 +67,7 @@ export function useActiveSavedView(
|
||||
isStagedQueryCurrent,
|
||||
stagedQuery,
|
||||
panelType,
|
||||
optionsDataSource,
|
||||
preferenceSource,
|
||||
options,
|
||||
]);
|
||||
|
||||
|
||||
@@ -15,27 +15,29 @@ import {
|
||||
import { ErrorType } from 'api/generatedAPIInstance';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import useOptionsMenu from 'container/OptionsMenu/useOptionsMenu';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { toAPIError } from 'utils/errorUtils';
|
||||
import { getUnstableCurrentSearchParams } from 'utils/getUnstableCurrentSearchParams';
|
||||
|
||||
import {
|
||||
SAVED_VIEW_OPTIONS_DATA_SOURCE,
|
||||
SAVED_VIEW_SCHEMA_VERSION,
|
||||
SAVED_VIEW_TOAST_POSITION,
|
||||
SAVED_VIEW_URL_PARAMS,
|
||||
} from '../constants';
|
||||
import { UseSavedViewActionsResult } from '../types';
|
||||
import {
|
||||
getPreferenceSlice,
|
||||
getSavedViewPreferenceSource,
|
||||
} from '../utils/getSavedViewPreferenceSource';
|
||||
import { getSavedViewQuery } from '../utils/getSavedViewQuery';
|
||||
import { getViewColumnsAndFormatting } from '../utils/getViewColumnsAndFormatting';
|
||||
import { invalidateSavedViews } from '../utils/invalidateSavedViews';
|
||||
import { toSavedViewSpec } from '../utils/toSavedViewSpec';
|
||||
import { useLastUsedView } from './useLastUsedView';
|
||||
import { useSavedViewOptions } from './useSavedViewOptions';
|
||||
|
||||
// Columns and formatting need no handling here: with a viewKey in the url the
|
||||
// preference provider reads them from the view, and without one it falls back
|
||||
@@ -51,15 +53,8 @@ export function useSavedViewActions(
|
||||
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
||||
const { setLastUsedView, clearLastUsedView } = useLastUsedView(source);
|
||||
|
||||
const optionsDataSource = SAVED_VIEW_OPTIONS_DATA_SOURCE[source];
|
||||
const { options } = useOptionsMenu({
|
||||
dataSource: optionsDataSource ?? DataSource.LOGS,
|
||||
});
|
||||
const { options } = useSavedViewOptions(source, panelType);
|
||||
const preferences = usePreferenceContext();
|
||||
const preferencesSlice =
|
||||
optionsDataSource === DataSource.TRACES
|
||||
? preferences.traces
|
||||
: preferences.logs;
|
||||
|
||||
const { mutateAsync: createSavedView, isLoading: isCreating } =
|
||||
useCreateSavedView();
|
||||
@@ -77,10 +72,10 @@ export function useSavedViewActions(
|
||||
query: stagedQuery ?? currentQuery,
|
||||
panelType: panelType ?? PANEL_TYPES.LIST,
|
||||
displayName,
|
||||
options: optionsDataSource ? options : undefined,
|
||||
options,
|
||||
color,
|
||||
}),
|
||||
[stagedQuery, currentQuery, panelType, optionsDataSource, options],
|
||||
[stagedQuery, currentQuery, panelType, options],
|
||||
);
|
||||
|
||||
const selectView = useCallback(
|
||||
@@ -96,19 +91,27 @@ export function useSavedViewActions(
|
||||
|
||||
const revertView = useCallback(
|
||||
(view: SavedviewtypesSavedViewDTO): void => {
|
||||
if (optionsDataSource) {
|
||||
const viewPreferenceSource = getSavedViewPreferenceSource(
|
||||
source,
|
||||
view.spec.panelType,
|
||||
);
|
||||
const viewPreferencesSlice = getPreferenceSlice(
|
||||
preferences,
|
||||
viewPreferenceSource,
|
||||
);
|
||||
if (viewPreferenceSource && viewPreferencesSlice) {
|
||||
const { columns, formatting } = getViewColumnsAndFormatting(
|
||||
view.spec,
|
||||
optionsDataSource,
|
||||
viewPreferenceSource,
|
||||
);
|
||||
preferencesSlice.updateColumns(columns);
|
||||
viewPreferencesSlice.updateColumns(columns);
|
||||
if (formatting) {
|
||||
preferencesSlice.updateFormatting(formatting);
|
||||
viewPreferencesSlice.updateFormatting(formatting);
|
||||
}
|
||||
}
|
||||
selectView(view);
|
||||
},
|
||||
[optionsDataSource, preferencesSlice, selectView],
|
||||
[source, preferences, selectView],
|
||||
);
|
||||
|
||||
// The query builder redirects only ever add params, so the url is rebuilt
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useMemo } from 'react';
|
||||
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { defaultOptionsQuery } from 'container/OptionsMenu/constants';
|
||||
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import { PreferenceSource } from 'providers/preferences/types';
|
||||
|
||||
import { SavedViewOptions } from '../types';
|
||||
import {
|
||||
getPreferenceSlice,
|
||||
getSavedViewPreferenceSource,
|
||||
} from '../utils/getSavedViewPreferenceSource';
|
||||
|
||||
interface UseSavedViewOptionsResult {
|
||||
preferenceSource?: PreferenceSource;
|
||||
options?: SavedViewOptions;
|
||||
}
|
||||
|
||||
export function useSavedViewOptions(
|
||||
source: SavedviewtypesSourceDTO,
|
||||
panelType: string | null | undefined,
|
||||
): UseSavedViewOptionsResult {
|
||||
const preferences = usePreferenceContext();
|
||||
const preferenceSource = getSavedViewPreferenceSource(source, panelType);
|
||||
const preferencesSlice = getPreferenceSlice(preferences, preferenceSource);
|
||||
const slicePreferences = preferencesSlice?.preferences;
|
||||
|
||||
const options = useMemo((): SavedViewOptions | undefined => {
|
||||
if (!preferencesSlice) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
selectColumns: slicePreferences?.columns || [],
|
||||
format: slicePreferences?.formatting?.format || defaultOptionsQuery.format,
|
||||
maxLines:
|
||||
slicePreferences?.formatting?.maxLines || defaultOptionsQuery.maxLines,
|
||||
fontSize:
|
||||
slicePreferences?.formatting?.fontSize || defaultOptionsQuery.fontSize,
|
||||
};
|
||||
}, [preferencesSlice, slicePreferences]);
|
||||
|
||||
return { preferenceSource, options };
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { OptionsQuery } from 'container/OptionsMenu/types';
|
||||
import { PreferenceSource } from 'providers/preferences/types';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
@@ -29,6 +30,7 @@ export interface HasUnsavedViewChangesArgs {
|
||||
view: SavedviewtypesSavedViewDTO;
|
||||
stagedQuery: Query | null;
|
||||
panelType: PANEL_TYPES;
|
||||
preferenceSource?: PreferenceSource;
|
||||
options?: SavedViewOptions;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
SavedviewtypesPanelTypeDTO,
|
||||
SavedviewtypesSavedViewSpecDTO,
|
||||
SavedviewtypesSourceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { defaultSelectedColumns as defaultAIListColumns } from 'container/LLMObservability/Explorer/ListView/configs';
|
||||
import {
|
||||
AI_OBSERVABILITY_PREFERENCE_SOURCE,
|
||||
AI_OBSERVABILITY_TRACE_VIEW_PREFERENCE_SOURCE,
|
||||
} from 'providers/preferences/types';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { getSavedViewPreferenceSource } from '../getSavedViewPreferenceSource';
|
||||
import { getViewColumnsAndFormatting } from '../getViewColumnsAndFormatting';
|
||||
|
||||
const traceColumns = [
|
||||
{ name: 'trace_id' },
|
||||
{ name: 'total_tokens' },
|
||||
] as TelemetryFieldKey[];
|
||||
|
||||
const specFor = (
|
||||
panelType: SavedviewtypesPanelTypeDTO,
|
||||
): SavedviewtypesSavedViewSpecDTO =>
|
||||
({
|
||||
displayName: 'Costly traces',
|
||||
panelType,
|
||||
queries: [],
|
||||
selectedFields: traceColumns,
|
||||
}) as unknown as SavedviewtypesSavedViewSpecDTO;
|
||||
|
||||
describe('getSavedViewPreferenceSource', () => {
|
||||
it.each([
|
||||
[SavedviewtypesSourceDTO.logs, PANEL_TYPES.LIST, DataSource.LOGS],
|
||||
[SavedviewtypesSourceDTO.traces, PANEL_TYPES.TRACE, DataSource.TRACES],
|
||||
[
|
||||
SavedviewtypesSourceDTO.ai_observability,
|
||||
PANEL_TYPES.LIST,
|
||||
AI_OBSERVABILITY_PREFERENCE_SOURCE,
|
||||
],
|
||||
[
|
||||
SavedviewtypesSourceDTO.ai_observability,
|
||||
PANEL_TYPES.TRACE,
|
||||
AI_OBSERVABILITY_TRACE_VIEW_PREFERENCE_SOURCE,
|
||||
],
|
||||
[
|
||||
SavedviewtypesSourceDTO.ai_observability,
|
||||
PANEL_TYPES.TIME_SERIES,
|
||||
undefined,
|
||||
],
|
||||
[SavedviewtypesSourceDTO.metrics, PANEL_TYPES.LIST, undefined],
|
||||
])('maps %s on %s to %s', (source, panelType, expected) => {
|
||||
expect(getSavedViewPreferenceSource(source, panelType)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getViewColumnsAndFormatting for AI views', () => {
|
||||
it('gives a trace view its own columns', () => {
|
||||
expect(
|
||||
getViewColumnsAndFormatting(
|
||||
specFor(SavedviewtypesPanelTypeDTO.trace),
|
||||
AI_OBSERVABILITY_TRACE_VIEW_PREFERENCE_SOURCE,
|
||||
).columns,
|
||||
).toStrictEqual(traceColumns);
|
||||
});
|
||||
|
||||
it('keeps the list tab on its defaults for a trace view', () => {
|
||||
expect(
|
||||
getViewColumnsAndFormatting(
|
||||
specFor(SavedviewtypesPanelTypeDTO.trace),
|
||||
AI_OBSERVABILITY_PREFERENCE_SOURCE,
|
||||
).columns,
|
||||
).toStrictEqual(defaultAIListColumns);
|
||||
});
|
||||
|
||||
it('keeps the trace tab on its defaults for a list view', () => {
|
||||
expect(
|
||||
getViewColumnsAndFormatting(
|
||||
specFor(SavedviewtypesPanelTypeDTO.list),
|
||||
AI_OBSERVABILITY_TRACE_VIEW_PREFERENCE_SOURCE,
|
||||
).columns,
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -120,6 +120,7 @@ describe('hasUnsavedViewChanges', () => {
|
||||
view,
|
||||
stagedQuery: staged,
|
||||
panelType: PANEL_TYPES.LIST,
|
||||
preferenceSource: DataSource.TRACES,
|
||||
options: { ...options, selectColumns: [columns[0]] },
|
||||
}),
|
||||
).toBe(true);
|
||||
@@ -139,6 +140,7 @@ describe('hasUnsavedViewChanges', () => {
|
||||
view: bare,
|
||||
stagedQuery: stagedFrom(bare, DataSource.TRACES),
|
||||
panelType: PANEL_TYPES.LIST,
|
||||
preferenceSource: DataSource.TRACES,
|
||||
options: { ...options, selectColumns: defaultTraceSelectedColumns },
|
||||
}),
|
||||
).toBe(false);
|
||||
@@ -163,6 +165,7 @@ describe('hasUnsavedViewChanges', () => {
|
||||
view: logsView,
|
||||
stagedQuery: logsStaged,
|
||||
panelType: PANEL_TYPES.LIST,
|
||||
preferenceSource: DataSource.LOGS,
|
||||
options: { selectColumns: shownColumns, ...formatting },
|
||||
}),
|
||||
).toBe(false);
|
||||
@@ -174,6 +177,7 @@ describe('hasUnsavedViewChanges', () => {
|
||||
view: logsView,
|
||||
stagedQuery: logsStaged,
|
||||
panelType: PANEL_TYPES.LIST,
|
||||
preferenceSource: DataSource.LOGS,
|
||||
options: { selectColumns: shownColumns, ...formatting, maxLines: 5 },
|
||||
}),
|
||||
).toBe(true);
|
||||
@@ -193,6 +197,7 @@ describe('hasUnsavedViewChanges', () => {
|
||||
view: bare,
|
||||
stagedQuery: stagedFrom(bare, DataSource.LOGS),
|
||||
panelType: PANEL_TYPES.LIST,
|
||||
preferenceSource: DataSource.LOGS,
|
||||
options: {
|
||||
selectColumns: defaultLogsSelectedColumns,
|
||||
format: 'table',
|
||||
@@ -373,6 +378,7 @@ describe('hasUnsavedViewChanges', () => {
|
||||
view,
|
||||
stagedQuery: staged,
|
||||
panelType: PANEL_TYPES.LIST,
|
||||
preferenceSource: DataSource.TRACES,
|
||||
options: { ...options, ...formatting, maxLines: 5 },
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import {
|
||||
AI_OBSERVABILITY_PREFERENCE_SOURCE,
|
||||
AI_OBSERVABILITY_TRACE_VIEW_PREFERENCE_SOURCE,
|
||||
PreferenceContextValue,
|
||||
PreferenceSlice,
|
||||
PreferenceSource,
|
||||
} from 'providers/preferences/types';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
// The preference slice whose columns a view of this source and panel saves;
|
||||
// none when that explorer tab has no columns. The AI explorer keeps one per tab.
|
||||
export function getSavedViewPreferenceSource(
|
||||
source: SavedviewtypesSourceDTO,
|
||||
panelType: string | null | undefined,
|
||||
): PreferenceSource | undefined {
|
||||
switch (source) {
|
||||
case SavedviewtypesSourceDTO.logs:
|
||||
return DataSource.LOGS;
|
||||
case SavedviewtypesSourceDTO.traces:
|
||||
return DataSource.TRACES;
|
||||
case SavedviewtypesSourceDTO.ai_observability:
|
||||
if (panelType === PANEL_TYPES.LIST) {
|
||||
return AI_OBSERVABILITY_PREFERENCE_SOURCE;
|
||||
}
|
||||
if (panelType === PANEL_TYPES.TRACE) {
|
||||
return AI_OBSERVABILITY_TRACE_VIEW_PREFERENCE_SOURCE;
|
||||
}
|
||||
return undefined;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function getPreferenceSlice(
|
||||
preferences: PreferenceContextValue,
|
||||
preferenceSource: PreferenceSource | undefined,
|
||||
): PreferenceSlice | undefined {
|
||||
switch (preferenceSource) {
|
||||
case DataSource.LOGS:
|
||||
return preferences.logs;
|
||||
case DataSource.TRACES:
|
||||
return preferences.traces;
|
||||
case AI_OBSERVABILITY_PREFERENCE_SOURCE:
|
||||
return preferences.aiObservability;
|
||||
case AI_OBSERVABILITY_TRACE_VIEW_PREFERENCE_SOURCE:
|
||||
return preferences.aiObservabilityTraceView;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import { SavedviewtypesSavedViewSpecDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
SavedviewtypesPanelTypeDTO,
|
||||
SavedviewtypesSavedViewSpecDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import {
|
||||
defaultLogsSelectedColumns,
|
||||
@@ -6,7 +9,13 @@ import {
|
||||
ensureLogsRequiredColumns,
|
||||
} from 'container/OptionsMenu/constants';
|
||||
import { FontSize, LogViewMode } from 'container/OptionsMenu/types';
|
||||
import { Preferences } from 'providers/preferences/types';
|
||||
import { defaultSelectedColumns as defaultAIObservabilitySelectedColumns } from 'container/LLMObservability/Explorer/ListView/configs';
|
||||
import {
|
||||
AI_OBSERVABILITY_PREFERENCE_SOURCE,
|
||||
AI_OBSERVABILITY_TRACE_VIEW_PREFERENCE_SOURCE,
|
||||
Preferences,
|
||||
PreferenceSource,
|
||||
} from 'providers/preferences/types';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
function withColumnNames(
|
||||
@@ -25,7 +34,7 @@ function withColumnNames(
|
||||
// defaults filled in where the view has none.
|
||||
export function getViewColumnsAndFormatting(
|
||||
spec: SavedviewtypesSavedViewSpecDTO | undefined,
|
||||
dataSource: DataSource,
|
||||
dataSource: PreferenceSource,
|
||||
): Preferences {
|
||||
const selectedFields = spec?.selectedFields as TelemetryFieldKey[] | undefined;
|
||||
|
||||
@@ -45,5 +54,20 @@ export function getViewColumnsAndFormatting(
|
||||
if (dataSource === DataSource.TRACES) {
|
||||
return { columns: selectedFields || defaultTraceSelectedColumns };
|
||||
}
|
||||
// An AI view saves the columns of its own tab only; the other tab keeps its defaults.
|
||||
if (dataSource === AI_OBSERVABILITY_PREFERENCE_SOURCE) {
|
||||
return {
|
||||
columns:
|
||||
(spec?.panelType === SavedviewtypesPanelTypeDTO.list && selectedFields) ||
|
||||
defaultAIObservabilitySelectedColumns,
|
||||
};
|
||||
}
|
||||
if (dataSource === AI_OBSERVABILITY_TRACE_VIEW_PREFERENCE_SOURCE) {
|
||||
return {
|
||||
columns:
|
||||
(spec?.panelType === SavedviewtypesPanelTypeDTO.trace && selectedFields) ||
|
||||
[],
|
||||
};
|
||||
}
|
||||
return { columns: [] };
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ export function hasUnsavedViewChanges({
|
||||
view,
|
||||
stagedQuery,
|
||||
panelType,
|
||||
preferenceSource,
|
||||
options,
|
||||
}: HasUnsavedViewChangesArgs): boolean {
|
||||
if (!stagedQuery) {
|
||||
@@ -72,14 +73,13 @@ export function hasUnsavedViewChanges({
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!options) {
|
||||
if (!options || !preferenceSource) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const dataSource = stagedQuery.builder.queryData[0]?.dataSource;
|
||||
const { columns, formatting } = getViewColumnsAndFormatting(
|
||||
view.spec,
|
||||
dataSource,
|
||||
preferenceSource,
|
||||
);
|
||||
const toNames = (fields: { name?: string; key?: string }[]): unknown[] =>
|
||||
fields.map((field) => field.name ?? field.key);
|
||||
@@ -87,7 +87,7 @@ export function hasUnsavedViewChanges({
|
||||
return true;
|
||||
}
|
||||
|
||||
if (dataSource !== DataSource.LOGS || !formatting) {
|
||||
if (preferenceSource !== DataSource.LOGS || !formatting) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/* eslint-disable no-empty */
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { defaultSelectedColumns } from 'container/LLMObservability/Explorer/ListView/configs';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
const isValidTraceColumn = (col: {
|
||||
name?: string;
|
||||
signal?: string;
|
||||
[key: string]: unknown;
|
||||
}): boolean => !(col?.signal && col.signal !== 'traces');
|
||||
|
||||
// --- AI OBSERVABILITY preferences loader config ---
|
||||
const aiObservabilityLoaders = {
|
||||
local: (): {
|
||||
columns: BaseAutocompleteData[];
|
||||
} => {
|
||||
const local = getLocalStorageKey(LOCALSTORAGE.AI_OBSERVABILITY_LIST_OPTIONS);
|
||||
if (local) {
|
||||
try {
|
||||
const parsed = JSON.parse(local);
|
||||
const localColumns = parsed.selectColumns || [];
|
||||
return { columns: localColumns.filter(isValidTraceColumn) };
|
||||
} catch {}
|
||||
}
|
||||
return { columns: [] };
|
||||
},
|
||||
url: (): {
|
||||
columns: BaseAutocompleteData[];
|
||||
} => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
try {
|
||||
const options = JSON.parse(urlParams.get('options') || '{}');
|
||||
const urlColumns = options.selectColumns || [];
|
||||
return { columns: urlColumns.filter(isValidTraceColumn) };
|
||||
} catch {}
|
||||
return { columns: [] };
|
||||
},
|
||||
default: (): {
|
||||
columns: TelemetryFieldKey[];
|
||||
} => ({
|
||||
columns: defaultSelectedColumns,
|
||||
}),
|
||||
priority: ['local', 'url', 'default'] as const,
|
||||
};
|
||||
|
||||
export default aiObservabilityLoaders;
|
||||
@@ -0,0 +1,55 @@
|
||||
/* eslint-disable no-empty */
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { AI_OBSERVABILITY_TRACE_VIEW_URL_OPTIONS } from '../types';
|
||||
|
||||
const isValidTraceColumn = (col: {
|
||||
name?: string;
|
||||
signal?: string;
|
||||
[key: string]: unknown;
|
||||
}): boolean => !(col?.signal && col.signal !== 'traces');
|
||||
|
||||
// --- AI OBSERVABILITY TRACE VIEW preferences loader config ---
|
||||
// Empty columns mean "use the defaults": they depend on the field keys the view fetches.
|
||||
const aiObservabilityTraceViewLoaders = {
|
||||
local: (): {
|
||||
columns: BaseAutocompleteData[];
|
||||
} => {
|
||||
const local = getLocalStorageKey(
|
||||
LOCALSTORAGE.AI_OBSERVABILITY_TRACE_VIEW_OPTIONS,
|
||||
);
|
||||
if (local) {
|
||||
try {
|
||||
const parsed = JSON.parse(local);
|
||||
const localColumns = parsed.selectColumns || [];
|
||||
return { columns: localColumns.filter(isValidTraceColumn) };
|
||||
} catch {}
|
||||
}
|
||||
return { columns: [] };
|
||||
},
|
||||
url: (): {
|
||||
columns: BaseAutocompleteData[];
|
||||
} => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
try {
|
||||
const options = JSON.parse(
|
||||
urlParams.get(AI_OBSERVABILITY_TRACE_VIEW_URL_OPTIONS) || '{}',
|
||||
);
|
||||
const urlColumns = options.selectColumns || [];
|
||||
return { columns: urlColumns.filter(isValidTraceColumn) };
|
||||
} catch {}
|
||||
return { columns: [] };
|
||||
},
|
||||
default: (): {
|
||||
columns: TelemetryFieldKey[];
|
||||
} => ({
|
||||
columns: [],
|
||||
}),
|
||||
// A shared link opens with the sender's columns.
|
||||
priority: ['url', 'local', 'default'] as const,
|
||||
};
|
||||
|
||||
export default aiObservabilityTraceViewLoaders;
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import setLocalStorageKey from 'api/browser/localstorage/set';
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { defaultOptionsQuery } from 'container/OptionsMenu/constants';
|
||||
import { OptionsQuery } from 'container/OptionsMenu/types';
|
||||
|
||||
import { PreferenceMode, Preferences } from '../types';
|
||||
|
||||
// --- AI OBSERVABILITY TRACE VIEW preferences updater config ---
|
||||
const getAIObservabilityTraceViewUpdaterConfig = (
|
||||
redirectWithOptionsData: (options: OptionsQuery) => void,
|
||||
setSavedViewPreferences: Dispatch<SetStateAction<Preferences | null>>,
|
||||
): {
|
||||
updateColumns: (newColumns: TelemetryFieldKey[], mode: string) => void;
|
||||
updateFormatting: () => void;
|
||||
} => ({
|
||||
updateColumns: (newColumns: TelemetryFieldKey[], mode: string): void => {
|
||||
if (mode === PreferenceMode.SAVED_VIEW) {
|
||||
setSavedViewPreferences({ columns: newColumns });
|
||||
}
|
||||
|
||||
if (mode === PreferenceMode.DIRECT) {
|
||||
redirectWithOptionsData({
|
||||
...defaultOptionsQuery,
|
||||
selectColumns: newColumns,
|
||||
});
|
||||
|
||||
const local = JSON.parse(
|
||||
getLocalStorageKey(LOCALSTORAGE.AI_OBSERVABILITY_TRACE_VIEW_OPTIONS) ||
|
||||
'{}',
|
||||
);
|
||||
local.selectColumns = newColumns;
|
||||
setLocalStorageKey(
|
||||
LOCALSTORAGE.AI_OBSERVABILITY_TRACE_VIEW_OPTIONS,
|
||||
JSON.stringify(local),
|
||||
);
|
||||
}
|
||||
},
|
||||
updateFormatting: (): void => {}, // no-op for AI observability
|
||||
});
|
||||
|
||||
export default getAIObservabilityTraceViewUpdaterConfig;
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import setLocalStorageKey from 'api/browser/localstorage/set';
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { defaultOptionsQuery } from 'container/OptionsMenu/constants';
|
||||
import { OptionsQuery } from 'container/OptionsMenu/types';
|
||||
|
||||
import { PreferenceMode, Preferences } from '../types';
|
||||
|
||||
// --- AI OBSERVABILITY preferences updater config ---
|
||||
const getAIObservabilityUpdaterConfig = (
|
||||
redirectWithOptionsData: (options: OptionsQuery) => void,
|
||||
setSavedViewPreferences: Dispatch<SetStateAction<Preferences | null>>,
|
||||
): {
|
||||
updateColumns: (newColumns: TelemetryFieldKey[], mode: string) => void;
|
||||
updateFormatting: () => void;
|
||||
} => ({
|
||||
updateColumns: (newColumns: TelemetryFieldKey[], mode: string): void => {
|
||||
if (mode === PreferenceMode.SAVED_VIEW) {
|
||||
setSavedViewPreferences({ columns: newColumns });
|
||||
}
|
||||
|
||||
if (mode === PreferenceMode.DIRECT) {
|
||||
redirectWithOptionsData({
|
||||
...defaultOptionsQuery,
|
||||
selectColumns: newColumns,
|
||||
});
|
||||
|
||||
const local = JSON.parse(
|
||||
getLocalStorageKey(LOCALSTORAGE.AI_OBSERVABILITY_LIST_OPTIONS) || '{}',
|
||||
);
|
||||
local.selectColumns = newColumns;
|
||||
setLocalStorageKey(
|
||||
LOCALSTORAGE.AI_OBSERVABILITY_LIST_OPTIONS,
|
||||
JSON.stringify(local),
|
||||
);
|
||||
}
|
||||
},
|
||||
updateFormatting: (): void => {}, // no-op for AI observability
|
||||
});
|
||||
|
||||
export default getAIObservabilityUpdaterConfig;
|
||||
@@ -7,6 +7,8 @@ import React, {
|
||||
} from 'react';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import {
|
||||
AI_OBSERVABILITY_PREFERENCE_SOURCE,
|
||||
AI_OBSERVABILITY_TRACE_VIEW_PREFERENCE_SOURCE,
|
||||
PreferenceContextValue,
|
||||
PreferenceMode,
|
||||
} from 'providers/preferences/types';
|
||||
@@ -47,12 +49,26 @@ export function PreferenceContextProvider({
|
||||
dataSource: DataSource.TRACES,
|
||||
});
|
||||
|
||||
const aiObservabilitySlice = usePreferenceSync({
|
||||
mode: savedViewId ? PreferenceMode.SAVED_VIEW : PreferenceMode.DIRECT,
|
||||
savedViewId: savedViewId || undefined,
|
||||
dataSource: AI_OBSERVABILITY_PREFERENCE_SOURCE,
|
||||
});
|
||||
|
||||
const aiObservabilityTraceViewSlice = usePreferenceSync({
|
||||
mode: savedViewId ? PreferenceMode.SAVED_VIEW : PreferenceMode.DIRECT,
|
||||
savedViewId: savedViewId || undefined,
|
||||
dataSource: AI_OBSERVABILITY_TRACE_VIEW_PREFERENCE_SOURCE,
|
||||
});
|
||||
|
||||
const value = useMemo<PreferenceContextValue>(
|
||||
() => ({
|
||||
logs: logsSlice,
|
||||
traces: tracesSlice,
|
||||
aiObservability: aiObservabilitySlice,
|
||||
aiObservabilityTraceView: aiObservabilityTraceViewSlice,
|
||||
}),
|
||||
[logsSlice, tracesSlice],
|
||||
[logsSlice, tracesSlice, aiObservabilitySlice, aiObservabilityTraceViewSlice],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,9 +4,17 @@ import { ensureLogsRequiredColumns } from 'container/OptionsMenu/constants';
|
||||
import { has } from 'lodash-es';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import aiObservabilityLoaderConfig from '../configs/aiObservabilityLoaderConfig';
|
||||
import aiObservabilityTraceViewLoaderConfig from '../configs/aiObservabilityTraceViewLoaderConfig';
|
||||
import logsLoaderConfig from '../configs/logsLoaderConfig';
|
||||
import tracesLoaderConfig from '../configs/tracesLoaderConfig';
|
||||
import { FormattingOptions, Preferences } from '../types';
|
||||
import {
|
||||
AI_OBSERVABILITY_PREFERENCE_SOURCE,
|
||||
AI_OBSERVABILITY_TRACE_VIEW_PREFERENCE_SOURCE,
|
||||
FormattingOptions,
|
||||
Preferences,
|
||||
PreferenceSource,
|
||||
} from '../types';
|
||||
|
||||
const migrateColumns = (columns: any): any =>
|
||||
columns.map((column: any) => {
|
||||
@@ -65,12 +73,24 @@ function tracesPreferencesLoader(): {
|
||||
return preferencesLoader(tracesLoaderConfig);
|
||||
}
|
||||
|
||||
function aiObservabilityPreferencesLoader(): {
|
||||
columns: TelemetryFieldKey[];
|
||||
} {
|
||||
return preferencesLoader(aiObservabilityLoaderConfig);
|
||||
}
|
||||
|
||||
function aiObservabilityTraceViewPreferencesLoader(): {
|
||||
columns: TelemetryFieldKey[];
|
||||
} {
|
||||
return preferencesLoader(aiObservabilityTraceViewLoaderConfig);
|
||||
}
|
||||
|
||||
export function usePreferenceLoader({
|
||||
dataSource,
|
||||
reSync,
|
||||
setReSync,
|
||||
}: {
|
||||
dataSource: DataSource;
|
||||
dataSource: PreferenceSource;
|
||||
reSync: boolean;
|
||||
setReSync: (value: boolean) => void;
|
||||
}): {
|
||||
@@ -87,6 +107,14 @@ export function usePreferenceLoader({
|
||||
const { columns } = tracesPreferencesLoader();
|
||||
return { columns };
|
||||
}
|
||||
if (dataSource === AI_OBSERVABILITY_PREFERENCE_SOURCE) {
|
||||
const { columns } = aiObservabilityPreferencesLoader();
|
||||
return { columns };
|
||||
}
|
||||
if (dataSource === AI_OBSERVABILITY_TRACE_VIEW_PREFERENCE_SOURCE) {
|
||||
const { columns } = aiObservabilityTraceViewPreferencesLoader();
|
||||
return { columns };
|
||||
}
|
||||
return null;
|
||||
});
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
@@ -105,6 +133,16 @@ export function usePreferenceLoader({
|
||||
const { columns } = tracesPreferencesLoader();
|
||||
setPreferences({ columns });
|
||||
}
|
||||
|
||||
if (dataSource === AI_OBSERVABILITY_PREFERENCE_SOURCE) {
|
||||
const { columns } = aiObservabilityPreferencesLoader();
|
||||
setPreferences({ columns });
|
||||
}
|
||||
|
||||
if (dataSource === AI_OBSERVABILITY_TRACE_VIEW_PREFERENCE_SOURCE) {
|
||||
const { columns } = aiObservabilityTraceViewPreferencesLoader();
|
||||
setPreferences({ columns });
|
||||
}
|
||||
} catch (e) {
|
||||
setError(e as Error);
|
||||
} finally {
|
||||
|
||||
@@ -3,10 +3,14 @@ import { useEffect, useRef, useState } from 'react';
|
||||
import { useGetSavedView } from 'api/generated/services/saved-view';
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import { getViewColumnsAndFormatting } from 'container/SavedViews/utils/getViewColumnsAndFormatting';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { usePreferenceLoader } from '../loader/usePreferenceLoader';
|
||||
import { FormattingOptions, PreferenceMode, Preferences } from '../types';
|
||||
import {
|
||||
FormattingOptions,
|
||||
PreferenceMode,
|
||||
Preferences,
|
||||
PreferenceSource,
|
||||
} from '../types';
|
||||
import { usePreferenceUpdater } from '../updater/usePreferenceUpdater';
|
||||
|
||||
export function usePreferenceSync({
|
||||
@@ -15,7 +19,7 @@ export function usePreferenceSync({
|
||||
savedViewId,
|
||||
}: {
|
||||
mode: PreferenceMode;
|
||||
dataSource: DataSource;
|
||||
dataSource: PreferenceSource;
|
||||
savedViewId: string | undefined;
|
||||
}): {
|
||||
preferences: Preferences | null;
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import { LogViewMode } from 'container/OptionsMenu/types';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
// AI Observability queries run on traces, so its slice needs a key of its own.
|
||||
export const AI_OBSERVABILITY_PREFERENCE_SOURCE = 'ai_observability';
|
||||
|
||||
// The trace view shares a page with the list view, so it keeps its columns under a URL param of its own.
|
||||
export const AI_OBSERVABILITY_TRACE_VIEW_PREFERENCE_SOURCE =
|
||||
'ai_observability_trace_view';
|
||||
export const AI_OBSERVABILITY_TRACE_VIEW_URL_OPTIONS = 'traceViewOptions';
|
||||
|
||||
export type PreferenceSource =
|
||||
| DataSource
|
||||
| typeof AI_OBSERVABILITY_PREFERENCE_SOURCE
|
||||
| typeof AI_OBSERVABILITY_TRACE_VIEW_PREFERENCE_SOURCE;
|
||||
|
||||
export enum PreferenceMode {
|
||||
SAVED_VIEW = 'savedView',
|
||||
@@ -18,6 +32,8 @@ export interface PreferenceSlice {
|
||||
export interface PreferenceContextValue {
|
||||
logs: PreferenceSlice;
|
||||
traces: PreferenceSlice;
|
||||
aiObservability: PreferenceSlice;
|
||||
aiObservabilityTraceView: PreferenceSlice;
|
||||
}
|
||||
|
||||
export interface FormattingOptions {
|
||||
|
||||
@@ -8,9 +8,18 @@ import { OptionsQuery } from 'container/OptionsMenu/types';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import getAIObservabilityTraceViewUpdaterConfig from '../configs/aiObservabilityTraceViewUpdaterConfig';
|
||||
import getAIObservabilityUpdaterConfig from '../configs/aiObservabilityUpdaterConfig';
|
||||
import getLogsUpdaterConfig from '../configs/logsUpdaterConfig';
|
||||
import getTracesUpdaterConfig from '../configs/tracesUpdaterConfig';
|
||||
import { FormattingOptions, Preferences } from '../types';
|
||||
import {
|
||||
AI_OBSERVABILITY_PREFERENCE_SOURCE,
|
||||
AI_OBSERVABILITY_TRACE_VIEW_PREFERENCE_SOURCE,
|
||||
AI_OBSERVABILITY_TRACE_VIEW_URL_OPTIONS,
|
||||
FormattingOptions,
|
||||
Preferences,
|
||||
PreferenceSource,
|
||||
} from '../types';
|
||||
|
||||
const metricsUpdater = {
|
||||
updateColumns: (): void => {}, // no-op for metrics
|
||||
@@ -22,7 +31,7 @@ const getUpdaterConfig = (
|
||||
redirectWithOptionsData: (options: OptionsQuery) => void,
|
||||
setSavedViewPreferences: Dispatch<SetStateAction<Preferences | null>>,
|
||||
): Record<
|
||||
DataSource,
|
||||
PreferenceSource,
|
||||
{
|
||||
updateColumns: (newColumns: TelemetryFieldKey[], mode: string) => void;
|
||||
updateFormatting: (newFormatting: FormattingOptions, mode: string) => void;
|
||||
@@ -38,8 +47,22 @@ const getUpdaterConfig = (
|
||||
setSavedViewPreferences,
|
||||
),
|
||||
[DataSource.METRICS]: metricsUpdater,
|
||||
[AI_OBSERVABILITY_PREFERENCE_SOURCE]: getAIObservabilityUpdaterConfig(
|
||||
redirectWithOptionsData,
|
||||
setSavedViewPreferences,
|
||||
),
|
||||
[AI_OBSERVABILITY_TRACE_VIEW_PREFERENCE_SOURCE]:
|
||||
getAIObservabilityTraceViewUpdaterConfig(
|
||||
redirectWithOptionsData,
|
||||
setSavedViewPreferences,
|
||||
),
|
||||
});
|
||||
|
||||
const getUrlOptionsKey = (dataSource: PreferenceSource): string =>
|
||||
dataSource === AI_OBSERVABILITY_TRACE_VIEW_PREFERENCE_SOURCE
|
||||
? AI_OBSERVABILITY_TRACE_VIEW_URL_OPTIONS
|
||||
: URL_OPTIONS;
|
||||
|
||||
export function usePreferenceUpdater({
|
||||
dataSource,
|
||||
mode,
|
||||
@@ -47,7 +70,7 @@ export function usePreferenceUpdater({
|
||||
setReSync,
|
||||
setSavedViewPreferences,
|
||||
}: {
|
||||
dataSource: DataSource;
|
||||
dataSource: PreferenceSource;
|
||||
mode: string;
|
||||
preferences: Preferences | null;
|
||||
setReSync: Dispatch<SetStateAction<boolean>>;
|
||||
@@ -57,7 +80,10 @@ export function usePreferenceUpdater({
|
||||
updateFormatting: (newFormatting: FormattingOptions) => void;
|
||||
} {
|
||||
const { redirectWithQuery: redirectWithOptionsData } =
|
||||
useUrlQueryData<OptionsQuery>(URL_OPTIONS, defaultOptionsQuery);
|
||||
useUrlQueryData<OptionsQuery>(
|
||||
getUrlOptionsKey(dataSource),
|
||||
defaultOptionsQuery,
|
||||
);
|
||||
const updater = getUpdaterConfig(
|
||||
preferences,
|
||||
redirectWithOptionsData,
|
||||
|
||||
Reference in New Issue
Block a user