diff --git a/frontend/src/container/ExplorerOptions/ExplorerOptions.tsx b/frontend/src/container/ExplorerOptions/ExplorerOptions.tsx index 3090babe1d..ac442789b9 100644 --- a/frontend/src/container/ExplorerOptions/ExplorerOptions.tsx +++ b/frontend/src/container/ExplorerOptions/ExplorerOptions.tsx @@ -240,6 +240,7 @@ function ExplorerOptions({ dataSource: sourcepage, aggregateOperator: StringOperators.NOOP, }); + console.log('uncaught options in saved views', options); const getUpdatedExtraData = ( extraData: string | undefined, @@ -338,6 +339,12 @@ function ExplorerOptions({ backwardCompatibleOptions = omit(options, 'version'); } + console.log('uncaught backwardCompatibleOptions', { + backwardCompatibleOptions, + esc: extraData?.selectColumns, + osc: options.selectColumns, + }); + if (extraData.selectColumns?.length) { handleOptionsChange({ ...backwardCompatibleOptions, @@ -419,6 +426,7 @@ function ExplorerOptions({ updatePreservedViewInLocalStorage(option); + console.log('uncaught options in saved views before call', options); updateOrRestoreSelectColumns( option.key, viewsData?.data?.data, diff --git a/frontend/src/pages/LogsExplorer/index.tsx b/frontend/src/pages/LogsExplorer/index.tsx index b988a43b5f..df7bc2cd29 100644 --- a/frontend/src/pages/LogsExplorer/index.tsx +++ b/frontend/src/pages/LogsExplorer/index.tsx @@ -23,6 +23,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import useUrlQueryData from 'hooks/useUrlQueryData'; import { isEqual, isNull } from 'lodash-es'; import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback'; +import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { DataSource } from 'types/common/queryBuilder'; @@ -35,6 +36,9 @@ function LogsExplorer(): JSX.Element { const [selectedView, setSelectedView] = useState( SELECTED_VIEWS.SEARCH, ); + const { preferences, updateFormatting } = usePreferenceContext(); + + console.log('uncaught preferences', preferences); const [showFilters, setShowFilters] = useState(() => { const localStorageValue = getLocalStorageKey( LOCALSTORAGE.SHOW_LOGS_QUICK_FILTERS, @@ -222,6 +226,21 @@ function LogsExplorer(): JSX.Element { )}
+ {/* dummy button to test the updateFormatting function */} +
+ +
+ {preferences && ( +
+

Preferences

+
{JSON.stringify(preferences, null, 2)}
+
+ )} ( + + + + ), name: (
Explorer diff --git a/frontend/src/providers/preferences/configs/logsLoaderConfig.ts b/frontend/src/providers/preferences/configs/logsLoaderConfig.ts new file mode 100644 index 0000000000..40bfefd1b3 --- /dev/null +++ b/frontend/src/providers/preferences/configs/logsLoaderConfig.ts @@ -0,0 +1,66 @@ +/* eslint-disable no-empty */ +import getLocalStorageKey from 'api/browser/localstorage/get'; +import { LOCALSTORAGE } from 'constants/localStorage'; +import { defaultLogsSelectedColumns } from 'container/OptionsMenu/constants'; +import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; + +import { FormattingOptions } from '../types'; + +// --- LOGS preferences loader config --- +const logsLoaders = { + local: async (): Promise<{ + columns: BaseAutocompleteData[]; + formatting: FormattingOptions; + }> => { + const local = getLocalStorageKey(LOCALSTORAGE.LOGS_LIST_OPTIONS); + if (local) { + try { + const parsed = JSON.parse(local); + return { + columns: parsed.selectColumns || [], + formatting: { + maxLines: parsed.maxLines ?? 2, + format: parsed.format ?? 'table', + fontSize: parsed.fontSize ?? 'small', + version: parsed.version ?? 1, + }, + }; + } catch {} + } + return { columns: [], formatting: undefined } as any; + }, + url: async (): Promise<{ + columns: BaseAutocompleteData[]; + formatting: FormattingOptions; + }> => { + const urlParams = new URLSearchParams(window.location.search); + try { + const options = JSON.parse(urlParams.get('options') || '{}'); + return { + columns: options.selectColumns || [], + formatting: { + maxLines: options.maxLines ?? 2, + format: options.format ?? 'table', + fontSize: options.fontSize ?? 'small', + version: options.version ?? 1, + }, + }; + } catch {} + return { columns: [], formatting: undefined } as any; + }, + default: async (): Promise<{ + columns: BaseAutocompleteData[]; + formatting: FormattingOptions; + }> => ({ + columns: defaultLogsSelectedColumns as BaseAutocompleteData[], + formatting: { + maxLines: 2, + format: 'table', + fontSize: 'small', + version: 1, + }, + }), + priority: ['local', 'url', 'default'] as const, +}; + +export default logsLoaders; diff --git a/frontend/src/providers/preferences/configs/logsUpdaterConfig.ts b/frontend/src/providers/preferences/configs/logsUpdaterConfig.ts new file mode 100644 index 0000000000..d2e0c54f66 --- /dev/null +++ b/frontend/src/providers/preferences/configs/logsUpdaterConfig.ts @@ -0,0 +1,45 @@ +import setLocalStorageKey from 'api/browser/localstorage/set'; +import { LOCALSTORAGE } from 'constants/localStorage'; +import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; + +import { FormattingOptions } from '../types'; + +// --- LOGS preferences updater config --- +const logsUpdater = { + updateColumns: (newColumns: BaseAutocompleteData[], mode: string): void => { + // Always update URL + const url = new URL(window.location.href); + const options = JSON.parse(url.searchParams.get('options') || '{}'); + options.selectColumns = newColumns; + url.searchParams.set('options', JSON.stringify(options)); + window.history.replaceState({}, '', url.toString()); + + if (mode === 'direct') { + // Also update local storage + const local = JSON.parse( + localStorage.getItem(LOCALSTORAGE.LOGS_LIST_OPTIONS) || '{}', + ); + local.selectColumns = newColumns; + setLocalStorageKey(LOCALSTORAGE.LOGS_LIST_OPTIONS, JSON.stringify(local)); + } + }, + updateFormatting: (newFormatting: FormattingOptions, mode: string): void => { + // Always update URL + const url = new URL(window.location.href); + const options = JSON.parse(url.searchParams.get('options') || '{}'); + Object.assign(options, newFormatting); + url.searchParams.set('options', JSON.stringify(options)); + window.history.replaceState({}, '', url.toString()); + + if (mode === 'direct') { + // Also update local storage + const local = JSON.parse( + localStorage.getItem(LOCALSTORAGE.LOGS_LIST_OPTIONS) || '{}', + ); + Object.assign(local, newFormatting); + setLocalStorageKey(LOCALSTORAGE.LOGS_LIST_OPTIONS, JSON.stringify(local)); + } + }, +}; + +export default logsUpdater; diff --git a/frontend/src/providers/preferences/configs/tracesLoaderConfig.ts b/frontend/src/providers/preferences/configs/tracesLoaderConfig.ts new file mode 100644 index 0000000000..cb323b6aec --- /dev/null +++ b/frontend/src/providers/preferences/configs/tracesLoaderConfig.ts @@ -0,0 +1,43 @@ +/* eslint-disable no-empty */ +import getLocalStorageKey from 'api/browser/localstorage/get'; +import { LOCALSTORAGE } from 'constants/localStorage'; +import { defaultTraceSelectedColumns } from 'container/OptionsMenu/constants'; +import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; + +// --- TRACES preferences loader config --- +const tracesLoaders = { + local: async (): Promise<{ + columns: BaseAutocompleteData[]; + }> => { + const local = getLocalStorageKey(LOCALSTORAGE.TRACES_LIST_OPTIONS); + if (local) { + try { + const parsed = JSON.parse(local); + return { + columns: parsed.selectColumns || [], + }; + } catch {} + } + return { columns: [] }; + }, + url: async (): Promise<{ + columns: BaseAutocompleteData[]; + }> => { + const urlParams = new URLSearchParams(window.location.search); + try { + const options = JSON.parse(urlParams.get('options') || '{}'); + return { + columns: options.selectColumns || [], + }; + } catch {} + return { columns: [] }; + }, + default: async (): Promise<{ + columns: BaseAutocompleteData[]; + }> => ({ + columns: defaultTraceSelectedColumns as BaseAutocompleteData[], + }), + priority: ['local', 'url', 'default'] as const, +}; + +export default tracesLoaders; diff --git a/frontend/src/providers/preferences/configs/tracesUpdaterConfig.ts b/frontend/src/providers/preferences/configs/tracesUpdaterConfig.ts new file mode 100644 index 0000000000..ed84a7a7ac --- /dev/null +++ b/frontend/src/providers/preferences/configs/tracesUpdaterConfig.ts @@ -0,0 +1,25 @@ +import setLocalStorageKey from 'api/browser/localstorage/set'; +import { LOCALSTORAGE } from 'constants/localStorage'; +import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; + +// --- TRACES preferences updater config --- +const tracesUpdater = { + updateColumns: (newColumns: BaseAutocompleteData[], mode: string): void => { + const url = new URL(window.location.href); + const options = JSON.parse(url.searchParams.get('options') || '{}'); + options.selectColumns = newColumns; + url.searchParams.set('options', JSON.stringify(options)); + window.history.replaceState({}, '', url.toString()); + + if (mode === 'direct') { + const local = JSON.parse( + localStorage.getItem(LOCALSTORAGE.TRACES_LIST_OPTIONS) || '{}', + ); + local.selectColumns = newColumns; + setLocalStorageKey(LOCALSTORAGE.TRACES_LIST_OPTIONS, JSON.stringify(local)); + } + }, + updateFormatting: (): void => {}, // no-op for traces +}; + +export default tracesUpdater; diff --git a/frontend/src/providers/preferences/context/PreferenceContextProvider.tsx b/frontend/src/providers/preferences/context/PreferenceContextProvider.tsx new file mode 100644 index 0000000000..68c1f93dbb --- /dev/null +++ b/frontend/src/providers/preferences/context/PreferenceContextProvider.tsx @@ -0,0 +1,78 @@ +import { PreferenceContextValue } from 'providers/preferences/types'; +import React, { createContext, useContext, useMemo } from 'react'; +import { useLocation } from 'react-router-dom'; +import { DataSource } from 'types/common/queryBuilder'; + +import { usePreferenceSync } from '../sync/usePreferenceSync'; + +// This will help in identifying the mode of the preference context +// savedView - when the preference is loaded from a saved view +// direct - when the preference is loaded from a direct query + +export type PreferenceMode = 'savedView' | 'direct'; + +const PreferenceContext = createContext( + undefined, +); + +export function PreferenceContextProvider({ + children, +}: { + children: React.ReactNode; +}): JSX.Element { + const location = useLocation(); + const params = new URLSearchParams(location.search); + + const savedViewId = params.get('view'); + let dataSource: DataSource = DataSource.LOGS; + if (location.pathname.includes('traces')) dataSource = DataSource.TRACES; + + const { + preferences, + loading, + error, + updateColumns, + updateFormatting, + } = usePreferenceSync({ + mode: savedViewId ? 'savedView' : 'direct', + savedViewId: savedViewId || undefined, + dataSource, + }); + + const value = useMemo( + () => ({ + preferences, + loading, + error, + mode: savedViewId ? 'savedView' : 'direct', + savedViewId: savedViewId || undefined, + dataSource, + updateColumns, + updateFormatting, + }), + [ + savedViewId, + dataSource, + preferences, + loading, + error, + updateColumns, + updateFormatting, + ], + ); + + return ( + + {children} + + ); +} + +export function usePreferenceContext(): PreferenceContextValue { + const ctx = useContext(PreferenceContext); + if (!ctx) + throw new Error( + 'usePreferenceContext must be used within PreferenceContextProvider', + ); + return ctx; +} diff --git a/frontend/src/providers/preferences/loader/usePreferenceLoader.ts b/frontend/src/providers/preferences/loader/usePreferenceLoader.ts new file mode 100644 index 0000000000..31b8ccb9ff --- /dev/null +++ b/frontend/src/providers/preferences/loader/usePreferenceLoader.ts @@ -0,0 +1,130 @@ +/* eslint-disable sonarjs/cognitive-complexity */ +/* eslint-disable no-empty */ +import { + defaultLogsSelectedColumns, + defaultTraceSelectedColumns, +} from 'container/OptionsMenu/constants'; +import { useGetAllViews } from 'hooks/saveViews/useGetAllViews'; +import { useEffect, useState } from 'react'; +import { useLocation } from 'react-router-dom'; +import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { DataSource } from 'types/common/queryBuilder'; + +import logsLoaderConfig from '../configs/logsLoaderConfig'; +import tracesLoaderConfig from '../configs/tracesLoaderConfig'; +import { FormattingOptions, PreferenceMode, Preferences } from '../types'; + +// Generic preferences loader that works with any config +async function preferencesLoader(config: { + priority: readonly string[]; + [key: string]: any; +}): Promise { + const findValidLoader = async (): Promise => { + // Try each loader in priority order + const results = await Promise.all( + config.priority.map(async (source) => ({ + source, + result: await config[source](), + })), + ); + // Find the first result with columns + const validResult = results.find(({ result }) => result.columns.length); + if (validResult) { + return validResult.result; + } + // fallback to default + return config.default(); + }; + + return findValidLoader(); +} + +// Use the generic loader with specific configs +async function logsPreferencesLoader(): Promise<{ + columns: BaseAutocompleteData[]; + formatting: FormattingOptions; +}> { + return preferencesLoader(logsLoaderConfig); +} + +async function tracesPreferencesLoader(): Promise<{ + columns: BaseAutocompleteData[]; +}> { + return preferencesLoader(tracesLoaderConfig); +} + +export function usePreferenceLoader({ + mode, + savedViewId, + dataSource, + reSync, +}: { + mode: PreferenceMode; + savedViewId: string; + dataSource: DataSource; + reSync: number; +}): { + preferences: Preferences | null; + loading: boolean; + error: Error | null; +} { + const [preferences, setPreferences] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const location = useLocation(); + + const { data: viewsData } = useGetAllViews(dataSource); + + console.log('uncaught viewsData', viewsData); + + useEffect((): void => { + async function loadPreferences(): Promise { + setLoading(true); + setError(null); + + try { + if (mode === 'savedView' && savedViewId) { + // we can also switch to the URL options params + // as we are essentially setting the options in the URL + // in ExplorerOptions.tsx#430 (updateOrRestoreSelectColumns) + const extraData = viewsData?.data?.data?.find( + (view) => view.id === savedViewId, + )?.extraData; + + const parsedExtraData = JSON.parse(extraData || '{}'); + let columns: BaseAutocompleteData[] = []; + let formatting: FormattingOptions | undefined; + if (dataSource === DataSource.LOGS) { + columns = parsedExtraData?.selectColumns || defaultLogsSelectedColumns; + formatting = { + maxLines: parsedExtraData?.maxLines ?? 2, + format: parsedExtraData?.format ?? 'table', + fontSize: parsedExtraData?.fontSize ?? 'small', + version: parsedExtraData?.version ?? 1, + }; + } else if (dataSource === DataSource.TRACES) { + columns = parsedExtraData?.selectColumns || defaultTraceSelectedColumns; + } + setPreferences({ columns, formatting }); + } else { + if (dataSource === DataSource.LOGS) { + const { columns, formatting } = await logsPreferencesLoader(); + setPreferences({ columns, formatting }); + } + + if (dataSource === DataSource.TRACES) { + const { columns } = await tracesPreferencesLoader(); + setPreferences({ columns }); + } + } + } catch (e) { + setError(e as Error); + } finally { + setLoading(false); + } + } + loadPreferences(); + }, [mode, savedViewId, dataSource, location, reSync, viewsData]); + + return { preferences, loading, error }; +} diff --git a/frontend/src/providers/preferences/sync/usePreferenceSync.ts b/frontend/src/providers/preferences/sync/usePreferenceSync.ts new file mode 100644 index 0000000000..de98c522ec --- /dev/null +++ b/frontend/src/providers/preferences/sync/usePreferenceSync.ts @@ -0,0 +1,41 @@ +import { useState } from 'react'; +import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { DataSource } from 'types/common/queryBuilder'; + +import { usePreferenceLoader } from '../loader/usePreferenceLoader'; +import { FormattingOptions, PreferenceMode, Preferences } from '../types'; +import { usePreferenceUpdater } from '../updater/usePreferenceUpdater'; + +export function usePreferenceSync({ + mode, + dataSource, + savedViewId, +}: { + mode: PreferenceMode; + dataSource: DataSource; + savedViewId: string | undefined; +}): { + preferences: Preferences | null; + loading: boolean; + error: Error | null; + updateColumns: (newColumns: BaseAutocompleteData[]) => void; + updateFormatting: (newFormatting: FormattingOptions) => void; +} { + // We are using a reSync state because we have URL updates as well as local storage updates + // and we want to make sure we are always using the latest preferences + const [reSync, setReSync] = useState(0); + const { preferences, loading, error } = usePreferenceLoader({ + mode, + savedViewId: savedViewId || '', + dataSource, + reSync, + }); + + const { updateColumns, updateFormatting } = usePreferenceUpdater({ + dataSource, + mode, + setReSync, + }); + + return { preferences, loading, error, updateColumns, updateFormatting }; +} diff --git a/frontend/src/providers/preferences/types/index.ts b/frontend/src/providers/preferences/types/index.ts new file mode 100644 index 0000000000..c163c711c8 --- /dev/null +++ b/frontend/src/providers/preferences/types/index.ts @@ -0,0 +1,27 @@ +import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { DataSource } from 'types/common/queryBuilder'; + +export type PreferenceMode = 'savedView' | 'direct'; + +export interface PreferenceContextValue { + preferences: Preferences | null; + loading: boolean; + error: Error | null; + mode: PreferenceMode; + savedViewId?: string; + dataSource: DataSource; + updateColumns: (newColumns: BaseAutocompleteData[]) => void; + updateFormatting: (newFormatting: FormattingOptions) => void; +} + +export interface FormattingOptions { + maxLines?: number; + format?: 'raw' | 'table'; + fontSize?: 'small' | 'medium' | 'large'; + version?: number; +} + +export interface Preferences { + columns: BaseAutocompleteData[]; + formatting?: FormattingOptions; +} diff --git a/frontend/src/providers/preferences/updater/usePreferenceUpdater.ts b/frontend/src/providers/preferences/updater/usePreferenceUpdater.ts new file mode 100644 index 0000000000..faf11a4cf1 --- /dev/null +++ b/frontend/src/providers/preferences/updater/usePreferenceUpdater.ts @@ -0,0 +1,50 @@ +import { Dispatch, SetStateAction } from 'react'; +import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { DataSource } from 'types/common/queryBuilder'; + +import logsUpdater from '../configs/logsUpdaterConfig'; +import tracesUpdater from '../configs/tracesUpdaterConfig'; +import { FormattingOptions } from '../types'; + +const metricsUpdater = { + updateColumns: (): void => {}, // no-op for metrics + updateFormatting: (): void => {}, // no-op for metrics +}; + +const updaterConfig: Record< + DataSource, + { + updateColumns: (newColumns: BaseAutocompleteData[], mode: string) => void; + updateFormatting: (newFormatting: FormattingOptions, mode: string) => void; + } +> = { + [DataSource.LOGS]: logsUpdater, + [DataSource.TRACES]: tracesUpdater, + [DataSource.METRICS]: metricsUpdater, +}; + +export function usePreferenceUpdater({ + dataSource, + mode, + setReSync, +}: { + dataSource: DataSource; + mode: string; + setReSync: Dispatch>; +}): { + updateColumns: (newColumns: BaseAutocompleteData[]) => void; + updateFormatting: (newFormatting: FormattingOptions) => void; +} { + const updater = updaterConfig[dataSource]; + + return { + updateColumns: (newColumns: BaseAutocompleteData[]): void => { + updater.updateColumns(newColumns, mode); + setReSync((prev: number) => prev + 1); + }, + updateFormatting: (newFormatting: FormattingOptions): void => { + updater.updateFormatting(newFormatting, mode); + setReSync((prev: number) => prev + 1); + }, + }; +}