From 0c2b19be24d5ca9271f7451e02e11015dd83c33d Mon Sep 17 00:00:00 2001 From: mufazalov Date: Tue, 11 Nov 2025 22:25:41 +0300 Subject: [PATCH 1/3] feat: configure meta settings, use for table columns width --- .../DiskStateProgressBar.tsx | 4 +- src/components/JsonViewer/JsonViewer.tsx | 4 +- src/components/NetworkTable/hooks.ts | 6 +- .../ResizeablePaginatedTable.tsx | 8 +- .../ResizeableDataTable.tsx | 5 +- src/containers/App/Providers.tsx | 4 +- .../utils/useAdditionalTenantsProps.tsx | 6 +- .../AsideNavigation/AsideNavigation.tsx | 4 +- .../InformationPopup/InformationPopup.tsx | 7 +- .../ClusterOverview/ClusterOverview.tsx | 6 +- .../components/ClusterMetricsNetwork.tsx | 4 +- .../Tenant/Diagnostics/HotKeys/HotKeys.tsx | 5 +- .../MetricsTabs/MetricsTabs.tsx | 4 +- .../TenantNetwork/TenantNetwork.tsx | 4 +- .../Tenant/Query/QueryEditor/QueryEditor.tsx | 4 +- .../Tenant/Query/QueryEditor/YqlEditor.tsx | 6 +- .../Tenant/Query/QueryEditor/helpers.ts | 6 +- .../Query/QueryResult/QueryResultViewer.tsx | 4 +- .../QuerySettingsDialog.tsx | 4 +- .../QueryStoppedBanner/QueryStoppedBanner.tsx | 4 +- .../Tenant/Query/utils/useSavedQueries.tsx | 4 +- .../TenantNavigation/useTenantNavigation.tsx | 4 +- src/containers/Tenants/Tenants.tsx | 12 +- src/containers/UserSettings/settings.tsx | 60 +++----- src/lib.ts | 2 +- src/services/api/baseMeta.ts | 23 +++ src/services/api/index.ts | 9 +- src/services/api/meta.ts | 24 +-- src/services/api/metaSettings.ts | 99 ++++++++++++ src/services/api/streaming.ts | 8 +- src/services/api/viewer.ts | 4 +- src/services/settings.ts | 68 +-------- .../reducers/authentication/authentication.ts | 13 +- src/store/reducers/authentication/types.ts | 2 + src/store/reducers/query/query.ts | 13 +- .../reducers/queryActions/queryActions.ts | 15 +- src/store/reducers/settings/api.ts | 109 +++++++++++++ src/store/reducers/settings/constants.ts | 86 +++++++++++ src/store/reducers/settings/settings.ts | 19 ++- src/store/reducers/settings/types.ts | 9 +- src/store/reducers/settings/useSetting.ts | 143 ++++++++++++++++++ src/store/reducers/settings/utils.ts | 43 ++++++ src/store/reducers/tenant/tenant.ts | 8 +- src/types/api/settings.ts | 24 +++ src/types/api/whoami.ts | 2 + src/types/window.d.ts | 2 +- src/uiFactory/types.ts | 1 + src/utils/constants.ts | 82 ---------- src/utils/hooks/useAclSyntax.ts | 10 +- src/utils/hooks/useAutoRefreshInterval.ts | 7 +- src/utils/hooks/useChangedQuerySettings.ts | 5 +- .../hooks/useLastQueryExecutionSettings.ts | 4 +- src/utils/hooks/useQueryExecutionSettings.ts | 8 +- src/utils/hooks/useQueryStreamingSetting.ts | 11 +- src/utils/hooks/useSetting.ts | 2 + src/utils/hooks/useTableResize.ts | 53 ++++--- 56 files changed, 742 insertions(+), 345 deletions(-) create mode 100644 src/services/api/baseMeta.ts create mode 100644 src/services/api/metaSettings.ts create mode 100644 src/store/reducers/settings/api.ts create mode 100644 src/store/reducers/settings/constants.ts create mode 100644 src/store/reducers/settings/useSetting.ts create mode 100644 src/store/reducers/settings/utils.ts create mode 100644 src/types/api/settings.ts diff --git a/src/components/DiskStateProgressBar/DiskStateProgressBar.tsx b/src/components/DiskStateProgressBar/DiskStateProgressBar.tsx index a5e806de34..e5e4252734 100644 --- a/src/components/DiskStateProgressBar/DiskStateProgressBar.tsx +++ b/src/components/DiskStateProgressBar/DiskStateProgressBar.tsx @@ -1,7 +1,7 @@ import React from 'react'; +import {SETTING_KEYS} from '../../store/reducers/settings/constants'; import {cn} from '../../utils/cn'; -import {INVERTED_DISKS_KEY} from '../../utils/constants'; import {getSeverityColor} from '../../utils/disks/helpers'; import {useSetting} from '../../utils/hooks'; @@ -30,7 +30,7 @@ export function DiskStateProgressBar({ content, className, }: DiskStateProgressBarProps) { - const [inverted] = useSetting(INVERTED_DISKS_KEY); + const [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 e30e5b863e..4a1518744b 100644 --- a/src/components/JsonViewer/JsonViewer.tsx +++ b/src/components/JsonViewer/JsonViewer.tsx @@ -5,7 +5,7 @@ import type * as DT100 from '@gravity-ui/react-data-table'; import DataTable from '@gravity-ui/react-data-table'; import {ActionTooltip, Button, Flex, Icon} from '@gravity-ui/uikit'; -import {CASE_SENSITIVE_JSON_SEARCH} from '../../utils/constants'; +import {SETTING_KEYS} from '../../store/reducers/settings/constants'; import {useSetting} from '../../utils/hooks'; import type {ClipboardButtonProps} from '../ClipboardButton/ClipboardButton'; import {ClipboardButton} from '../ClipboardButton/ClipboardButton'; @@ -122,7 +122,7 @@ function JsonViewerComponent({ withClipboardButton, }: JsonViewerComponentProps) { const [caseSensitiveSearch, setCaseSensitiveSearch] = useSetting( - CASE_SENSITIVE_JSON_SEARCH, + SETTING_KEYS.CASE_SENSITIVE_JSON_SEARCH, false, ); diff --git a/src/components/NetworkTable/hooks.ts b/src/components/NetworkTable/hooks.ts index d961de340c..0005d2b581 100644 --- a/src/components/NetworkTable/hooks.ts +++ b/src/components/NetworkTable/hooks.ts @@ -2,19 +2,19 @@ import { useNodesHandlerHasWorkingClusterNetworkStats, useViewerNodesHandlerHasNetworkStats, } from '../../store/reducers/capabilities/hooks'; -import {ENABLE_NETWORK_TABLE_KEY} from '../../utils/constants'; +import {SETTING_KEYS} from '../../store/reducers/settings/constants'; import {useSetting} from '../../utils/hooks'; export function useShouldShowDatabaseNetworkTable() { const viewerNodesHasNetworkStats = useViewerNodesHandlerHasNetworkStats(); - const [networkTableEnabled] = useSetting(ENABLE_NETWORK_TABLE_KEY); + const [networkTableEnabled] = useSetting(SETTING_KEYS.ENABLE_NETWORK_TABLE); return Boolean(viewerNodesHasNetworkStats && networkTableEnabled); } export function useShouldShowClusterNetworkTable() { const nodesHasWorkingClusterNetworkStats = useNodesHandlerHasWorkingClusterNetworkStats(); - const [networkTableEnabled] = useSetting(ENABLE_NETWORK_TABLE_KEY); + const [networkTableEnabled] = useSetting(SETTING_KEYS.ENABLE_NETWORK_TABLE); return Boolean(nodesHasWorkingClusterNetworkStats && networkTableEnabled); } diff --git a/src/components/PaginatedTable/ResizeablePaginatedTable.tsx b/src/components/PaginatedTable/ResizeablePaginatedTable.tsx index b0e082ea03..2e92008e85 100644 --- a/src/components/PaginatedTable/ResizeablePaginatedTable.tsx +++ b/src/components/PaginatedTable/ResizeablePaginatedTable.tsx @@ -1,6 +1,7 @@ import type {ColumnWidthByName} from '@gravity-ui/react-data-table'; import {useTableResize} from '../../utils/hooks/useTableResize'; +import {TableSkeleton} from '../TableSkeleton/TableSkeleton'; import type {PaginatedTableProps} from './PaginatedTable'; import {PaginatedTable} from './PaginatedTable'; @@ -23,10 +24,15 @@ export function ResizeablePaginatedTable({ columns, ...props }: ResizeablePaginatedTableProps) { - const [tableColumnsWidth, setTableColumnsWidth] = useTableResize(columnsWidthLSKey); + const [tableColumnsWidth, setTableColumnsWidth, isTableWidthLoading] = + useTableResize(columnsWidthLSKey); const updatedColumns = updateColumnsWidth(columns, tableColumnsWidth); + if (isTableWidthLoading) { + return ; + } + return ( ({ data, ...props }: ResizeableDataTableProps) { - const [tableColumnsWidth, setTableColumnsWidth] = useTableResize(columnsWidthLSKey); + const [tableColumnsWidth, setTableColumnsWidth, isTableWidthLoading] = + useTableResize(columnsWidthLSKey); const handleSort = React.useCallback( (params: SortOrder | SortOrder[] | undefined) => { @@ -82,7 +83,7 @@ export function ResizeableDataTable({ }; }, [settings]); - if (isLoading) { + if (isLoading || isTableWidthLoading) { return ; } diff --git a/src/containers/App/Providers.tsx b/src/containers/App/Providers.tsx index 3ad9a7b4b7..f2c8b32591 100644 --- a/src/containers/App/Providers.tsx +++ b/src/containers/App/Providers.tsx @@ -13,7 +13,7 @@ import {ReactRouter5Adapter} from 'use-query-params/adapters/react-router-5'; import {ComponentsProvider} from '../../components/ComponentsProvider/ComponentsProvider'; import {componentsRegistry as defaultComponentsRegistry} from '../../components/ComponentsProvider/componentsRegistry'; import type {ComponentsRegistry} from '../../components/ComponentsProvider/componentsRegistry'; -import {THEME_KEY} from '../../utils/constants'; +import {SETTING_KEYS} from '../../store/reducers/settings/constants'; import {toaster} from '../../utils/createToast'; import {useSetting} from '../../utils/hooks'; @@ -57,7 +57,7 @@ export function Providers({ } function Theme({children}: {children: React.ReactNode}) { - const [theme] = useSetting(THEME_KEY); + const [theme] = useSetting(SETTING_KEYS.THEME); return {children}; } diff --git a/src/containers/AppWithClusters/utils/useAdditionalTenantsProps.tsx b/src/containers/AppWithClusters/utils/useAdditionalTenantsProps.tsx index 158dc10440..5a5f45c92f 100644 --- a/src/containers/AppWithClusters/utils/useAdditionalTenantsProps.tsx +++ b/src/containers/AppWithClusters/utils/useAdditionalTenantsProps.tsx @@ -1,11 +1,11 @@ import {isNil} from 'lodash'; import {useClusterBaseInfo} from '../../../store/reducers/cluster/cluster'; +import {SETTING_KEYS} from '../../../store/reducers/settings/constants'; 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 {USE_CLUSTER_BALANCER_AS_BACKEND_KEY} from '../../../utils/constants'; import {useSetting} from '../../../utils/hooks'; import type {GetLogsLink} from '../../../utils/logs'; import type {GetMonitoringLink} from '../../../utils/monitoring'; @@ -23,7 +23,9 @@ export function useAdditionalTenantsProps({ getDatabaseLinks, }: GetAdditionalTenantsProps) { const clusterInfo = useClusterBaseInfo(); - const [useClusterBalancerAsBackend] = useSetting(USE_CLUSTER_BALANCER_AS_BACKEND_KEY); + const [useClusterBalancerAsBackend] = useSetting( + SETTING_KEYS.USE_CLUSTER_BALANCER_AS_BACKEND, + ); const {balancer, monitoring, logging, name: clusterName} = clusterInfo; diff --git a/src/containers/AsideNavigation/AsideNavigation.tsx b/src/containers/AsideNavigation/AsideNavigation.tsx index 6d7e95760c..18d14e88d7 100644 --- a/src/containers/AsideNavigation/AsideNavigation.tsx +++ b/src/containers/AsideNavigation/AsideNavigation.tsx @@ -6,8 +6,8 @@ import {AsideHeader, FooterItem} from '@gravity-ui/navigation'; import type {IconData} from '@gravity-ui/uikit'; import {useHistory} from 'react-router-dom'; +import {SETTING_KEYS} from '../../store/reducers/settings/constants'; import {cn} from '../../utils/cn'; -import {ASIDE_HEADER_COMPACT_KEY} from '../../utils/constants'; import {useSetting} from '../../utils/hooks'; import {InformationPopup} from './InformationPopup'; @@ -76,7 +76,7 @@ export function AsideNavigation(props: AsideNavigationProps) { const [visiblePanel, setVisiblePanel] = React.useState(); const [informationPopupVisible, setInformationPopupVisible] = React.useState(false); - const [compact, setIsCompact] = useSetting(ASIDE_HEADER_COMPACT_KEY); + const [compact, setIsCompact] = useSetting(SETTING_KEYS.ASIDE_HEADER_COMPACT); const toggleInformationPopup = () => setInformationPopupVisible((prev) => !prev); diff --git a/src/containers/AsideNavigation/InformationPopup/InformationPopup.tsx b/src/containers/AsideNavigation/InformationPopup/InformationPopup.tsx index e390d62214..ccef8bbe32 100644 --- a/src/containers/AsideNavigation/InformationPopup/InformationPopup.tsx +++ b/src/containers/AsideNavigation/InformationPopup/InformationPopup.tsx @@ -2,8 +2,8 @@ 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 {LANGUAGE_KEY} from '../../../utils/constants'; import {SHORTCUTS_HOTKEY} from '../hooks/useHotkeysPanel'; import i18n from '../i18n'; @@ -17,7 +17,10 @@ export interface InformationPopupProps { export function InformationPopup({onKeyboardShortcutsClick}: InformationPopupProps) { const getDocumentationLink = () => { - const lang = settingsManager.readUserSettingsValue(LANGUAGE_KEY, navigator.language); + const lang = settingsManager.readUserSettingsValue( + SETTING_KEYS.LANGUAGE, + navigator.language, + ); return lang === 'ru' ? 'https://ydb.tech/docs/ru/' : 'https://ydb.tech/docs/en/'; }; diff --git a/src/containers/Cluster/ClusterOverview/ClusterOverview.tsx b/src/containers/Cluster/ClusterOverview/ClusterOverview.tsx index 9a9a24dfde..20d31e52bd 100644 --- a/src/containers/Cluster/ClusterOverview/ClusterOverview.tsx +++ b/src/containers/Cluster/ClusterOverview/ClusterOverview.tsx @@ -8,12 +8,12 @@ import { useClusterDashboardAvailable, } from '../../../store/reducers/capabilities/hooks'; import type {ClusterGroupsStats} from '../../../store/reducers/cluster/types'; +import {SETTING_KEYS} from '../../../store/reducers/settings/constants'; 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 {EXPAND_CLUSTER_DASHBOARD} from '../../../utils/constants'; import {useSetting} from '../../../utils/hooks/useSetting'; import {ClusterInfo} from '../ClusterInfo/ClusterInfo'; import i18n from '../i18n'; @@ -40,7 +40,9 @@ interface ClusterOverviewProps { } export function ClusterOverview(props: ClusterOverviewProps) { - const [expandDashboard, setExpandDashboard] = useSetting(EXPAND_CLUSTER_DASHBOARD); + const [expandDashboard, setExpandDashboard] = useSetting( + SETTING_KEYS.EXPAND_CLUSTER_DASHBOARD, + ); const bridgeModeEnabled = useBridgeModeEnabled(); const bridgePiles = React.useMemo(() => { diff --git a/src/containers/Cluster/ClusterOverview/components/ClusterMetricsNetwork.tsx b/src/containers/Cluster/ClusterOverview/components/ClusterMetricsNetwork.tsx index ab61aab06d..3f06f99445 100644 --- a/src/containers/Cluster/ClusterOverview/components/ClusterMetricsNetwork.tsx +++ b/src/containers/Cluster/ClusterOverview/components/ClusterMetricsNetwork.tsx @@ -1,6 +1,6 @@ import {DoughnutMetrics} from '../../../../components/DoughnutMetrics/DoughnutMetrics'; +import {SETTING_KEYS} from '../../../../store/reducers/settings/constants'; import {formatBytes} from '../../../../utils/bytesParsers'; -import {SHOW_NETWORK_UTILIZATION} from '../../../../utils/constants'; import {useSetting} from '../../../../utils/hooks/useSetting'; import i18n from '../../i18n'; import type {ClusterMetricsBaseProps} from '../shared'; @@ -23,7 +23,7 @@ export function ClusterMetricsNetwork({ collapsed, ...rest }: ClusterMetricsNetworkProps) { - const [showNetworkUtilization] = useSetting(SHOW_NETWORK_UTILIZATION); + const [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 2bbaf89e40..5bfa29ea1e 100644 --- a/src/containers/Tenant/Diagnostics/HotKeys/HotKeys.tsx +++ b/src/containers/Tenant/Diagnostics/HotKeys/HotKeys.tsx @@ -9,9 +9,10 @@ import {ResponseError} from '../../../../components/Errors/ResponseError'; import {ResizeableDataTable} from '../../../../components/ResizeableDataTable/ResizeableDataTable'; import {hotKeysApi} from '../../../../store/reducers/hotKeys/hotKeys'; import {overviewApi} from '../../../../store/reducers/overview/overview'; +import {SETTING_KEYS} from '../../../../store/reducers/settings/constants'; import type {HotKey} from '../../../../types/api/hotkeys'; import {cn} from '../../../../utils/cn'; -import {DEFAULT_TABLE_SETTINGS, IS_HOTKEYS_HELP_HIDDEN_KEY} from '../../../../utils/constants'; +import {DEFAULT_TABLE_SETTINGS} from '../../../../utils/constants'; import {useAutoRefreshInterval, useSetting} from '../../../../utils/hooks'; import i18n from './i18n'; @@ -120,7 +121,7 @@ export function HotKeys({path, database, databaseFullPath}: HotKeysProps) { } function HelpCard() { - const [helpHidden, setHelpHidden] = useSetting(IS_HOTKEYS_HELP_HIDDEN_KEY); + const [helpHidden, 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 f39b8031d6..3a6d15720b 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/MetricsTabs/MetricsTabs.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/MetricsTabs/MetricsTabs.tsx @@ -4,6 +4,7 @@ import {Flex} from '@gravity-ui/uikit'; import {useLocation} from 'react-router-dom'; import {getTenantPath, parseQuery} from '../../../../../routes'; +import {SETTING_KEYS} from '../../../../../store/reducers/settings/constants'; import {TENANT_METRICS_TABS_IDS} from '../../../../../store/reducers/tenant/constants'; import type {TenantMetricsTab} from '../../../../../store/reducers/tenant/types'; import type { @@ -13,7 +14,6 @@ import type { } from '../../../../../store/reducers/tenants/utils'; import type {ETenantType} from '../../../../../types/api/tenant'; import {cn} from '../../../../../utils/cn'; -import {SHOW_NETWORK_UTILIZATION} from '../../../../../utils/constants'; import {useSetting} from '../../../../../utils/hooks'; import {calculateMetricAggregates} from '../../../../../utils/metrics'; // no direct legend formatters needed here – handled in subcomponents @@ -99,7 +99,7 @@ export function MetricsTabs({ ); // Pass raw network values; DedicatedMetricsTabs computes percent and legend - const [showNetworkUtilization] = useSetting(SHOW_NETWORK_UTILIZATION); + const [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 9767aebbdb..f4522a16d9 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantNetwork/TenantNetwork.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantNetwork/TenantNetwork.tsx @@ -1,9 +1,9 @@ import {Flex} from '@gravity-ui/uikit'; import {getTenantPath} from '../../../../../routes'; +import {SETTING_KEYS} from '../../../../../store/reducers/settings/constants'; import {TENANT_DIAGNOSTICS_TABS_IDS} from '../../../../../store/reducers/tenant/constants'; import {cn} from '../../../../../utils/cn'; -import {ENABLE_NETWORK_TABLE_KEY} from '../../../../../utils/constants'; import {useSearchQuery, useSetting} from '../../../../../utils/hooks'; import {TenantTabsGroups} from '../../../TenantPages'; import {StatsWrapper} from '../StatsWrapper/StatsWrapper'; @@ -22,7 +22,7 @@ interface TenantNetworkProps { export function TenantNetwork({database}: TenantNetworkProps) { const query = useSearchQuery(); - const [networkTableEnabled] = useSetting(ENABLE_NETWORK_TABLE_KEY); + const [networkTableEnabled] = useSetting(SETTING_KEYS.ENABLE_NETWORK_TABLE); const tab = networkTableEnabled ? {[TenantTabsGroups.diagnosticsTab]: TENANT_DIAGNOSTICS_TABS_IDS.network} diff --git a/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx b/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx index dcf8f187e2..46a7552e0f 100644 --- a/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx +++ b/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx @@ -22,13 +22,13 @@ import { import type {QueryResult} from '../../../../store/reducers/query/types'; import {setQueryAction} from '../../../../store/reducers/queryActions/queryActions'; import {selectShowPreview, setShowPreview} from '../../../../store/reducers/schema/schema'; +import {SETTING_KEYS} from '../../../../store/reducers/settings/constants'; import type {EPathSubType, EPathType} from '../../../../types/api/schema'; import type {QueryAction} from '../../../../types/store/query'; import {cn} from '../../../../utils/cn'; import { DEFAULT_IS_QUERY_RESULT_COLLAPSED, DEFAULT_SIZE_RESULT_PANE_KEY, - LAST_USED_QUERY_ACTION_KEY, } from '../../../../utils/constants'; import { useEventHandler, @@ -90,7 +90,7 @@ export default function QueryEditor(props: QueryEditorProps) { const {resetBanner} = useChangedQuerySettings(); const [lastUsedQueryAction, setLastUsedQueryAction] = useSetting( - LAST_USED_QUERY_ACTION_KEY, + SETTING_KEYS.LAST_USED_QUERY_ACTION, ); const [lastExecutedQueryText, setLastExecutedQueryText] = React.useState(''); const [isQueryStreamingEnabled] = useQueryStreamingSetting(); diff --git a/src/containers/Tenant/Query/QueryEditor/YqlEditor.tsx b/src/containers/Tenant/Query/QueryEditor/YqlEditor.tsx index 8de2ba1b54..2606f2b863 100644 --- a/src/containers/Tenant/Query/QueryEditor/YqlEditor.tsx +++ b/src/containers/Tenant/Query/QueryEditor/YqlEditor.tsx @@ -13,8 +13,8 @@ import { selectUserInput, setIsDirty, } from '../../../../store/reducers/query/query'; +import {SETTING_KEYS} from '../../../../store/reducers/settings/constants'; import type {QueryAction} from '../../../../types/store/query'; -import {ENABLE_CODE_ASSISTANT, LAST_USED_QUERY_ACTION_KEY} from '../../../../utils/constants'; import { useEventHandler, useSetting, @@ -50,12 +50,12 @@ export function YqlEditor({ const [monacoGhostInstance, setMonacoGhostInstance] = React.useState>(); const historyQueries = useTypedSelector(selectQueriesHistory); - const [isCodeAssistEnabled] = useSetting(ENABLE_CODE_ASSISTANT); + const [isCodeAssistEnabled] = useSetting(SETTING_KEYS.ENABLE_CODE_ASSISTANT); const editorOptions = useEditorOptions(); const updateErrorsHighlighting = useUpdateErrorsHighlighting(); - const [lastUsedQueryAction] = useSetting(LAST_USED_QUERY_ACTION_KEY); + const [lastUsedQueryAction] = useSetting(SETTING_KEYS.LAST_USED_QUERY_ACTION); const getLastQueryText = useEventHandler(() => { if (!historyQueries || historyQueries.length === 0) { diff --git a/src/containers/Tenant/Query/QueryEditor/helpers.ts b/src/containers/Tenant/Query/QueryEditor/helpers.ts index 22af5639a9..53c7f9d734 100644 --- a/src/containers/Tenant/Query/QueryEditor/helpers.ts +++ b/src/containers/Tenant/Query/QueryEditor/helpers.ts @@ -5,8 +5,8 @@ import type Monaco from 'monaco-editor'; import {codeAssistApi} from '../../../../store/reducers/codeAssist/codeAssist'; import {selectQueriesHistory} from '../../../../store/reducers/query/query'; +import {SETTING_KEYS} from '../../../../store/reducers/settings/constants'; import type {TelemetryOpenTabs} from '../../../../types/api/codeAssist'; -import {AUTOCOMPLETE_ON_ENTER, ENABLE_AUTOCOMPLETE} from '../../../../utils/constants'; import {useSetting, useTypedSelector} from '../../../../utils/hooks'; import {YQL_LANGUAGE_ID} from '../../../../utils/monaco/constats'; import {useSavedQueries} from '../utils/useSavedQueries'; @@ -23,8 +23,8 @@ const EDITOR_OPTIONS: EditorOptions = { }; export function useEditorOptions() { - const [enableAutocomplete] = useSetting(ENABLE_AUTOCOMPLETE); - const [autocompleteOnEnter] = useSetting(AUTOCOMPLETE_ON_ENTER); + const [enableAutocomplete] = useSetting(SETTING_KEYS.ENABLE_AUTOCOMPLETE); + const [autocompleteOnEnter] = useSetting(SETTING_KEYS.AUTOCOMPLETE_ON_ENTER); const options = React.useMemo(() => { const useAutocomplete = Boolean(enableAutocomplete); diff --git a/src/containers/Tenant/Query/QueryResult/QueryResultViewer.tsx b/src/containers/Tenant/Query/QueryResult/QueryResultViewer.tsx index 6ee609b68d..4c0d99d5fd 100644 --- a/src/containers/Tenant/Query/QueryResult/QueryResultViewer.tsx +++ b/src/containers/Tenant/Query/QueryResult/QueryResultViewer.tsx @@ -13,10 +13,10 @@ import {QueryExecutionStatus} from '../../../../components/QueryExecutionStatus' import {disableFullscreen} from '../../../../store/reducers/fullscreen'; import {selectResultTab, setResultTab} from '../../../../store/reducers/query/query'; import type {QueryResult} from '../../../../store/reducers/query/types'; +import {SETTING_KEYS} from '../../../../store/reducers/settings/constants'; import type {ValueOf} from '../../../../types/common'; import type {QueryAction} from '../../../../types/store/query'; import {cn} from '../../../../utils/cn'; -import {USE_SHOW_PLAN_SVG_KEY} from '../../../../utils/constants'; import {getStringifiedData} from '../../../../utils/dataFormatters/dataFormatters'; import {useSetting, useTypedDispatch, useTypedSelector} from '../../../../utils/hooks'; import {PaneVisibilityToggleButtons} from '../../utils/paneVisibilityToggleHelpers'; @@ -106,7 +106,7 @@ export function QueryResultViewer({ const isExplain = resultType === 'explain'; const [selectedResultSet, setSelectedResultSet] = React.useState(0); - const [useShowPlanToSvg] = useSetting(USE_SHOW_PLAN_SVG_KEY); + const [useShowPlanToSvg] = useSetting(SETTING_KEYS.USE_SHOW_PLAN_SVG); // Get the saved tab for the current query type, or use default const getDefaultSection = (): SectionID => { diff --git a/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsDialog.tsx b/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsDialog.tsx index 88009ca53f..b37edde230 100644 --- a/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsDialog.tsx +++ b/src/containers/Tenant/Query/QuerySettingsDialog/QuerySettingsDialog.tsx @@ -9,9 +9,9 @@ import { selectQueryAction, setQueryAction, } from '../../../../store/reducers/queryActions/queryActions'; +import {SETTING_KEYS} from '../../../../store/reducers/settings/constants'; import type {QuerySettings} from '../../../../types/store/query'; import {cn} from '../../../../utils/cn'; -import {USE_SHOW_PLAN_SVG_KEY} from '../../../../utils/constants'; import { useQueryExecutionSettings, useSetting, @@ -82,7 +82,7 @@ function QuerySettingsForm({initialValues, onSubmit, onClose}: QuerySettingsForm resolver: zodResolver(querySettingsValidationSchema), }); - const [useShowPlanToSvg] = useSetting(USE_SHOW_PLAN_SVG_KEY); + const [useShowPlanToSvg] = useSetting(SETTING_KEYS.USE_SHOW_PLAN_SVG); const enableTracingLevel = useTracingLevelOptionAvailable(); const timeout = watch('timeout'); diff --git a/src/containers/Tenant/Query/QueryStoppedBanner/QueryStoppedBanner.tsx b/src/containers/Tenant/Query/QueryStoppedBanner/QueryStoppedBanner.tsx index 38dea50e1f..d19ef94d66 100644 --- a/src/containers/Tenant/Query/QueryStoppedBanner/QueryStoppedBanner.tsx +++ b/src/containers/Tenant/Query/QueryStoppedBanner/QueryStoppedBanner.tsx @@ -2,8 +2,8 @@ import React from 'react'; import {Alert} from '@gravity-ui/uikit'; +import {SETTING_KEYS} from '../../../../store/reducers/settings/constants'; import {cn} from '../../../../utils/cn'; -import {QUERY_STOPPED_BANNER_CLOSED_KEY} from '../../../../utils/constants'; import {useSetting} from '../../../../utils/hooks'; import i18n from '../i18n'; const b = cn('ydb-query-stopped-banner'); @@ -12,7 +12,7 @@ import './QueryStoppedBanner.scss'; export function QueryStoppedBanner() { const [isQueryStoppedBannerClosed, setIsQueryStoppedBannerClosed] = useSetting( - QUERY_STOPPED_BANNER_CLOSED_KEY, + SETTING_KEYS.QUERY_STOPPED_BANNER_CLOSED, ); const closeBanner = React.useCallback(() => { diff --git a/src/containers/Tenant/Query/utils/useSavedQueries.tsx b/src/containers/Tenant/Query/utils/useSavedQueries.tsx index 3ef9009495..6e0b0f0e26 100644 --- a/src/containers/Tenant/Query/utils/useSavedQueries.tsx +++ b/src/containers/Tenant/Query/utils/useSavedQueries.tsx @@ -1,12 +1,12 @@ import React from 'react'; import {selectSavedQueriesFilter} from '../../../../store/reducers/queryActions/queryActions'; +import {SETTING_KEYS} from '../../../../store/reducers/settings/constants'; import type {SavedQuery} from '../../../../types/store/query'; -import {SAVED_QUERIES_KEY} from '../../../../utils/constants'; import {useSetting, useTypedSelector} from '../../../../utils/hooks'; export function useSavedQueries() { - const [savedQueries] = useSetting(SAVED_QUERIES_KEY, []); + const [savedQueries] = useSetting(SETTING_KEYS.SAVED_QUERIES, []); return savedQueries; } diff --git a/src/containers/Tenant/TenantNavigation/useTenantNavigation.tsx b/src/containers/Tenant/TenantNavigation/useTenantNavigation.tsx index 892b3f7644..f287a137ac 100644 --- a/src/containers/Tenant/TenantNavigation/useTenantNavigation.tsx +++ b/src/containers/Tenant/TenantNavigation/useTenantNavigation.tsx @@ -4,8 +4,8 @@ import {Pulse, Terminal} from '@gravity-ui/icons'; import {useHistory, useLocation, useRouteMatch} from 'react-router-dom'; import routes, {getTenantPath, parseQuery} from '../../../routes'; +import {SETTING_KEYS} from '../../../store/reducers/settings/constants'; import {TENANT_PAGE, TENANT_PAGES_IDS} from '../../../store/reducers/tenant/constants'; -import {TENANT_INITIAL_PAGE_KEY} from '../../../utils/constants'; import {useSetting, useTypedSelector} from '../../../utils/hooks'; import i18n from '../i18n'; @@ -25,7 +25,7 @@ export function useTenantNavigation() { const queryParams = parseQuery(location); const match = useRouteMatch(routes.tenant); - const [, setInitialTenantPage] = useSetting(TENANT_INITIAL_PAGE_KEY); + const [, setInitialTenantPage] = useSetting(SETTING_KEYS.TENANT_INITIAL_PAGE); const {tenantPage} = useTypedSelector((state) => state.tenant); const menuItems = React.useMemo(() => { diff --git a/src/containers/Tenants/Tenants.tsx b/src/containers/Tenants/Tenants.tsx index 4105c397c5..7fdf9098f3 100644 --- a/src/containers/Tenants/Tenants.tsx +++ b/src/containers/Tenants/Tenants.tsx @@ -21,6 +21,7 @@ import { useDeleteDatabaseFeatureAvailable, useEditDatabaseFeatureAvailable, } from '../../store/reducers/capabilities/hooks'; +import {SETTING_KEYS} from '../../store/reducers/settings/constants'; import { filterTenantsByDomain, filterTenantsByProblems, @@ -33,12 +34,7 @@ import {State} from '../../types/api/tenant'; import {uiFactory} from '../../uiFactory/uiFactory'; import {formatBytes} from '../../utils/bytesParsers'; import {cn} from '../../utils/cn'; -import { - DEFAULT_TABLE_SETTINGS, - EMPTY_DATA_PLACEHOLDER, - SHOW_DOMAIN_DATABASE_KEY, - SHOW_NETWORK_UTILIZATION, -} from '../../utils/constants'; +import {DEFAULT_TABLE_SETTINGS, EMPTY_DATA_PLACEHOLDER} from '../../utils/constants'; import { formatCPU, formatNumber, @@ -101,8 +97,8 @@ export const Tenants = ({additionalTenantsProps, scrollContainerRef}: TenantsPro const {search, withProblems, handleSearchChange, handleWithProblemsChange} = useTenantsQueryParams(); - const [showNetworkUtilization] = useSetting(SHOW_NETWORK_UTILIZATION); - const [showDomainDatabase] = useSetting(SHOW_DOMAIN_DATABASE_KEY); + const [showNetworkUtilization] = useSetting(SETTING_KEYS.SHOW_NETWORK_UTILIZATION); + const [showDomainDatabase] = useSetting(SETTING_KEYS.SHOW_DOMAIN_DATABASE); // We should apply domain filter before other filters // It is done to ensure proper entities count diff --git a/src/containers/UserSettings/settings.tsx b/src/containers/UserSettings/settings.tsx index fb1181a994..adf5933481 100644 --- a/src/containers/UserSettings/settings.tsx +++ b/src/containers/UserSettings/settings.tsx @@ -3,28 +3,9 @@ import type {IconProps} from '@gravity-ui/uikit'; import {createNextState} from '@reduxjs/toolkit'; import {codeAssistBackend} from '../../store'; -import { - ACL_SYNTAX_KEY, - AUTOCOMPLETE_ON_ENTER, - AclSyntax, - BINARY_DATA_IN_PLAIN_TEXT_DISPLAY, - ENABLE_AUTOCOMPLETE, - ENABLE_CODE_ASSISTANT, - ENABLE_NETWORK_TABLE_KEY, - ENABLE_QUERY_STREAMING, - ENABLE_QUERY_STREAMING_OLD_BACKEND, - INVERTED_DISKS_KEY, - LANGUAGE_KEY, - OLD_BACKEND_CLUSTER_NAMES, - PAGE_IDS, - SECTION_IDS, - SHOW_DOMAIN_DATABASE_KEY, - SHOW_NETWORK_UTILIZATION, - THEME_KEY, - USE_CLUSTER_BALANCER_AS_BACKEND_KEY, - USE_SHOW_PLAN_SVG_KEY, -} from '../../utils/constants'; -import {Lang, defaultLang} from '../../utils/i18n'; +import {DEFAULT_USER_SETTINGS, 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'; import type {SettingProps, SettingsInfoFieldProps} from './Setting'; import i18n from './i18n'; @@ -63,7 +44,7 @@ const themeOptions = [ ]; export const themeSetting: SettingProps = { - settingKey: THEME_KEY, + settingKey: SETTING_KEYS.THEME, title: i18n('settings.theme.title'), type: 'radio', options: themeOptions, @@ -81,68 +62,68 @@ const languageOptions = [ ]; export const languageSetting: SettingProps = { - settingKey: LANGUAGE_KEY, + settingKey: SETTING_KEYS.LANGUAGE, title: i18n('settings.language.title'), type: 'radio', options: languageOptions, - defaultValue: defaultLang, + defaultValue: DEFAULT_USER_SETTINGS[SETTING_KEYS.LANGUAGE], onValueUpdate: () => { window.location.reload(); }, }; export const binaryDataInPlainTextDisplay: SettingProps = { - settingKey: BINARY_DATA_IN_PLAIN_TEXT_DISPLAY, + settingKey: SETTING_KEYS.BINARY_DATA_IN_PLAIN_TEXT_DISPLAY, title: i18n('settings.binaryDataInPlainTextDisplay.title'), }; export const invertedDisksSetting: SettingProps = { - settingKey: INVERTED_DISKS_KEY, + settingKey: SETTING_KEYS.INVERTED_DISKS, title: i18n('settings.invertedDisks.title'), }; export const enableNetworkTable: SettingProps = { - settingKey: ENABLE_NETWORK_TABLE_KEY, + settingKey: SETTING_KEYS.ENABLE_NETWORK_TABLE, title: i18n('settings.enableNetworkTable.title'), }; export const useShowPlanToSvgTables: SettingProps = { - settingKey: USE_SHOW_PLAN_SVG_KEY, + settingKey: SETTING_KEYS.USE_SHOW_PLAN_SVG, title: i18n('settings.useShowPlanToSvg.title'), description: i18n('settings.useShowPlanToSvg.description'), }; export const showDomainDatabase: SettingProps = { - settingKey: SHOW_DOMAIN_DATABASE_KEY, + settingKey: SETTING_KEYS.SHOW_DOMAIN_DATABASE, title: i18n('settings.showDomainDatabase.title'), }; export const useClusterBalancerAsBackendSetting: SettingProps = { - settingKey: USE_CLUSTER_BALANCER_AS_BACKEND_KEY, + settingKey: SETTING_KEYS.USE_CLUSTER_BALANCER_AS_BACKEND, title: i18n('settings.useClusterBalancerAsBackend.title'), description: i18n('settings.useClusterBalancerAsBackend.description'), }; export const enableAutocompleteSetting: SettingProps = { - settingKey: ENABLE_AUTOCOMPLETE, + settingKey: SETTING_KEYS.ENABLE_AUTOCOMPLETE, title: i18n('settings.editor.autocomplete.title'), description: i18n('settings.editor.autocomplete.description'), }; export const enableCodeAssistantSetting: SettingProps = { - settingKey: ENABLE_CODE_ASSISTANT, + settingKey: SETTING_KEYS.ENABLE_CODE_ASSISTANT, title: i18n('settings.editor.codeAssistant.title'), description: i18n('settings.editor.codeAssistant.description'), }; export const enableQueryStreamingSetting: SettingProps = { - settingKey: ENABLE_QUERY_STREAMING, + settingKey: SETTING_KEYS.ENABLE_QUERY_STREAMING, title: i18n('settings.editor.queryStreaming.title'), description: i18n('settings.editor.queryStreaming.description'), }; export const enableQueryStreamingOldBackendSetting: SettingProps = { - settingKey: ENABLE_QUERY_STREAMING_OLD_BACKEND, + settingKey: SETTING_KEYS.ENABLE_QUERY_STREAMING_OLD_BACKEND, title: i18n('settings.editor.queryStreaming.title'), description: i18n('settings.editor.queryStreaming.description'), }; @@ -165,7 +146,8 @@ export function applyClusterSpecificQueryStreamingSetting( const section = draft.sections[0]; // experimentsSection const settingIndex = section.settings.findIndex( (setting) => - 'settingKey' in setting && setting.settingKey === ENABLE_QUERY_STREAMING, + 'settingKey' in setting && + setting.settingKey === SETTING_KEYS.ENABLE_QUERY_STREAMING, ); if (settingIndex !== -1) { @@ -178,12 +160,12 @@ export function applyClusterSpecificQueryStreamingSetting( } export const showNetworkUtilizationSetting: SettingProps = { - settingKey: SHOW_NETWORK_UTILIZATION, + settingKey: SETTING_KEYS.SHOW_NETWORK_UTILIZATION, title: i18n('settings.showNetworkUtilization.title'), }; export const autocompleteOnEnterSetting: SettingProps = { - settingKey: AUTOCOMPLETE_ON_ENTER, + settingKey: SETTING_KEYS.AUTOCOMPLETE_ON_ENTER, title: i18n('settings.editor.autocomplete-on-enter.title'), description: i18n('settings.editor.autocomplete-on-enter.description'), }; @@ -208,7 +190,7 @@ const aclSyntaxOptions = [ ]; export const aclSyntaxSetting: SettingProps = { - settingKey: ACL_SYNTAX_KEY, + settingKey: SETTING_KEYS.ACL_SYNTAX, title: i18n('settings.aclSyntax.title'), type: 'radio', options: aclSyntaxOptions, diff --git a/src/lib.ts b/src/lib.ts index 5e1013c8d2..c27b38492e 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -24,7 +24,7 @@ export * from './utils/constants'; export {default as reportWebVitals} from './reportWebVitals'; -export type {SettingsObject} from './services/settings'; +export type {SettingsObject} from './store/reducers/settings/types'; export type { YDBEmbeddedUISettings, SettingsPage, diff --git a/src/services/api/baseMeta.ts b/src/services/api/baseMeta.ts new file mode 100644 index 0000000000..bc01d1f450 --- /dev/null +++ b/src/services/api/baseMeta.ts @@ -0,0 +1,23 @@ +import type {AxiosWrapperOptions} from '@gravity-ui/axios-wrapper'; + +import {environment as ENVIRONMENT, metaBackend as META_BACKEND} from '../../store'; + +import type {BaseAPIParams} from './base'; +import {BaseYdbAPI} from './base'; + +export class BaseMetaAPI extends BaseYdbAPI { + proxyMeta: BaseAPIParams['proxyMeta']; + + constructor(axiosOptions: AxiosWrapperOptions, baseApiParams: BaseAPIParams) { + super(axiosOptions, baseApiParams); + + this.proxyMeta = baseApiParams.proxyMeta; + } + getPath(path: string, clusterName?: string) { + if (this.proxyMeta && clusterName) { + const envPrefix = ENVIRONMENT ? `/${ENVIRONMENT}` : ''; + return `${envPrefix}${META_BACKEND}/proxy/cluster/${clusterName}${path}`; + } + return `${META_BACKEND ?? ''}${path}`; + } +} diff --git a/src/services/api/index.ts b/src/services/api/index.ts index b7c6e524c3..37dd66fcea 100644 --- a/src/services/api/index.ts +++ b/src/services/api/index.ts @@ -2,10 +2,12 @@ import type {AxiosWrapperOptions} from '@gravity-ui/axios-wrapper'; import type {AxiosRequestConfig} from 'axios'; import {codeAssistBackend} from '../../store'; +import {uiFactory} from '../../uiFactory/uiFactory'; import {AuthAPI} from './auth'; import {CodeAssistAPI} from './codeAssist'; import {MetaAPI} from './meta'; +import {MetaSettingsAPI} from './metaSettings'; import {OperationAPI} from './operation'; import {PDiskAPI} from './pdisk'; import {SchemeAPI} from './scheme'; @@ -41,6 +43,7 @@ export class YdbEmbeddedAPI { viewer: ViewerAPI; meta?: MetaAPI; + metaSettings?: MetaSettingsAPI; codeAssist?: CodeAssistAPI; constructor({ @@ -59,6 +62,9 @@ export class YdbEmbeddedAPI { if (webVersion) { this.meta = new MetaAPI(axiosParams, baseApiParams); } + if (uiFactory.useMetaSettings) { + this.metaSettings = new MetaSettingsAPI(axiosParams, baseApiParams); + } if (webVersion || codeAssistBackend) { this.codeAssist = new CodeAssistAPI(axiosParams, baseApiParams); @@ -76,9 +82,8 @@ export class YdbEmbeddedAPI { const token = csrfTokenGetter(); if (token) { this.auth.setCSRFToken(token); - // Use optional chaining as `meta` may not be initialized. this.meta?.setCSRFToken(token); - // Use optional chaining as `codeAssist` may not be initialized. + this.metaSettings?.setCSRFToken(token); this.codeAssist?.setCSRFToken(token); this.operation.setCSRFToken(token); this.pdisk.setCSRFToken(token); diff --git a/src/services/api/meta.ts b/src/services/api/meta.ts index 7b4096cd22..e36c44f9e9 100644 --- a/src/services/api/meta.ts +++ b/src/services/api/meta.ts @@ -1,6 +1,3 @@ -import type {AxiosWrapperOptions} from '@gravity-ui/axios-wrapper'; - -import {environment as ENVIRONMENT, metaBackend as META_BACKEND} from '../../store'; import type {MetaCapabilitiesResponse} from '../../types/api/capabilities'; import type { MetaBaseClusterInfo, @@ -11,25 +8,10 @@ import type { import type {TUserToken} from '../../types/api/whoami'; import {parseMetaTenants} from '../parsers/parseMetaTenants'; -import type {AxiosOptions, BaseAPIParams} from './base'; -import {BaseYdbAPI} from './base'; - -export class MetaAPI extends BaseYdbAPI { - proxyMeta: BaseAPIParams['proxyMeta']; - - constructor(axiosOptions: AxiosWrapperOptions, baseApiParams: BaseAPIParams) { - super(axiosOptions, baseApiParams); - - this.proxyMeta = baseApiParams.proxyMeta; - } - getPath(path: string, clusterName?: string) { - if (this.proxyMeta && clusterName) { - const envPrefix = ENVIRONMENT ? `/${ENVIRONMENT}` : ''; - return `${envPrefix}${META_BACKEND}/proxy/cluster/${clusterName}${path}`; - } - return `${META_BACKEND ?? ''}${path}`; - } +import type {AxiosOptions} from './base'; +import {BaseMetaAPI} from './baseMeta'; +export class MetaAPI extends BaseMetaAPI { metaAuthenticate(params: {user: string; password: string}) { return this.post(this.getPath('/meta/login'), params, {}); } diff --git a/src/services/api/metaSettings.ts b/src/services/api/metaSettings.ts new file mode 100644 index 0000000000..e79432946f --- /dev/null +++ b/src/services/api/metaSettings.ts @@ -0,0 +1,99 @@ +import type { + GetSettingResponse, + GetSettingsParams, + GetSingleSettingParams, + SetSettingResponse, + SetSingleSettingParams, + Setting, +} from '../../types/api/settings'; + +import {BaseMetaAPI} from './baseMeta'; + +interface PendingRequest { + resolve: (value: Setting) => void; + reject: (error: unknown) => void; +} + +export class MetaSettingsAPI extends BaseMetaAPI { + private batchTimeout: NodeJS.Timeout | undefined = undefined; + private currentUser: string | undefined = undefined; + private requestQueue: Map | undefined = undefined; + + getSingleSetting({ + name, + user, + preventBatching, + }: GetSingleSettingParams & {preventBatching?: boolean}) { + if (preventBatching) { + return this.get(this.getPath('/meta/user_settings'), {name, user}); + } + + return new Promise((resolve, reject) => { + // Always request settings for current user + this.currentUser = user; + + if (!this.requestQueue) { + this.initBatch(); + } + + if (!this.requestQueue?.has(name)) { + this.requestQueue?.set(name, []); + } + + this.requestQueue?.get(name)?.push({ + resolve, + reject, + }); + }); + } + + setSingleSetting(params: SetSingleSettingParams) { + return this.post(this.getPath('/meta/user_settings'), params, {}); + } + getSettings(params: GetSettingsParams) { + return this.post(this.getPath('/meta/get_user_settings'), params, {}); + } + + private initBatch() { + this.requestQueue = new Map(); + this.batchTimeout = setTimeout(() => { + this.flushBatch(); + }, 100); + } + + private flushBatch() { + if (!this.requestQueue || !this.requestQueue.size || !this.currentUser) { + return; + } + + const batch = this.requestQueue; + const user = this.currentUser; + this.requestQueue = undefined; + clearTimeout(this.batchTimeout); + + const settingNames = Array.from(batch.keys()); + + this.getSettings({user, name: settingNames}) + .then((response) => { + batch.forEach((pendingRequests, name) => { + const settingResult = response[name]; + if (settingResult) { + pendingRequests.forEach((request) => { + request.resolve(settingResult); + }); + } else { + pendingRequests.forEach((request) => { + request.resolve({name, user, value: undefined}); + }); + } + }); + }) + .catch((error) => { + batch.forEach((pendingRequests) => { + pendingRequests.forEach((request) => { + request.reject(error); + }); + }); + }); + } +} diff --git a/src/services/api/streaming.ts b/src/services/api/streaming.ts index dd9a8f0f3e..3263553316 100644 --- a/src/services/api/streaming.ts +++ b/src/services/api/streaming.ts @@ -8,6 +8,7 @@ import { isSessionChunk, isStreamDataChunk, } from '../../store/reducers/query/utils'; +import {SETTING_KEYS} from '../../store/reducers/settings/constants'; import type {Actions, StreamQueryParams} from '../../types/api/query'; import type { QueryResponseChunk, @@ -15,10 +16,7 @@ import type { StreamDataChunk, StreamingChunk, } from '../../types/store/streaming'; -import { - BINARY_DATA_IN_PLAIN_TEXT_DISPLAY, - DEV_ENABLE_TRACING_FOR_ALL_REQUESTS, -} from '../../utils/constants'; +import {DEV_ENABLE_TRACING_FOR_ALL_REQUESTS} from '../../utils/constants'; import {isRedirectToAuth} from '../../utils/response'; import {settingsManager} from '../settings'; @@ -45,7 +43,7 @@ export class StreamingAPI extends BaseYdbAPI { options: StreamQueryOptions, ) { const base64 = !settingsManager.readUserSettingsValue( - BINARY_DATA_IN_PLAIN_TEXT_DISPLAY, + SETTING_KEYS.BINARY_DATA_IN_PLAIN_TEXT_DISPLAY, true, ); diff --git a/src/services/api/viewer.ts b/src/services/api/viewer.ts index 2c22773020..201104f487 100644 --- a/src/services/api/viewer.ts +++ b/src/services/api/viewer.ts @@ -1,4 +1,5 @@ import type {PlanToSvgQueryParams} from '../../store/reducers/planToSvg'; +import {SETTING_KEYS} from '../../store/reducers/settings/constants'; import type {VDiskBlobIndexStatParams} from '../../store/reducers/vdisk/vdisk'; import type { AccessRightsUpdateRequest, @@ -38,7 +39,6 @@ import type {DescribeTopicResult, TopicDataRequest, TopicDataResponse} from '../ import type {VDiskBlobIndexResponse} from '../../types/api/vdiskBlobIndex'; import type {TUserToken} from '../../types/api/whoami'; import type {TabletsApiRequestParams} from '../../types/store/tablets'; -import {BINARY_DATA_IN_PLAIN_TEXT_DISPLAY} from '../../utils/constants'; import type {Nullable} from '../../utils/typecheckers'; import {settingsManager} from '../settings'; @@ -417,7 +417,7 @@ export class ViewerAPI extends BaseYdbAPI { {concurrentId, signal, withRetries}: AxiosOptions = {}, ) { const base64 = !settingsManager.readUserSettingsValue( - BINARY_DATA_IN_PLAIN_TEXT_DISPLAY, + SETTING_KEYS.BINARY_DATA_IN_PLAIN_TEXT_DISPLAY, true, ); diff --git a/src/services/settings.ts b/src/services/settings.ts index d2f184055a..0aed443f1a 100644 --- a/src/services/settings.ts +++ b/src/services/settings.ts @@ -1,72 +1,6 @@ -import {TENANT_PAGES_IDS} from '../store/reducers/tenant/constants'; -import { - ACL_SYNTAX_KEY, - ASIDE_HEADER_COMPACT_KEY, - AUTOCOMPLETE_ON_ENTER, - AUTO_REFRESH_INTERVAL, - AclSyntax, - BINARY_DATA_IN_PLAIN_TEXT_DISPLAY, - CASE_SENSITIVE_JSON_SEARCH, - ENABLE_AUTOCOMPLETE, - ENABLE_CODE_ASSISTANT, - ENABLE_NETWORK_TABLE_KEY, - ENABLE_QUERY_STREAMING, - ENABLE_QUERY_STREAMING_OLD_BACKEND, - EXPAND_CLUSTER_DASHBOARD, - INVERTED_DISKS_KEY, - IS_HOTKEYS_HELP_HIDDEN_KEY, - LANGUAGE_KEY, - LAST_QUERY_EXECUTION_SETTINGS_KEY, - LAST_USED_QUERY_ACTION_KEY, - PARTITIONS_HIDDEN_COLUMNS_KEY, - QUERY_EXECUTION_SETTINGS_KEY, - QUERY_SETTINGS_BANNER_LAST_CLOSED_KEY, - QUERY_STOPPED_BANNER_CLOSED_KEY, - SAVED_QUERIES_KEY, - SHOW_DOMAIN_DATABASE_KEY, - SHOW_NETWORK_UTILIZATION, - TENANT_INITIAL_PAGE_KEY, - THEME_KEY, - USE_CLUSTER_BALANCER_AS_BACKEND_KEY, - USE_SHOW_PLAN_SVG_KEY, -} from '../utils/constants'; -import {DEFAULT_QUERY_SETTINGS, QUERY_ACTIONS} from '../utils/query'; +import type {SettingsObject} from '../store/reducers/settings/types'; import {parseJson} from '../utils/utils'; -export type SettingsObject = Record; - -/** User settings keys and their default values */ -export const DEFAULT_USER_SETTINGS = { - [THEME_KEY]: 'system', - [LANGUAGE_KEY]: undefined, - [INVERTED_DISKS_KEY]: false, - [BINARY_DATA_IN_PLAIN_TEXT_DISPLAY]: true, - [SAVED_QUERIES_KEY]: [], - [TENANT_INITIAL_PAGE_KEY]: TENANT_PAGES_IDS.query, - [LAST_USED_QUERY_ACTION_KEY]: QUERY_ACTIONS.execute, - [ASIDE_HEADER_COMPACT_KEY]: true, - [PARTITIONS_HIDDEN_COLUMNS_KEY]: [], - [ENABLE_NETWORK_TABLE_KEY]: true, - [USE_SHOW_PLAN_SVG_KEY]: false, - [USE_CLUSTER_BALANCER_AS_BACKEND_KEY]: true, - [ENABLE_AUTOCOMPLETE]: true, - [ENABLE_CODE_ASSISTANT]: true, - [ENABLE_QUERY_STREAMING]: true, - [ENABLE_QUERY_STREAMING_OLD_BACKEND]: false, - [SHOW_NETWORK_UTILIZATION]: true, - [EXPAND_CLUSTER_DASHBOARD]: true, - [AUTOCOMPLETE_ON_ENTER]: true, - [IS_HOTKEYS_HELP_HIDDEN_KEY]: false, - [AUTO_REFRESH_INTERVAL]: 0, - [CASE_SENSITIVE_JSON_SEARCH]: false, - [SHOW_DOMAIN_DATABASE_KEY]: false, - [QUERY_STOPPED_BANNER_CLOSED_KEY]: false, - [LAST_QUERY_EXECUTION_SETTINGS_KEY]: undefined, - [QUERY_SETTINGS_BANNER_LAST_CLOSED_KEY]: undefined, - [QUERY_EXECUTION_SETTINGS_KEY]: DEFAULT_QUERY_SETTINGS, - [ACL_SYNTAX_KEY]: AclSyntax.YdbShort, -} as const satisfies SettingsObject; - class SettingsManager { /** * Returns parsed settings value. diff --git a/src/store/reducers/authentication/authentication.ts b/src/store/reducers/authentication/authentication.ts index 0783a2f765..f8b1ecd9a7 100644 --- a/src/store/reducers/authentication/authentication.ts +++ b/src/store/reducers/authentication/authentication.ts @@ -9,7 +9,8 @@ import type {AuthenticationState} from './types'; const initialState: AuthenticationState = { isAuthenticated: true, - user: '', + user: undefined, + id: undefined, }; export const slice = createSlice({ @@ -22,13 +23,15 @@ export const slice = createSlice({ state.isAuthenticated = isAuthenticated; if (!isAuthenticated) { - state.user = ''; + state.user = undefined; } }, setUser: (state, action: PayloadAction) => { - const {UserSID, AuthType, IsMonitoringAllowed, IsViewerAllowed} = action.payload; + const {UserSID, UserID, AuthType, IsMonitoringAllowed, IsViewerAllowed} = + action.payload; state.user = AuthType === 'Login' ? UserSID : undefined; + state.id = UserID; // If ydb version supports this feature, // There should be explicit flag in whoami response @@ -42,12 +45,14 @@ export const slice = createSlice({ selectIsUserAllowedToMakeChanges: (state) => state.isUserAllowedToMakeChanges, selectIsViewerUser: (state) => state.isViewerUser, selectUser: (state) => state.user, + selectID: (state) => state.id, }, }); export default slice.reducer; export const {setIsAuthenticated, setUser} = slice.actions; -export const {selectIsUserAllowedToMakeChanges, selectIsViewerUser, selectUser} = slice.selectors; +export const {selectIsUserAllowedToMakeChanges, selectIsViewerUser, selectUser, selectID} = + slice.selectors; export const authenticationApi = api.injectEndpoints({ endpoints: (build) => ({ diff --git a/src/store/reducers/authentication/types.ts b/src/store/reducers/authentication/types.ts index ba2218bf67..0a8879b31d 100644 --- a/src/store/reducers/authentication/types.ts +++ b/src/store/reducers/authentication/types.ts @@ -2,5 +2,7 @@ export interface AuthenticationState { isAuthenticated: boolean; isUserAllowedToMakeChanges?: boolean; isViewerUser?: boolean; + user: string | undefined; + id: string | undefined; } diff --git a/src/store/reducers/query/query.ts b/src/store/reducers/query/query.ts index 47b9f8dcbd..ed77fc696a 100644 --- a/src/store/reducers/query/query.ts +++ b/src/store/reducers/query/query.ts @@ -6,15 +6,12 @@ import {TracingLevelNumber} from '../../../types/api/query'; import type {QueryAction, QueryRequestParams, QuerySettings} from '../../../types/store/query'; import type {StreamDataChunk} from '../../../types/store/streaming'; import {loadFromSessionStorage, saveToSessionStorage} from '../../../utils'; -import { - QUERIES_HISTORY_KEY, - QUERY_EDITOR_CURRENT_QUERY_KEY, - QUERY_EDITOR_DIRTY_KEY, -} from '../../../utils/constants'; +import {QUERY_EDITOR_CURRENT_QUERY_KEY, QUERY_EDITOR_DIRTY_KEY} from '../../../utils/constants'; 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 { @@ -28,7 +25,7 @@ import {getActionAndSyntaxFromQueryMode, getQueryInHistory, prepareQueryWithPrag const MAXIMUM_QUERIES_IN_HISTORY = 20; const queriesHistoryInitial = settingsManager.readUserSettingsValue( - QUERIES_HISTORY_KEY, + SETTING_KEYS.QUERIES_HISTORY, [], ) as string[]; @@ -78,7 +75,7 @@ const slice = createSlice({ const newQueries = [...state.history.queries, {queryText, queryId}].slice( state.history.queries.length >= MAXIMUM_QUERIES_IN_HISTORY ? 1 : 0, ); - settingsManager.setUserSettingsValue(QUERIES_HISTORY_KEY, newQueries); + settingsManager.setUserSettingsValue(SETTING_KEYS.QUERIES_HISTORY, newQueries); const currentIndex = newQueries.length - 1; state.history = { @@ -110,7 +107,7 @@ const slice = createSlice({ endTime, }); - settingsManager.setUserSettingsValue(QUERIES_HISTORY_KEY, newQueries); + settingsManager.setUserSettingsValue(SETTING_KEYS.QUERIES_HISTORY, newQueries); state.history.queries = newQueries; }, diff --git a/src/store/reducers/queryActions/queryActions.ts b/src/store/reducers/queryActions/queryActions.ts index fc16d2039c..47cfc7fc5e 100644 --- a/src/store/reducers/queryActions/queryActions.ts +++ b/src/store/reducers/queryActions/queryActions.ts @@ -1,9 +1,10 @@ import type {PayloadAction} from '@reduxjs/toolkit'; import {createSlice} from '@reduxjs/toolkit'; +import {settingsManager} from '../../../services/settings'; import type {SavedQuery} from '../../../types/store/query'; -import {SAVED_QUERIES_KEY} from '../../../utils/constants'; import type {AppDispatch, GetState} from '../../defaultStore'; +import {SETTING_KEYS} from '../settings/constants'; import {getSettingValue, setSettingValue} from '../settings/settings'; import type {QueryActions, QueryActionsState} from './types'; @@ -46,18 +47,21 @@ export const {selectQueryName, selectQueryAction, selectSavedQueriesFilter} = sl export function deleteSavedQuery(queryName: string) { return function deleteSavedQueryThunk(dispatch: AppDispatch, getState: GetState) { const state = getState(); - const savedQueries = (getSettingValue(state, SAVED_QUERIES_KEY) as SavedQuery[]) ?? []; + const savedQueries = + (getSettingValue(state, SETTING_KEYS.SAVED_QUERIES) as SavedQuery[]) ?? []; const newSavedQueries = savedQueries.filter( (el) => el.name.toLowerCase() !== queryName.toLowerCase(), ); - dispatch(setSettingValue(SAVED_QUERIES_KEY, newSavedQueries)); + dispatch(setSettingValue(SETTING_KEYS.SAVED_QUERIES, newSavedQueries)); + settingsManager.setUserSettingsValue(SETTING_KEYS.SAVED_QUERIES, newSavedQueries); }; } export function saveQuery(queryName: string | null) { return function saveQueryThunk(dispatch: AppDispatch, getState: GetState) { const state = getState(); - const savedQueries = (getSettingValue(state, SAVED_QUERIES_KEY) as SavedQuery[]) ?? []; + const savedQueries = + (getSettingValue(state, SETTING_KEYS.SAVED_QUERIES) as SavedQuery[]) ?? []; const queryBody = state.query.input; if (queryName === null) { return; @@ -74,6 +78,7 @@ export function saveQuery(queryName: string | null) { nextSavedQueries.push({name: queryName, body: queryBody}); } - dispatch(setSettingValue(SAVED_QUERIES_KEY, nextSavedQueries)); + dispatch(setSettingValue(SETTING_KEYS.SAVED_QUERIES, nextSavedQueries)); + settingsManager.setUserSettingsValue(SETTING_KEYS.SAVED_QUERIES, nextSavedQueries); }; } diff --git a/src/store/reducers/settings/api.ts b/src/store/reducers/settings/api.ts new file mode 100644 index 0000000000..737de6665a --- /dev/null +++ b/src/store/reducers/settings/api.ts @@ -0,0 +1,109 @@ +import type {Action, ThunkAction} from '@reduxjs/toolkit'; + +import type { + GetSettingsParams, + GetSingleSettingParams, + SetSingleSettingParams, +} from '../../../types/api/settings'; +import type {RootState} from '../../defaultStore'; +import {api} from '../api'; + +import {SETTINGS_OPTIONS} from './constants'; + +export const settingsApi = api.injectEndpoints({ + endpoints: (builder) => ({ + getSingleSetting: builder.query({ + queryFn: async ({name, user}: GetSingleSettingParams) => { + try { + if (!window.api.metaSettings) { + throw new Error('MetaSettings API is not available'); + } + const data = await window.api.metaSettings.getSingleSetting({ + name, + user, + // Directly access options here to avoid them in cache key + preventBatching: SETTINGS_OPTIONS[name]?.preventBatching, + }); + return {data}; + } catch (error) { + return {error}; + } + }, + providesTags: (_, __, args) => [{type: 'UserData', id: `Setting_${args.name}`}], + }), + setSingleSetting: builder.mutation({ + queryFn: async (params: SetSingleSettingParams) => { + try { + if (!window.api.metaSettings) { + throw new Error('MetaSettings API is not available'); + } + + const data = await window.api.metaSettings.setSingleSetting(params); + + if (data.status !== 'SUCCESS') { + throw new Error('Setting status is not SUCCESS'); + } + + return {data}; + } catch (error) { + return {error}; + } + }, + async onQueryStarted(args, {dispatch, queryFulfilled}) { + 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}; + }), + ); + try { + await queryFulfilled; + } catch { + patchResult.undo(); + } + }, + }), + getSettings: builder.query({ + queryFn: async ({name, user}: GetSettingsParams, {dispatch}) => { + try { + if (!window.api.metaSettings) { + throw new Error('MetaSettings API is not available'); + } + const data = await window.api.metaSettings.getSettings({name, user}); + + const patches: ThunkAction[] = []; + + // Upsert received data in getSingleSetting cache + name.forEach((settingName) => { + const settingData = data[settingName] ?? {}; + + const cacheEntryParams: GetSingleSettingParams = { + name: settingName, + user, + }; + const newValue = {name: settingName, user, value: settingData?.value}; + + const patch = dispatch( + settingsApi.util.upsertQueryData( + 'getSingleSetting', + cacheEntryParams, + newValue, + ), + ); + + patches.push(patch); + }); + + await Promise.all(patches); + + return {data}; + } catch (error) { + return {error}; + } + }, + }), + }), + overrideExisting: 'throw', +}); diff --git a/src/store/reducers/settings/constants.ts b/src/store/reducers/settings/constants.ts new file mode 100644 index 0000000000..cbc1c8bed4 --- /dev/null +++ b/src/store/reducers/settings/constants.ts @@ -0,0 +1,86 @@ +import type {ValueOf} from '../../../types/common'; +import {AclSyntax} from '../../../utils/constants'; +import {Lang} from '../../../utils/i18n'; +import {DEFAULT_QUERY_SETTINGS, QUERY_ACTIONS} from '../../../utils/query'; +import {TENANT_PAGES_IDS} from '../tenant/constants'; + +import type {SettingOptions} from './types'; + +export const SETTING_KEYS = { + THEME: 'theme', + LANGUAGE: 'language', + INVERTED_DISKS: 'invertedDisks', + BINARY_DATA_IN_PLAIN_TEXT_DISPLAY: 'binaryDataInPlainTextDisplay', + SAVED_QUERIES: 'saved_queries', + QUERIES_HISTORY: 'queries_history', + TENANT_INITIAL_PAGE: 'saved_tenant_initial_tab', + LAST_USED_QUERY_ACTION: 'last_used_query_action', + ASIDE_HEADER_COMPACT: 'asideHeaderCompact', + PARTITIONS_HIDDEN_COLUMNS: 'partitionsHiddenColumns', + ENABLE_NETWORK_TABLE: 'enableNetworkTable', + USE_SHOW_PLAN_SVG: 'useShowPlanToSvg', + USE_CLUSTER_BALANCER_AS_BACKEND: 'useClusterBalancerAsBacked', + ENABLE_AUTOCOMPLETE: 'enableAutocomplete', + ENABLE_CODE_ASSISTANT: 'enableCodeAssistant', + ENABLE_QUERY_STREAMING: 'enableQueryStreaming', + ENABLE_QUERY_STREAMING_OLD_BACKEND: 'enableQueryStreamingOldBackend', + SHOW_NETWORK_UTILIZATION: 'enableNetworkUtilization', + EXPAND_CLUSTER_DASHBOARD: 'expandClusterDashboard', + AUTOCOMPLETE_ON_ENTER: 'autocompleteOnEnter', + IS_HOTKEYS_HELP_HIDDEN: 'isHotKeysHelpHidden', + AUTO_REFRESH_INTERVAL: 'auto-refresh-interval', + CASE_SENSITIVE_JSON_SEARCH: 'caseSensitiveJsonSearch', + SHOW_DOMAIN_DATABASE: 'showDomainDatabase', + QUERY_STOPPED_BANNER_CLOSED: 'queryStoppedBannerClosed', + LAST_QUERY_EXECUTION_SETTINGS: 'last_query_execution_settings', + QUERY_SETTINGS_BANNER_LAST_CLOSED: 'querySettingsBannerLastClosed', + QUERY_EXECUTION_SETTINGS: 'queryExecutionSettings', + ACL_SYNTAX: 'aclSyntax', +} as const; + +export type SettingKey = ValueOf; + +/** User settings keys and their default values */ +export const DEFAULT_USER_SETTINGS = { + [SETTING_KEYS.THEME]: 'system', + [SETTING_KEYS.LANGUAGE]: Lang.En, + [SETTING_KEYS.INVERTED_DISKS]: false, + [SETTING_KEYS.BINARY_DATA_IN_PLAIN_TEXT_DISPLAY]: true, + [SETTING_KEYS.SAVED_QUERIES]: [], + [SETTING_KEYS.QUERIES_HISTORY]: [], + [SETTING_KEYS.TENANT_INITIAL_PAGE]: TENANT_PAGES_IDS.query, + [SETTING_KEYS.LAST_USED_QUERY_ACTION]: QUERY_ACTIONS.execute, + [SETTING_KEYS.ASIDE_HEADER_COMPACT]: true, + [SETTING_KEYS.PARTITIONS_HIDDEN_COLUMNS]: [], + [SETTING_KEYS.ENABLE_NETWORK_TABLE]: true, + [SETTING_KEYS.USE_SHOW_PLAN_SVG]: false, + [SETTING_KEYS.USE_CLUSTER_BALANCER_AS_BACKEND]: true, + [SETTING_KEYS.ENABLE_AUTOCOMPLETE]: true, + [SETTING_KEYS.ENABLE_CODE_ASSISTANT]: true, + [SETTING_KEYS.ENABLE_QUERY_STREAMING]: true, + [SETTING_KEYS.ENABLE_QUERY_STREAMING_OLD_BACKEND]: false, + [SETTING_KEYS.SHOW_NETWORK_UTILIZATION]: true, + [SETTING_KEYS.EXPAND_CLUSTER_DASHBOARD]: true, + [SETTING_KEYS.AUTOCOMPLETE_ON_ENTER]: true, + [SETTING_KEYS.IS_HOTKEYS_HELP_HIDDEN]: false, + [SETTING_KEYS.AUTO_REFRESH_INTERVAL]: 0, + [SETTING_KEYS.CASE_SENSITIVE_JSON_SEARCH]: false, + [SETTING_KEYS.SHOW_DOMAIN_DATABASE]: false, + [SETTING_KEYS.QUERY_STOPPED_BANNER_CLOSED]: false, + [SETTING_KEYS.LAST_QUERY_EXECUTION_SETTINGS]: undefined, + [SETTING_KEYS.QUERY_SETTINGS_BANNER_LAST_CLOSED]: undefined, + [SETTING_KEYS.QUERY_EXECUTION_SETTINGS]: DEFAULT_QUERY_SETTINGS, + [SETTING_KEYS.ACL_SYNTAX]: AclSyntax.YdbShort, +} as const satisfies 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 8a38f988a4..4267f8fde2 100644 --- a/src/store/reducers/settings/settings.ts +++ b/src/store/reducers/settings/settings.ts @@ -1,10 +1,11 @@ import type {Store} from '@reduxjs/toolkit'; import {createSlice} from '@reduxjs/toolkit'; -import {DEFAULT_USER_SETTINGS, settingsManager} from '../../../services/settings'; +import {settingsManager} from '../../../services/settings'; import {parseJson} from '../../../utils/utils'; import type {AppDispatch} from '../../defaultStore'; +import {DEFAULT_USER_SETTINGS} from './constants'; import type {SettingsState} from './types'; const userSettings = settingsManager.extractSettingsFromLS(DEFAULT_USER_SETTINGS); @@ -24,17 +25,23 @@ const settingsSlice = createSlice({ }), }), selectors: { - getSettingValue: (state, name: string) => state.userSettings[name], + getSettingValue: (state, name?: string) => { + if (!name) { + return undefined; + } + + return state.userSettings[name]; + }, }, }); export const {getSettingValue} = settingsSlice.selectors; -export const setSettingValue = (name: string, value: unknown) => { +export const setSettingValue = (name: string | undefined, value: unknown) => { return (dispatch: AppDispatch) => { - dispatch(settingsSlice.actions.setSettingValue({name, value})); - - settingsManager.setUserSettingsValue(name, value); + if (name) { + dispatch(settingsSlice.actions.setSettingValue({name, value})); + } }; }; diff --git a/src/store/reducers/settings/types.ts b/src/store/reducers/settings/types.ts index 62f752d92d..a7b86019e9 100644 --- a/src/store/reducers/settings/types.ts +++ b/src/store/reducers/settings/types.ts @@ -1,6 +1,13 @@ -import type {SettingsObject} from '../../../services/settings'; +export type SettingsObject = Record; export interface SettingsState { userSettings: SettingsObject; systemSettings: SettingsObject; } + +export interface SettingOptions { + /** Save setting only to meta service */ + preventSyncWithLS?: boolean; + /** Request setting immediatelly */ + preventBatching?: boolean; +} diff --git a/src/store/reducers/settings/useSetting.ts b/src/store/reducers/settings/useSetting.ts new file mode 100644 index 0000000000..0361dceb88 --- /dev/null +++ b/src/store/reducers/settings/useSetting.ts @@ -0,0 +1,143 @@ +import React from 'react'; + +import {skipToken} from '@reduxjs/toolkit/query'; +import {debounce} from 'lodash'; + +import type {SetSingleSettingParams} from '../../../types/api/settings'; +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 { + deleteValueFromLS, + parseSettingValue, + readSettingValueFromLS, + setSettingValueToLS, + stringifySettingValue, +} from './utils'; + +type SaveSettingValue = (value: T | undefined) => void; + +interface UseSettingOptions { + /** Time before setting will be set */ + debounceTime?: number; +} + +export function useSetting( + name?: string, + {debounceTime = 0}: UseSettingOptions = {}, +): {value: T | undefined; 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 anonymosUserId = useTypedSelector(selectID); + + const user = authUserSID || anonymosUserId; + 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 [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)); + } + }, [shouldUseOnlyExternalSettings, metaSetting, name, dispatch]); + + // Load local value to backend + React.useEffect(() => { + const savedValue = readSettingValueFromLS(name); + + const isMetaSettingEmpty = !isSettingLoading && !metaSetting?.value; + + if (shouldUseMetaSettings && isMetaSettingEmpty && savedValue) { + setMetaSetting({name, user, value: stringifySettingValue(savedValue)}) + .unwrap() + .then(() => { + if (shouldUseOnlyExternalSettings) { + deleteValueFromLS(name); + } + }) + .catch((error) => { + console.error('Failed to set setting via meta API:', error); + }); + } + }, [ + shouldUseMetaSettings, + shouldUseOnlyExternalSettings, + metaSetting, + isSettingLoading, + name, + user, + setMetaSetting, + ]); + + const debouncedSetMetaSetting = React.useMemo( + () => + debounce((params: SetSingleSettingParams) => { + setMetaSetting(params); + }, debounceTime), + [debounceTime, setMetaSetting], + ); + + // Call debounced func on component unmount + React.useEffect(() => { + return () => debouncedSetMetaSetting.flush(); + }, [debouncedSetMetaSetting]); + + const saveValue = React.useCallback>( + (value) => { + if (shouldUseMetaSettings) { + debouncedSetMetaSetting({ + user, + name: name, + value: stringifySettingValue(value), + }); + } + + if (!shouldUseOnlyExternalSettings) { + setSettingValueToLS(name, value); + } + }, + [shouldUseOnlyExternalSettings, user, name, debouncedSetMetaSetting], + ); + + return {value: settingValue, saveValue, isLoading} as const; +} diff --git a/src/store/reducers/settings/utils.ts b/src/store/reducers/settings/utils.ts new file mode 100644 index 0000000000..9bef19a4d0 --- /dev/null +++ b/src/store/reducers/settings/utils.ts @@ -0,0 +1,43 @@ +import type {SettingValue} from '../../../types/api/settings'; +import {parseJson} from '../../../utils/utils'; + +export function stringifySettingValue(value?: T): string { + return typeof value === 'string' ? value : JSON.stringify(value); +} +export function parseSettingValue(value: SettingValue) { + try { + return (typeof value === 'string' ? parseJson(value) : value) as T; + } catch { + return undefined; + } +} +export function readSettingValueFromLS(name: string | undefined): T | undefined { + if (!name) { + return undefined; + } + + try { + const value = localStorage.getItem(name); + + return parseJson(value); + } catch { + return undefined; + } +} +export function setSettingValueToLS(name: string | undefined, value: unknown): void { + if (!name) { + return; + } + + try { + const preparedValue = stringifySettingValue(value); + localStorage.setItem(name, preparedValue); + } catch {} +} +export function deleteValueFromLS(name: string | undefined) { + if (!name) { + return; + } + + delete localStorage[name]; +} diff --git a/src/store/reducers/tenant/tenant.ts b/src/store/reducers/tenant/tenant.ts index 9cd40d182e..26347c36bf 100644 --- a/src/store/reducers/tenant/tenant.ts +++ b/src/store/reducers/tenant/tenant.ts @@ -1,12 +1,12 @@ import {createSlice} from '@reduxjs/toolkit'; import type {PayloadAction} from '@reduxjs/toolkit'; -import {DEFAULT_USER_SETTINGS, settingsManager} from '../../../services/settings'; +import {settingsManager} from '../../../services/settings'; import type {TTenantInfo} from '../../../types/api/tenant'; -import {TENANT_INITIAL_PAGE_KEY} from '../../../utils/constants'; 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'; @@ -21,8 +21,8 @@ import type { } from './types'; const tenantPage = tenantPageSchema - .catch(DEFAULT_USER_SETTINGS[TENANT_INITIAL_PAGE_KEY]) - .parse(settingsManager.readUserSettingsValue(TENANT_INITIAL_PAGE_KEY)); + .catch(DEFAULT_USER_SETTINGS[SETTING_KEYS.TENANT_INITIAL_PAGE]) + .parse(settingsManager.readUserSettingsValue(SETTING_KEYS.TENANT_INITIAL_PAGE)); export const initialState: TenantState = { tenantPage, diff --git a/src/types/api/settings.ts b/src/types/api/settings.ts new file mode 100644 index 0000000000..9798df7010 --- /dev/null +++ b/src/types/api/settings.ts @@ -0,0 +1,24 @@ +export interface SetSettingResponse { + ready: boolean; + status: 'SUCCESS'; +} +export type GetSettingResponse = Record; +export interface Setting { + user: string; + name: string; + value?: string | Record; +} +export type SettingValue = string | Record; +export interface GetSingleSettingParams { + user: string; + name: string; +} +export interface SetSingleSettingParams { + user: string; + name: string; + value: SettingValue; +} +export interface GetSettingsParams { + user: string; + name: string[]; +} diff --git a/src/types/api/whoami.ts b/src/types/api/whoami.ts index 7313e4c5de..2b7ff074e1 100644 --- a/src/types/api/whoami.ts +++ b/src/types/api/whoami.ts @@ -5,6 +5,8 @@ */ export interface TUserToken { UserSID?: string; + // Generated ID when user is not authenticated + UserID?: string; GroupSIDs?: TProtoHashTable; OriginalUserToken?: string; AuthType?: string; diff --git a/src/types/window.d.ts b/src/types/window.d.ts index 8bc682785c..cb10abf0a8 100644 --- a/src/types/window.d.ts +++ b/src/types/window.d.ts @@ -41,7 +41,7 @@ interface Window { react_app_disable_checks?: boolean; - systemSettings?: import('../services/settings').SettingsObject; + systemSettings?: import('../store/reducers/settings/types').SettingsObject; api: import('../services/api/index').YdbEmbeddedAPI; diff --git a/src/uiFactory/types.ts b/src/uiFactory/types.ts index d66a79424e..0dcafa541b 100644 --- a/src/uiFactory/types.ts +++ b/src/uiFactory/types.ts @@ -50,6 +50,7 @@ export interface UIFactory; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 0f77b4d7e0..86d35e3dbf 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -48,38 +48,6 @@ export const getTabletLabel = (type?: string) => { return isTabletType(type) ? TABLET_SYMBOLS[type] : defaultValue; }; -// Settings Keys Dictionary -export const SETTING_KEYS = { - THEME: 'theme', - LANGUAGE: 'language', - INVERTED_DISKS: 'invertedDisks', - BINARY_DATA_IN_PLAIN_TEXT_DISPLAY: 'binaryDataInPlainTextDisplay', - SAVED_QUERIES: 'saved_queries', - TENANT_INITIAL_PAGE: 'saved_tenant_initial_tab', - LAST_USED_QUERY_ACTION: 'last_used_query_action', - ASIDE_HEADER_COMPACT: 'asideHeaderCompact', - PARTITIONS_HIDDEN_COLUMNS: 'partitionsHiddenColumns', - ENABLE_NETWORK_TABLE: 'enableNetworkTable', - USE_SHOW_PLAN_SVG: 'useShowPlanToSvg', - USE_CLUSTER_BALANCER_AS_BACKEND: 'useClusterBalancerAsBacked', - ENABLE_AUTOCOMPLETE: 'enableAutocomplete', - ENABLE_CODE_ASSISTANT: 'enableCodeAssistant', - ENABLE_QUERY_STREAMING: 'enableQueryStreaming', - ENABLE_QUERY_STREAMING_OLD_BACKEND: 'enableQueryStreamingOldBackend', - SHOW_NETWORK_UTILIZATION: 'enableNetworkUtilization', - EXPAND_CLUSTER_DASHBOARD: 'expandClusterDashboard', - AUTOCOMPLETE_ON_ENTER: 'autocompleteOnEnter', - IS_HOTKEYS_HELP_HIDDEN: 'isHotKeysHelpHidden', - AUTO_REFRESH_INTERVAL: 'auto-refresh-interval', - CASE_SENSITIVE_JSON_SEARCH: 'caseSensitiveJsonSearch', - SHOW_DOMAIN_DATABASE: 'showDomainDatabase', - QUERY_STOPPED_BANNER_CLOSED: 'queryStoppedBannerClosed', - LAST_QUERY_EXECUTION_SETTINGS: 'last_query_execution_settings', - QUERY_SETTINGS_BANNER_LAST_CLOSED: 'querySettingsBannerLastClosed', - QUERY_EXECUTION_SETTINGS: 'queryExecutionSettings', - ACL_SYNTAX: 'aclSyntax', -} as const; - // Page IDs Dictionary export const PAGE_IDS = { GENERAL: 'generalPage', @@ -111,21 +79,10 @@ export const CLUSTER_DEFAULT_TITLE = 'Cluster'; export const TENANT_DEFAULT_TITLE = 'Database'; // ==== Settings ==== -export const THEME_KEY = SETTING_KEYS.THEME; -export const LANGUAGE_KEY = SETTING_KEYS.LANGUAGE; -export const INVERTED_DISKS_KEY = SETTING_KEYS.INVERTED_DISKS; -export const SAVED_QUERIES_KEY = SETTING_KEYS.SAVED_QUERIES; -export const ASIDE_HEADER_COMPACT_KEY = SETTING_KEYS.ASIDE_HEADER_COMPACT; -export const QUERIES_HISTORY_KEY = 'queries_history'; export const QUERY_EDITOR_CURRENT_QUERY_KEY = 'query_editor_current_query'; export const QUERY_EDITOR_DIRTY_KEY = 'query_editor_dirty'; -export const BINARY_DATA_IN_PLAIN_TEXT_DISPLAY = SETTING_KEYS.BINARY_DATA_IN_PLAIN_TEXT_DISPLAY; -export const AUTO_REFRESH_INTERVAL = SETTING_KEYS.AUTO_REFRESH_INTERVAL; - -export const CASE_SENSITIVE_JSON_SEARCH = SETTING_KEYS.CASE_SENSITIVE_JSON_SEARCH; - export const DEFAULT_SIZE_RESULT_PANE_KEY = 'default-size-result-pane'; export const DEFAULT_SIZE_TENANT_SUMMARY_KEY = 'default-size-tenant-summary-pane'; export const DEFAULT_SIZE_TENANT_KEY = 'default-size-tenant-pane'; @@ -153,53 +110,14 @@ export const TENANT_OVERVIEW_TABLES_SETTINGS: Settings = { sortable: false, } as const; -export const QUERY_EXECUTION_SETTINGS_KEY = SETTING_KEYS.QUERY_EXECUTION_SETTINGS; -export const LAST_QUERY_EXECUTION_SETTINGS_KEY = SETTING_KEYS.LAST_QUERY_EXECUTION_SETTINGS; -export const QUERY_SETTINGS_BANNER_LAST_CLOSED_KEY = SETTING_KEYS.QUERY_SETTINGS_BANNER_LAST_CLOSED; -export const QUERY_STOPPED_BANNER_CLOSED_KEY = SETTING_KEYS.QUERY_STOPPED_BANNER_CLOSED; - -export const LAST_USED_QUERY_ACTION_KEY = SETTING_KEYS.LAST_USED_QUERY_ACTION; - -export const PARTITIONS_HIDDEN_COLUMNS_KEY = SETTING_KEYS.PARTITIONS_HIDDEN_COLUMNS; - -// Remain "tab" in key name for backward compatibility -export const TENANT_INITIAL_PAGE_KEY = SETTING_KEYS.TENANT_INITIAL_PAGE; - -export const ENABLE_NETWORK_TABLE_KEY = SETTING_KEYS.ENABLE_NETWORK_TABLE; - -export const USE_SHOW_PLAN_SVG_KEY = SETTING_KEYS.USE_SHOW_PLAN_SVG; - -// Setting to hide domain in database list -export const SHOW_DOMAIN_DATABASE_KEY = SETTING_KEYS.SHOW_DOMAIN_DATABASE; - -export const USE_CLUSTER_BALANCER_AS_BACKEND_KEY = SETTING_KEYS.USE_CLUSTER_BALANCER_AS_BACKEND; - -export const ENABLE_AUTOCOMPLETE = SETTING_KEYS.ENABLE_AUTOCOMPLETE; - -export const ENABLE_CODE_ASSISTANT = SETTING_KEYS.ENABLE_CODE_ASSISTANT; - -export const ENABLE_QUERY_STREAMING = SETTING_KEYS.ENABLE_QUERY_STREAMING; - -export const ENABLE_QUERY_STREAMING_OLD_BACKEND = SETTING_KEYS.ENABLE_QUERY_STREAMING_OLD_BACKEND; - export const OLD_BACKEND_CLUSTER_NAMES = [ 'cloud_prod_kikimr_global', 'cloud_preprod_kikimr_global', 'cloud_prod_kikimr_ydb_public_storage', ]; -export const AUTOCOMPLETE_ON_ENTER = SETTING_KEYS.AUTOCOMPLETE_ON_ENTER; - -export const IS_HOTKEYS_HELP_HIDDEN_KEY = SETTING_KEYS.IS_HOTKEYS_HELP_HIDDEN; - export const DEV_ENABLE_TRACING_FOR_ALL_REQUESTS = 'enable_tracing_for_all_requests'; -export const SHOW_NETWORK_UTILIZATION = SETTING_KEYS.SHOW_NETWORK_UTILIZATION; - -export const EXPAND_CLUSTER_DASHBOARD = SETTING_KEYS.EXPAND_CLUSTER_DASHBOARD; - -export const ACL_SYNTAX_KEY = SETTING_KEYS.ACL_SYNTAX; - export enum AclSyntax { Kikimr = 'kikimr', YdbShort = 'ydb-short', diff --git a/src/utils/hooks/useAclSyntax.ts b/src/utils/hooks/useAclSyntax.ts index 41d91da6bc..41bca7c597 100644 --- a/src/utils/hooks/useAclSyntax.ts +++ b/src/utils/hooks/useAclSyntax.ts @@ -1,10 +1,10 @@ -import {ACL_SYNTAX_KEY, AclSyntax} from '../constants'; +import {SETTING_KEYS} from '../../store/reducers/settings/constants'; +import {AclSyntax} from '../constants'; -import {useTypedSelector} from './useTypedSelector'; +import {useSetting} from './useSetting'; export function useAclSyntax(): string { - const aclSyntax = useTypedSelector( - (state) => state.settings.userSettings[ACL_SYNTAX_KEY] as string | undefined, - ); + const [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 690b392665..aa960fb825 100644 --- a/src/utils/hooks/useAutoRefreshInterval.ts +++ b/src/utils/hooks/useAutoRefreshInterval.ts @@ -1,6 +1,6 @@ import React from 'react'; -import {AUTO_REFRESH_INTERVAL} from '../constants'; +import {SETTING_KEYS} from '../../store/reducers/settings/constants'; import {useSetting} from './useSetting'; @@ -8,7 +8,10 @@ const IMMEDIATE_UPDATE_INTERVAL = 1; const DISABLED_INTERVAL = 0; export function useAutoRefreshInterval(): [number, (value: number) => void] { - const [settingValue, setSettingValue] = useSetting(AUTO_REFRESH_INTERVAL, DISABLED_INTERVAL); + const [settingValue, setSettingValue] = useSetting( + SETTING_KEYS.AUTO_REFRESH_INTERVAL, + DISABLED_INTERVAL, + ); const [effectiveInterval, setEffectiveInterval] = React.useState( document.visibilityState === 'visible' ? settingValue : DISABLED_INTERVAL, ); diff --git a/src/utils/hooks/useChangedQuerySettings.ts b/src/utils/hooks/useChangedQuerySettings.ts index 6f8011556a..486c388f0b 100644 --- a/src/utils/hooks/useChangedQuerySettings.ts +++ b/src/utils/hooks/useChangedQuerySettings.ts @@ -1,6 +1,7 @@ import getChangedQueryExecutionSettings from '../../containers/Tenant/Query/QueryEditorControls/utils/getChangedQueryExecutionSettings'; import getChangedQueryExecutionSettingsDescription from '../../containers/Tenant/Query/QueryEditorControls/utils/getChangedQueryExecutionSettingsDescription'; -import {QUERY_SETTINGS_BANNER_LAST_CLOSED_KEY, WEEK_IN_SECONDS} from '../constants'; +import {SETTING_KEYS} from '../../store/reducers/settings/constants'; +import {WEEK_IN_SECONDS} from '../constants'; import {DEFAULT_QUERY_SETTINGS} from '../query'; import {useLastQueryExecutionSettings} from './useLastQueryExecutionSettings'; @@ -10,7 +11,7 @@ import {useSetting} from './useSetting'; export const useChangedQuerySettings = () => { const [bannerLastClosedTimestamp, setBannerLastClosedTimestamp] = useSetting< number | undefined - >(QUERY_SETTINGS_BANNER_LAST_CLOSED_KEY); + >(SETTING_KEYS.QUERY_SETTINGS_BANNER_LAST_CLOSED); const [lastQuerySettings] = useLastQueryExecutionSettings(); const [currentQuerySettings] = useQueryExecutionSettings(); diff --git a/src/utils/hooks/useLastQueryExecutionSettings.ts b/src/utils/hooks/useLastQueryExecutionSettings.ts index d594058b0c..825eb3130d 100644 --- a/src/utils/hooks/useLastQueryExecutionSettings.ts +++ b/src/utils/hooks/useLastQueryExecutionSettings.ts @@ -1,12 +1,12 @@ +import {SETTING_KEYS} from '../../store/reducers/settings/constants'; import type {QuerySettings} from '../../types/store/query'; -import {LAST_QUERY_EXECUTION_SETTINGS_KEY} from '../constants'; import {querySettingsValidationSchema} from '../query'; import {useSetting} from './useSetting'; export const useLastQueryExecutionSettings = () => { const [lastStorageSettings, setLastSettings] = useSetting( - LAST_QUERY_EXECUTION_SETTINGS_KEY, + SETTING_KEYS.LAST_QUERY_EXECUTION_SETTINGS, ); let lastSettings: QuerySettings | undefined; diff --git a/src/utils/hooks/useQueryExecutionSettings.ts b/src/utils/hooks/useQueryExecutionSettings.ts index 028ddbb4d8..900d960b41 100644 --- a/src/utils/hooks/useQueryExecutionSettings.ts +++ b/src/utils/hooks/useQueryExecutionSettings.ts @@ -1,8 +1,8 @@ import React from 'react'; import {useTracingLevelOptionAvailable} from '../../store/reducers/capabilities/hooks'; +import {SETTING_KEYS} from '../../store/reducers/settings/constants'; import type {QuerySettings} from '../../types/store/query'; -import {QUERY_EXECUTION_SETTINGS_KEY, USE_SHOW_PLAN_SVG_KEY} from '../constants'; import { DEFAULT_QUERY_SETTINGS, QUERY_MODES, @@ -15,10 +15,12 @@ import {useSetting} from './useSetting'; export const useQueryExecutionSettings = () => { const enableTracingLevel = useTracingLevelOptionAvailable(); - const [storageSettings, setSettings] = useSetting(QUERY_EXECUTION_SETTINGS_KEY); + const [storageSettings, setSettings] = useSetting( + SETTING_KEYS.QUERY_EXECUTION_SETTINGS, + ); const validatedSettings = querySettingsRestoreSchema.parse(storageSettings); - const [useShowPlanToSvg] = useSetting(USE_SHOW_PLAN_SVG_KEY); + const [useShowPlanToSvg] = useSetting(SETTING_KEYS.USE_SHOW_PLAN_SVG); const [enableQueryStreaming] = useQueryStreamingSetting(); const setQueryExecutionSettings = React.useCallback( diff --git a/src/utils/hooks/useQueryStreamingSetting.ts b/src/utils/hooks/useQueryStreamingSetting.ts index 46a24edfb1..2e1ad99a5c 100644 --- a/src/utils/hooks/useQueryStreamingSetting.ts +++ b/src/utils/hooks/useQueryStreamingSetting.ts @@ -1,8 +1,5 @@ -import { - ENABLE_QUERY_STREAMING, - ENABLE_QUERY_STREAMING_OLD_BACKEND, - OLD_BACKEND_CLUSTER_NAMES, -} from '../constants'; +import {SETTING_KEYS} from '../../store/reducers/settings/constants'; +import {OLD_BACKEND_CLUSTER_NAMES} from '../constants'; import {useClusterNameFromQuery} from './useDatabaseFromQuery'; import {useSetting} from './useSetting'; @@ -13,8 +10,8 @@ export const useQueryStreamingSetting = (): [boolean, (value: boolean) => void] const isOldBackendCluster = clusterName && OLD_BACKEND_CLUSTER_NAMES.includes(clusterName); const settingKey = isOldBackendCluster - ? ENABLE_QUERY_STREAMING_OLD_BACKEND - : ENABLE_QUERY_STREAMING; + ? SETTING_KEYS.ENABLE_QUERY_STREAMING_OLD_BACKEND + : SETTING_KEYS.ENABLE_QUERY_STREAMING; return useSetting(settingKey); }; diff --git a/src/utils/hooks/useSetting.ts b/src/utils/hooks/useSetting.ts index f9e06e7da6..5432062152 100644 --- a/src/utils/hooks/useSetting.ts +++ b/src/utils/hooks/useSetting.ts @@ -1,5 +1,6 @@ import React from 'react'; +import {settingsManager} from '../../services/settings'; import {getSettingValue, setSettingValue} from '../../store/reducers/settings/settings'; import {useTypedDispatch} from './useTypedDispatch'; @@ -16,6 +17,7 @@ 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], ); diff --git a/src/utils/hooks/useTableResize.ts b/src/utils/hooks/useTableResize.ts index 5214d4b858..3daa1e5e8e 100644 --- a/src/utils/hooks/useTableResize.ts +++ b/src/utils/hooks/useTableResize.ts @@ -1,31 +1,40 @@ import React from 'react'; -import type { - ColumnWidthByName, - GetSavedColumnWidthByName, - HandleResize, - SaveColumnWidthByName, -} from '@gravity-ui/react-data-table'; -import {useTableResize as libUseTableResize} from '@gravity-ui/react-data-table'; +import type {ColumnWidthByName, HandleResize} from '@gravity-ui/react-data-table'; -import {settingsManager} from '../../services/settings'; +import {useSetting} from '../../store/reducers/settings/useSetting'; -export const useTableResize = (localStorageKey?: string): [ColumnWidthByName, HandleResize] => { - const getSizes: GetSavedColumnWidthByName = React.useCallback(() => { - if (!localStorageKey) { - return {}; - } - return settingsManager.readUserSettingsValue(localStorageKey, {}) as ColumnWidthByName; - }, [localStorageKey]); +export const useTableResize = ( + localStorageKey?: string, +): [ColumnWidthByName, HandleResize, boolean] => { + const { + value: sizes, + saveValue: saveSizes, + isLoading, + } = useSetting(localStorageKey, { + debounceTime: 300, + }); - const saveSizes: SaveColumnWidthByName = React.useCallback( - (value) => { - if (localStorageKey) { - settingsManager.setUserSettingsValue(localStorageKey, value); - } + const [actualSizes, setActualSizes] = React.useState(() => { + return sizes ?? ({} as ColumnWidthByName); + }); + + React.useEffect(() => { + setActualSizes(sizes ?? {}); + }, [sizes]); + + const handleSetupChange: HandleResize = React.useCallback( + (columnId, columnWidth) => { + setActualSizes((previousSetup) => { + const setup = Object.assign(Object.assign({}, previousSetup), { + [columnId]: columnWidth, + }); + saveSizes(setup); + return setup; + }); }, - [localStorageKey], + [saveSizes], ); - return libUseTableResize({saveSizes, getSizes}); + return [actualSizes, handleSetupChange, isLoading]; }; From f18e12ba20ddb71cfa565ca519b701f541f818f9 Mon Sep 17 00:00:00 2001 From: mufazalov Date: Wed, 12 Nov 2025 12:58:16 +0300 Subject: [PATCH 2/3] fix: bot review --- src/services/api/metaSettings.ts | 3 ++- src/store/reducers/settings/useSetting.ts | 8 ++++---- src/store/reducers/settings/utils.ts | 2 +- src/utils/hooks/useTableResize.ts | 5 +++-- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/services/api/metaSettings.ts b/src/services/api/metaSettings.ts index e79432946f..a8ec9505d4 100644 --- a/src/services/api/metaSettings.ts +++ b/src/services/api/metaSettings.ts @@ -68,8 +68,9 @@ export class MetaSettingsAPI extends BaseMetaAPI { const batch = this.requestQueue; const user = this.currentUser; - this.requestQueue = undefined; clearTimeout(this.batchTimeout); + this.requestQueue = undefined; + this.batchTimeout = undefined; const settingNames = Array.from(batch.keys()); diff --git a/src/store/reducers/settings/useSetting.ts b/src/store/reducers/settings/useSetting.ts index 0361dceb88..7c367faad8 100644 --- a/src/store/reducers/settings/useSetting.ts +++ b/src/store/reducers/settings/useSetting.ts @@ -39,9 +39,9 @@ export function useSetting( const settingValue = useTypedSelector((state) => getSettingValue(state, name)) as T | undefined; const authUserSID = useTypedSelector(selectUser); - const anonymosUserId = useTypedSelector(selectID); + const anonymousUserId = useTypedSelector(selectID); - const user = authUserSID || anonymosUserId; + const user = authUserSID || anonymousUserId; const shouldUseMetaSettings = uiFactory.useMetaSettings && user && name; const shouldUseOnlyExternalSettings = shouldUseMetaSettings && preventSyncWithLS; @@ -79,7 +79,7 @@ export function useSetting( const parsedValue = parseSettingValue(metaSetting.value); dispatch(setSettingValue(name, parsedValue)); } - }, [shouldUseOnlyExternalSettings, metaSetting, name, dispatch]); + }, [shouldUseMetaSettings, shouldUseOnlyExternalSettings, metaSetting, name, dispatch]); // Load local value to backend React.useEffect(() => { @@ -136,7 +136,7 @@ export function useSetting( setSettingValueToLS(name, value); } }, - [shouldUseOnlyExternalSettings, user, name, debouncedSetMetaSetting], + [shouldUseMetaSettings, shouldUseOnlyExternalSettings, user, name, debouncedSetMetaSetting], ); return {value: settingValue, saveValue, isLoading} as const; diff --git a/src/store/reducers/settings/utils.ts b/src/store/reducers/settings/utils.ts index 9bef19a4d0..82fba5a238 100644 --- a/src/store/reducers/settings/utils.ts +++ b/src/store/reducers/settings/utils.ts @@ -39,5 +39,5 @@ export function deleteValueFromLS(name: string | undefined) { return; } - delete localStorage[name]; + localStorage.removeItem(name); } diff --git a/src/utils/hooks/useTableResize.ts b/src/utils/hooks/useTableResize.ts index 3daa1e5e8e..9e32f6b889 100644 --- a/src/utils/hooks/useTableResize.ts +++ b/src/utils/hooks/useTableResize.ts @@ -26,9 +26,10 @@ export const useTableResize = ( const handleSetupChange: HandleResize = React.useCallback( (columnId, columnWidth) => { setActualSizes((previousSetup) => { - const setup = Object.assign(Object.assign({}, previousSetup), { + const setup = { + ...previousSetup, [columnId]: columnWidth, - }); + }; saveSizes(setup); return setup; }); From 5daa10ad6b5513e5cc32464e6a451c4ddd237bfe Mon Sep 17 00:00:00 2001 From: mufazalov Date: Fri, 14 Nov 2025 14:27:00 +0300 Subject: [PATCH 3/3] fix: sync local value to meta in api --- src/store/reducers/settings/api.ts | 44 +++++++++++++++++++---- src/store/reducers/settings/useSetting.ts | 29 --------------- src/store/reducers/settings/utils.ts | 11 ++---- src/types/api/settings.ts | 2 +- 4 files changed, 40 insertions(+), 46 deletions(-) diff --git a/src/store/reducers/settings/api.ts b/src/store/reducers/settings/api.ts index 737de6665a..f4595de3c6 100644 --- a/src/store/reducers/settings/api.ts +++ b/src/store/reducers/settings/api.ts @@ -1,11 +1,12 @@ -import type {Action, ThunkAction} from '@reduxjs/toolkit'; +import {isNil} from 'lodash'; import type { GetSettingsParams, GetSingleSettingParams, SetSingleSettingParams, + Setting, } from '../../../types/api/settings'; -import type {RootState} from '../../defaultStore'; +import type {AppDispatch} from '../../defaultStore'; import {api} from '../api'; import {SETTINGS_OPTIONS} from './constants'; @@ -13,7 +14,7 @@ import {SETTINGS_OPTIONS} from './constants'; export const settingsApi = api.injectEndpoints({ endpoints: (builder) => ({ getSingleSetting: builder.query({ - queryFn: async ({name, user}: GetSingleSettingParams) => { + queryFn: async ({name, user}: GetSingleSettingParams, baseApi) => { try { if (!window.api.metaSettings) { throw new Error('MetaSettings API is not available'); @@ -24,12 +25,17 @@ export const settingsApi = api.injectEndpoints({ // Directly access options here to avoid them in cache key 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}; } }, - providesTags: (_, __, args) => [{type: 'UserData', id: `Setting_${args.name}`}], }), setSingleSetting: builder.mutation({ queryFn: async (params: SetSingleSettingParams) => { @@ -66,14 +72,15 @@ export const settingsApi = api.injectEndpoints({ }, }), getSettings: builder.query({ - queryFn: async ({name, user}: GetSettingsParams, {dispatch}) => { + queryFn: async ({name, user}: GetSettingsParams, baseApi) => { try { if (!window.api.metaSettings) { throw new Error('MetaSettings API is not available'); } const data = await window.api.metaSettings.getSettings({name, user}); - const patches: ThunkAction[] = []; + const patches: Promise[] = []; + const dispatch = baseApi.dispatch as AppDispatch; // Upsert received data in getSingleSetting cache name.forEach((settingName) => { @@ -91,11 +98,20 @@ export const settingsApi = api.injectEndpoints({ cacheEntryParams, newValue, ), - ); + ).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); }); + // Wait for all patches for proper loading state await Promise.all(patches); return {data}; @@ -107,3 +123,17 @@ export const settingsApi = api.injectEndpoints({ }), 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/useSetting.ts b/src/store/reducers/settings/useSetting.ts index 7c367faad8..70e5cde325 100644 --- a/src/store/reducers/settings/useSetting.ts +++ b/src/store/reducers/settings/useSetting.ts @@ -14,7 +14,6 @@ import type {SettingKey} from './constants'; import {DEFAULT_USER_SETTINGS, SETTINGS_OPTIONS} from './constants'; import {getSettingValue, setSettingValue} from './settings'; import { - deleteValueFromLS, parseSettingValue, readSettingValueFromLS, setSettingValueToLS, @@ -81,34 +80,6 @@ export function useSetting( } }, [shouldUseMetaSettings, shouldUseOnlyExternalSettings, metaSetting, name, dispatch]); - // Load local value to backend - React.useEffect(() => { - const savedValue = readSettingValueFromLS(name); - - const isMetaSettingEmpty = !isSettingLoading && !metaSetting?.value; - - if (shouldUseMetaSettings && isMetaSettingEmpty && savedValue) { - setMetaSetting({name, user, value: stringifySettingValue(savedValue)}) - .unwrap() - .then(() => { - if (shouldUseOnlyExternalSettings) { - deleteValueFromLS(name); - } - }) - .catch((error) => { - console.error('Failed to set setting via meta API:', error); - }); - } - }, [ - shouldUseMetaSettings, - shouldUseOnlyExternalSettings, - metaSetting, - isSettingLoading, - name, - user, - setMetaSetting, - ]); - const debouncedSetMetaSetting = React.useMemo( () => debounce((params: SetSingleSettingParams) => { diff --git a/src/store/reducers/settings/utils.ts b/src/store/reducers/settings/utils.ts index 82fba5a238..916e1e5d42 100644 --- a/src/store/reducers/settings/utils.ts +++ b/src/store/reducers/settings/utils.ts @@ -4,14 +4,14 @@ import {parseJson} from '../../../utils/utils'; export function stringifySettingValue(value?: T): string { return typeof value === 'string' ? value : JSON.stringify(value); } -export function parseSettingValue(value: SettingValue) { +export function parseSettingValue(value?: SettingValue) { try { return (typeof value === 'string' ? parseJson(value) : value) as T; } catch { return undefined; } } -export function readSettingValueFromLS(name: string | undefined): T | undefined { +export function readSettingValueFromLS(name: string | undefined): T | undefined { if (!name) { return undefined; } @@ -34,10 +34,3 @@ export function setSettingValueToLS(name: string | undefined, value: unknown): v localStorage.setItem(name, preparedValue); } catch {} } -export function deleteValueFromLS(name: string | undefined) { - if (!name) { - return; - } - - localStorage.removeItem(name); -} diff --git a/src/types/api/settings.ts b/src/types/api/settings.ts index 9798df7010..6de550c4c5 100644 --- a/src/types/api/settings.ts +++ b/src/types/api/settings.ts @@ -6,7 +6,7 @@ export type GetSettingResponse = Record; export interface Setting { user: string; name: string; - value?: string | Record; + value?: SettingValue; } export type SettingValue = string | Record; export interface GetSingleSettingParams {