mirror of
https://github.com/SigNoz/signoz.git
synced 2026-10-30 13:00:42 +00:00
Compare commits
4 Commits
test/qb-e2
...
feat/saved
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88539fd51e | ||
|
|
338d4317c7 | ||
|
|
65acd62a9d | ||
|
|
94339f3132 |
@@ -14,6 +14,8 @@ export enum LOCALSTORAGE {
|
||||
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_OPTIONS = 'AI_OBSERVABILITY_TRACE_OPTIONS',
|
||||
LOGS_LIST_COLUMNS = 'LOGS_LIST_COLUMNS',
|
||||
LOGS_LIST_COLUMN_SIZING = 'LOGS_LIST_COLUMN_SIZING',
|
||||
LOGGED_IN_USER_NAME = 'LOGGED_IN_USER_NAME',
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useQueryClient } from 'react-query';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
|
||||
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import QueryCancelledPlaceholder from 'components/QueryCancelledPlaceholder';
|
||||
import QuickFiltersLayout from 'components/QuickFilters/QuickFiltersLayout/QuickFiltersLayout';
|
||||
import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalFieldApis';
|
||||
@@ -191,6 +191,7 @@ function Explorer(): JSX.Element {
|
||||
testId="llm-observability-explorer"
|
||||
showFilters={isOpen}
|
||||
onToggleFilters={(): void => setOpen(!isOpen)}
|
||||
savedViewProps={{ source: SavedviewtypesSourceDTO.ai_observability }}
|
||||
quickFilterProps={{
|
||||
className: 'qf-traces-explorer',
|
||||
quickFilterSource: QuickFiltersSource.AI_OBSERVABILITY,
|
||||
@@ -225,11 +226,11 @@ function Explorer(): JSX.Element {
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<ExplorerCard sourcepage={DataSource.TRACES}>
|
||||
<div>
|
||||
<div className="query-section-container">
|
||||
<QuerySection />
|
||||
</div>
|
||||
</ExplorerCard>
|
||||
</div>
|
||||
|
||||
<div className="traces-explorer-views">
|
||||
{isCancelled && (
|
||||
|
||||
@@ -18,7 +18,9 @@ import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueryAIWithType, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { AIObservabilityPreference } from 'providers/preferences/types';
|
||||
import { getTraceLink, transformSpanRows } from './utils';
|
||||
import { getFieldColumn, TracesTableRow } from '../TracesTable/getFieldColumn';
|
||||
import TracesTable from '../TracesTable/TracesTable';
|
||||
@@ -37,11 +39,7 @@ 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, TIMESTAMP_FIELD } from './configs';
|
||||
import './ListView.styles.scss';
|
||||
|
||||
import styles from './ListView.module.scss';
|
||||
@@ -77,6 +75,24 @@ function ListView({
|
||||
loading: timeRangeUpdateLoading,
|
||||
} = useSelector<AppState, GlobalReducer>((state) => state.globalTime);
|
||||
|
||||
const { options, config } = useOptionsMenu({
|
||||
dataSource: AIObservabilityPreference.LIST,
|
||||
});
|
||||
const selectedFields = useMemo(
|
||||
() => options?.selectColumns ?? [],
|
||||
[options?.selectColumns],
|
||||
);
|
||||
|
||||
// Sorted names: stable on reorder, changes on add/remove (the API only returns selected columns).
|
||||
const selectColumnsSignature = useMemo(
|
||||
() =>
|
||||
selectedFields
|
||||
.map((field) => field.name)
|
||||
.sort()
|
||||
.join(','),
|
||||
[selectedFields],
|
||||
);
|
||||
|
||||
const { queryData: paginationQueryData } = useUrlQueryData<Pagination>(
|
||||
QueryParams.pagination,
|
||||
);
|
||||
@@ -97,12 +113,14 @@ function ListView({
|
||||
stagedQuery,
|
||||
panelType,
|
||||
paginationConfig,
|
||||
selectColumnsSignature,
|
||||
],
|
||||
[
|
||||
stagedQuery,
|
||||
panelType,
|
||||
globalSelectedTime,
|
||||
paginationConfig,
|
||||
selectColumnsSignature,
|
||||
maxTime,
|
||||
minTime,
|
||||
],
|
||||
@@ -123,7 +141,7 @@ function ListView({
|
||||
},
|
||||
tableParams: {
|
||||
pagination: paginationConfig,
|
||||
selectColumns: defaultSelectedColumns,
|
||||
selectColumns: selectedFields,
|
||||
},
|
||||
},
|
||||
ENTITY_VERSION_V5,
|
||||
@@ -131,7 +149,10 @@ function ListView({
|
||||
queryKey,
|
||||
enabled:
|
||||
// don't make api call while the time range state in redux is loading
|
||||
!timeRangeUpdateLoading && !!stagedQuery && panelType === PANEL_TYPES.LIST,
|
||||
!timeRangeUpdateLoading &&
|
||||
!!stagedQuery &&
|
||||
panelType === PANEL_TYPES.LIST &&
|
||||
!!selectedFields.length,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -156,13 +177,20 @@ 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),
|
||||
),
|
||||
[],
|
||||
[
|
||||
TIMESTAMP_FIELD,
|
||||
...selectedFields.filter((field) => field.name !== TIMESTAMP_FIELD.name),
|
||||
].map((field) => getFieldColumn(field)),
|
||||
[selectedFields],
|
||||
);
|
||||
|
||||
const handleColumnOrderChange = useCallback(
|
||||
(cols: TableColumnDef<TracesTableRow>[]): void => {
|
||||
config?.addColumn?.onReorder(cols.map((c) => c.id));
|
||||
},
|
||||
[config],
|
||||
);
|
||||
|
||||
const rows = useMemo(
|
||||
@@ -204,6 +232,7 @@ function ListView({
|
||||
isLoading={isFetching}
|
||||
totalCount={rows.length}
|
||||
perPageOptions={PER_PAGE_OPTIONS}
|
||||
config={config}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -211,7 +240,6 @@ function ListView({
|
||||
data={rows}
|
||||
columns={columns}
|
||||
columnStorageKey={LOCALSTORAGE.AI_OBSERVABILITY_LIST_COLUMNS}
|
||||
respectColumnOrder
|
||||
panelType="LIST"
|
||||
getRowHref={getTraceLink}
|
||||
isLoading={isLoading}
|
||||
@@ -219,6 +247,8 @@ function ListView({
|
||||
isError={isError}
|
||||
error={error}
|
||||
isFilterApplied={isFilterApplied}
|
||||
onColumnOrderChange={handleColumnOrderChange}
|
||||
onColumnRemove={config?.addColumn?.onRemove}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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,12 @@ function TracesView({
|
||||
|
||||
<TracesTable
|
||||
data={rows}
|
||||
columns={tableColumns}
|
||||
columns={columns}
|
||||
columnStorageKey={
|
||||
canPersistColumns
|
||||
? LOCALSTORAGE.AI_OBSERVABILITY_TRACE_VIEW_COLUMNS
|
||||
: undefined
|
||||
}
|
||||
respectColumnOrder
|
||||
panelType="TRACE"
|
||||
getRowHref={getTraceLink}
|
||||
isLoading={isLoading || isColumnsLoading}
|
||||
@@ -240,6 +232,8 @@ function TracesView({
|
||||
isError={isError}
|
||||
error={error}
|
||||
isFilterApplied={isFilterApplied}
|
||||
onColumnOrderChange={onColumnOrderChange}
|
||||
onColumnRemove={onColumnRemove}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import type { TableColumnDef } from 'components/TanStackTableView/types';
|
||||
import { buildCompositeKey } from 'container/OptionsMenu/utils';
|
||||
import { getFieldColumn, TracesTableRow } from '../TracesTable/getFieldColumn';
|
||||
import { DEFAULT_PER_PAGE_OPTIONS } from 'hooks/queryPagination';
|
||||
|
||||
@@ -40,15 +41,51 @@ 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 while no selection is stored. */
|
||||
const DEFAULT_VISIBLE_FIELDS = new Set(DEFAULT_COLUMN_ORDER);
|
||||
|
||||
/** Matches the id getFieldColumn derives, so fields and columns address alike. */
|
||||
export const columnIdOf = (field: TelemetryFieldKey): string =>
|
||||
buildCompositeKey(field.name, field.fieldContext, field.fieldDataType);
|
||||
|
||||
export const getDefaultTraceViewFields = (
|
||||
availableFields: TelemetryFieldKey[],
|
||||
): TelemetryFieldKey[] =>
|
||||
availableFields.filter((field) => DEFAULT_VISIBLE_FIELDS.has(field.name));
|
||||
|
||||
/** The selection the old column store encoded; `null` when it matches the defaults. */
|
||||
export const getLegacyTraceViewSelection = (
|
||||
availableFields: TelemetryFieldKey[],
|
||||
hiddenColumnIds: string[],
|
||||
columnOrder: string[],
|
||||
): TelemetryFieldKey[] | null => {
|
||||
const hidden = new Set(hiddenColumnIds);
|
||||
|
||||
// A saved order lists exactly the columns the user kept.
|
||||
if (columnOrder.length) {
|
||||
const orderIndex = new Map(columnOrder.map((id, index) => [id, index]));
|
||||
const kept = availableFields
|
||||
.filter((field) => orderIndex.has(columnIdOf(field)))
|
||||
.filter((field) => !hidden.has(columnIdOf(field)));
|
||||
return kept.length
|
||||
? kept.sort(
|
||||
(a, b) =>
|
||||
(orderIndex.get(columnIdOf(a)) ?? 0) -
|
||||
(orderIndex.get(columnIdOf(b)) ?? 0),
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
const defaults = getDefaultTraceViewFields(availableFields);
|
||||
const kept = defaults.filter((field) => !hidden.has(columnIdOf(field)));
|
||||
return kept.length === defaults.length ? null : kept;
|
||||
};
|
||||
|
||||
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,16 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef } 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 { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { AIObservabilityPreference } from 'providers/preferences/types';
|
||||
import { TracesTableRow } from '../TracesTable/getFieldColumn';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
import { DATA_SOURCE_TO_SIGNAL, DataSource } from 'types/common/queryBuilder';
|
||||
@@ -23,27 +22,41 @@ import {
|
||||
} from '../constants';
|
||||
import {
|
||||
buildTraceViewColumns,
|
||||
columnIdOf,
|
||||
getDefaultTraceViewFields,
|
||||
getLegacyTraceViewSelection,
|
||||
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);
|
||||
/** Now only column widths; it held the selection as hidden ids + order before the preference slice. */
|
||||
const TABLE_STORAGE_KEY = LOCALSTORAGE.AI_OBSERVABILITY_TRACE_VIEW_COLUMNS;
|
||||
|
||||
interface UseTraceViewColumns {
|
||||
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.
|
||||
const withTraceId = (
|
||||
fields: TelemetryFieldKey[],
|
||||
availableFields: TelemetryFieldKey[],
|
||||
): TelemetryFieldKey[] => {
|
||||
if (fields.some((field) => field.name === TRACE_ID_COLUMN_ID)) {
|
||||
return fields;
|
||||
}
|
||||
const traceId = availableFields.find(
|
||||
(field) => field.name === TRACE_ID_COLUMN_ID,
|
||||
);
|
||||
return traceId ? [traceId, ...fields] : fields;
|
||||
};
|
||||
|
||||
export function useTraceViewColumns(): UseTraceViewColumns {
|
||||
const {
|
||||
data: fetchedFields = [],
|
||||
@@ -66,60 +79,104 @@ export function useTraceViewColumns(): UseTraceViewColumns {
|
||||
[fetchedFields],
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() => buildTraceViewColumns(availableFields),
|
||||
[availableFields],
|
||||
const { options, config } = useOptionsMenu({
|
||||
dataSource: AIObservabilityPreference.TRACE,
|
||||
});
|
||||
// Nothing stored loads as an empty list; the defaults then come from the pool.
|
||||
const storedFields = options?.selectColumns?.length
|
||||
? options.selectColumns
|
||||
: null;
|
||||
const updateColumns = config?.fieldsSelector?.onFieldsChange;
|
||||
|
||||
// Captured on first render, before the table prunes hidden ids it no longer renders.
|
||||
const legacyHiddenIds = useHiddenColumnIds(TABLE_STORAGE_KEY);
|
||||
const legacyOrder = useColumnOrder(TABLE_STORAGE_KEY);
|
||||
const legacy = useRef({ hiddenIds: legacyHiddenIds, order: legacyOrder });
|
||||
|
||||
const migratedFields = useMemo(
|
||||
() =>
|
||||
isSuccess && !storedFields
|
||||
? getLegacyTraceViewSelection(
|
||||
availableFields,
|
||||
legacy.current.hiddenIds,
|
||||
legacy.current.order,
|
||||
)
|
||||
: null,
|
||||
[isSuccess, storedFields, availableFields],
|
||||
);
|
||||
|
||||
// Defaults from a partial column set would persist as the user's own choice.
|
||||
// updateColumns changes identity every render; the migration must still run once.
|
||||
const hasMigrated = useRef(false);
|
||||
useEffect(() => {
|
||||
if (isSuccess) {
|
||||
initializeFromDefaults(STORAGE_KEY, columns);
|
||||
if (!migratedFields || hasMigrated.current) {
|
||||
return;
|
||||
}
|
||||
}, [isSuccess, columns]);
|
||||
|
||||
const hiddenColumnIds = useHiddenColumnIds(STORAGE_KEY);
|
||||
const columnOrder = useColumnOrder(STORAGE_KEY);
|
||||
hasMigrated.current = true;
|
||||
updateColumns?.(migratedFields);
|
||||
// Clear the old encoding so a later reset to defaults isn't migrated again.
|
||||
setColumnOrder(TABLE_STORAGE_KEY, []);
|
||||
legacy.current.hiddenIds.forEach((id) => showColumn(TABLE_STORAGE_KEY, id));
|
||||
legacy.current = { hiddenIds: [], order: [] };
|
||||
}, [migratedFields, updateColumns]);
|
||||
|
||||
const selectedFields = useMemo(() => {
|
||||
const hidden = new Set(hiddenColumnIds);
|
||||
const orderIndex = new Map(columnOrder.map((id, index) => [id, index]));
|
||||
const byName = new Map(availableFields.map((field) => [field.name, field]));
|
||||
const source =
|
||||
storedFields ?? migratedFields ?? getDefaultTraceViewFields(availableFields);
|
||||
// Stored fields the endpoint no longer reports are dropped.
|
||||
const resolved = source
|
||||
.map((field) => byName.get(field.name))
|
||||
.filter((field): field is TelemetryFieldKey => !!field);
|
||||
return withTraceId(resolved, availableFields);
|
||||
}, [availableFields, storedFields, migratedFields]);
|
||||
|
||||
return availableFields
|
||||
.filter((field) => !hidden.has(columnIdOf(field)))
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(orderIndex.get(columnIdOf(a)) ?? Infinity) -
|
||||
(orderIndex.get(columnIdOf(b)) ?? Infinity),
|
||||
);
|
||||
}, [availableFields, hiddenColumnIds, columnOrder]);
|
||||
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));
|
||||
updateColumns?.(withTraceId(next, availableFields));
|
||||
},
|
||||
[columns, isSuccess],
|
||||
[isSuccess, availableFields, updateColumns],
|
||||
);
|
||||
|
||||
const onColumnOrderChange = useCallback(
|
||||
(cols: TableColumnDef<TracesTableRow>[]): void => {
|
||||
const orderIndex = new Map(cols.map((col, index) => [col.id, index]));
|
||||
onFieldsChange(
|
||||
[...selectedFields].sort(
|
||||
(a, b) =>
|
||||
(orderIndex.get(columnIdOf(a)) ?? Infinity) -
|
||||
(orderIndex.get(columnIdOf(b)) ?? Infinity),
|
||||
),
|
||||
);
|
||||
},
|
||||
[selectedFields, onFieldsChange],
|
||||
);
|
||||
|
||||
const onColumnRemove = useCallback(
|
||||
(columnId: string): void => {
|
||||
if (columnId === TRACE_ID_COLUMN_ID) {
|
||||
return;
|
||||
}
|
||||
onFieldsChange(
|
||||
selectedFields.filter((field) => columnIdOf(field) !== columnId),
|
||||
);
|
||||
},
|
||||
[selectedFields, onFieldsChange],
|
||||
);
|
||||
|
||||
return {
|
||||
columns,
|
||||
selectedFields,
|
||||
onFieldsChange,
|
||||
onColumnOrderChange,
|
||||
onColumnRemove,
|
||||
requiredFields: [TRACE_ID_COLUMN_ID],
|
||||
isLoading: !isFetched,
|
||||
canPersistColumns: isSuccess,
|
||||
|
||||
@@ -10,7 +10,13 @@ import { useNotifications } from 'hooks/useNotifications';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { has } from 'lodash-es';
|
||||
import { AllTraceFilterKeyValue } from 'constants/traceFilterKeys';
|
||||
import {
|
||||
AI_OBSERVABILITY_URL_PARAMS,
|
||||
getAIObservabilityDefaultColumns,
|
||||
isAIObservabilityPreference,
|
||||
} from 'providers/preferences/configs/aiObservabilityConfig';
|
||||
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import { PreferenceDataSource } from 'providers/preferences/types';
|
||||
import {
|
||||
QueryKeyRequestProps,
|
||||
QueryKeySuggestionsResponseProps,
|
||||
@@ -40,7 +46,8 @@ import { buildCompositeKey, getOptionsFromKeys } from './utils';
|
||||
|
||||
interface UseOptionsMenuProps {
|
||||
storageKey?: string;
|
||||
dataSource: DataSource;
|
||||
/** Whose preferences; AI explorer values still search fields as traces. */
|
||||
dataSource: PreferenceDataSource;
|
||||
aggregateOperator?: string;
|
||||
initialOptions?: InitialOptions;
|
||||
}
|
||||
@@ -52,14 +59,20 @@ interface UseOptionsMenu {
|
||||
}
|
||||
|
||||
const useOptionsMenu = ({
|
||||
dataSource,
|
||||
dataSource: preferenceDataSource,
|
||||
// aggregateOperator,
|
||||
initialOptions = {},
|
||||
}: UseOptionsMenuProps): UseOptionsMenu => {
|
||||
const isAIObservability = isAIObservabilityPreference(preferenceDataSource);
|
||||
const dataSource = isAIObservability ? DataSource.TRACES : preferenceDataSource;
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
const prefCtx = usePreferenceContext();
|
||||
// TODO: send null to updateColumns and updateFormatting if dataSource is not logs or traces
|
||||
const slice = dataSource === DataSource.TRACES ? prefCtx.traces : prefCtx.logs;
|
||||
let slice = dataSource === DataSource.TRACES ? prefCtx.traces : prefCtx.logs;
|
||||
if (isAIObservability) {
|
||||
slice = prefCtx[preferenceDataSource];
|
||||
}
|
||||
const { preferences, updateColumns, updateFormatting } = slice;
|
||||
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
@@ -75,7 +88,12 @@ const useOptionsMenu = ({
|
||||
);
|
||||
|
||||
const { query: optionsQuery, redirectWithQuery: redirectWithOptionsData } =
|
||||
useUrlQueryData<OptionsQuery>(URL_OPTIONS, defaultOptionsQuery);
|
||||
useUrlQueryData<OptionsQuery>(
|
||||
isAIObservability
|
||||
? AI_OBSERVABILITY_URL_PARAMS[preferenceDataSource]
|
||||
: URL_OPTIONS,
|
||||
defaultOptionsQuery,
|
||||
);
|
||||
|
||||
const initialQueriesV5 = useMemo(
|
||||
() =>
|
||||
@@ -223,6 +241,10 @@ const useOptionsMenu = ({
|
||||
}));
|
||||
}
|
||||
|
||||
if (isAIObservability) {
|
||||
defaultColumns = getAIObservabilityDefaultColumns(preferenceDataSource);
|
||||
}
|
||||
|
||||
const finalSelectColumns = initialOptions?.selectColumns
|
||||
? initialSelectedColumns
|
||||
: defaultColumns;
|
||||
@@ -232,7 +254,13 @@ const useOptionsMenu = ({
|
||||
...initialOptions,
|
||||
selectColumns: finalSelectColumns,
|
||||
};
|
||||
}, [dataSource, initialOptions, initialSelectedColumns]);
|
||||
}, [
|
||||
dataSource,
|
||||
isAIObservability,
|
||||
preferenceDataSource,
|
||||
initialOptions,
|
||||
initialSelectedColumns,
|
||||
]);
|
||||
|
||||
const selectedColumnKeys = useMemo(
|
||||
() => preferences?.columns?.map(({ name }) => name) || [],
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
buildSavedViewDeletePermission,
|
||||
buildSavedViewUpdatePermission,
|
||||
} from 'lib/authz/hooks/useAuthZ/permissions/saved-view.permissions';
|
||||
import { panelTypeToAIObservabilityPreference } from 'providers/preferences/configs/aiObservabilityConfig';
|
||||
import { PreferenceDataSource } from 'providers/preferences/types';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { SavedViewRowActionConfig, SaveViewModalMode } from './types';
|
||||
@@ -22,6 +24,17 @@ export const SAVED_VIEW_OPTIONS_DATA_SOURCE: Partial<
|
||||
[SavedviewtypesSourceDTO.traces]: DataSource.TRACES,
|
||||
};
|
||||
|
||||
/** Whose column preferences a view saves; AI views use the tab they were saved from. */
|
||||
export function getSavedViewPreferenceDataSource(
|
||||
source: SavedviewtypesSourceDTO,
|
||||
panelType: string | undefined,
|
||||
): PreferenceDataSource | undefined {
|
||||
if (source === SavedviewtypesSourceDTO.ai_observability) {
|
||||
return panelTypeToAIObservabilityPreference(panelType);
|
||||
}
|
||||
return SAVED_VIEW_OPTIONS_DATA_SOURCE[source];
|
||||
}
|
||||
|
||||
// What selecting a view writes to the url, so clearing it removes exactly these.
|
||||
export const SAVED_VIEW_URL_PARAMS = [
|
||||
QueryParams.viewKey,
|
||||
|
||||
@@ -11,7 +11,7 @@ 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 { getSavedViewPreferenceDataSource } from '../constants';
|
||||
import { UseActiveSavedViewResult } from '../types';
|
||||
import { hasUnsavedViewChanges } from '../utils/hasUnsavedViewChanges';
|
||||
|
||||
@@ -21,7 +21,10 @@ export function useActiveSavedView(
|
||||
const { viewKey } = useGetSavedViewParams();
|
||||
const { stagedQuery, panelType } = useQueryBuilder();
|
||||
|
||||
const optionsDataSource = SAVED_VIEW_OPTIONS_DATA_SOURCE[source];
|
||||
const optionsDataSource = getSavedViewPreferenceDataSource(
|
||||
source,
|
||||
panelType ?? undefined,
|
||||
);
|
||||
const { options } = useOptionsMenu({
|
||||
dataSource: optionsDataSource ?? DataSource.LOGS,
|
||||
});
|
||||
@@ -64,6 +67,7 @@ export function useActiveSavedView(
|
||||
stagedQuery,
|
||||
panelType: panelType ?? PANEL_TYPES.LIST,
|
||||
options: optionsDataSource ? options : undefined,
|
||||
optionsDataSource,
|
||||
});
|
||||
}, [
|
||||
view,
|
||||
|
||||
@@ -20,13 +20,14 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useGetSavedViewParams } from 'hooks/saveViews/useGetSavedViewParams';
|
||||
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { isAIObservabilityPreference } from 'providers/preferences/configs/aiObservabilityConfig';
|
||||
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,
|
||||
getSavedViewPreferenceDataSource,
|
||||
SAVED_VIEW_SCHEMA_VERSION,
|
||||
SAVED_VIEW_TOAST_POSITION,
|
||||
SAVED_VIEW_URL_PARAMS,
|
||||
@@ -60,15 +61,14 @@ export function useSavedViewActions(
|
||||
const { viewKey } = useGetSavedViewParams();
|
||||
const logSavedViewEvent = useSavedViewLogEvent(source);
|
||||
|
||||
const optionsDataSource = SAVED_VIEW_OPTIONS_DATA_SOURCE[source];
|
||||
const optionsDataSource = getSavedViewPreferenceDataSource(
|
||||
source,
|
||||
panelType ?? undefined,
|
||||
);
|
||||
const { options } = useOptionsMenu({
|
||||
dataSource: optionsDataSource ?? DataSource.LOGS,
|
||||
});
|
||||
const preferences = usePreferenceContext();
|
||||
const preferencesSlice =
|
||||
optionsDataSource === DataSource.TRACES
|
||||
? preferences.traces
|
||||
: preferences.logs;
|
||||
|
||||
const { mutateAsync: createSavedView, isLoading: isCreating } =
|
||||
useCreateSavedView();
|
||||
@@ -105,10 +105,22 @@ export function useSavedViewActions(
|
||||
|
||||
const revertView = useCallback(
|
||||
(view: SavedviewtypesSavedViewDTO): void => {
|
||||
if (optionsDataSource) {
|
||||
// The view's own tab, which can differ from the open one.
|
||||
const viewDataSource = getSavedViewPreferenceDataSource(
|
||||
source,
|
||||
view.spec.panelType,
|
||||
);
|
||||
if (viewDataSource) {
|
||||
let preferencesSlice =
|
||||
viewDataSource === DataSource.TRACES
|
||||
? preferences.traces
|
||||
: preferences.logs;
|
||||
if (isAIObservabilityPreference(viewDataSource)) {
|
||||
preferencesSlice = preferences[viewDataSource];
|
||||
}
|
||||
const { columns, formatting } = getViewColumnsAndFormatting(
|
||||
view.spec,
|
||||
optionsDataSource,
|
||||
viewDataSource,
|
||||
);
|
||||
preferencesSlice.updateColumns(columns);
|
||||
if (formatting) {
|
||||
@@ -120,7 +132,7 @@ export function useSavedViewActions(
|
||||
[SavedViewEventKeys.ViewId]: view.id,
|
||||
});
|
||||
},
|
||||
[optionsDataSource, preferencesSlice, selectView, logSavedViewEvent],
|
||||
[source, preferences, selectView, logSavedViewEvent],
|
||||
);
|
||||
|
||||
// The query builder redirects only ever add params, so the url is rebuilt
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { OptionsQuery } from 'container/OptionsMenu/types';
|
||||
import { PreferenceDataSource } from 'providers/preferences/types';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
@@ -32,6 +33,8 @@ export interface HasUnsavedViewChangesArgs {
|
||||
stagedQuery: Query | null;
|
||||
panelType: PANEL_TYPES;
|
||||
options?: SavedViewOptions;
|
||||
/** Whose defaults the options are compared against; the query's data source when unset. */
|
||||
optionsDataSource?: PreferenceDataSource;
|
||||
}
|
||||
|
||||
export interface UseLastUsedViewResult {
|
||||
|
||||
@@ -6,7 +6,15 @@ import {
|
||||
ensureLogsRequiredColumns,
|
||||
} from 'container/OptionsMenu/constants';
|
||||
import { FontSize, LogViewMode } from 'container/OptionsMenu/types';
|
||||
import { Preferences } from 'providers/preferences/types';
|
||||
import {
|
||||
getAIObservabilityDefaultColumns,
|
||||
isAIObservabilityPreference,
|
||||
sanitizeColumns,
|
||||
} from 'providers/preferences/configs/aiObservabilityConfig';
|
||||
import {
|
||||
PreferenceDataSource,
|
||||
Preferences,
|
||||
} from 'providers/preferences/types';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
function withColumnNames(
|
||||
@@ -25,8 +33,16 @@ function withColumnNames(
|
||||
// defaults filled in where the view has none.
|
||||
export function getViewColumnsAndFormatting(
|
||||
spec: SavedviewtypesSavedViewSpecDTO | undefined,
|
||||
dataSource: DataSource,
|
||||
dataSource: PreferenceDataSource,
|
||||
): Preferences {
|
||||
if (isAIObservabilityPreference(dataSource)) {
|
||||
return {
|
||||
columns:
|
||||
sanitizeColumns(dataSource, spec?.selectedFields) ??
|
||||
getAIObservabilityDefaultColumns(dataSource),
|
||||
};
|
||||
}
|
||||
|
||||
const selectedFields = spec?.selectedFields as TelemetryFieldKey[] | undefined;
|
||||
|
||||
if (dataSource === DataSource.LOGS) {
|
||||
|
||||
@@ -50,6 +50,7 @@ export function hasUnsavedViewChanges({
|
||||
stagedQuery,
|
||||
panelType,
|
||||
options,
|
||||
optionsDataSource,
|
||||
}: HasUnsavedViewChangesArgs): boolean {
|
||||
if (!stagedQuery) {
|
||||
return false;
|
||||
@@ -79,7 +80,7 @@ export function hasUnsavedViewChanges({
|
||||
const dataSource = stagedQuery.builder.queryData[0]?.dataSource;
|
||||
const { columns, formatting } = getViewColumnsAndFormatting(
|
||||
view.spec,
|
||||
dataSource,
|
||||
optionsDataSource ?? dataSource,
|
||||
);
|
||||
const toNames = (fields: { name?: string; key?: string }[]): unknown[] =>
|
||||
fields.map((field) => field.name ?? field.key);
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import {
|
||||
SavedviewtypesSavedViewDTO,
|
||||
SavedviewtypesSourceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { defaultSelectedColumns } from 'container/LLMObservability/Explorer/ListView/configs';
|
||||
import { buildCompositeKey } from 'container/OptionsMenu/utils';
|
||||
|
||||
import { AIObservabilityPreference, PreferenceDataSource } from '../types';
|
||||
|
||||
// TODO(ai-explorer): share `options`, keyed by `panelTypes`, once Logs/Traces no longer read or write it on this page.
|
||||
export const AI_OBSERVABILITY_URL_PARAMS: Record<
|
||||
AIObservabilityPreference,
|
||||
string
|
||||
> = {
|
||||
[AIObservabilityPreference.LIST]: 'listOptions',
|
||||
[AIObservabilityPreference.TRACE]: 'traceOptions',
|
||||
};
|
||||
|
||||
export const AI_OBSERVABILITY_STORAGE_KEYS: Record<
|
||||
AIObservabilityPreference,
|
||||
LOCALSTORAGE
|
||||
> = {
|
||||
[AIObservabilityPreference.LIST]: LOCALSTORAGE.AI_OBSERVABILITY_LIST_OPTIONS,
|
||||
[AIObservabilityPreference.TRACE]: LOCALSTORAGE.AI_OBSERVABILITY_TRACE_OPTIONS,
|
||||
};
|
||||
|
||||
// Trace view defaults depend on the aggregates the keys endpoint reports, so the view applies them.
|
||||
const AI_OBSERVABILITY_DEFAULT_COLUMNS: Record<
|
||||
AIObservabilityPreference,
|
||||
TelemetryFieldKey[]
|
||||
> = {
|
||||
[AIObservabilityPreference.LIST]: defaultSelectedColumns,
|
||||
[AIObservabilityPreference.TRACE]: [],
|
||||
};
|
||||
|
||||
export const isAIObservabilityPreference = (
|
||||
dataSource: PreferenceDataSource,
|
||||
): dataSource is AIObservabilityPreference =>
|
||||
Object.values(AIObservabilityPreference).includes(
|
||||
dataSource as AIObservabilityPreference,
|
||||
);
|
||||
|
||||
export const getAIObservabilityDefaultColumns = (
|
||||
preference: AIObservabilityPreference,
|
||||
): TelemetryFieldKey[] => AI_OBSERVABILITY_DEFAULT_COLUMNS[preference];
|
||||
|
||||
type RawColumn = Partial<TelemetryFieldKey> & { key?: string };
|
||||
|
||||
/** Drops malformed, non-trace and duplicate fields; `null` when nothing usable is left. */
|
||||
export const sanitizeColumns = (
|
||||
preference: AIObservabilityPreference,
|
||||
columns: unknown,
|
||||
): TelemetryFieldKey[] | null => {
|
||||
if (!Array.isArray(columns)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const seen = new Set<string>();
|
||||
const sanitized = (columns as RawColumn[]).reduce<TelemetryFieldKey[]>(
|
||||
(acc, column) => {
|
||||
const name = column?.name ?? column?.key;
|
||||
if (typeof name !== 'string' || !name) {
|
||||
return acc;
|
||||
}
|
||||
if (column.signal && column.signal !== 'traces') {
|
||||
return acc;
|
||||
}
|
||||
// Per-trace aggregates have no value on a span row.
|
||||
if (
|
||||
preference === AIObservabilityPreference.LIST &&
|
||||
column.fieldContext === 'trace'
|
||||
) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
const id = buildCompositeKey(
|
||||
name,
|
||||
column.fieldContext,
|
||||
column.fieldDataType,
|
||||
);
|
||||
if (seen.has(id)) {
|
||||
return acc;
|
||||
}
|
||||
seen.add(id);
|
||||
|
||||
const { key: _key, ...field } = column;
|
||||
acc.push({ ...field, name } as TelemetryFieldKey);
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return sanitized.length ? sanitized : null;
|
||||
};
|
||||
|
||||
const PANEL_TYPE_TO_PREFERENCE: Record<string, AIObservabilityPreference> = {
|
||||
list: AIObservabilityPreference.LIST,
|
||||
trace: AIObservabilityPreference.TRACE,
|
||||
};
|
||||
|
||||
/** The AI tab whose columns a panel type shows; `undefined` for tabs without columns. */
|
||||
export const panelTypeToAIObservabilityPreference = (
|
||||
panelType: string | undefined,
|
||||
): AIObservabilityPreference | undefined =>
|
||||
panelType ? PANEL_TYPE_TO_PREFERENCE[panelType] : undefined;
|
||||
|
||||
/** A saved view's columns for this tab, defaults filled in; `null` when the view belongs to another tab. */
|
||||
export const getAIObservabilityViewColumns = (
|
||||
preference: AIObservabilityPreference,
|
||||
view: SavedviewtypesSavedViewDTO,
|
||||
): TelemetryFieldKey[] | null => {
|
||||
if (
|
||||
view.source !== SavedviewtypesSourceDTO.ai_observability ||
|
||||
panelTypeToAIObservabilityPreference(view.spec.panelType) !== preference
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
sanitizeColumns(preference, view.spec.selectedFields) ??
|
||||
AI_OBSERVABILITY_DEFAULT_COLUMNS[preference]
|
||||
);
|
||||
};
|
||||
|
||||
/** `selectColumns` of a stored options object; `null` when absent or unparseable. */
|
||||
export const parseSelectColumns = (raw: string | null): unknown => {
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(raw)?.selectColumns;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
|
||||
import { AIObservabilityPreference } from '../types';
|
||||
import {
|
||||
AI_OBSERVABILITY_STORAGE_KEYS,
|
||||
AI_OBSERVABILITY_URL_PARAMS,
|
||||
getAIObservabilityDefaultColumns,
|
||||
parseSelectColumns,
|
||||
sanitizeColumns,
|
||||
} from './aiObservabilityConfig';
|
||||
|
||||
// --- AI OBSERVABILITY preferences loader config, one per explorer tab ---
|
||||
const getAIObservabilityLoaderConfig = (
|
||||
preference: AIObservabilityPreference,
|
||||
): {
|
||||
local: () => { columns: TelemetryFieldKey[] };
|
||||
url: () => { columns: TelemetryFieldKey[] };
|
||||
default: () => { columns: TelemetryFieldKey[] };
|
||||
priority: readonly ['local', 'url', 'default'];
|
||||
} => ({
|
||||
local: () => ({
|
||||
columns:
|
||||
sanitizeColumns(
|
||||
preference,
|
||||
parseSelectColumns(
|
||||
getLocalStorageKey(AI_OBSERVABILITY_STORAGE_KEYS[preference]),
|
||||
),
|
||||
) ?? [],
|
||||
}),
|
||||
url: () => ({
|
||||
columns:
|
||||
sanitizeColumns(
|
||||
preference,
|
||||
parseSelectColumns(
|
||||
new URLSearchParams(window.location.search).get(
|
||||
AI_OBSERVABILITY_URL_PARAMS[preference],
|
||||
),
|
||||
),
|
||||
) ?? [],
|
||||
}),
|
||||
default: () => ({ columns: getAIObservabilityDefaultColumns(preference) }),
|
||||
priority: ['local', 'url', 'default'] as const,
|
||||
});
|
||||
|
||||
export default getAIObservabilityLoaderConfig;
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import removeLocalStorageKey from 'api/browser/localstorage/remove';
|
||||
import setLocalStorageKey from 'api/browser/localstorage/set';
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import { defaultOptionsQuery } from 'container/OptionsMenu/constants';
|
||||
import { OptionsQuery } from 'container/OptionsMenu/types';
|
||||
|
||||
import { AIObservabilityPreference, PreferenceMode, Preferences } from '../types';
|
||||
import {
|
||||
AI_OBSERVABILITY_STORAGE_KEYS,
|
||||
sanitizeColumns,
|
||||
} from './aiObservabilityConfig';
|
||||
|
||||
// --- AI OBSERVABILITY preferences updater config, one per explorer tab ---
|
||||
const getAIObservabilityUpdaterConfig = (
|
||||
preference: AIObservabilityPreference,
|
||||
redirectWithOptionsData: (options: OptionsQuery) => void,
|
||||
setSavedViewPreferences: Dispatch<SetStateAction<Preferences | null>>,
|
||||
): {
|
||||
updateColumns: (newColumns: TelemetryFieldKey[], mode: string) => void;
|
||||
updateFormatting: () => void;
|
||||
} => ({
|
||||
updateColumns: (newColumns: TelemetryFieldKey[], mode: string): void => {
|
||||
// An emptied selection is stored as none, so the loader falls back to the defaults.
|
||||
const columns = sanitizeColumns(preference, newColumns) ?? [];
|
||||
|
||||
if (mode === PreferenceMode.SAVED_VIEW) {
|
||||
setSavedViewPreferences({ columns });
|
||||
return;
|
||||
}
|
||||
|
||||
redirectWithOptionsData({ ...defaultOptionsQuery, selectColumns: columns });
|
||||
if (columns.length) {
|
||||
setLocalStorageKey(
|
||||
AI_OBSERVABILITY_STORAGE_KEYS[preference],
|
||||
JSON.stringify({ selectColumns: columns }),
|
||||
);
|
||||
} else {
|
||||
removeLocalStorageKey(AI_OBSERVABILITY_STORAGE_KEYS[preference]);
|
||||
}
|
||||
},
|
||||
updateFormatting: (): void => {}, // no-op for the AI explorer
|
||||
});
|
||||
|
||||
export default getAIObservabilityUpdaterConfig;
|
||||
@@ -7,6 +7,7 @@ import React, {
|
||||
} from 'react';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import {
|
||||
AIObservabilityPreference,
|
||||
PreferenceContextValue,
|
||||
PreferenceMode,
|
||||
} from 'providers/preferences/types';
|
||||
@@ -47,12 +48,32 @@ export function PreferenceContextProvider({
|
||||
dataSource: DataSource.TRACES,
|
||||
});
|
||||
|
||||
const aiObservabilityListSlice = usePreferenceSync({
|
||||
mode: savedViewId ? PreferenceMode.SAVED_VIEW : PreferenceMode.DIRECT,
|
||||
savedViewId: savedViewId || undefined,
|
||||
dataSource: AIObservabilityPreference.LIST,
|
||||
});
|
||||
|
||||
const aiObservabilityTraceSlice = usePreferenceSync({
|
||||
mode: savedViewId ? PreferenceMode.SAVED_VIEW : PreferenceMode.DIRECT,
|
||||
savedViewId: savedViewId || undefined,
|
||||
dataSource: AIObservabilityPreference.TRACE,
|
||||
});
|
||||
|
||||
|
||||
const value = useMemo<PreferenceContextValue>(
|
||||
() => ({
|
||||
logs: logsSlice,
|
||||
traces: tracesSlice,
|
||||
[AIObservabilityPreference.LIST]: aiObservabilityListSlice,
|
||||
[AIObservabilityPreference.TRACE]: aiObservabilityTraceSlice,
|
||||
}),
|
||||
[logsSlice, tracesSlice],
|
||||
[
|
||||
logsSlice,
|
||||
tracesSlice,
|
||||
aiObservabilityListSlice,
|
||||
aiObservabilityTraceSlice,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,7 +6,13 @@ import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import logsLoaderConfig from '../configs/logsLoaderConfig';
|
||||
import tracesLoaderConfig from '../configs/tracesLoaderConfig';
|
||||
import { FormattingOptions, Preferences } from '../types';
|
||||
import { isAIObservabilityPreference } from '../configs/aiObservabilityConfig';
|
||||
import getAIObservabilityLoaderConfig from '../configs/aiObservabilityLoaderConfig';
|
||||
import {
|
||||
FormattingOptions,
|
||||
PreferenceDataSource,
|
||||
Preferences,
|
||||
} from '../types';
|
||||
|
||||
const migrateColumns = (columns: any): any =>
|
||||
columns.map((column: any) => {
|
||||
@@ -65,12 +71,30 @@ function tracesPreferencesLoader(): {
|
||||
return preferencesLoader(tracesLoaderConfig);
|
||||
}
|
||||
|
||||
function readPreferences(dataSource: PreferenceDataSource): Preferences | null {
|
||||
if (dataSource === DataSource.LOGS) {
|
||||
const { columns, formatting } = logsPreferencesLoader();
|
||||
return { columns, formatting };
|
||||
}
|
||||
if (dataSource === DataSource.TRACES) {
|
||||
const { columns } = tracesPreferencesLoader();
|
||||
return { columns };
|
||||
}
|
||||
if (isAIObservabilityPreference(dataSource)) {
|
||||
const { columns } = preferencesLoader<{ columns: TelemetryFieldKey[] }>(
|
||||
getAIObservabilityLoaderConfig(dataSource),
|
||||
);
|
||||
return { columns };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function usePreferenceLoader({
|
||||
dataSource,
|
||||
reSync,
|
||||
setReSync,
|
||||
}: {
|
||||
dataSource: DataSource;
|
||||
dataSource: PreferenceDataSource;
|
||||
reSync: boolean;
|
||||
setReSync: (value: boolean) => void;
|
||||
}): {
|
||||
@@ -78,17 +102,9 @@ export function usePreferenceLoader({
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
} {
|
||||
const [preferences, setPreferences] = useState<Preferences | null>(() => {
|
||||
if (dataSource === DataSource.LOGS) {
|
||||
const { columns, formatting } = logsPreferencesLoader();
|
||||
return { columns, formatting };
|
||||
}
|
||||
if (dataSource === DataSource.TRACES) {
|
||||
const { columns } = tracesPreferencesLoader();
|
||||
return { columns };
|
||||
}
|
||||
return null;
|
||||
});
|
||||
const [preferences, setPreferences] = useState<Preferences | null>(() =>
|
||||
readPreferences(dataSource),
|
||||
);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect((): void => {
|
||||
@@ -96,15 +112,7 @@ export function usePreferenceLoader({
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
if (dataSource === DataSource.LOGS) {
|
||||
const { columns, formatting } = logsPreferencesLoader();
|
||||
setPreferences({ columns, formatting });
|
||||
}
|
||||
|
||||
if (dataSource === DataSource.TRACES) {
|
||||
const { columns } = tracesPreferencesLoader();
|
||||
setPreferences({ columns });
|
||||
}
|
||||
setPreferences(readPreferences(dataSource));
|
||||
} catch (e) {
|
||||
setError(e as Error);
|
||||
} finally {
|
||||
|
||||
@@ -3,10 +3,18 @@ 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 {
|
||||
getAIObservabilityViewColumns,
|
||||
isAIObservabilityPreference,
|
||||
} from '../configs/aiObservabilityConfig';
|
||||
import {
|
||||
FormattingOptions,
|
||||
PreferenceDataSource,
|
||||
PreferenceMode,
|
||||
Preferences,
|
||||
} from '../types';
|
||||
import { usePreferenceUpdater } from '../updater/usePreferenceUpdater';
|
||||
|
||||
export function usePreferenceSync({
|
||||
@@ -15,7 +23,7 @@ export function usePreferenceSync({
|
||||
savedViewId,
|
||||
}: {
|
||||
mode: PreferenceMode;
|
||||
dataSource: DataSource;
|
||||
dataSource: PreferenceDataSource;
|
||||
savedViewId: string | undefined;
|
||||
}): {
|
||||
preferences: Preferences | null;
|
||||
@@ -37,6 +45,10 @@ export function usePreferenceSync({
|
||||
// memory. Discard writes the saved ones back through the updater.
|
||||
const seededViewId = useRef<string>();
|
||||
useEffect(() => {
|
||||
// AI explorer tabs are seeded below, once their own preferences have loaded.
|
||||
if (isAIObservabilityPreference(dataSource)) {
|
||||
return;
|
||||
}
|
||||
const view = viewData?.data;
|
||||
if (view && view.id === savedViewId) {
|
||||
if (seededViewId.current !== savedViewId) {
|
||||
@@ -58,6 +70,28 @@ export function usePreferenceSync({
|
||||
setReSync,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAIObservabilityPreference(dataSource)) {
|
||||
return;
|
||||
}
|
||||
const view = viewData?.data;
|
||||
if (view && view.id === savedViewId) {
|
||||
if (seededViewId.current !== savedViewId) {
|
||||
seededViewId.current = savedViewId;
|
||||
// Only the tab the view was saved from takes its columns; the other keeps the user's own.
|
||||
setSavedViewPreferences({
|
||||
columns:
|
||||
getAIObservabilityViewColumns(dataSource, view) ??
|
||||
preferences?.columns ??
|
||||
[],
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
seededViewId.current = undefined;
|
||||
setSavedViewPreferences(null);
|
||||
}, [viewData, dataSource, savedViewId, preferences]);
|
||||
|
||||
const { updateColumns, updateFormatting } = usePreferenceUpdater({
|
||||
dataSource,
|
||||
mode,
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import { LogViewMode } from 'container/OptionsMenu/types';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { LogViewMode, FontSize } from 'container/OptionsMenu/types';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export enum PreferenceMode {
|
||||
SAVED_VIEW = 'savedView',
|
||||
DIRECT = 'direct',
|
||||
}
|
||||
|
||||
/** AI Explorer tabs; their queries still run on the traces data source. */
|
||||
export enum AIObservabilityPreference {
|
||||
LIST = 'ai_observability_list',
|
||||
TRACE = 'ai_observability_trace',
|
||||
}
|
||||
|
||||
/** Whose preferences: a data source, or an explorer sharing one with another. */
|
||||
export type PreferenceDataSource = DataSource | AIObservabilityPreference;
|
||||
|
||||
export interface PreferenceSlice {
|
||||
preferences: Preferences | null;
|
||||
loading: boolean;
|
||||
@@ -18,6 +27,8 @@ export interface PreferenceSlice {
|
||||
export interface PreferenceContextValue {
|
||||
logs: PreferenceSlice;
|
||||
traces: PreferenceSlice;
|
||||
[AIObservabilityPreference.LIST]: PreferenceSlice;
|
||||
[AIObservabilityPreference.TRACE]: PreferenceSlice;
|
||||
}
|
||||
|
||||
export interface FormattingOptions {
|
||||
|
||||
@@ -10,7 +10,16 @@ import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import getLogsUpdaterConfig from '../configs/logsUpdaterConfig';
|
||||
import getTracesUpdaterConfig from '../configs/tracesUpdaterConfig';
|
||||
import { FormattingOptions, Preferences } from '../types';
|
||||
import {
|
||||
AI_OBSERVABILITY_URL_PARAMS,
|
||||
isAIObservabilityPreference,
|
||||
} from '../configs/aiObservabilityConfig';
|
||||
import getAIObservabilityUpdaterConfig from '../configs/aiObservabilityUpdaterConfig';
|
||||
import {
|
||||
FormattingOptions,
|
||||
PreferenceDataSource,
|
||||
Preferences,
|
||||
} from '../types';
|
||||
|
||||
const metricsUpdater = {
|
||||
updateColumns: (): void => {}, // no-op for metrics
|
||||
@@ -47,7 +56,7 @@ export function usePreferenceUpdater({
|
||||
setReSync,
|
||||
setSavedViewPreferences,
|
||||
}: {
|
||||
dataSource: DataSource;
|
||||
dataSource: PreferenceDataSource;
|
||||
mode: string;
|
||||
preferences: Preferences | null;
|
||||
setReSync: Dispatch<SetStateAction<boolean>>;
|
||||
@@ -57,12 +66,23 @@ export function usePreferenceUpdater({
|
||||
updateFormatting: (newFormatting: FormattingOptions) => void;
|
||||
} {
|
||||
const { redirectWithQuery: redirectWithOptionsData } =
|
||||
useUrlQueryData<OptionsQuery>(URL_OPTIONS, defaultOptionsQuery);
|
||||
const updater = getUpdaterConfig(
|
||||
preferences,
|
||||
redirectWithOptionsData,
|
||||
setSavedViewPreferences,
|
||||
)[dataSource];
|
||||
useUrlQueryData<OptionsQuery>(
|
||||
isAIObservabilityPreference(dataSource)
|
||||
? AI_OBSERVABILITY_URL_PARAMS[dataSource]
|
||||
: URL_OPTIONS,
|
||||
defaultOptionsQuery,
|
||||
);
|
||||
const updater = isAIObservabilityPreference(dataSource)
|
||||
? getAIObservabilityUpdaterConfig(
|
||||
dataSource,
|
||||
redirectWithOptionsData,
|
||||
setSavedViewPreferences,
|
||||
)
|
||||
: getUpdaterConfig(
|
||||
preferences,
|
||||
redirectWithOptionsData,
|
||||
setSavedViewPreferences,
|
||||
)[dataSource];
|
||||
|
||||
return {
|
||||
updateColumns: (newColumns: TelemetryFieldKey[]): void => {
|
||||
|
||||
Reference in New Issue
Block a user