diff --git a/src/components/DiskStateProgressBar/DiskStateProgressBar.tsx b/src/components/DiskStateProgressBar/DiskStateProgressBar.tsx index e5e4252734..913841ae04 100644 --- a/src/components/DiskStateProgressBar/DiskStateProgressBar.tsx +++ b/src/components/DiskStateProgressBar/DiskStateProgressBar.tsx @@ -1,9 +1,9 @@ import React from 'react'; import {SETTING_KEYS} from '../../store/reducers/settings/constants'; +import {useSetting} from '../../store/reducers/settings/useSetting'; import {cn} from '../../utils/cn'; import {getSeverityColor} from '../../utils/disks/helpers'; -import {useSetting} from '../../utils/hooks'; import './DiskStateProgressBar.scss'; @@ -30,7 +30,7 @@ export function DiskStateProgressBar({ content, className, }: DiskStateProgressBarProps) { - const [inverted] = useSetting(SETTING_KEYS.INVERTED_DISKS); + const {value: inverted} = useSetting(SETTING_KEYS.INVERTED_DISKS); const mods: Record = {inverted, compact, faded, empty, inactive}; diff --git a/src/components/JsonViewer/JsonViewer.tsx b/src/components/JsonViewer/JsonViewer.tsx index 4a1518744b..d8b2a41ee9 100644 --- a/src/components/JsonViewer/JsonViewer.tsx +++ b/src/components/JsonViewer/JsonViewer.tsx @@ -6,7 +6,7 @@ import DataTable from '@gravity-ui/react-data-table'; import {ActionTooltip, Button, Flex, Icon} from '@gravity-ui/uikit'; import {SETTING_KEYS} from '../../store/reducers/settings/constants'; -import {useSetting} from '../../utils/hooks'; +import {useSetting} from '../../store/reducers/settings/useSetting'; import type {ClipboardButtonProps} from '../ClipboardButton/ClipboardButton'; import {ClipboardButton} from '../ClipboardButton/ClipboardButton'; @@ -121,9 +121,8 @@ function JsonViewerComponent({ toolbarClassName, withClipboardButton, }: JsonViewerComponentProps) { - const [caseSensitiveSearch, setCaseSensitiveSearch] = useSetting( + const {value: caseSensitiveSearch, saveValue: setCaseSensitiveSearch} = useSetting( SETTING_KEYS.CASE_SENSITIVE_JSON_SEARCH, - false, ); const [collapsedState, setCollapsedState] = React.useState(() => { diff --git a/src/components/NetworkTable/hooks.ts b/src/components/NetworkTable/hooks.ts index 0005d2b581..528a0ab624 100644 --- a/src/components/NetworkTable/hooks.ts +++ b/src/components/NetworkTable/hooks.ts @@ -3,18 +3,18 @@ import { useViewerNodesHandlerHasNetworkStats, } from '../../store/reducers/capabilities/hooks'; import {SETTING_KEYS} from '../../store/reducers/settings/constants'; -import {useSetting} from '../../utils/hooks'; +import {useSetting} from '../../store/reducers/settings/useSetting'; export function useShouldShowDatabaseNetworkTable() { const viewerNodesHasNetworkStats = useViewerNodesHandlerHasNetworkStats(); - const [networkTableEnabled] = useSetting(SETTING_KEYS.ENABLE_NETWORK_TABLE); + const {value: networkTableEnabled} = useSetting(SETTING_KEYS.ENABLE_NETWORK_TABLE); return Boolean(viewerNodesHasNetworkStats && networkTableEnabled); } export function useShouldShowClusterNetworkTable() { const nodesHasWorkingClusterNetworkStats = useNodesHandlerHasWorkingClusterNetworkStats(); - const [networkTableEnabled] = useSetting(SETTING_KEYS.ENABLE_NETWORK_TABLE); + const {value: networkTableEnabled} = useSetting(SETTING_KEYS.ENABLE_NETWORK_TABLE); return Boolean(nodesHasWorkingClusterNetworkStats && networkTableEnabled); } diff --git a/src/containers/App/Providers.tsx b/src/containers/App/Providers.tsx index f2c8b32591..198f68bfa2 100644 --- a/src/containers/App/Providers.tsx +++ b/src/containers/App/Providers.tsx @@ -14,8 +14,8 @@ import {ComponentsProvider} from '../../components/ComponentsProvider/Components import {componentsRegistry as defaultComponentsRegistry} from '../../components/ComponentsProvider/componentsRegistry'; import type {ComponentsRegistry} from '../../components/ComponentsProvider/componentsRegistry'; import {SETTING_KEYS} from '../../store/reducers/settings/constants'; +import {useSetting} from '../../store/reducers/settings/useSetting'; import {toaster} from '../../utils/createToast'; -import {useSetting} from '../../utils/hooks'; import {AppTitleProvider} from './AppTitleContext'; @@ -57,7 +57,7 @@ export function Providers({ } function Theme({children}: {children: React.ReactNode}) { - const [theme] = useSetting(SETTING_KEYS.THEME); + const {value: theme} = useSetting(SETTING_KEYS.THEME); return {children}; } diff --git a/src/containers/AppWithClusters/utils/useAdditionalTenantsProps.tsx b/src/containers/AppWithClusters/utils/useAdditionalTenantsProps.tsx index 5a5f45c92f..2461e7c371 100644 --- a/src/containers/AppWithClusters/utils/useAdditionalTenantsProps.tsx +++ b/src/containers/AppWithClusters/utils/useAdditionalTenantsProps.tsx @@ -2,11 +2,11 @@ import {isNil} from 'lodash'; import {useClusterBaseInfo} from '../../../store/reducers/cluster/cluster'; import {SETTING_KEYS} from '../../../store/reducers/settings/constants'; +import {useSetting} from '../../../store/reducers/settings/useSetting'; import type {AdditionalTenantsProps} from '../../../types/additionalProps'; import type {ETenantType} from '../../../types/api/tenant'; import type {GetDatabaseLinks} from '../../../uiFactory/types'; import {uiFactory} from '../../../uiFactory/uiFactory'; -import {useSetting} from '../../../utils/hooks'; import type {GetLogsLink} from '../../../utils/logs'; import type {GetMonitoringLink} from '../../../utils/monitoring'; import {prepareBackendFromBalancer} from '../../../utils/parseBalancer'; @@ -23,7 +23,7 @@ export function useAdditionalTenantsProps({ getDatabaseLinks, }: GetAdditionalTenantsProps) { const clusterInfo = useClusterBaseInfo(); - const [useClusterBalancerAsBackend] = useSetting( + const {value: useClusterBalancerAsBackend} = useSetting( SETTING_KEYS.USE_CLUSTER_BALANCER_AS_BACKEND, ); diff --git a/src/containers/AsideNavigation/AsideNavigation.tsx b/src/containers/AsideNavigation/AsideNavigation.tsx index 18d14e88d7..8b550d20ba 100644 --- a/src/containers/AsideNavigation/AsideNavigation.tsx +++ b/src/containers/AsideNavigation/AsideNavigation.tsx @@ -7,8 +7,8 @@ import type {IconData} from '@gravity-ui/uikit'; import {useHistory} from 'react-router-dom'; import {SETTING_KEYS} from '../../store/reducers/settings/constants'; +import {useSetting} from '../../store/reducers/settings/useSetting'; import {cn} from '../../utils/cn'; -import {useSetting} from '../../utils/hooks'; import {InformationPopup} from './InformationPopup'; import {useHotkeysPanel} from './hooks/useHotkeysPanel'; @@ -76,7 +76,9 @@ export function AsideNavigation(props: AsideNavigationProps) { const [visiblePanel, setVisiblePanel] = React.useState(); const [informationPopupVisible, setInformationPopupVisible] = React.useState(false); - const [compact, setIsCompact] = useSetting(SETTING_KEYS.ASIDE_HEADER_COMPACT); + const {value: compact, saveValue: setIsCompact} = useSetting( + SETTING_KEYS.ASIDE_HEADER_COMPACT, + ); const toggleInformationPopup = () => setInformationPopupVisible((prev) => !prev); @@ -110,7 +112,7 @@ export function AsideNavigation(props: AsideNavigationProps) { onClick: () => history.push('/'), }} menuItems={props.menuItems} - compact={compact} + compact={Boolean(compact)} onChangeCompact={setIsCompact} className={b()} renderContent={() => props.content} diff --git a/src/containers/AsideNavigation/InformationPopup/InformationPopup.tsx b/src/containers/AsideNavigation/InformationPopup/InformationPopup.tsx index ccef8bbe32..1c3134aa03 100644 --- a/src/containers/AsideNavigation/InformationPopup/InformationPopup.tsx +++ b/src/containers/AsideNavigation/InformationPopup/InformationPopup.tsx @@ -1,8 +1,6 @@ import {Keyboard} from '@gravity-ui/icons'; import {Flex, Hotkey, Icon, Link, List, Text} from '@gravity-ui/uikit'; -import {settingsManager} from '../../../services/settings'; -import {SETTING_KEYS} from '../../../store/reducers/settings/constants'; import {cn} from '../../../utils/cn'; import {SHORTCUTS_HOTKEY} from '../hooks/useHotkeysPanel'; import i18n from '../i18n'; @@ -16,14 +14,6 @@ export interface InformationPopupProps { } export function InformationPopup({onKeyboardShortcutsClick}: InformationPopupProps) { - const getDocumentationLink = () => { - const lang = settingsManager.readUserSettingsValue( - SETTING_KEYS.LANGUAGE, - navigator.language, - ); - return lang === 'ru' ? 'https://ydb.tech/docs/ru/' : 'https://ydb.tech/docs/en/'; - }; - return (
@@ -35,7 +25,7 @@ export function InformationPopup({onKeyboardShortcutsClick}: InformationPopupPro items={[ { text: i18n('help-center.item.documentation'), - url: getDocumentationLink(), + url: 'https://ydb.tech/docs', }, ]} filterable={false} diff --git a/src/containers/Cluster/ClusterOverview/ClusterOverview.tsx b/src/containers/Cluster/ClusterOverview/ClusterOverview.tsx index 56c3769fb8..8d9d19f3df 100644 --- a/src/containers/Cluster/ClusterOverview/ClusterOverview.tsx +++ b/src/containers/Cluster/ClusterOverview/ClusterOverview.tsx @@ -9,13 +9,13 @@ import { } from '../../../store/reducers/capabilities/hooks'; import type {ClusterGroupsStats} from '../../../store/reducers/cluster/types'; import {SETTING_KEYS} from '../../../store/reducers/settings/constants'; +import {useSetting} from '../../../store/reducers/settings/useSetting'; import type {AdditionalClusterProps} from '../../../types/additionalProps'; import {isClusterInfoV2, isClusterInfoV5} from '../../../types/api/cluster'; import type {TClusterInfo} from '../../../types/api/cluster'; import type {IResponseError} from '../../../types/api/error'; import {valueIsDefined} from '../../../utils'; import {useResizeObserverTrigger} from '../../../utils/hooks/useResizeObserverTrigger'; -import {useSetting} from '../../../utils/hooks/useSetting'; import {ClusterInfo} from '../ClusterInfo/ClusterInfo'; import i18n from '../i18n'; import {getTotalStorageGroupsUsed} from '../utils'; @@ -41,7 +41,7 @@ interface ClusterOverviewProps { } export function ClusterOverview(props: ClusterOverviewProps) { - const [expandDashboard, setExpandDashboard] = useSetting( + const {value: expandDashboard, saveValue: setExpandDashboard} = useSetting( SETTING_KEYS.EXPAND_CLUSTER_DASHBOARD, ); diff --git a/src/containers/Cluster/ClusterOverview/components/ClusterMetricsNetwork.tsx b/src/containers/Cluster/ClusterOverview/components/ClusterMetricsNetwork.tsx index 3f06f99445..fa79d365aa 100644 --- a/src/containers/Cluster/ClusterOverview/components/ClusterMetricsNetwork.tsx +++ b/src/containers/Cluster/ClusterOverview/components/ClusterMetricsNetwork.tsx @@ -1,7 +1,7 @@ import {DoughnutMetrics} from '../../../../components/DoughnutMetrics/DoughnutMetrics'; import {SETTING_KEYS} from '../../../../store/reducers/settings/constants'; +import {useSetting} from '../../../../store/reducers/settings/useSetting'; import {formatBytes} from '../../../../utils/bytesParsers'; -import {useSetting} from '../../../../utils/hooks/useSetting'; import i18n from '../../i18n'; import type {ClusterMetricsBaseProps} from '../shared'; import {calculateBaseDiagramValues} from '../utils'; @@ -23,7 +23,9 @@ export function ClusterMetricsNetwork({ collapsed, ...rest }: ClusterMetricsNetworkProps) { - const [showNetworkUtilization] = useSetting(SETTING_KEYS.SHOW_NETWORK_UTILIZATION); + const {value: showNetworkUtilization} = useSetting( + SETTING_KEYS.SHOW_NETWORK_UTILIZATION, + ); if (!showNetworkUtilization) { return null; } diff --git a/src/containers/Tenant/Diagnostics/HotKeys/HotKeys.tsx b/src/containers/Tenant/Diagnostics/HotKeys/HotKeys.tsx index 5bfa29ea1e..5701ddfd55 100644 --- a/src/containers/Tenant/Diagnostics/HotKeys/HotKeys.tsx +++ b/src/containers/Tenant/Diagnostics/HotKeys/HotKeys.tsx @@ -10,10 +10,11 @@ import {ResizeableDataTable} from '../../../../components/ResizeableDataTable/Re import {hotKeysApi} from '../../../../store/reducers/hotKeys/hotKeys'; import {overviewApi} from '../../../../store/reducers/overview/overview'; import {SETTING_KEYS} from '../../../../store/reducers/settings/constants'; +import {useSetting} from '../../../../store/reducers/settings/useSetting'; import type {HotKey} from '../../../../types/api/hotkeys'; import {cn} from '../../../../utils/cn'; import {DEFAULT_TABLE_SETTINGS} from '../../../../utils/constants'; -import {useAutoRefreshInterval, useSetting} from '../../../../utils/hooks'; +import {useAutoRefreshInterval} from '../../../../utils/hooks'; import i18n from './i18n'; @@ -121,7 +122,9 @@ export function HotKeys({path, database, databaseFullPath}: HotKeysProps) { } function HelpCard() { - const [helpHidden, setHelpHidden] = useSetting(SETTING_KEYS.IS_HOTKEYS_HELP_HIDDEN); + const {value: helpHidden, saveValue: setHelpHidden} = useSetting( + SETTING_KEYS.IS_HOTKEYS_HELP_HIDDEN, + ); if (helpHidden) { return null; diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/MetricsTabs/MetricsTabs.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/MetricsTabs/MetricsTabs.tsx index 3a6d15720b..697774c03c 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/MetricsTabs/MetricsTabs.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/MetricsTabs/MetricsTabs.tsx @@ -5,6 +5,7 @@ import {useLocation} from 'react-router-dom'; import {getTenantPath, parseQuery} from '../../../../../routes'; import {SETTING_KEYS} from '../../../../../store/reducers/settings/constants'; +import {useSetting} from '../../../../../store/reducers/settings/useSetting'; import {TENANT_METRICS_TABS_IDS} from '../../../../../store/reducers/tenant/constants'; import type {TenantMetricsTab} from '../../../../../store/reducers/tenant/types'; import type { @@ -14,7 +15,6 @@ import type { } from '../../../../../store/reducers/tenants/utils'; import type {ETenantType} from '../../../../../types/api/tenant'; import {cn} from '../../../../../utils/cn'; -import {useSetting} from '../../../../../utils/hooks'; import {calculateMetricAggregates} from '../../../../../utils/metrics'; // no direct legend formatters needed here – handled in subcomponents import {TenantTabsGroups} from '../../../TenantPages'; @@ -99,7 +99,9 @@ export function MetricsTabs({ ); // Pass raw network values; DedicatedMetricsTabs computes percent and legend - const [showNetworkUtilization] = useSetting(SETTING_KEYS.SHOW_NETWORK_UTILIZATION); + const {value: showNetworkUtilization} = useSetting( + SETTING_KEYS.SHOW_NETWORK_UTILIZATION, + ); // card variant is handled within subcomponents diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantNetwork/TenantNetwork.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantNetwork/TenantNetwork.tsx index f4522a16d9..bd8bd1c7f0 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantNetwork/TenantNetwork.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantNetwork/TenantNetwork.tsx @@ -2,9 +2,10 @@ import {Flex} from '@gravity-ui/uikit'; import {getTenantPath} from '../../../../../routes'; import {SETTING_KEYS} from '../../../../../store/reducers/settings/constants'; +import {useSetting} from '../../../../../store/reducers/settings/useSetting'; import {TENANT_DIAGNOSTICS_TABS_IDS} from '../../../../../store/reducers/tenant/constants'; import {cn} from '../../../../../utils/cn'; -import {useSearchQuery, useSetting} from '../../../../../utils/hooks'; +import {useSearchQuery} from '../../../../../utils/hooks'; import {TenantTabsGroups} from '../../../TenantPages'; import {StatsWrapper} from '../StatsWrapper/StatsWrapper'; import i18n from '../i18n'; @@ -22,7 +23,7 @@ interface TenantNetworkProps { export function TenantNetwork({database}: TenantNetworkProps) { const query = useSearchQuery(); - const [networkTableEnabled] = useSetting(SETTING_KEYS.ENABLE_NETWORK_TABLE); + const {value: networkTableEnabled} = useSetting(SETTING_KEYS.ENABLE_NETWORK_TABLE); const tab = networkTableEnabled ? {[TenantTabsGroups.diagnosticsTab]: TENANT_DIAGNOSTICS_TABS_IDS.network} diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx index bb1e6f248c..bd61d040e6 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx @@ -12,11 +12,7 @@ import { TENANT_METRICS_TABS_IDS, TENANT_PAGES_IDS, } from '../../../../store/reducers/tenant/constants'; -import { - setDiagnosticsTab, - setTenantPage, - tenantApi, -} from '../../../../store/reducers/tenant/tenant'; +import {setDiagnosticsTab, tenantApi} from '../../../../store/reducers/tenant/tenant'; import {calculateTenantMetrics} from '../../../../store/reducers/tenants/utils'; import type {AdditionalTenantsProps} from '../../../../types/additionalProps'; import {getInfoTabLinks} from '../../../../utils/additionalProps'; @@ -24,6 +20,7 @@ import {TENANT_DEFAULT_TITLE} from '../../../../utils/constants'; import {useAutoRefreshInterval, useTypedDispatch, useTypedSelector} from '../../../../utils/hooks'; import {useClusterNameFromQuery} from '../../../../utils/hooks/useDatabaseFromQuery'; import {canShowTenantMonitoringTab} from '../../../../utils/monitoringVisibility'; +import {useTenantPage} from '../../TenantNavigation/useTenantNavigation'; import {mapDatabaseTypeToDBName} from '../../utils/schema'; import {HealthcheckPreview} from './Healthcheck/HealthcheckPreview'; @@ -53,6 +50,8 @@ export function TenantOverview({ const clusterName = useClusterNameFromQuery(); const dispatch = useTypedDispatch(); + const {handleTenantPageChange} = useTenantPage(); + const isMetaDatabasesAvailable = useDatabasesAvailable(); const {currentData: tenant, isFetching} = tenantApi.useGetTenantInfoQuery( @@ -196,7 +195,7 @@ export function TenantOverview({ ); const handleOpenMonitoring = () => { - dispatch(setTenantPage(TENANT_PAGES_IDS.diagnostics)); + handleTenantPageChange(TENANT_PAGES_IDS.diagnostics); dispatch(setDiagnosticsTab(TENANT_DIAGNOSTICS_TABS_IDS.monitoring)); }; diff --git a/src/containers/Tenant/ObjectGeneral/ObjectGeneral.tsx b/src/containers/Tenant/ObjectGeneral/ObjectGeneral.tsx index c0565c746e..ee90784ba9 100644 --- a/src/containers/Tenant/ObjectGeneral/ObjectGeneral.tsx +++ b/src/containers/Tenant/ObjectGeneral/ObjectGeneral.tsx @@ -3,10 +3,10 @@ import {useThemeValue} from '@gravity-ui/uikit'; import {TENANT_PAGES_IDS} from '../../../store/reducers/tenant/constants'; import type {AdditionalTenantsProps} from '../../../types/additionalProps'; import {cn} from '../../../utils/cn'; -import {useTypedSelector} from '../../../utils/hooks'; import Diagnostics from '../Diagnostics/Diagnostics'; import {Query} from '../Query/Query'; import {TenantNavigation} from '../TenantNavigation/TenantNavigation'; +import {useTenantPage} from '../TenantNavigation/useTenantNavigation'; import './ObjectGeneral.scss'; @@ -19,7 +19,7 @@ interface ObjectGeneralProps { function ObjectGeneral({additionalTenantProps}: ObjectGeneralProps) { const theme = useThemeValue(); - const {tenantPage} = useTypedSelector((state) => state.tenant); + const {tenantPage} = useTenantPage(); const renderPageContent = () => { switch (tenantPage) { diff --git a/src/containers/Tenant/ObjectSummary/ObjectSummary.tsx b/src/containers/Tenant/ObjectSummary/ObjectSummary.tsx index c3fe7b3e86..c1e4c854d2 100644 --- a/src/containers/Tenant/ObjectSummary/ObjectSummary.tsx +++ b/src/containers/Tenant/ObjectSummary/ObjectSummary.tsx @@ -36,6 +36,7 @@ import {prepareSystemViewType} from '../../../utils/schema'; import {EntityTitle} from '../EntityTitle/EntityTitle'; import {SchemaViewer} from '../Schema/SchemaViewer/SchemaViewer'; import {useCurrentSchema} from '../TenantContext'; +import {useTenantPage} from '../TenantNavigation/useTenantNavigation'; import {TENANT_INFO_TABS, TENANT_SCHEMA_TAB, TenantTabsGroups} from '../TenantPages'; import {useTenantQueryParams} from '../useTenantQueryParams'; import {getSummaryControls} from '../utils/controls'; @@ -91,6 +92,8 @@ export function ObjectSummary({ (state) => state.tenant, ); + const {handleTenantPageChange} = useTenantPage(); + const location = useLocation(); const queryParams = qs.parse(location.search, { @@ -411,7 +414,7 @@ export function ObjectSummary({ {showPreview && getSummaryControls( dispatch, - {setActivePath: handleSchemaChange}, + {setActivePath: handleSchemaChange, setTenantPage: handleTenantPageChange}, 'm', )(path, 'preview')} state.tenant); + const {diagnosticsTab} = useTypedSelector((state) => state.tenant); + + const {tenantPage, handleTenantPageChange} = useTenantPage(); + const diagnosticsSchemaActive = tenantPage === TENANT_PAGES_IDS.diagnostics && diagnosticsTab === TENANT_DIAGNOSTICS_TABS_IDS.schema; @@ -24,7 +28,7 @@ export function SchemaActions() {
diff --git a/src/containers/UserSettings/Setting.tsx b/src/containers/UserSettings/Setting.tsx index de980b0d15..9c837a6ee2 100644 --- a/src/containers/UserSettings/Setting.tsx +++ b/src/containers/UserSettings/Setting.tsx @@ -1,6 +1,6 @@ import {SegmentedRadioGroup, Switch} from '@gravity-ui/uikit'; -import {useSetting} from '../../utils/hooks'; +import {useSetting} from '../../store/reducers/settings/useSetting'; export interface SettingsInfoFieldProps { type: 'info'; @@ -17,18 +17,11 @@ export interface SettingProps { description?: React.ReactNode; settingKey: string; options?: {value: string; content: string}[]; - defaultValue?: unknown; onValueUpdate?: VoidFunction; } -export const Setting = ({ - type = 'switch', - settingKey, - options, - defaultValue, - onValueUpdate, -}: SettingProps) => { - const [settingValue, setValue] = useSetting(settingKey, defaultValue); +export const Setting = ({type = 'switch', settingKey, options, onValueUpdate}: SettingProps) => { + const {value, saveValue: setValue} = useSetting(settingKey); const onUpdate = (value: unknown) => { setValue(value); @@ -37,7 +30,7 @@ export const Setting = ({ switch (type) { case 'switch': { - return ; + return ; } case 'radio': { @@ -46,7 +39,7 @@ export const Setting = ({ } return ( - + {options.map(({value, content}) => { return ( diff --git a/src/containers/UserSettings/UserSettings.tsx b/src/containers/UserSettings/UserSettings.tsx index 858faa8c65..41cc22fbaa 100644 --- a/src/containers/UserSettings/UserSettings.tsx +++ b/src/containers/UserSettings/UserSettings.tsx @@ -1,5 +1,13 @@ +import React from 'react'; + import {Settings} from '@gravity-ui/navigation'; import {Text} from '@gravity-ui/uikit'; +import {skipToken} from '@reduxjs/toolkit/query'; + +import {selectID, selectUser} from '../../store/reducers/authentication/authentication'; +import {settingsApi} from '../../store/reducers/settings/api'; +import {uiFactory} from '../../uiFactory/uiFactory'; +import {useTypedSelector} from '../../utils/hooks'; import {Setting} from './Setting'; import type {YDBEmbeddedUISettings} from './settings'; @@ -9,8 +17,39 @@ interface UserSettingsProps { } export const UserSettings = ({settings: userSettings}: UserSettingsProps) => { + const settingsNames = React.useMemo(() => { + const names: string[] = []; + + userSettings.forEach((page) => { + page.sections.forEach((section) => { + section.settings.forEach((setting) => { + if (setting.type !== 'info') { + names.push(setting.settingKey); + } + }); + }); + }); + + return names; + }, [userSettings]); + + const authUserSID = useTypedSelector(selectUser); + const anonymosUserId = useTypedSelector(selectID); + + const user = authUserSID || anonymosUserId; + const shouldUseMetaSettings = uiFactory.useMetaSettings && user; + + const params = shouldUseMetaSettings + ? { + name: settingsNames, + user, + } + : skipToken; + + const {isLoading} = settingsApi.useGetSettingsQuery(params); + return ( - + {userSettings.map((page) => { const {id, title, icon, sections = [], hideTitle} = page; diff --git a/src/containers/UserSettings/settings.tsx b/src/containers/UserSettings/settings.tsx index adf5933481..89da177389 100644 --- a/src/containers/UserSettings/settings.tsx +++ b/src/containers/UserSettings/settings.tsx @@ -3,7 +3,7 @@ import type {IconProps} from '@gravity-ui/uikit'; import {createNextState} from '@reduxjs/toolkit'; import {codeAssistBackend} from '../../store'; -import {DEFAULT_USER_SETTINGS, SETTING_KEYS} from '../../store/reducers/settings/constants'; +import {SETTING_KEYS} from '../../store/reducers/settings/constants'; import {AclSyntax, OLD_BACKEND_CLUSTER_NAMES, PAGE_IDS, SECTION_IDS} from '../../utils/constants'; import {Lang} from '../../utils/i18n'; @@ -66,7 +66,6 @@ export const languageSetting: SettingProps = { title: i18n('settings.language.title'), type: 'radio', options: languageOptions, - defaultValue: DEFAULT_USER_SETTINGS[SETTING_KEYS.LANGUAGE], onValueUpdate: () => { window.location.reload(); }, diff --git a/src/lib.ts b/src/lib.ts index c27b38492e..35cf1d650a 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -10,12 +10,12 @@ export {configureStore, rootReducer} from './store'; export {default as appRoutes} from './routes'; export {YdbEmbeddedAPI} from './services/api'; -export {settingsManager} from './services/settings'; export {getUserSettings} from './containers/UserSettings/settings'; export {setSettingValue, getSettingValue} from './store/reducers/settings/settings'; export {componentsRegistry} from './components/ComponentsProvider/componentsRegistry'; -export {useSetting, useTypedSelector} from './utils/hooks'; +export {useSetting} from './store/reducers/settings/useSetting'; +export {useTypedSelector} from './utils/hooks'; export {getMonitoringLink, getMonitoringClusterLink, parseMonitoringData} from './utils/monitoring'; export {i18n, Lang, registerKeysets} from './utils/i18n'; export {toaster} from './utils/createToast'; diff --git a/src/services/api/index.ts b/src/services/api/index.ts index 37dd66fcea..9ee0083ea7 100644 --- a/src/services/api/index.ts +++ b/src/services/api/index.ts @@ -51,9 +51,11 @@ export class YdbEmbeddedAPI { withCredentials = false, singleClusterMode = true, proxyMeta = false, + // proxyMeta = true, csrfTokenGetter = () => undefined, defaults = {}, useRelativePath = false, + // useRelativePath = true, }: YdbEmbeddedAPIProps) { const axiosParams: AxiosWrapperOptions = {config: {withCredentials, ...defaults}}; const baseApiParams = {singleClusterMode, proxyMeta, useRelativePath}; 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/configureStore.ts b/src/store/configureStore.ts index c29d078f3e..b72d1d10a0 100644 --- a/src/store/configureStore.ts +++ b/src/store/configureStore.ts @@ -9,7 +9,6 @@ import {YdbEmbeddedAPI} from '../services/api'; import {getUrlData} from './getUrlData'; import rootReducer from './reducers'; import {api as storeApi} from './reducers/api'; -import {syncUserSettingsFromLS} from './reducers/settings/settings'; import {UPDATE_REF} from './reducers/tooltip'; import getLocationMiddleware from './state-url-mapping'; @@ -45,8 +44,6 @@ function _configureStore< }).concat(locationMiddleware, ...middleware), }); - syncUserSettingsFromLS(store); - return store; } diff --git a/src/store/reducers/authentication/authentication.ts b/src/store/reducers/authentication/authentication.ts index f8b1ecd9a7..d2d083a85a 100644 --- a/src/store/reducers/authentication/authentication.ts +++ b/src/store/reducers/authentication/authentication.ts @@ -33,6 +33,11 @@ export const slice = createSlice({ state.user = AuthType === 'Login' ? UserSID : undefined; state.id = UserID; + // TODO: test, remove after + // state.user = 'root'; + // state.id = 'root'; + // TODO: test, remove after + // If ydb version supports this feature, // There should be explicit flag in whoami response // Otherwise every user is allowed to make changes diff --git a/src/store/reducers/query/__test__/tabPersistence.test.tsx b/src/store/reducers/query/__test__/tabPersistence.test.tsx index d04a1f3026..b8bb2829e6 100644 --- a/src/store/reducers/query/__test__/tabPersistence.test.tsx +++ b/src/store/reducers/query/__test__/tabPersistence.test.tsx @@ -4,10 +4,6 @@ import type {QueryState} from '../types'; describe('QueryResultViewer tab persistence integration', () => { const initialState: QueryState = { input: '', - history: { - queries: [], - currentIndex: -1, - }, }; test('should save and retrieve tab selection for explain queries', () => { diff --git a/src/store/reducers/query/query.ts b/src/store/reducers/query/query.ts index 6895c4a423..99dfed1dde 100644 --- a/src/store/reducers/query/query.ts +++ b/src/store/reducers/query/query.ts @@ -1,7 +1,6 @@ import {createSelector, createSlice} from '@reduxjs/toolkit'; import type {PayloadAction} from '@reduxjs/toolkit'; -import {settingsManager} from '../../../services/settings'; import {TracingLevelNumber} from '../../../types/api/query'; import type {QueryAction, QueryRequestParams, QuerySettings} from '../../../types/store/query'; import type {StreamDataChunk} from '../../../types/store/streaming'; @@ -11,7 +10,6 @@ import {isQueryErrorResponse} from '../../../utils/query'; import {isNumeric} from '../../../utils/utils'; import type {RootState} from '../../defaultStore'; import {api} from '../api'; -import {SETTING_KEYS} from '../settings/constants'; import {prepareQueryData} from './prepareQueryData'; import { @@ -19,17 +17,8 @@ import { setStreamQueryResponse as setStreamQueryResponseReducer, setStreamSession as setStreamSessionReducer, } from './streamingReducers'; -import type {QueryResult, QueryState} from './types'; -import {getActionAndSyntaxFromQueryMode, getQueryInHistory, prepareQueryWithPragmas} from './utils'; - -const MAXIMUM_QUERIES_IN_HISTORY = 20; - -const queriesHistoryInitial = settingsManager.readUserSettingsValue( - SETTING_KEYS.QUERIES_HISTORY, - [], -) as string[]; - -const sliceLimit = queriesHistoryInitial.length - MAXIMUM_QUERIES_IN_HISTORY; +import type {QueryResult, QueryState, QueryStats} from './types'; +import {getActionAndSyntaxFromQueryMode, prepareQueryWithPragmas} from './utils'; const rawQuery = loadFromSessionStorage(QUERY_EDITOR_CURRENT_QUERY_KEY); const input = typeof rawQuery === 'string' ? rawQuery : ''; @@ -39,16 +28,7 @@ const isDirty = Boolean(loadFromSessionStorage(QUERY_EDITOR_DIRTY_KEY)); const initialState: QueryState = { input, isDirty, - history: { - queries: queriesHistoryInitial - .slice(sliceLimit < 0 ? 0 : sliceLimit) - .map(getQueryInHistory), - currentIndex: - queriesHistoryInitial.length > MAXIMUM_QUERIES_IN_HISTORY - ? MAXIMUM_QUERIES_IN_HISTORY - 1 - : queriesHistoryInitial.length - 1, - filter: '', - }, + historyFilter: '', }; const slice = createSlice({ @@ -66,76 +46,11 @@ const slice = createSlice({ setQueryResult: (state, action: PayloadAction) => { state.result = action.payload; }, - saveQueryToHistory: ( - state, - action: PayloadAction<{queryText: string; queryId: string}>, - ) => { - const {queryText, queryId} = action.payload; - - const newQueries = [...state.history.queries, {queryText, queryId}].slice( - state.history.queries.length >= MAXIMUM_QUERIES_IN_HISTORY ? 1 : 0, - ); - settingsManager.setUserSettingsValue(SETTING_KEYS.QUERIES_HISTORY, newQueries); - const currentIndex = newQueries.length - 1; - - state.history = { - queries: newQueries, - currentIndex, - }; - }, - updateQueryInHistory: ( - state, - action: PayloadAction<{queryId: string; stats: QueryStats}>, - ) => { - const {queryId, stats} = action.payload; - - if (!stats) { - return; - } - - const index = state.history.queries.findIndex((item) => item.queryId === queryId); - - if (index === -1) { - return; - } - - const newQueries = [...state.history.queries]; - const {durationUs, endTime} = stats; - newQueries.splice(index, 1, { - ...state.history.queries[index], - durationUs, - endTime, - }); - - settingsManager.setUserSettingsValue(SETTING_KEYS.QUERIES_HISTORY, newQueries); - - state.history.queries = newQueries; - }, - goToPreviousQuery: (state) => { - const currentIndex = state.history.currentIndex; - if (currentIndex <= 0) { - return; - } - const newCurrentIndex = currentIndex - 1; - const query = state.history.queries[newCurrentIndex]; - state.input = query.queryText; - state.history.currentIndex = newCurrentIndex; - }, - goToNextQuery: (state) => { - const currentIndex = state.history.currentIndex; - if (currentIndex >= state.history.queries.length - 1) { - return; - } - const newCurrentIndex = currentIndex + 1; - const query = state.history.queries[newCurrentIndex]; - state.input = query.queryText; - state.history.currentIndex = newCurrentIndex; - }, setTenantPath: (state, action: PayloadAction) => { state.tenantPath = action.payload; }, setQueryHistoryFilter: (state, action: PayloadAction) => { - state.history.filter = action.payload; + state.historyFilter = action.payload; }, setResultTab: ( state, @@ -152,14 +67,13 @@ const slice = createSlice({ setStreamQueryResponse: setStreamQueryResponseReducer, }, selectors: { - selectQueriesHistoryFilter: (state) => state.history.filter || '', + selectQueriesHistoryFilter: (state) => state.historyFilter || '', selectTenantPath: (state) => state.tenantPath, selectResult: (state) => state.result, selectStartTime: (state) => state.result?.startTime, selectEndTime: (state) => state.result?.endTime, selectUserInput: (state) => state.input, selectIsDirty: (state) => state.isDirty, - selectQueriesHistoryCurrentIndex: (state) => state.history?.currentIndex, selectResultTab: (state) => state.selectedResultTab, }, }); @@ -175,27 +89,10 @@ export const selectQueryDuration = createSelector( }, ); -export const selectQueriesHistory = createSelector( - [ - (state: RootState) => state.query.history.queries, - (state: RootState) => state.query.history.filter, - ], - (queries, filter) => { - const normalizedFilter = filter?.toLowerCase(); - return normalizedFilter - ? queries.filter((item) => item.queryText.toLowerCase().includes(normalizedFilter)) - : queries; - }, -); - export default slice.reducer; export const { changeUserInput, setQueryResult, - saveQueryToHistory, - updateQueryInHistory, - goToPreviousQuery, - goToNextQuery, setTenantPath, setQueryHistoryFilter, addStreamingChunks, @@ -207,7 +104,6 @@ export const { export const { selectQueriesHistoryFilter, - selectQueriesHistoryCurrentIndex, selectTenantPath, selectResult, selectUserInput, @@ -228,11 +124,6 @@ interface SendQueryParams extends QueryRequestParams { // Stream query receives queryId from session chunk. type StreamQueryParams = Omit; -interface QueryStats { - durationUs?: string | number; - endTime?: string | number; -} - const DEFAULT_STREAM_CHUNK_SIZE = 1000; const DEFAULT_CONCURRENT_RESULTS = false; @@ -345,7 +236,7 @@ export const queryApi = api.injectEndpoints({ } }, }), - useSendQuery: build.mutation({ + useSendQuery: build.mutation<{queryStats: QueryStats; queryId: string}, SendQueryParams>({ queryFn: async ( { actionType = 'execute', @@ -355,7 +246,7 @@ export const queryApi = api.injectEndpoints({ enableTracingLevel, queryId, base64, - }: SendQueryParams, + }, {signal, dispatch, getState}, ) => { const startTime = Date.now(); @@ -421,8 +312,9 @@ export const queryApi = api.injectEndpoints({ const data = prepareQueryData(response); data.traceId = response?._meta?.traceId; + const queryStats: QueryStats = {}; + if (actionType === 'execute') { - const queryStats: QueryStats = {}; if (data.stats) { const {DurationUs, Executions: [{FinishTimeMs}] = [{}]} = data.stats; queryStats.durationUs = DurationUs; @@ -432,8 +324,6 @@ export const queryApi = api.injectEndpoints({ queryStats.durationUs = (now - timeStart) * 1000; queryStats.endTime = now; } - - dispatch(updateQueryInHistory({stats: queryStats, queryId})); } dispatch( @@ -446,7 +336,7 @@ export const queryApi = api.injectEndpoints({ endTime: Date.now(), }), ); - return {data: null}; + return {data: {queryStats, queryId}}; } catch (error) { const state = getState() as RootState; if (state.query.result?.startTime !== startTime) { diff --git a/src/store/reducers/query/types.ts b/src/store/reducers/query/types.ts index 9d753b5210..e7975ab1eb 100644 --- a/src/store/reducers/query/types.ts +++ b/src/store/reducers/query/types.ts @@ -62,14 +62,15 @@ export interface QueryState { input: string; result?: QueryResult; isDirty?: boolean; - history: { - queries: QueryInHistory[]; - currentIndex: number; - filter?: string; - }; + historyFilter?: string; tenantPath?: string; selectedResultTab?: { execute?: string; explain?: string; }; } + +export interface QueryStats { + durationUs?: string | number; + endTime?: string | number; +} diff --git a/src/store/reducers/query/useQueriesHistory.ts b/src/store/reducers/query/useQueriesHistory.ts new file mode 100644 index 0000000000..16842a3067 --- /dev/null +++ b/src/store/reducers/query/useQueriesHistory.ts @@ -0,0 +1,124 @@ +import React from 'react'; + +import {useEventHandler, useTypedDispatch, useTypedSelector} from '../../../utils/hooks'; +import {SETTING_KEYS} from '../settings/constants'; +import {useSetting} from '../settings/useSetting'; + +import {changeUserInput, selectQueriesHistoryFilter} from './query'; +import type {QueryInHistory, QueryStats} from './types'; +import {getQueryInHistory} from './utils'; + +const MAXIMUM_QUERIES_IN_HISTORY = 20; + +export function useQueriesHistory() { + const dispatch = useTypedDispatch(); + const queriesFilter = useTypedSelector(selectQueriesHistoryFilter); + + const { + value: savedHistoryQueries, + saveValue: saveHistoryQueries, + isLoading, + } = useSetting(SETTING_KEYS.QUERIES_HISTORY); + + const [historyQueries, setQueries] = React.useState([]); + const [historyCurrentIndex, setCurrentIndex] = React.useState(-1); + + React.useEffect(() => { + if (!savedHistoryQueries || savedHistoryQueries.length === 0) { + setQueries([]); + setCurrentIndex(-1); + } else { + const sliceLimit = savedHistoryQueries.length - MAXIMUM_QUERIES_IN_HISTORY; + + const preparedQueries = savedHistoryQueries + .slice(sliceLimit < 0 ? 0 : sliceLimit) + .map(getQueryInHistory); + + setQueries(preparedQueries); + setCurrentIndex(preparedQueries.length - 1); + } + }, [savedHistoryQueries]); + + const filteredHistoryQueries = React.useMemo(() => { + const normalizedFilter = queriesFilter?.toLowerCase(); + return normalizedFilter + ? historyQueries.filter((item) => + item.queryText.toLowerCase().includes(normalizedFilter), + ) + : historyQueries; + }, [historyQueries, queriesFilter]); + + // These functions are used inside Monaco editorDidMount + // They should be stable to work properly + const goToPreviousQuery = useEventHandler(() => { + setCurrentIndex((index) => { + if (historyQueries.length > 0 && index > 0) { + const newIndex = index - 1; + const query = historyQueries[newIndex]; + + if (query) { + dispatch(changeUserInput({input: query.queryText})); + return newIndex; + } + } + return index; + }); + }); + + const goToNextQuery = useEventHandler(() => { + setCurrentIndex((index) => { + if (historyQueries.length > 0 && index < historyQueries.length - 1) { + const newIndex = index + 1; + const query = historyQueries[newIndex]; + if (query) { + dispatch(changeUserInput({input: query.queryText})); + return newIndex; + } + } + + return index; + }); + }); + + const saveQueryToHistory = useEventHandler((queryText: string, queryId: string) => { + const newQueries = [...historyQueries, {queryText, queryId}].slice( + historyQueries.length >= MAXIMUM_QUERIES_IN_HISTORY ? 1 : 0, + ); + saveHistoryQueries(newQueries); + setQueries(newQueries); + // Update currentIndex to point to the newly added query + const newCurrentIndex = newQueries.length - 1; + setCurrentIndex(newCurrentIndex); + }); + + const updateQueryInHistory = useEventHandler((queryId: string, stats: QueryStats) => { + if (!stats || !historyQueries.length) { + return; + } + + const index = historyQueries.findIndex((item) => item.queryId === queryId); + + if (index !== -1) { + const newQueries = [...historyQueries]; + const {durationUs, endTime} = stats; + newQueries.splice(index, 1, { + ...historyQueries[index], + durationUs, + endTime, + }); + saveHistoryQueries(newQueries); + setQueries(newQueries); + } + }); + + return { + historyQueries, + historyCurrentIndex, + filteredHistoryQueries, + goToPreviousQuery, + goToNextQuery, + saveQueryToHistory, + updateQueryInHistory, + isLoading, + }; +} 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/settings.ts b/src/store/reducers/settings/settings.ts index 4267f8fde2..98a2e85b62 100644 --- a/src/store/reducers/settings/settings.ts +++ b/src/store/reducers/settings/settings.ts @@ -1,19 +1,18 @@ 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 {SettingKey} from './constants'; +import {DEFAULT_USER_SETTINGS, SETTINGS_OPTIONS} from './constants'; import type {SettingsState} from './types'; - -const userSettings = settingsManager.extractSettingsFromLS(DEFAULT_USER_SETTINGS); -const systemSettings = window.systemSettings || {}; +import {readSettingValueFromLS} from './utils'; export const initialState: SettingsState = { - userSettings, - systemSettings, + userSettings: {}, + systemSettings: window.systemSettings || {}, }; const settingsSlice = createSlice({ @@ -24,18 +23,36 @@ 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; +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 = DEFAULT_USER_SETTINGS[name as SettingKey] as unknown; + + // Do not load LS value from always sync values. + // In case there is no settings service + // Such setting will be loaded from LS with getSingleSetting + if (SETTINGS_OPTIONS[name]?.preventSyncWithLS) { + return defaultValue; + } + + const savedValue = readSettingValueFromLS(name); + + return savedValue; + }, +); export const setSettingValue = (name: string | undefined, value: unknown) => { return (dispatch: AppDispatch) => { 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; +} diff --git a/src/store/reducers/tenant/tenant.ts b/src/store/reducers/tenant/tenant.ts index 26347c36bf..1f1aaba8df 100644 --- a/src/store/reducers/tenant/tenant.ts +++ b/src/store/reducers/tenant/tenant.ts @@ -1,31 +1,22 @@ import {createSlice} from '@reduxjs/toolkit'; import type {PayloadAction} from '@reduxjs/toolkit'; -import {settingsManager} from '../../../services/settings'; import type {TTenantInfo} from '../../../types/api/tenant'; import {useClusterNameFromQuery} from '../../../utils/hooks/useDatabaseFromQuery'; import {api} from '../api'; import {useDatabasesAvailable} from '../capabilities/hooks'; -import {DEFAULT_USER_SETTINGS, SETTING_KEYS} from '../settings/constants'; import {prepareTenants} from '../tenants/utils'; import {TENANT_DIAGNOSTICS_TABS_IDS, TENANT_METRICS_TABS_IDS} from './constants'; -import {tenantPageSchema} from './types'; import type { TenantDiagnosticsTab, TenantMetricsTab, - TenantPage, TenantQueryTab, TenantState, TenantSummaryTab, } from './types'; -const tenantPage = tenantPageSchema - .catch(DEFAULT_USER_SETTINGS[SETTING_KEYS.TENANT_INITIAL_PAGE]) - .parse(settingsManager.readUserSettingsValue(SETTING_KEYS.TENANT_INITIAL_PAGE)); - export const initialState: TenantState = { - tenantPage, metricsTab: TENANT_METRICS_TABS_IDS.cpu, diagnosticsTab: TENANT_DIAGNOSTICS_TABS_IDS.overview, }; @@ -34,9 +25,6 @@ const slice = createSlice({ name: 'tenant', initialState, reducers: { - setTenantPage: (state, action: PayloadAction) => { - state.tenantPage = action.payload; - }, setQueryTab: (state, action: PayloadAction) => { state.queryTab = action.payload; }, @@ -56,8 +44,7 @@ const slice = createSlice({ }); export default slice.reducer; -export const {setTenantPage, setQueryTab, setDiagnosticsTab, setSummaryTab, setMetricsTab} = - slice.actions; +export const {setQueryTab, setDiagnosticsTab, setSummaryTab, setMetricsTab} = slice.actions; export const tenantApi = api.injectEndpoints({ endpoints: (builder) => ({ diff --git a/src/store/reducers/tenant/types.ts b/src/store/reducers/tenant/types.ts index 7e140abc88..2204fc95a6 100644 --- a/src/store/reducers/tenant/types.ts +++ b/src/store/reducers/tenant/types.ts @@ -19,7 +19,6 @@ export type TenantSummaryTab = ValueOf; export type TenantMetricsTab = ValueOf; export interface TenantState { - tenantPage: TenantPage; queryTab?: TenantQueryTab; diagnosticsTab: TenantDiagnosticsTab; summaryTab?: TenantSummaryTab; diff --git a/src/store/state-url-mapping.ts b/src/store/state-url-mapping.ts index df1933729a..db5ae8ba5b 100644 --- a/src/store/state-url-mapping.ts +++ b/src/store/state-url-mapping.ts @@ -32,9 +32,6 @@ export const paramSetup = { stateKey: 'heatmap.currentMetric', initialState: initialHeatmapState.currentMetric, }, - tenantPage: { - stateKey: 'tenant.tenantPage', - }, queryTab: { stateKey: 'tenant.queryTab', }, @@ -82,9 +79,6 @@ export const paramSetup = { stateKey: 'heatmap.currentMetric', initialState: initialHeatmapState.currentMetric, }, - tenantPage: { - stateKey: 'tenant.tenantPage', - }, queryTab: { stateKey: 'tenant.queryTab', }, diff --git a/src/uiFactory/uiFactory.ts b/src/uiFactory/uiFactory.ts index 521b7e4f8c..1354677608 100644 --- a/src/uiFactory/uiFactory.ts +++ b/src/uiFactory/uiFactory.ts @@ -20,6 +20,8 @@ const uiFactoryBase: UIFactory = { }, hasAccess: true, useDatabaseId: false, + // useMetaSettings: true, + useMetaSettings: false, }; export function configureUIFactory( diff --git a/src/utils/hooks/useAclSyntax.ts b/src/utils/hooks/useAclSyntax.ts index 41bca7c597..842f0b4539 100644 --- a/src/utils/hooks/useAclSyntax.ts +++ b/src/utils/hooks/useAclSyntax.ts @@ -1,10 +1,8 @@ import {SETTING_KEYS} from '../../store/reducers/settings/constants'; +import {useSetting} from '../../store/reducers/settings/useSetting'; import {AclSyntax} from '../constants'; -import {useSetting} from './useSetting'; - export function useAclSyntax(): string { - const [aclSyntax] = useSetting(SETTING_KEYS.ACL_SYNTAX); - + const {value: aclSyntax} = useSetting(SETTING_KEYS.ACL_SYNTAX); return aclSyntax ?? AclSyntax.YdbShort; } diff --git a/src/utils/hooks/useAutoRefreshInterval.ts b/src/utils/hooks/useAutoRefreshInterval.ts index aa960fb825..58750a7912 100644 --- a/src/utils/hooks/useAutoRefreshInterval.ts +++ b/src/utils/hooks/useAutoRefreshInterval.ts @@ -1,44 +1,44 @@ import React from 'react'; import {SETTING_KEYS} from '../../store/reducers/settings/constants'; - -import {useSetting} from './useSetting'; +import {useSetting} from '../../store/reducers/settings/useSetting'; const IMMEDIATE_UPDATE_INTERVAL = 1; const DISABLED_INTERVAL = 0; export function useAutoRefreshInterval(): [number, (value: number) => void] { - const [settingValue, setSettingValue] = useSetting( + const {value, saveValue: setSettingValue} = useSetting( SETTING_KEYS.AUTO_REFRESH_INTERVAL, - DISABLED_INTERVAL, ); + const intervalValue = value ?? DISABLED_INTERVAL; + const [effectiveInterval, setEffectiveInterval] = React.useState( - document.visibilityState === 'visible' ? settingValue : DISABLED_INTERVAL, + document.visibilityState === 'visible' ? intervalValue : DISABLED_INTERVAL, ); const lastHiddenTimeRef = React.useRef(null); React.useEffect(() => { - setEffectiveInterval(document.visibilityState === 'visible' ? settingValue : 0); + setEffectiveInterval(document.visibilityState === 'visible' ? intervalValue : 0); const handleVisibilityChange = () => { const isVisible = document.visibilityState === 'visible'; if (isVisible) { - // If more than settingValue milliseconds have passed since the page was hidden, + // If more than intervalValue milliseconds have passed since the page was hidden, // trigger an immediate update const shouldTriggerImmediate = lastHiddenTimeRef.current && - settingValue !== DISABLED_INTERVAL && - Date.now() - lastHiddenTimeRef.current >= settingValue; + intervalValue !== DISABLED_INTERVAL && + Date.now() - lastHiddenTimeRef.current >= intervalValue; if (shouldTriggerImmediate) { setEffectiveInterval(IMMEDIATE_UPDATE_INTERVAL); setTimeout(() => { - setEffectiveInterval(settingValue); + setEffectiveInterval(intervalValue); }, 0); } else { - setEffectiveInterval(settingValue); + setEffectiveInterval(intervalValue); } lastHiddenTimeRef.current = null; @@ -50,7 +50,7 @@ export function useAutoRefreshInterval(): [number, (value: number) => void] { document.addEventListener('visibilitychange', handleVisibilityChange); return () => document.removeEventListener('visibilitychange', handleVisibilityChange); - }, [settingValue]); + }, [intervalValue]); return [effectiveInterval, setSettingValue]; } diff --git a/src/utils/hooks/useChangedQuerySettings.ts b/src/utils/hooks/useChangedQuerySettings.ts index 486c388f0b..0de431b885 100644 --- a/src/utils/hooks/useChangedQuerySettings.ts +++ b/src/utils/hooks/useChangedQuerySettings.ts @@ -1,15 +1,15 @@ import getChangedQueryExecutionSettings from '../../containers/Tenant/Query/QueryEditorControls/utils/getChangedQueryExecutionSettings'; import getChangedQueryExecutionSettingsDescription from '../../containers/Tenant/Query/QueryEditorControls/utils/getChangedQueryExecutionSettingsDescription'; import {SETTING_KEYS} from '../../store/reducers/settings/constants'; +import {useSetting} from '../../store/reducers/settings/useSetting'; import {WEEK_IN_SECONDS} from '../constants'; import {DEFAULT_QUERY_SETTINGS} from '../query'; import {useLastQueryExecutionSettings} from './useLastQueryExecutionSettings'; import {useQueryExecutionSettings} from './useQueryExecutionSettings'; -import {useSetting} from './useSetting'; export const useChangedQuerySettings = () => { - const [bannerLastClosedTimestamp, setBannerLastClosedTimestamp] = useSetting< + const {value: bannerLastClosedTimestamp, saveValue: setBannerLastClosedTimestamp} = useSetting< number | undefined >(SETTING_KEYS.QUERY_SETTINGS_BANNER_LAST_CLOSED); const [lastQuerySettings] = useLastQueryExecutionSettings(); diff --git a/src/utils/hooks/useLastQueryExecutionSettings.ts b/src/utils/hooks/useLastQueryExecutionSettings.ts index 825eb3130d..3b7a00d576 100644 --- a/src/utils/hooks/useLastQueryExecutionSettings.ts +++ b/src/utils/hooks/useLastQueryExecutionSettings.ts @@ -1,13 +1,12 @@ import {SETTING_KEYS} from '../../store/reducers/settings/constants'; +import {useSetting} from '../../store/reducers/settings/useSetting'; import type {QuerySettings} from '../../types/store/query'; import {querySettingsValidationSchema} from '../query'; -import {useSetting} from './useSetting'; - export const useLastQueryExecutionSettings = () => { - const [lastStorageSettings, setLastSettings] = useSetting( - SETTING_KEYS.LAST_QUERY_EXECUTION_SETTINGS, - ); + const {value: lastStorageSettings, saveValue: setLastSettings} = useSetting< + QuerySettings | undefined + >(SETTING_KEYS.LAST_QUERY_EXECUTION_SETTINGS); let lastSettings: QuerySettings | undefined; try { diff --git a/src/utils/hooks/useQueryExecutionSettings.ts b/src/utils/hooks/useQueryExecutionSettings.ts index 900d960b41..f61d12997a 100644 --- a/src/utils/hooks/useQueryExecutionSettings.ts +++ b/src/utils/hooks/useQueryExecutionSettings.ts @@ -2,6 +2,7 @@ import React from 'react'; import {useTracingLevelOptionAvailable} from '../../store/reducers/capabilities/hooks'; import {SETTING_KEYS} from '../../store/reducers/settings/constants'; +import {useSetting} from '../../store/reducers/settings/useSetting'; import type {QuerySettings} from '../../types/store/query'; import { DEFAULT_QUERY_SETTINGS, @@ -11,17 +12,16 @@ import { } from '../query'; import {useQueryStreamingSetting} from './useQueryStreamingSetting'; -import {useSetting} from './useSetting'; export const useQueryExecutionSettings = () => { const enableTracingLevel = useTracingLevelOptionAvailable(); - const [storageSettings, setSettings] = useSetting( + const {value: storageSettings, saveValue: setSettings} = useSetting( SETTING_KEYS.QUERY_EXECUTION_SETTINGS, ); const validatedSettings = querySettingsRestoreSchema.parse(storageSettings); - const [useShowPlanToSvg] = useSetting(SETTING_KEYS.USE_SHOW_PLAN_SVG); - const [enableQueryStreaming] = useQueryStreamingSetting(); + const {value: useShowPlanToSvg} = useSetting(SETTING_KEYS.USE_SHOW_PLAN_SVG); + const {value: enableQueryStreaming} = useQueryStreamingSetting(); const setQueryExecutionSettings = React.useCallback( (settings: QuerySettings) => { diff --git a/src/utils/hooks/useQueryStreamingSetting.ts b/src/utils/hooks/useQueryStreamingSetting.ts index 2e1ad99a5c..20b48e56a8 100644 --- a/src/utils/hooks/useQueryStreamingSetting.ts +++ b/src/utils/hooks/useQueryStreamingSetting.ts @@ -1,10 +1,10 @@ import {SETTING_KEYS} from '../../store/reducers/settings/constants'; +import {useSetting} from '../../store/reducers/settings/useSetting'; import {OLD_BACKEND_CLUSTER_NAMES} from '../constants'; import {useClusterNameFromQuery} from './useDatabaseFromQuery'; -import {useSetting} from './useSetting'; -export const useQueryStreamingSetting = (): [boolean, (value: boolean) => void] => { +export const useQueryStreamingSetting = () => { const clusterName = useClusterNameFromQuery(); const isOldBackendCluster = clusterName && OLD_BACKEND_CLUSTER_NAMES.includes(clusterName); diff --git a/src/utils/hooks/useSelectedColumns.ts b/src/utils/hooks/useSelectedColumns.ts index b799179ff6..69c29000d6 100644 --- a/src/utils/hooks/useSelectedColumns.ts +++ b/src/utils/hooks/useSelectedColumns.ts @@ -2,7 +2,7 @@ import React from 'react'; import type {TableColumnSetupItem, TableColumnSetupProps} from '@gravity-ui/uikit'; -import {settingsManager} from '../../services/settings'; +import {useSetting} from '../../store/reducers/settings/useSetting'; type OrderedColumn = {id: string; selected?: boolean}; @@ -24,25 +24,48 @@ export const useSelectedColumns = ( defaultColumnsIds: string[], requiredColumnsIds?: string[], ) => { - const [orderedColumns, setOrderedColumns] = React.useState(() => { - const savedColumns = settingsManager.readUserSettingsValue( - storageKey, - defaultColumnsIds, - ) as unknown[]; - - const normalizedSavedColumns = savedColumns.map(parseSavedColumn); - - return columns.reduce((acc, column) => { - const savedColumn = normalizedSavedColumns.find((c) => c && c.id === column.name); - if (savedColumn) { - acc.push(savedColumn); - } else { - acc.push({id: column.name, selected: false}); + const {value: savedColumns, saveValue: saveColumns} = useSetting( + storageKey, + ); + + const normalizeSavedColumns = React.useCallback( + (columnsToUse: string[] | OrderedColumn[]) => { + const parsedSavedColumns = columnsToUse + .map(parseSavedColumn) + .filter((column): column is OrderedColumn => Boolean(column)); + + const needToNormalize = + (columnsToUse.length && typeof columnsToUse[0] === 'string') || + parsedSavedColumns.length !== columns.length; + + if (needToNormalize) { + return columns.reduce((acc, column) => { + const savedColumn = parsedSavedColumns.find((c) => c && c.id === column.name); + if (savedColumn) { + acc.push(savedColumn); + } else { + acc.push({id: column.name, selected: false}); + } + return acc; + }, []); } - return acc; - }, []); + + return parsedSavedColumns; + }, + [columns], + ); + + const [orderedColumns, setOrderedColumns] = React.useState(() => { + return normalizeSavedColumns(defaultColumnsIds); }); + React.useEffect(() => { + const rawColumns = savedColumns !== undefined ? savedColumns : defaultColumnsIds; + const normalizedColumns = normalizeSavedColumns(rawColumns); + + setOrderedColumns(normalizedColumns); + }, [savedColumns, defaultColumnsIds]); + const columnsToSelect = React.useMemo(() => { const preparedColumns = orderedColumns.reduce<(TableColumnSetupItem & {column: T})[]>( (acc, {id, selected}) => { @@ -76,10 +99,10 @@ export const useSelectedColumns = ( (value) => { const preparedColumns = value.map(({id, selected}) => ({id, selected})); - settingsManager.setUserSettingsValue(storageKey, preparedColumns); + saveColumns(preparedColumns); setOrderedColumns(preparedColumns); }, - [storageKey], + [saveColumns], ); return { diff --git a/src/utils/i18n/i18n.ts b/src/utils/i18n/i18n.ts index 8027c49617..a6a36911e5 100644 --- a/src/utils/i18n/i18n.ts +++ b/src/utils/i18n/i18n.ts @@ -7,8 +7,6 @@ enum Lang { Ru = 'ru', } -const defaultLang = Lang.En; - // Disable russian language while it is not properly supported // Force English locale for users who previously selected Russian when the setting was available // const currentLang = settingsManager.readUserSettingsValue(LANGUAGE_KEY, defaultLang) as Lang; @@ -33,4 +31,4 @@ export function registerKeysets(id: string, data: Recor return i18n.keyset(id); } -export {i18n, Lang, currentLang, defaultLang}; +export {i18n, Lang, currentLang};