diff --git a/src/containers/Tenant/Diagnostics/TopQueries/QueriesTableWithDrawer.tsx b/src/containers/Tenant/Diagnostics/TopQueries/QueriesTableWithDrawer.tsx index f1ef2fb47..2c42c82aa 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 c5fc7125f..330ea68bd 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} - + string | undefined); defaults: undefined | AxiosRequestConfig; } @@ -54,6 +54,7 @@ export class YdbEmbeddedAPI { csrfTokenGetter = () => undefined, defaults = {}, useRelativePath = false, + useMetaSettings = false, }: YdbEmbeddedAPIProps) { const axiosParams: AxiosWrapperOptions = {config: {withCredentials, ...defaults}}; const baseApiParams = {singleClusterMode, proxyMeta, useRelativePath}; @@ -62,7 +63,7 @@ export class YdbEmbeddedAPI { if (webVersion) { this.meta = new MetaAPI(axiosParams, baseApiParams); } - if (uiFactory.useMetaSettings) { + if (useMetaSettings) { this.metaSettings = new MetaSettingsAPI(axiosParams, baseApiParams); } diff --git a/src/services/api/metaSettings.ts b/src/services/api/metaSettings.ts index a8ec9505d..a5642913e 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/configureStore.ts b/src/store/configureStore.ts index 8ce666ba6..7cac00693 100644 --- a/src/store/configureStore.ts +++ b/src/store/configureStore.ts @@ -74,6 +74,7 @@ export function configureStore({ proxyMeta: false, csrfTokenGetter: undefined, useRelativePath: false, + useMetaSettings: false, defaults: undefined, }), } = {}) { diff --git a/src/store/reducers/authentication/authentication.ts b/src/store/reducers/authentication/authentication.ts index f8b1ecd9a..6b2d73af8 100644 --- a/src/store/reducers/authentication/authentication.ts +++ b/src/store/reducers/authentication/authentication.ts @@ -11,6 +11,7 @@ const initialState: AuthenticationState = { isAuthenticated: true, user: undefined, id: undefined, + metaUser: undefined, }; export const slice = createSlice({ @@ -45,13 +46,13 @@ export const slice = createSlice({ selectIsUserAllowedToMakeChanges: (state) => state.isUserAllowedToMakeChanges, selectIsViewerUser: (state) => state.isViewerUser, selectUser: (state) => state.user, - selectID: (state) => state.id, + selectMetaUser: (state) => state.metaUser ?? state.id, }, }); export default slice.reducer; export const {setIsAuthenticated, setUser} = slice.actions; -export const {selectIsUserAllowedToMakeChanges, selectIsViewerUser, selectUser, selectID} = +export const {selectIsUserAllowedToMakeChanges, selectIsViewerUser, selectUser, selectMetaUser} = slice.selectors; export const authenticationApi = api.injectEndpoints({ diff --git a/src/store/reducers/authentication/types.ts b/src/store/reducers/authentication/types.ts index 0a8879b31..f72b5d75b 100644 --- a/src/store/reducers/authentication/types.ts +++ b/src/store/reducers/authentication/types.ts @@ -5,4 +5,6 @@ export interface AuthenticationState { user: string | undefined; id: string | undefined; + + metaUser: string | undefined; } diff --git a/src/store/reducers/capabilities/capabilities.ts b/src/store/reducers/capabilities/capabilities.ts index 1c4a4f968..c0a71b9da 100644 --- a/src/store/reducers/capabilities/capabilities.ts +++ b/src/store/reducers/capabilities/capabilities.ts @@ -2,6 +2,7 @@ import {createSelector} from '@reduxjs/toolkit'; import type {Capability, MetaCapability, SecuritySetting} from '../../../types/api/capabilities'; import type {AppDispatch, RootState} from '../../defaultStore'; +import {serializeError} from '../../utils'; import {api} from './../api'; @@ -30,10 +31,7 @@ export const capabilitiesApi = api.injectEndpoints({ } catch (error) { // If capabilities endpoint is not available, there will be an error // That means no new features are available - // Serialize the error to make it Redux-compatible - const serializedError = - error instanceof Error ? {message: error.message, name: error.name} : error; - return {error: serializedError}; + return {error: serializeError(error)}; } }, }), diff --git a/src/store/reducers/settings/api.ts b/src/store/reducers/settings/api.ts index f4595de3c..39a909aba 100644 --- a/src/store/reducers/settings/api.ts +++ b/src/store/reducers/settings/api.ts @@ -1,24 +1,32 @@ -import {isNil} from 'lodash'; - import type { GetSettingsParams, GetSingleSettingParams, SetSingleSettingParams, - Setting, } from '../../../types/api/settings'; import type {AppDispatch} from '../../defaultStore'; +import {serializeError} from '../../utils'; import {api} from '../api'; import {SETTINGS_OPTIONS} from './constants'; +import {getSettingDefault, parseSettingValue, stringifySettingValue} from './utils'; 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'); + if (!name || !window.api?.metaSettings) { + throw new Error( + 'Cannot get setting, no MetaSettings API or necessary params are missing', + ); + } + + const defaultValue = getSettingDefault(name) as unknown; + + if (!user) { + return {data: defaultValue}; } + const data = await window.api.metaSettings.getSingleSetting({ name, user, @@ -26,43 +34,50 @@ 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}; + return {data: parseSettingValue(data?.value) ?? defaultValue}; } catch (error) { - return {error}; + return {error: serializeError(error)}; } }, }), 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( + 'Cannot set setting, no MetaSettings API or necessary params are missing', + ); } - 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'); + throw new Error('Cannot set setting - status is not SUCCESS'); } return {data}; } catch (error) { - return {error}; + return {error: serializeError(error)}; } }, async onQueryStarted(args, {dispatch, queryFulfilled}) { const {name, user, value} = args; + if (!name) { + return; + } + // Optimistically update existing cache entry const patchResult = dispatch( - settingsApi.util.updateQueryData('getSingleSetting', {name, user}, (draft) => { - return {...draft, name, user, value}; - }), + settingsApi.util.updateQueryData('getSingleSetting', {name, user}, () => value), ); try { await queryFulfilled; @@ -72,41 +87,38 @@ export const settingsApi = api.injectEndpoints({ }, }), 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( + 'Cannot get settings, no MetaSettings API or necessary params are missing', + ); } 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] ?? {}; + const settingData = data[settingName]; + + const defaultValue = getSettingDefault(settingName); const cacheEntryParams: GetSingleSettingParams = { name: settingName, user, }; - const newValue = {name: settingName, user, value: settingData?.value}; + const newSettingValue = + parseSettingValue(settingData?.value) ?? defaultValue; const patch = dispatch( settingsApi.util.upsertQueryData( 'getSingleSetting', cacheEntryParams, - newValue, + newSettingValue, ), - ).then(() => { - // 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); - }); + ); patches.push(patch); }); @@ -116,24 +128,10 @@ export const settingsApi = api.injectEndpoints({ return {data}; } catch (error) { - return {error}; + return {error: serializeError(error)}; } }, }), }), overrideExisting: 'throw', }); - -function syncLocalValueToMetaIfNoData(params: Setting, dispatch: AppDispatch) { - const localValue = localStorage.getItem(params.name); - - if (isNil(params.value) && !isNil(localValue)) { - dispatch( - settingsApi.endpoints.setSingleSetting.initiate({ - name: params.name, - user: params.user, - value: localValue, - }), - ); - } -} diff --git a/src/store/reducers/settings/constants.ts b/src/store/reducers/settings/constants.ts index eff66c6f8..8937dc8fa 100644 --- a/src/store/reducers/settings/constants.ts +++ b/src/store/reducers/settings/constants.ts @@ -76,14 +76,8 @@ export const DEFAULT_USER_SETTINGS = { [SETTING_KEYS.STORAGE_TYPE]: STORAGE_TYPES.groups, } as const satisfies Record; -export const SETTINGS_OPTIONS: Record = { +export const SETTINGS_OPTIONS: Record = { [SETTING_KEYS.THEME]: { preventBatching: true, }, - [SETTING_KEYS.SAVED_QUERIES]: { - preventSyncWithLS: true, - }, - [SETTING_KEYS.QUERIES_HISTORY]: { - preventSyncWithLS: true, - }, } as const; diff --git a/src/store/reducers/settings/settings.ts b/src/store/reducers/settings/settings.ts index 4267f8fde..de60ee52e 100644 --- a/src/store/reducers/settings/settings.ts +++ b/src/store/reducers/settings/settings.ts @@ -1,12 +1,14 @@ import type {Store} from '@reduxjs/toolkit'; -import {createSlice} from '@reduxjs/toolkit'; +import {createSelector, createSlice} from '@reduxjs/toolkit'; +import {isNil} from 'lodash'; import {settingsManager} from '../../../services/settings'; import {parseJson} from '../../../utils/utils'; -import type {AppDispatch} from '../../defaultStore'; +import type {AppDispatch, RootState} from '../../defaultStore'; import {DEFAULT_USER_SETTINGS} from './constants'; import type {SettingsState} from './types'; +import {getSettingDefault, readSettingValueFromLS, setSettingValueToLS} from './utils'; const userSettings = settingsManager.extractSettingsFromLS(DEFAULT_USER_SETTINGS); const systemSettings = window.systemSettings || {}; @@ -24,23 +26,39 @@ const settingsSlice = createSlice({ state.userSettings[action.payload.name] = action.payload.value; }), }), - selectors: { - getSettingValue: (state, name?: string) => { - if (!name) { - return undefined; - } - - return state.userSettings[name]; - }, - }, }); -export const {getSettingValue} = settingsSlice.selectors; +/** + * Reads LS value or use default when store value undefined + * + * If there is value in store, returns it + */ +export const getSettingValue = createSelector( + (state: RootState) => state.settings.userSettings, + (_state: RootState, name?: string) => name, + (userSettings, name) => { + if (!name) { + return undefined; + } + + const storeValue = userSettings[name]; + + if (!isNil(storeValue)) { + return storeValue; + } + + const defaultValue = getSettingDefault(name); + const savedValue = readSettingValueFromLS(name); + + return savedValue ?? defaultValue; + }, +); export const setSettingValue = (name: string | undefined, value: unknown) => { return (dispatch: AppDispatch) => { if (name) { dispatch(settingsSlice.actions.setSettingValue({name, value})); + setSettingValueToLS(name, value); } }; }; diff --git a/src/store/reducers/settings/useSetting.ts b/src/store/reducers/settings/useSetting.ts index 4a51f3a7e..ec5b0ebe1 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 {useSetting as useLSSetting} from '../../../utils/hooks'; import {useTypedSelector} from '../../../utils/hooks/useTypedSelector'; -import {selectID, selectUser} from '../authentication/authentication'; +import {selectMetaUser} 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'; type SaveSettingValue = (value: T | undefined) => void; @@ -25,71 +13,40 @@ 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 user = useTypedSelector(selectMetaUser); - const params = React.useMemo(() => { - return shouldUseMetaSettings ? {user, name} : skipToken; - }, [shouldUseMetaSettings, user, name]); - - const {currentData: metaSetting, isLoading: isSettingLoading} = - settingsApi.useGetSingleSettingQuery(params); + const {currentData: settingFromMeta, 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; + const [settingFromLS, saveSettingToLS] = useLSSetting(name); - if (!shouldUseOnlyExternalSettings) { - const savedValue = readSettingValueFromLS(name); - value = savedValue ?? value; + const settingValue = React.useMemo(() => { + if (!name) { + return undefined; } - - 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)); + if (window.api?.metaSettings) { + return settingFromMeta; } - }, [shouldUseMetaSettings, shouldUseOnlyExternalSettings, metaSetting, name, dispatch]); + return settingFromLS; + }, [name, settingFromMeta, settingFromLS]); const saveValue = React.useCallback>( (value) => { - if (shouldUseMetaSettings) { - setMetaSetting({ - user, - name: name, - value: stringifySettingValue(value), - }); + if (!name) { + return; } - - if (!shouldUseOnlyExternalSettings) { - setSettingValueToLS(name, value); + if (window.api?.metaSettings) { + setMetaSetting({user, name, value}); + } else { + saveSettingToLS(value); } }, - [shouldUseMetaSettings, shouldUseOnlyExternalSettings, user, name, setMetaSetting], + [user, name, setMetaSetting, saveSettingToLS], ); - return {value: settingValue, saveValue, isLoading} as const; + return {value: settingValue as T | undefined, saveValue, isLoading} as const; } diff --git a/src/store/reducers/settings/utils.ts b/src/store/reducers/settings/utils.ts index 916e1e5d4..8113e445d 100644 --- a/src/store/reducers/settings/utils.ts +++ b/src/store/reducers/settings/utils.ts @@ -1,7 +1,10 @@ import type {SettingValue} from '../../../types/api/settings'; import {parseJson} from '../../../utils/utils'; -export function stringifySettingValue(value?: T): string { +import type {SettingKey} from './constants'; +import {DEFAULT_USER_SETTINGS} from './constants'; + +export function stringifySettingValue(value?: unknown): string { return typeof value === 'string' ? value : JSON.stringify(value); } export function parseSettingValue(value?: SettingValue) { @@ -34,3 +37,6 @@ 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]; +} diff --git a/src/store/utils.ts b/src/store/utils.ts new file mode 100644 index 000000000..b56423709 --- /dev/null +++ b/src/store/utils.ts @@ -0,0 +1,11 @@ +/** + * Serialize the error to make it Redux-compatible + * + * It prevents redux console error on string error in code - `throw new Error("description")` + */ +export function serializeError(error: unknown) { + if (error instanceof Error) { + return {message: error.message, name: error.name}; + } + return error; +} diff --git a/src/uiFactory/types.ts b/src/uiFactory/types.ts index a3aea7469..169fe2d4f 100644 --- a/src/uiFactory/types.ts +++ b/src/uiFactory/types.ts @@ -51,7 +51,6 @@ export interface UIFactory; diff --git a/src/utils/hooks/useSetting.ts b/src/utils/hooks/useSetting.ts index 543206215..c9438ae05 100644 --- a/src/utils/hooks/useSetting.ts +++ b/src/utils/hooks/useSetting.ts @@ -1,12 +1,11 @@ import React from 'react'; -import {settingsManager} from '../../services/settings'; import {getSettingValue, setSettingValue} from '../../store/reducers/settings/settings'; import {useTypedDispatch} from './useTypedDispatch'; import {useTypedSelector} from './useTypedSelector'; -export const useSetting = (key: string, defaultValue?: T): [T, (value: T) => void] => { +export const useSetting = (key?: string, defaultValue?: T): [T, (value: T) => void] => { const dispatch = useTypedDispatch(); const settingValue = useTypedSelector((state) => { @@ -17,7 +16,6 @@ export const useSetting = (key: string, defaultValue?: T): [T, (value: T) => const setValue = React.useCallback( (value: T) => { dispatch(setSettingValue(key, value)); - settingsManager.setUserSettingsValue(key, value); }, [dispatch, key], );