From 7f5c82e066ba955cfbf8caea4a7b5318e9a28e6d Mon Sep 17 00:00:00 2001 From: mufazalov Date: Wed, 19 Nov 2025 18:27:12 +0300 Subject: [PATCH 1/4] feat(useSetting); sync values with LS and store in api --- src/services/api/metaSettings.ts | 2 +- src/store/reducers/settings/api.ts | 154 +++++++++++++++++----- src/store/reducers/settings/constants.ts | 2 +- src/store/reducers/settings/useSetting.ts | 69 +--------- src/store/reducers/settings/utils.ts | 9 ++ 5 files changed, 134 insertions(+), 102 deletions(-) diff --git a/src/services/api/metaSettings.ts b/src/services/api/metaSettings.ts index a8ec9505d4..a5642913e9 100644 --- a/src/services/api/metaSettings.ts +++ b/src/services/api/metaSettings.ts @@ -25,7 +25,7 @@ export class MetaSettingsAPI extends BaseMetaAPI { preventBatching, }: GetSingleSettingParams & {preventBatching?: boolean}) { if (preventBatching) { - return this.get(this.getPath('/meta/user_settings'), {name, user}); + return this.get(this.getPath('/meta/user_settings'), {name, user}); } return new Promise((resolve, reject) => { diff --git a/src/store/reducers/settings/api.ts b/src/store/reducers/settings/api.ts index f4595de3c6..c52c105cbc 100644 --- a/src/store/reducers/settings/api.ts +++ b/src/store/reducers/settings/api.ts @@ -6,19 +6,34 @@ import type { SetSingleSettingParams, Setting, } from '../../../types/api/settings'; -import type {AppDispatch} from '../../defaultStore'; +import type {AppDispatch, RootState} from '../../defaultStore'; import {api} from '../api'; import {SETTINGS_OPTIONS} from './constants'; +import {getSettingValue, setSettingValue} from './settings'; +import { + getSettingDefault, + parseSettingValue, + readSettingValueFromLS, + setSettingValueToLS, + shouldSyncSettingToLS, + stringifySettingValue, +} from './utils'; + +const invalidParamsError = + 'Missing required parameters (name, user) or MetaSettings API is not available'; export const settingsApi = api.injectEndpoints({ endpoints: (builder) => ({ - getSingleSetting: builder.query({ - queryFn: async ({name, user}: GetSingleSettingParams, baseApi) => { + getSingleSetting: builder.query>({ + queryFn: async ({name, user}) => { try { - if (!window.api.metaSettings) { - throw new Error('MetaSettings API is not available'); + // In this case localStorage should be used for settings + // Actual value will be loaded to store in onQueryStarted + if (!name || !user || !window.api.metaSettings) { + throw new Error(invalidParamsError); } + const data = await window.api.metaSettings.getSingleSetting({ name, user, @@ -26,25 +41,70 @@ export const settingsApi = api.injectEndpoints({ preventBatching: SETTINGS_OPTIONS[name]?.preventBatching, }); - const dispatch = baseApi.dispatch as AppDispatch; - - // Try to sync local value if there is no backend value - syncLocalValueToMetaIfNoData(data, dispatch); - return {data}; } catch (error) { return {error}; } }, + async onQueryStarted(args, {dispatch, queryFulfilled}) { + const {name, user} = args; + + if (!name) { + return; + } + + const shouldUseLocalSettings = + !user || !window.api.metaSettings || shouldSyncSettingToLS(name); + + const defaultValue = getSettingDefault(name); + + // Preload value from LS or default to store + if (shouldUseLocalSettings) { + const savedValue = readSettingValueFromLS(name); + const value = savedValue ?? defaultValue; + + dispatch(setSettingValue(name, value)); + } else { + dispatch(setSettingValue(name, defaultValue)); + } + + try { + const {data} = await queryFulfilled; + + // Load api value to store if present + // In case local storage should be used + // query will finish with an error and this code will not run + const parsedValue = parseSettingValue(data?.value); + + if (isNil(data?.value)) { + // Try to sync local value if there is no backend value + syncLocalValueToMetaIfNoData({...data}, dispatch); + } else { + dispatch(setSettingValue(name, parsedValue)); + + if (shouldSyncSettingToLS(name)) { + setSettingValueToLS(name, data.value); + } + } + } catch {} + }, }), setSingleSetting: builder.mutation({ - queryFn: async (params: SetSingleSettingParams) => { + queryFn: async ({ + name, + user, + value, + }: Partial> & {value: unknown}) => { try { - if (!window.api.metaSettings) { - throw new Error('MetaSettings API is not available'); + if (!name || !user || !window.api.metaSettings) { + throw new Error(invalidParamsError); } - const data = await window.api.metaSettings.setSingleSetting(params); + const data = await window.api.metaSettings.setSingleSetting({ + name, + user, + value: stringifySettingValue(value), + }); if (data.status !== 'SUCCESS') { throw new Error('Setting status is not SUCCESS'); @@ -55,34 +115,51 @@ export const settingsApi = api.injectEndpoints({ return {error}; } }, - async onQueryStarted(args, {dispatch, queryFulfilled}) { + async onQueryStarted(args, {dispatch, queryFulfilled, getState}) { const {name, user, value} = args; - // Optimistically update existing cache entry - const patchResult = dispatch( - settingsApi.util.updateQueryData('getSingleSetting', {name, user}, (draft) => { - return {...draft, name, user, value}; - }), - ); + if (!name) { + return; + } + + // Extract previous value to revert to it if set is not succesfull + const previousSettingValue = getSettingValue(getState() as RootState, name); + + // Optimistically update store + dispatch(setSettingValue(name, value)); + + // If local storage settings should be used + // Update LS and do not do any further code + if (!user || !window.api.metaSettings) { + setSettingValueToLS(name, value); + return; + } + try { await queryFulfilled; + + // If mutation is successful, we can store new value in LS + if (shouldSyncSettingToLS(name)) { + setSettingValueToLS(name, value); + } } catch { - patchResult.undo(); + // Set previous value to store in case of error + dispatch(setSettingValue(name, previousSettingValue)); } }, }), getSettings: builder.query({ - queryFn: async ({name, user}: GetSettingsParams, baseApi) => { + queryFn: async ({name, user}: Partial, baseApi) => { try { - if (!window.api.metaSettings) { - throw new Error('MetaSettings API is not available'); + if (!window.api.metaSettings || !name || !user) { + throw new Error(invalidParamsError); } const data = await window.api.metaSettings.getSettings({name, user}); - const patches: Promise[] = []; + const patches: Promise[] = []; const dispatch = baseApi.dispatch as AppDispatch; - // Upsert received data in getSingleSetting cache + // Upsert received data in getSingleSetting cache to prevent further redundant requests name.forEach((settingName) => { const settingData = data[settingName] ?? {}; @@ -98,15 +175,18 @@ export const settingsApi = api.injectEndpoints({ cacheEntryParams, newValue, ), - ).then(() => { + ); + if (isNil(settingData.value)) { // Try to sync local value if there is no backend value - // Do it after upsert if finished to ensure proper values update order - // 1. New entry added to cache with nil value - // 2. Positive entry update - local storage value replace nil in cache - // 3.1. Set is successful, local value in cache - // 3.2. Set is not successful, cache value reverted to previous nil syncLocalValueToMetaIfNoData(settingData, dispatch); - }); + } else { + const parsedValue = parseSettingValue(settingData.value); + dispatch(setSettingValue(settingName, parsedValue)); + + if (shouldSyncSettingToLS(settingName)) { + setSettingValueToLS(settingName, settingData.value); + } + } patches.push(patch); }); @@ -124,7 +204,11 @@ export const settingsApi = api.injectEndpoints({ overrideExisting: 'throw', }); -function syncLocalValueToMetaIfNoData(params: Setting, dispatch: AppDispatch) { +function syncLocalValueToMetaIfNoData(params: Partial, dispatch: AppDispatch) { + if (!params.name) { + return; + } + const localValue = localStorage.getItem(params.name); if (isNil(params.value) && !isNil(localValue)) { diff --git a/src/store/reducers/settings/constants.ts b/src/store/reducers/settings/constants.ts index cbc1c8bed4..cd4026fc93 100644 --- a/src/store/reducers/settings/constants.ts +++ b/src/store/reducers/settings/constants.ts @@ -73,7 +73,7 @@ export const DEFAULT_USER_SETTINGS = { [SETTING_KEYS.ACL_SYNTAX]: AclSyntax.YdbShort, } as const satisfies Record; -export const SETTINGS_OPTIONS: Record = { +export const SETTINGS_OPTIONS: Record = { [SETTING_KEYS.THEME]: { preventBatching: true, }, diff --git a/src/store/reducers/settings/useSetting.ts b/src/store/reducers/settings/useSetting.ts index 4a51f3a7e4..c5b2d7ce27 100644 --- a/src/store/reducers/settings/useSetting.ts +++ b/src/store/reducers/settings/useSetting.ts @@ -1,22 +1,10 @@ import React from 'react'; -import {skipToken} from '@reduxjs/toolkit/query'; - -import {uiFactory} from '../../../uiFactory/uiFactory'; -import {useTypedDispatch} from '../../../utils/hooks/useTypedDispatch'; import {useTypedSelector} from '../../../utils/hooks/useTypedSelector'; import {selectID, selectUser} from '../authentication/authentication'; import {settingsApi} from './api'; -import type {SettingKey} from './constants'; -import {DEFAULT_USER_SETTINGS, SETTINGS_OPTIONS} from './constants'; -import {getSettingValue, setSettingValue} from './settings'; -import { - parseSettingValue, - readSettingValueFromLS, - setSettingValueToLS, - stringifySettingValue, -} from './utils'; +import {getSettingValue} from './settings'; type SaveSettingValue = (value: T | undefined) => void; @@ -25,70 +13,21 @@ export function useSetting(name?: string): { saveValue: SaveSettingValue; isLoading: boolean; } { - const dispatch = useTypedDispatch(); - - const preventSyncWithLS = Boolean(name && SETTINGS_OPTIONS[name]?.preventSyncWithLS); - const settingValue = useTypedSelector((state) => getSettingValue(state, name)) as T | undefined; const authUserSID = useTypedSelector(selectUser); const anonymousUserId = useTypedSelector(selectID); - const user = authUserSID || anonymousUserId; - const shouldUseMetaSettings = uiFactory.useMetaSettings && user && name; - - const shouldUseOnlyExternalSettings = shouldUseMetaSettings && preventSyncWithLS; - - const params = React.useMemo(() => { - return shouldUseMetaSettings ? {user, name} : skipToken; - }, [shouldUseMetaSettings, user, name]); - const {currentData: metaSetting, isLoading: isSettingLoading} = - settingsApi.useGetSingleSettingQuery(params); + const {isLoading} = settingsApi.useGetSingleSettingQuery({user, name}); const [setMetaSetting] = settingsApi.useSetSingleSettingMutation(); - // Add loading state to settings that are stored externally - const isLoading = shouldUseMetaSettings ? isSettingLoading : false; - - // Load initial value - React.useEffect(() => { - let value = name ? (DEFAULT_USER_SETTINGS[name as SettingKey] as T | undefined) : undefined; - - if (!shouldUseOnlyExternalSettings) { - const savedValue = readSettingValueFromLS(name); - value = savedValue ?? value; - } - - dispatch(setSettingValue(name, value)); - }, [name, shouldUseOnlyExternalSettings, dispatch]); - - // Sync value from backend with LS and store - React.useEffect(() => { - if (shouldUseMetaSettings && metaSetting?.value) { - if (!shouldUseOnlyExternalSettings) { - setSettingValueToLS(name, metaSetting.value); - } - const parsedValue = parseSettingValue(metaSetting.value); - dispatch(setSettingValue(name, parsedValue)); - } - }, [shouldUseMetaSettings, shouldUseOnlyExternalSettings, metaSetting, name, dispatch]); - const saveValue = React.useCallback>( (value) => { - if (shouldUseMetaSettings) { - setMetaSetting({ - user, - name: name, - value: stringifySettingValue(value), - }); - } - - if (!shouldUseOnlyExternalSettings) { - setSettingValueToLS(name, value); - } + setMetaSetting({user, name: name, value: value}); }, - [shouldUseMetaSettings, shouldUseOnlyExternalSettings, user, name, setMetaSetting], + [user, name, setMetaSetting], ); return {value: settingValue, saveValue, isLoading} as const; diff --git a/src/store/reducers/settings/utils.ts b/src/store/reducers/settings/utils.ts index 916e1e5d42..601a176c12 100644 --- a/src/store/reducers/settings/utils.ts +++ b/src/store/reducers/settings/utils.ts @@ -1,6 +1,9 @@ import type {SettingValue} from '../../../types/api/settings'; import {parseJson} from '../../../utils/utils'; +import type {SettingKey} from './constants'; +import {DEFAULT_USER_SETTINGS, SETTINGS_OPTIONS} from './constants'; + export function stringifySettingValue(value?: T): string { return typeof value === 'string' ? value : JSON.stringify(value); } @@ -34,3 +37,9 @@ export function setSettingValueToLS(name: string | undefined, value: unknown): v localStorage.setItem(name, preparedValue); } catch {} } +export function getSettingDefault(name: string) { + return DEFAULT_USER_SETTINGS[name as SettingKey]; +} +export function shouldSyncSettingToLS(name: string) { + return !SETTINGS_OPTIONS[name]?.preventSyncWithLS; +} From cbfadb087e77b51f0fedb8c0d1c5a8baeed8645c Mon Sep 17 00:00:00 2001 From: mufazalov Date: Wed, 19 Nov 2025 22:06:30 +0300 Subject: [PATCH 2/4] fix: copilot review --- src/store/reducers/settings/api.ts | 36 ++++++++++++++++++---------- src/store/reducers/settings/utils.ts | 2 +- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/store/reducers/settings/api.ts b/src/store/reducers/settings/api.ts index c52c105cbc..d764d9db79 100644 --- a/src/store/reducers/settings/api.ts +++ b/src/store/reducers/settings/api.ts @@ -53,13 +53,12 @@ export const settingsApi = api.injectEndpoints({ return; } - const shouldUseLocalSettings = - !user || !window.api.metaSettings || shouldSyncSettingToLS(name); + const shouldUseLocalSettings = !user || !window.api.metaSettings; const defaultValue = getSettingDefault(name); // Preload value from LS or default to store - if (shouldUseLocalSettings) { + if (shouldUseLocalSettings || shouldSyncSettingToLS(name)) { const savedValue = readSettingValueFromLS(name); const value = savedValue ?? defaultValue; @@ -68,6 +67,12 @@ export const settingsApi = api.injectEndpoints({ dispatch(setSettingValue(name, defaultValue)); } + // Do not process query result when only LS is used + // There is not actual api call in this case + if (shouldUseLocalSettings) { + return; + } + try { const {data} = await queryFulfilled; @@ -77,8 +82,11 @@ export const settingsApi = api.injectEndpoints({ const parsedValue = parseSettingValue(data?.value); if (isNil(data?.value)) { + // Pass setting params without value if data is empty + const newValue = data ?? {name, user}; + // Try to sync local value if there is no backend value - syncLocalValueToMetaIfNoData({...data}, dispatch); + syncLocalValueToMetaIfNoData(newValue, dispatch); } else { dispatch(setSettingValue(name, parsedValue)); @@ -122,15 +130,17 @@ export const settingsApi = api.injectEndpoints({ return; } - // Extract previous value to revert to it if set is not succesfull + // Extract previous value to revert to it if set is not successful const previousSettingValue = getSettingValue(getState() as RootState, name); // Optimistically update store dispatch(setSettingValue(name, value)); + const shouldUseLocalSettings = !user || !window.api.metaSettings; + // If local storage settings should be used - // Update LS and do not do any further code - if (!user || !window.api.metaSettings) { + // Update LS and return early + if (shouldUseLocalSettings) { setSettingValueToLS(name, value); return; } @@ -167,24 +177,24 @@ export const settingsApi = api.injectEndpoints({ name: settingName, user, }; - const newValue = {name: settingName, user, value: settingData?.value}; + const newSetting = {name: settingName, user, value: settingData?.value}; const patch = dispatch( settingsApi.util.upsertQueryData( 'getSingleSetting', cacheEntryParams, - newValue, + newSetting, ), ); - if (isNil(settingData.value)) { + if (isNil(newSetting.value)) { // Try to sync local value if there is no backend value - syncLocalValueToMetaIfNoData(settingData, dispatch); + syncLocalValueToMetaIfNoData(newSetting, dispatch); } else { - const parsedValue = parseSettingValue(settingData.value); + const parsedValue = parseSettingValue(newSetting.value); dispatch(setSettingValue(settingName, parsedValue)); if (shouldSyncSettingToLS(settingName)) { - setSettingValueToLS(settingName, settingData.value); + setSettingValueToLS(settingName, newSetting.value); } } diff --git a/src/store/reducers/settings/utils.ts b/src/store/reducers/settings/utils.ts index 601a176c12..e2c824ae77 100644 --- a/src/store/reducers/settings/utils.ts +++ b/src/store/reducers/settings/utils.ts @@ -4,7 +4,7 @@ import {parseJson} from '../../../utils/utils'; import type {SettingKey} from './constants'; import {DEFAULT_USER_SETTINGS, SETTINGS_OPTIONS} from './constants'; -export function stringifySettingValue(value?: T): string { +export function stringifySettingValue(value?: unknown): string { return typeof value === 'string' ? value : JSON.stringify(value); } export function parseSettingValue(value?: SettingValue) { From b5eeee29684f0fece3dcf07d572e77533b8b5626 Mon Sep 17 00:00:00 2001 From: mufazalov Date: Wed, 19 Nov 2025 22:31:48 +0300 Subject: [PATCH 3/4] fix: failed test --- .../Diagnostics/TopQueries/QueriesTableWithDrawer.tsx | 9 ++++++--- .../Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx | 5 +++-- .../Tenant/Diagnostics/TopQueries/TopQueriesData.tsx | 5 +++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/containers/Tenant/Diagnostics/TopQueries/QueriesTableWithDrawer.tsx b/src/containers/Tenant/Diagnostics/TopQueries/QueriesTableWithDrawer.tsx index f1ef2fb474..2c42c82aa3 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/QueriesTableWithDrawer.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/QueriesTableWithDrawer.tsx @@ -18,7 +18,8 @@ const b = cn('kv-top-queries'); interface SimpleTableWithDrawerProps { columns: Column[]; data: KeyValueRow[]; - loading?: boolean; + isFetching?: boolean; + isLoading?: boolean; onRowClick?: ( row: KeyValueRow | null, index?: number, @@ -39,7 +40,8 @@ interface SimpleTableWithDrawerProps { export function QueriesTableWithDrawer({ columns, data, - loading, + isFetching, + isLoading, onRowClick, columnsWidthLSKey, emptyDataMessage, @@ -104,7 +106,8 @@ export function QueriesTableWithDrawer({ columnsWidthLSKey={columnsWidthLSKey} columns={columns} data={data} - isFetching={loading} + isFetching={isFetching} + isLoading={isLoading} settings={tableSettings} onRowClick={handleRowClick} rowClassName={(row) => b('row', {active: isEqual(row, selectedRow)})} diff --git a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx index c5fc7125ff..330ea68bdc 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx @@ -96,11 +96,12 @@ export const RunningQueriesData = ({ {error ? : null} - + {error ? : null} - + Date: Thu, 20 Nov 2025 14:51:20 +0300 Subject: [PATCH 4/4] fix: log errors --- src/store/reducers/settings/api.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/store/reducers/settings/api.ts b/src/store/reducers/settings/api.ts index d764d9db79..626df19bb1 100644 --- a/src/store/reducers/settings/api.ts +++ b/src/store/reducers/settings/api.ts @@ -43,6 +43,7 @@ export const settingsApi = api.injectEndpoints({ return {data}; } catch (error) { + console.error('Cannot get setting:', error); return {error}; } }, @@ -94,7 +95,10 @@ export const settingsApi = api.injectEndpoints({ setSettingValueToLS(name, data.value); } } - } catch {} + } catch { + // In case of an error there is no value to sync + // LS or default value was loaded to store + } }, }), setSingleSetting: builder.mutation({ @@ -120,6 +124,7 @@ export const settingsApi = api.injectEndpoints({ return {data}; } catch (error) { + console.error('Cannot update setting:', error); return {error}; } }, @@ -206,6 +211,7 @@ export const settingsApi = api.injectEndpoints({ return {data}; } catch (error) { + console.error('Cannot get settings:', error); return {error}; } },