diff --git a/src/components/ClipboardButton/ClipboardButton.tsx b/src/components/ClipboardButton/ClipboardButton.tsx new file mode 100644 index 0000000000..de641e40d5 --- /dev/null +++ b/src/components/ClipboardButton/ClipboardButton.tsx @@ -0,0 +1,31 @@ +import type {ButtonSize, ButtonView} from '@gravity-ui/uikit'; +import {ClipboardButton as ClipboardButtonUikit} from '@gravity-ui/uikit'; + +import i18n from './i18n'; + +export interface ClipboardButtonProps { + copyText?: string; + withLabel?: boolean | string; + size?: ButtonSize; + view?: ButtonView; + className?: string; +} + +export function ClipboardButton({ + size, + className, + copyText, + withLabel, + view, +}: ClipboardButtonProps) { + if (!copyText) { + return null; + } + const label = withLabel === false ? null : withLabel || i18n('copy'); + + return ( + + {label} + + ); +} diff --git a/src/components/SyntaxHighlighter/i18n/en.json b/src/components/ClipboardButton/i18n/en.json similarity index 100% rename from src/components/SyntaxHighlighter/i18n/en.json rename to src/components/ClipboardButton/i18n/en.json diff --git a/src/components/SyntaxHighlighter/i18n/index.ts b/src/components/ClipboardButton/i18n/index.ts similarity index 75% rename from src/components/SyntaxHighlighter/i18n/index.ts rename to src/components/ClipboardButton/i18n/index.ts index 4a8450b7ab..6a60f1e6d1 100644 --- a/src/components/SyntaxHighlighter/i18n/index.ts +++ b/src/components/ClipboardButton/i18n/index.ts @@ -2,6 +2,6 @@ import {registerKeysets} from '../../../utils/i18n'; import en from './en.json'; -const COMPONENT = 'ydb-syntax-highlighter'; +const COMPONENT = 'ydb-clipboard-button'; export default registerKeysets(COMPONENT, {en}); diff --git a/src/components/FixedHeightQuery/FixedHeightQuery.tsx b/src/components/FixedHeightQuery/FixedHeightQuery.tsx index 3af165a8fd..1c3f571c06 100644 --- a/src/components/FixedHeightQuery/FixedHeightQuery.tsx +++ b/src/components/FixedHeightQuery/FixedHeightQuery.tsx @@ -55,7 +55,7 @@ export const FixedHeightQuery = ({ withLabel: false, size: 'xs', } - : false + : undefined } /> diff --git a/src/components/JsonViewer/JsonViewer.scss b/src/components/JsonViewer/JsonViewer.scss index 9b3edc81de..4590feec84 100644 --- a/src/components/JsonViewer/JsonViewer.scss +++ b/src/components/JsonViewer/JsonViewer.scss @@ -4,15 +4,15 @@ --data-table-row-height: 20px; --toolbar-background-color: var(--g-color-base-background); - width: max-content; + width: 100%; &__toolbar { position: sticky; z-index: 2; - top: 0; + top: var(--ydb-json-viewer-toolbar-top, 0); left: 0; - padding-bottom: var(--g-spacing-2); + padding-bottom: var(--g-spacing-5); background-color: var(--toolbar-background-color); } @@ -45,8 +45,6 @@ align-content: center; text-wrap: nowrap; - - color: var(--g-color-text-secondary); } &__key { @@ -74,6 +72,8 @@ } &__filter { + --g-button-height: 24px; + --g-button-border-radius: 4px; width: 300px; } @@ -103,10 +103,6 @@ @include mixins.body-2-typography(); } - &__extra-tools { - margin-left: 1ex; - } - .data-table__head { display: none; } diff --git a/src/components/JsonViewer/JsonViewer.tsx b/src/components/JsonViewer/JsonViewer.tsx index 8112820b0f..e30e5b863e 100644 --- a/src/components/JsonViewer/JsonViewer.tsx +++ b/src/components/JsonViewer/JsonViewer.tsx @@ -1,11 +1,14 @@ import React from 'react'; +import {ChevronsCollapseVertical, ChevronsExpandVertical} from '@gravity-ui/icons'; 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 {useSetting} from '../../utils/hooks'; +import type {ClipboardButtonProps} from '../ClipboardButton/ClipboardButton'; +import {ClipboardButton} from '../ClipboardButton/ClipboardButton'; import {Cell} from './components/Cell'; import {Filter} from './components/Filter'; @@ -22,9 +25,6 @@ import type { } from './unipika/flattenUnipika'; import {unipika} from './unipika/unipika'; -import ArrowDownToLineIcon from '@gravity-ui/icons/svgs/arrow-down-to-line.svg'; -import ArrowUpFromLineIcon from '@gravity-ui/icons/svgs/arrow-up-from-line.svg'; - import './JsonViewer.scss'; interface JsonViewerCommonProps { @@ -35,6 +35,7 @@ interface JsonViewerCommonProps { collapsedInitially?: boolean; maxValueWidth?: number; toolbarClassName?: string; + withClipboardButton?: Omit; } interface JsonViewerProps extends JsonViewerCommonProps { @@ -118,6 +119,7 @@ function JsonViewerComponent({ collapsedInitially, maxValueWidth = 100, toolbarClassName, + withClipboardButton, }: JsonViewerComponentProps) { const [caseSensitiveSearch, setCaseSensitiveSearch] = useSetting( CASE_SENSITIVE_JSON_SEARCH, @@ -300,19 +302,12 @@ function JsonViewerComponent({ const renderToolbar = () => { return ( - - - - - - - - - + {search && ( )} - {extraTools} + + + + + + + + {withClipboardButton && ( + + )} + {extraTools} + ); }; diff --git a/src/components/JsonViewer/components/Filter.tsx b/src/components/JsonViewer/components/Filter.tsx index 966c579189..da6da3aa3f 100644 --- a/src/components/JsonViewer/components/Filter.tsx +++ b/src/components/JsonViewer/components/Filter.tsx @@ -38,7 +38,7 @@ export const Filter = React.forwardRef(function F const count = matchedRows.length; const matchPosition = count ? 1 + (matchIndex % count) : 0; return ( - + (function F } /> - - + + + + + + - - {matchPosition} / {count || 0} - - + {value && ( + + {matchPosition} / {count || 0} + + )} + ); }); diff --git a/src/components/JsonViewer/unipika/unipika.ts b/src/components/JsonViewer/unipika/unipika.ts index 7651419c3c..10e5ba4b02 100644 --- a/src/components/JsonViewer/unipika/unipika.ts +++ b/src/components/JsonViewer/unipika/unipika.ts @@ -16,6 +16,9 @@ export const defaultUnipikaSettings = { }; export function unipikaConvert(value: unknown) { + if (!value) { + return value; + } let result; try { result = unipika.converters.yson(value, defaultUnipikaSettings); diff --git a/src/components/SyntaxHighlighter/YDBSyntaxHighlighter.scss b/src/components/SyntaxHighlighter/YDBSyntaxHighlighter.scss index b549c666dc..ab328974c2 100644 --- a/src/components/SyntaxHighlighter/YDBSyntaxHighlighter.scss +++ b/src/components/SyntaxHighlighter/YDBSyntaxHighlighter.scss @@ -4,7 +4,7 @@ position: relative; z-index: 0; - height: 100%; + height: var(--ydb-syntax-highlighter-height, 100%); &__sticky-container { z-index: 1; diff --git a/src/components/SyntaxHighlighter/YDBSyntaxHighlighter.tsx b/src/components/SyntaxHighlighter/YDBSyntaxHighlighter.tsx index 52382ccbc9..c1c5da8c33 100644 --- a/src/components/SyntaxHighlighter/YDBSyntaxHighlighter.tsx +++ b/src/components/SyntaxHighlighter/YDBSyntaxHighlighter.tsx @@ -1,11 +1,11 @@ import React from 'react'; -import type {ButtonSize} from '@gravity-ui/uikit'; -import {ClipboardButton} from '@gravity-ui/uikit'; import {nanoid} from '@reduxjs/toolkit'; import {PrismLight as ReactSyntaxHighlighter} from 'react-syntax-highlighter'; -import i18n from './i18n'; +import type {ClipboardButtonProps} from '../ClipboardButton/ClipboardButton'; +import {ClipboardButton} from '../ClipboardButton/ClipboardButton'; + import {b} from './shared'; import {useSyntaxHighlighterStyle} from './themes'; import type {Language} from './types'; @@ -24,16 +24,11 @@ async function registerLanguage(lang: Language) { } } -interface ClipboardButtonOptions { +export interface WithClipboardButtonProp extends ClipboardButtonProps { alwaysVisible?: boolean; - copyText?: string; - withLabel?: boolean; - size?: ButtonSize; } -export type WithClipboardButtonProp = ClipboardButtonOptions | boolean; - -type YDBSyntaxHighlighterProps = { +export type YDBSyntaxHighlighterProps = { text: string; language: Language; className?: string; @@ -60,37 +55,28 @@ export function YDBSyntaxHighlighter({ registerLangAndUpdateKey(); }, [language]); - const clipboardButtonProps = - typeof withClipboardButton === 'object' ? withClipboardButton : undefined; - const renderCopyButton = () => { - if (withClipboardButton) { - return ( -
e.stopPropagation()}> - - {clipboardButtonProps?.withLabel === false ? null : i18n('copy')} - -
- ); + if (!withClipboardButton) { + return null; } - - return null; + const {alwaysVisible, copyText, ...rest} = withClipboardButton; + return ( +
e.stopPropagation()}> + +
+ ); }; let paddingStyles = {}; - if ( - withClipboardButton && - typeof withClipboardButton === 'object' && - withClipboardButton.alwaysVisible - ) { + if (withClipboardButton?.alwaysVisible) { if (withClipboardButton.withLabel) { paddingStyles = {paddingRight: 80}; } else { diff --git a/src/components/SyntaxHighlighter/types.ts b/src/components/SyntaxHighlighter/types.ts index 62c51d4596..28e6b0cdf1 100644 --- a/src/components/SyntaxHighlighter/types.ts +++ b/src/components/SyntaxHighlighter/types.ts @@ -7,4 +7,5 @@ export type Language = | 'javascript' | 'php' | 'python' - | 'yql'; + | 'yql' + | 'yaml'; diff --git a/src/components/TableWithControlsLayout/TableWithControlsLayout.scss b/src/components/TableWithControlsLayout/TableWithControlsLayout.scss index 26f5e2e2ca..56b2f2f3dc 100644 --- a/src/components/TableWithControlsLayout/TableWithControlsLayout.scss +++ b/src/components/TableWithControlsLayout/TableWithControlsLayout.scss @@ -2,7 +2,9 @@ .ydb-table-with-controls-layout { // Total height of all fixed elements above table for sticky header positioning - --data-table-sticky-header-offset: 62px; + --data-table-sticky-header-offset: calc( + var(--data-table-start, 0px) + var(--ydb-table-with-controls-layout-controls-height, 62px) + ); display: inline-block; @@ -14,23 +16,26 @@ } &__controls-wrapper { + $padding-top: var(--ydb-table-with-controls-layout-controls-padding-top, 0px); z-index: 3; - align-items: center; + align-items: if($padding-top == 0px, flex-start, center); box-sizing: border-box; width: 100%; - @include mixins.sticky-top(); + @include mixins.sticky-top(var(--data-table-start, 0px)); } &__controls { z-index: 3; width: max-content; - height: 62px; + height: var(--ydb-table-with-controls-layout-controls-height, 62px); - @include mixins.controls(); + @include mixins.controls( + var(--ydb-table-with-controls-layout-controls-padding-top, var(--g-spacing-4)) + ); @include mixins.sticky-top(); } diff --git a/src/components/TruncatedQuery/TruncatedQuery.tsx b/src/components/TruncatedQuery/TruncatedQuery.tsx index cf2bc61885..b54647d3cd 100644 --- a/src/components/TruncatedQuery/TruncatedQuery.tsx +++ b/src/components/TruncatedQuery/TruncatedQuery.tsx @@ -40,7 +40,7 @@ export const TruncatedQuery = ({ copyText: value, withLabel: false, } - : false + : undefined } /> {message} @@ -58,7 +58,7 @@ export const TruncatedQuery = ({ copyText: value, withLabel: false, } - : false + : undefined } /> ); diff --git a/src/containers/App/App.scss b/src/containers/App/App.scss index 7f57e81a54..a5161ae5a2 100644 --- a/src/containers/App/App.scss +++ b/src/containers/App/App.scss @@ -22,6 +22,7 @@ html, body, #root { + --g-scrollbar-width: 8px; overflow: auto; box-sizing: border-box; @@ -41,6 +42,9 @@ body, } .g-root { + --g-scrollbar-width: 8px; + --header-height: 40px; + --ydb-data-table-color-hover: var(--g-color-base-simple-hover-solid); // Colors for tablets, status icons and progress bars @@ -54,6 +58,13 @@ body, --g-popover-max-width: 500px; + -webkit-overflow-scrolling: touch; + @include mixins.scrollbar(); + ::-webkit-scrollbar-corner, + ::-webkit-scrollbar-thumb { + border-radius: 8px; + } + &_theme_light, &_theme_light-hc { --code-background-color: var(--g-color-base-simple-hover); diff --git a/src/containers/Cluster/Cluster.scss b/src/containers/Cluster/Cluster.scss index 7f096f8210..6152b70a58 100644 --- a/src/containers/Cluster/Cluster.scss +++ b/src/containers/Cluster/Cluster.scss @@ -4,6 +4,8 @@ --cluster-side-padding: var(--g-spacing-5); --sticky-tabs-height: 40px; --extra-controls-right: calc(var(--cluster-side-padding) * 2); + --data-table-start: var(--sticky-tabs-height); + --ydb-table-with-controls-layout-controls-padding-top: var(--g-spacing-4); position: relative; @@ -74,12 +76,13 @@ background-color: var(--g-color-base-background); } - .ydb-table-with-controls-layout__controls-wrapper { - top: 40px; - } - - .ydb-table-with-controls-layout { - // Total height of all fixed elements above table for sticky header positioning - --data-table-sticky-header-offset: 102px; + &__cluster-configs { + --ydb-configs-controls-height: 62px; + + //calculate height of configs container + --ydb-configs-container-height: calc( + 100vh - var(--sticky-tabs-height) - var(--header-height) + ); + padding-right: var(--g-spacing-5); } } diff --git a/src/containers/Cluster/Cluster.tsx b/src/containers/Cluster/Cluster.tsx index 3ccaf31a56..37b2ddec3e 100644 --- a/src/containers/Cluster/Cluster.tsx +++ b/src/containers/Cluster/Cluster.tsx @@ -13,7 +13,10 @@ import {InternalLink} from '../../components/InternalLink'; import {NetworkTable} from '../../components/NetworkTable/NetworkTable'; import {useShouldShowClusterNetworkTable} from '../../components/NetworkTable/hooks'; import routes, {getLocationObjectFromHref} from '../../routes'; -import {useClusterDashboardAvailable} from '../../store/reducers/capabilities/hooks'; +import { + useClusterDashboardAvailable, + useConfigAvailable, +} from '../../store/reducers/capabilities/hooks'; import { INITIAL_DEFAULT_CLUSTER_TAB, clusterApi, @@ -32,7 +35,9 @@ import {EFlag} from '../../types/api/enums'; import {uiFactory} from '../../uiFactory/uiFactory'; import {cn} from '../../utils/cn'; import {useAutoRefreshInterval, useTypedDispatch, useTypedSelector} from '../../utils/hooks'; +import {useIsViewerUser} from '../../utils/hooks/useIsUserAllowedToMakeChanges'; import {useAppTitle} from '../App/AppTitleContext'; +import {Configs} from '../Configs/Configs'; import {Nodes} from '../Nodes/Nodes'; import {PaginatedStorage} from '../Storage/PaginatedStorage'; import {TabletsTable} from '../Tablets/TabletsTable'; @@ -69,6 +74,10 @@ export function Cluster({ const shouldShowNetworkTable = useShouldShowClusterNetworkTable(); const shouldShowEventsTab = useShouldShowEventsTab(); + const isViewerUser = useIsViewerUser(); + const isConfigsAvailable = useConfigAvailable(); + + const showConfigs = isViewerUser && isConfigsAvailable; const [autoRefreshInterval] = useAutoRefreshInterval(); @@ -108,17 +117,20 @@ export function Cluster({ }, [dispatch]); const actualClusterTabs = React.useMemo(() => { - let tabs = clusterTabs; + const skippedTabs: ClusterTab[] = []; if (!shouldShowNetworkTable) { - tabs = tabs.filter((tab) => tab.id !== clusterTabsIds.network); + skippedTabs.push(clusterTabsIds.network); } if (!shouldShowEventsTab) { - tabs = tabs.filter((tab) => tab.id !== clusterTabsIds.events); + skippedTabs.push(clusterTabsIds.events); + } + if (!showConfigs) { + skippedTabs.push(clusterTabsIds.configs); } - return tabs; - }, [shouldShowEventsTab, shouldShowNetworkTable]); + return clusterTabs.filter((el) => !skippedTabs.includes(el.id)); + }, [shouldShowEventsTab, shouldShowNetworkTable, showConfigs]); const getClusterTitle = () => { if (infoLoading) { @@ -270,6 +282,20 @@ export function Cluster({ {uiFactory.renderEvents?.({scrollContainerRef: container})} )} + {showConfigs && ( + + + + )} ( diff --git a/src/containers/Cluster/i18n/en.json b/src/containers/Cluster/i18n/en.json index a78205562b..f19b9843b1 100644 --- a/src/containers/Cluster/i18n/en.json +++ b/src/containers/Cluster/i18n/en.json @@ -48,5 +48,6 @@ "tab_network": "Network", "tab_versions": "Versions", "tab_tablets": "Tablets", - "tab_events": "Events" + "tab_events": "Events", + "tab_configs": "Configs" } diff --git a/src/containers/Cluster/utils.tsx b/src/containers/Cluster/utils.tsx index 5560aeacc5..4846786a61 100644 --- a/src/containers/Cluster/utils.tsx +++ b/src/containers/Cluster/utils.tsx @@ -15,6 +15,7 @@ export const clusterTabsIds = { versions: 'versions', tablets: 'tablets', events: 'events', + configs: 'configs', } as const; export type ClusterTab = ValueOf; @@ -61,8 +62,14 @@ const events = { return i18n('tab_events'); }, }; +const configs = { + id: clusterTabsIds.configs, + get title() { + return i18n('tab_configs'); + }, +}; -export const clusterTabs = [tenants, nodes, storage, network, tablets, versions, events]; +export const clusterTabs = [tenants, nodes, storage, network, tablets, versions, events, configs]; export function isClusterTab(tab: any): tab is ClusterTab { return Object.values(clusterTabsIds).includes(tab); diff --git a/src/containers/Configs/Configs.scss b/src/containers/Configs/Configs.scss new file mode 100644 index 0000000000..74d9ac842e --- /dev/null +++ b/src/containers/Configs/Configs.scss @@ -0,0 +1,25 @@ +@use '../../styles/mixins.scss'; + +.ydb-configs { + --ydb-table-with-controls-layout-controls-height: var(--ydb-configs-controls-height); + --configs-padding-bottom: var(--g-spacing-4); + --ydb-json-viewer-toolbar-top: var(--data-table-sticky-header-offset); + --ydb-syntax-highlighter-height: calc( + var(--ydb-configs-container-height) - var( + --ydb-table-with-controls-layout-controls-height + ) - var(--configs-padding-bottom) + ); + max-width: 100%; + padding-bottom: var(--configs-padding-bottom); + + &__feature-flags { + --ydb-table-with-controls-layout-controls-padding-top: 0px; + --data-table-start: calc( + var(--sticky-tabs-height, 0px) + var(--ydb-configs-controls-height) + ); + --ydb-table-with-controls-layout-controls-height: 44px; + } + &__startup { + height: var(--ydb-syntax-highlighter-height); + } +} diff --git a/src/containers/Configs/Configs.tsx b/src/containers/Configs/Configs.tsx new file mode 100644 index 0000000000..f2d91eafc5 --- /dev/null +++ b/src/containers/Configs/Configs.tsx @@ -0,0 +1,86 @@ +import React from 'react'; + +import {SegmentedRadioGroup} from '@gravity-ui/uikit'; + +import {TableWithControlsLayout} from '../../components/TableWithControlsLayout/TableWithControlsLayout'; +import { + useBaseConfigAvailable, + useFeatureFlagsAvailable, +} from '../../store/reducers/capabilities/hooks'; +import {cn} from '../../utils/cn'; + +import {Config} from './components/Config/Config'; +import {FeatureFlags} from './components/FeatureFlags/FeatureFlags'; +import {Startup} from './components/Startup/Startup'; +import type {ConfigType} from './types'; +import {ConfigTypeTitles, ConfigTypes} from './types'; +import {useConfigQueryParams} from './useConfigsQueryParams'; + +import './Configs.scss'; + +interface ConfigsProps { + database?: string; + className?: string; + scrollContainerRef?: React.RefObject; +} + +const b = cn('ydb-configs'); + +export function Configs({database, className, scrollContainerRef}: ConfigsProps) { + const {configType} = useConfigQueryParams(); + + const isFeaturesAvailable = useFeatureFlagsAvailable(); + const isConfigsAvailable = useBaseConfigAvailable(); + + const options = React.useMemo(() => { + const options: ConfigType[] = []; + if (isFeaturesAvailable) { + options.push(ConfigTypes.features); + } + if (isConfigsAvailable) { + options.push(ConfigTypes.current); + options.push(ConfigTypes.startup); + } + return options; + }, [isFeaturesAvailable, isConfigsAvailable]); + + const renderContent = () => { + switch (configType) { + case ConfigTypes.current: + return ; + case ConfigTypes.startup: + return ; + case ConfigTypes.features: + return ; + } + }; + + return ( + + + + + + {renderContent()} + + + ); +} + +function ConfigSelector({options}: {options: ConfigType[]}) { + const {configType, handleConfigTypeChange} = useConfigQueryParams(); + + if (!options.length) { + return null; + } + + return ( + + {options.map((option) => ( + + {ConfigTypeTitles[option]} + + ))} + + ); +} diff --git a/src/containers/Configs/components/Config/Config.tsx b/src/containers/Configs/components/Config/Config.tsx new file mode 100644 index 0000000000..ae8c0d04e6 --- /dev/null +++ b/src/containers/Configs/components/Config/Config.tsx @@ -0,0 +1,39 @@ +import React from 'react'; + +import {ResponseError} from '../../../../components/Errors/ResponseError'; +import {JsonViewer} from '../../../../components/JsonViewer/JsonViewer'; +import {useUnipikaConvert} from '../../../../components/JsonViewer/unipika/unipika'; +import {LoaderWrapper} from '../../../../components/LoaderWrapper/LoaderWrapper'; +import {configsApi} from '../../../../store/reducers/configs'; +import {useAutoRefreshInterval} from '../../../../utils/hooks/useAutoRefreshInterval'; +import i18n from '../../i18n'; + +interface ConfigProps { + database?: string; +} +export function Config({database}: ConfigProps) { + const [autoRefreshInterval] = useAutoRefreshInterval(); + const {currentData, isLoading, error} = configsApi.useGetConfigQuery( + {database}, + {pollingInterval: autoRefreshInterval}, + ); + + const {current} = currentData || {}; + + const convertedValue = useUnipikaConvert(current); + + const copyText = React.useMemo(() => JSON.stringify(current, null, 4), [current]); + + return ( + + {current ? ( + + ) : null} + {error ? : null} + + ); +} diff --git a/src/containers/Tenant/Diagnostics/Configs/Configs.scss b/src/containers/Configs/components/FeatureFlags/FeatureFlags.scss similarity index 83% rename from src/containers/Tenant/Diagnostics/Configs/Configs.scss rename to src/containers/Configs/components/FeatureFlags/FeatureFlags.scss index 760224d401..b352039c6d 100644 --- a/src/containers/Tenant/Diagnostics/Configs/Configs.scss +++ b/src/containers/Configs/components/FeatureFlags/FeatureFlags.scss @@ -1,4 +1,4 @@ -.ydb-diagnostics-configs { +.ydb-feature-flags { &__icon-touched { line-height: 1; cursor: default !important; diff --git a/src/containers/Tenant/Diagnostics/Configs/Configs.tsx b/src/containers/Configs/components/FeatureFlags/FeatureFlags.tsx similarity index 81% rename from src/containers/Tenant/Diagnostics/Configs/Configs.tsx rename to src/containers/Configs/components/FeatureFlags/FeatureFlags.tsx index c961d0ee27..b3671d4d9f 100644 --- a/src/containers/Tenant/Diagnostics/Configs/Configs.tsx +++ b/src/containers/Configs/components/FeatureFlags/FeatureFlags.tsx @@ -1,25 +1,24 @@ import {PersonPencil} from '@gravity-ui/icons'; import type {Column} from '@gravity-ui/react-data-table'; import {Icon, Popover, Switch} from '@gravity-ui/uikit'; -import {StringParam, useQueryParam} from 'use-query-params'; import {ResponseError} from '../../../../components/Errors/ResponseError'; import {ResizeableDataTable} from '../../../../components/ResizeableDataTable/ResizeableDataTable'; import {Search} from '../../../../components/Search'; import {TableWithControlsLayout} from '../../../../components/TableWithControlsLayout/TableWithControlsLayout'; -import {tenantApi} from '../../../../store/reducers/tenant/tenant'; +import {configsApi} from '../../../../store/reducers/configs'; import type {FeatureFlagConfig} from '../../../../types/api/featureFlags'; import {cn} from '../../../../utils/cn'; -import {DEFAULT_TABLE_SETTINGS} from '../../../../utils/constants'; +import {DEFAULT_TABLE_SETTINGS, YDB_POPOVER_CLASS_NAME} from '../../../../utils/constants'; import {useAutoRefreshInterval} from '../../../../utils/hooks'; +import i18n from '../../i18n'; +import {useConfigQueryParams} from '../../useConfigsQueryParams'; -import i18n from './i18n'; - -import './Configs.scss'; +import './FeatureFlags.scss'; const FEATURE_FLAGS_COLUMNS_WIDTH_LS_KEY = 'featureFlagsColumnsWidth'; -const b = cn('ydb-diagnostics-configs'); +const b = cn('ydb-feature-flags'); const columns: Column[] = [ { @@ -29,7 +28,7 @@ const columns: Column[] = [ row.Current ? ( @@ -82,22 +81,19 @@ const columns: Column[] = [ }, ]; -interface ConfigsProps { - database: string; +interface FeatureFlagsProps { + database?: string; + className?: string; } -export const Configs = ({database}: ConfigsProps) => { - const [search, setSearch] = useQueryParam('search', StringParam); +export const FeatureFlags = ({database, className}: FeatureFlagsProps) => { + const {search, handleSearchChange} = useConfigQueryParams(); const [autoRefreshInterval] = useAutoRefreshInterval(); const { currentData = [], isLoading, error, - } = tenantApi.useGetClusterConfigQuery({database}, {pollingInterval: autoRefreshInterval}); - - const onChange = (value: string) => { - setSearch(value || undefined, 'replaceIn'); - }; + } = configsApi.useGetFeatureFlagsQuery({database}, {pollingInterval: autoRefreshInterval}); const featureFlagsFilter = search?.toLocaleLowerCase(); const featureFlags = featureFlagsFilter @@ -105,12 +101,13 @@ export const Configs = ({database}: ConfigsProps) => { : currentData; return ( - + diff --git a/src/containers/Configs/components/Startup/Startup.tsx b/src/containers/Configs/components/Startup/Startup.tsx new file mode 100644 index 0000000000..1a59840e19 --- /dev/null +++ b/src/containers/Configs/components/Startup/Startup.tsx @@ -0,0 +1,49 @@ +import {useThemeValue} from '@gravity-ui/uikit'; +import MonacoEditor from 'react-monaco-editor'; + +import {ResponseError} from '../../../../components/Errors/ResponseError'; +import {LoaderWrapper} from '../../../../components/LoaderWrapper/LoaderWrapper'; +import {configsApi} from '../../../../store/reducers/configs'; +import {useAutoRefreshInterval} from '../../../../utils/hooks/useAutoRefreshInterval'; + +interface StartupProps { + database?: string; + className?: string; +} + +const EDITOR_OPTIONS = { + automaticLayout: true, + selectOnLineNumbers: true, + readOnly: true, + minimap: { + enabled: false, + }, + wrappingIndent: 'indent' as const, +}; + +export function Startup({database, className}: StartupProps) { + const [autoRefreshInterval] = useAutoRefreshInterval(); + const theme = useThemeValue(); + const {currentData, isLoading, error} = configsApi.useGetConfigQuery( + {database}, + {pollingInterval: autoRefreshInterval}, + ); + + const {startup} = currentData || {}; + + return ( + + {error ? : null} + {startup ? ( +
+ +
+ ) : null} +
+ ); +} diff --git a/src/containers/Tenant/Diagnostics/Configs/i18n/en.json b/src/containers/Configs/i18n/en.json similarity index 53% rename from src/containers/Tenant/Diagnostics/Configs/i18n/en.json rename to src/containers/Configs/i18n/en.json index 6c8b079f1c..f009c88cf5 100644 --- a/src/containers/Tenant/Diagnostics/Configs/i18n/en.json +++ b/src/containers/Configs/i18n/en.json @@ -7,7 +7,13 @@ "disabled": "Disabled", "flag-touched": "Flag is changed", - "search-placeholder": "Search by feature flag", + "search-placeholder": "Search...", "search-empty": "Empty search result", - "no-data": "No data" + "no-data": "No data", + + "title_current": "Current", + "title_startup": "Startup", + "title_features": "Feature flags", + + "action_copy-config": "Copy config" } diff --git a/src/containers/Tenant/Diagnostics/Configs/i18n/index.ts b/src/containers/Configs/i18n/index.ts similarity index 67% rename from src/containers/Tenant/Diagnostics/Configs/i18n/index.ts rename to src/containers/Configs/i18n/index.ts index b730046175..a146812817 100644 --- a/src/containers/Tenant/Diagnostics/Configs/i18n/index.ts +++ b/src/containers/Configs/i18n/index.ts @@ -1,4 +1,4 @@ -import {registerKeysets} from '../../../../../utils/i18n'; +import {registerKeysets} from '../../../utils/i18n'; import en from './en.json'; diff --git a/src/containers/Configs/types.ts b/src/containers/Configs/types.ts new file mode 100644 index 0000000000..08d5efc3f0 --- /dev/null +++ b/src/containers/Configs/types.ts @@ -0,0 +1,24 @@ +import {z} from 'zod'; + +import i18n from './i18n'; + +export const ConfigTypes = { + current: 'current', + features: 'features', + startup: 'startup', +} as const; + +export const configTypesSchema = z.nativeEnum(ConfigTypes).catch(ConfigTypes.current); +export type ConfigType = z.infer; + +export const ConfigTypeTitles: Record = { + get current() { + return i18n('title_current'); + }, + get features() { + return i18n('title_features'); + }, + get startup() { + return i18n('title_startup'); + }, +}; diff --git a/src/containers/Configs/useConfigsQueryParams.ts b/src/containers/Configs/useConfigsQueryParams.ts new file mode 100644 index 0000000000..a1fd50e01b --- /dev/null +++ b/src/containers/Configs/useConfigsQueryParams.ts @@ -0,0 +1,61 @@ +import React from 'react'; + +import {StringParam, createEnumParam, useQueryParams, withDefault} from 'use-query-params'; + +import { + useBaseConfigAvailable, + useFeatureFlagsAvailable, +} from '../../store/reducers/capabilities/hooks'; + +import type {ConfigType} from './types'; +import {ConfigTypes} from './types'; + +const configTypesArray: ConfigType[] = [ + ConfigTypes.current, + ConfigTypes.startup, + ConfigTypes.features, +]; + +export const ConfigTypeValueEnum = createEnumParam(configTypesArray); +export const ConfigTypeValueParam = withDefault( + ConfigTypeValueEnum, + 'current', +); + +export function useConfigQueryParams() { + const isFeaturesAvailable = useFeatureFlagsAvailable(); + const isConfigsAvailable = useBaseConfigAvailable(); + const [{configType, search}, setQueryParams] = useQueryParams({ + configType: ConfigTypeValueParam, + search: StringParam, + }); + const handleConfigTypeChange = React.useCallback( + (value?: ConfigType) => { + setQueryParams({configType: value || undefined}, 'replaceIn'); + }, + [setQueryParams], + ); + const handleSearchChange = React.useCallback( + (value?: string) => { + setQueryParams({search: value || undefined}, 'replaceIn'); + }, + [setQueryParams], + ); + + React.useEffect(() => { + if (!isConfigsAvailable && !isFeaturesAvailable) { + handleConfigTypeChange(undefined); + } else if (!isFeaturesAvailable && configType === ConfigTypes.features) { + handleConfigTypeChange(ConfigTypes.current); + } else { + handleConfigTypeChange(ConfigTypes.features); + } + }, [isFeaturesAvailable, isConfigsAvailable]); + + return { + configType, + handleConfigTypeChange, + search, + handleSearchChange, + }; +} diff --git a/src/containers/Header/Header.scss b/src/containers/Header/Header.scss index 96fe8ff7c1..10a34be38f 100644 --- a/src/containers/Header/Header.scss +++ b/src/containers/Header/Header.scss @@ -4,6 +4,8 @@ justify-content: space-between; align-items: center; + height: var(--header-height, auto); + max-height: var(--header-height, auto); padding: 0 var(--g-spacing-5); border-bottom: 1px solid var(--g-color-line-generic); diff --git a/src/containers/Node/Node.scss b/src/containers/Node/Node.scss index d0db7fa48a..9a6470fbf1 100644 --- a/src/containers/Node/Node.scss +++ b/src/containers/Node/Node.scss @@ -1,6 +1,8 @@ @use '../../styles/mixins'; .node { + --ydb-configs-controls-height: 62px; + --ydb-configs-container-height: calc(100vh - var(--header-height)); position: relative; overflow: auto; @@ -31,4 +33,7 @@ &__tabs { @include mixins.tabs-wrapper-styles(); } + &__treads { + --ydb-table-with-controls-layout-controls-height: 0px; + } } diff --git a/src/containers/Node/Node.tsx b/src/containers/Node/Node.tsx index 6de74259f0..09421b36fd 100644 --- a/src/containers/Node/Node.tsx +++ b/src/containers/Node/Node.tsx @@ -15,6 +15,7 @@ import {PageMetaWithAutorefresh} from '../../components/PageMeta/PageMeta'; import routes from '../../routes'; import { useCapabilitiesLoaded, + useConfigAvailable, useDiskPagesAvailable, } from '../../store/reducers/capabilities/hooks'; import {setHeaderBreadcrumbs} from '../../store/reducers/header/header'; @@ -22,7 +23,9 @@ import {nodeApi} from '../../store/reducers/node/node'; import type {PreparedNode} from '../../store/reducers/node/types'; import {cn} from '../../utils/cn'; import {useAutoRefreshInterval, useTypedDispatch} from '../../utils/hooks'; +import {useIsViewerUser} from '../../utils/hooks/useIsUserAllowedToMakeChanges'; import {useAppTitle} from '../App/AppTitleContext'; +import {Configs} from '../Configs/Configs'; import {PaginatedStorage} from '../Storage/PaginatedStorage'; import {Tablets} from '../Tablets/Tablets'; @@ -40,6 +43,10 @@ const STORAGE_ROLE = 'Storage'; export function Node() { const container = React.useRef(null); + const isViewerUser = useIsViewerUser(); + const hasConfigs = useConfigAvailable(); + + const configsAvailable = isViewerUser && hasConfigs; const dispatch = useTypedDispatch(); @@ -71,22 +78,26 @@ export function Node() { const threadsQuantity = node?.Threads?.length; const {activeTab, nodeTabs} = React.useMemo(() => { - let actualNodeTabs = isStorageNode - ? NODE_TABS - : NODE_TABS.filter((el) => el.id !== 'storage'); + const skippedTabs: NodeTab[] = []; + if (!isStorageNode) { + skippedTabs.push('storage'); + } + if (!configsAvailable) { + skippedTabs.push('configs'); + } if (isDiskPagesAvailable) { - actualNodeTabs = actualNodeTabs.filter((el) => el.id !== 'structure'); + skippedTabs.push('structure'); } - // Filter out threads tab if there's no thread data in the API response if (!threadsQuantity) { - actualNodeTabs = actualNodeTabs.filter((el) => el.id !== 'threads'); + skippedTabs.push('threads'); } + const actualNodeTabs = NODE_TABS.filter((el) => !skippedTabs.includes(el.id)); const actualActiveTab = actualNodeTabs.find(({id}) => id === activeTabId) ?? actualNodeTabs[0]; return {activeTab: actualActiveTab, nodeTabs: actualNodeTabs}; - }, [isStorageNode, isDiskPagesAvailable, activeTabId, threadsQuantity]); + }, [isStorageNode, isDiskPagesAvailable, activeTabId, threadsQuantity, configsAvailable]); const database = tenantNameFromQuery?.toString(); @@ -255,7 +266,17 @@ function NodePageContent({ } case 'threads': { - return ; + return ( + + ); + } + + case 'configs': { + return ; } default: diff --git a/src/containers/Node/NodePages.ts b/src/containers/Node/NodePages.ts index 3f4c23ac13..2b8a5a5638 100644 --- a/src/containers/Node/NodePages.ts +++ b/src/containers/Node/NodePages.ts @@ -12,6 +12,7 @@ const NODE_TABS_IDS = { tablets: 'tablets', structure: 'structure', threads: 'threads', + configs: 'configs', } as const; export type NodeTab = ValueOf; @@ -41,6 +42,12 @@ export const NODE_TABS = [ return i18n('tabs.threads'); }, }, + { + id: NODE_TABS_IDS.configs, + get title() { + return i18n('tabs.configs'); + }, + }, ]; export const nodePageTabSchema = z.nativeEnum(NODE_TABS_IDS).catch(NODE_TABS_IDS.tablets); diff --git a/src/containers/Node/Threads/Threads.tsx b/src/containers/Node/Threads/Threads.tsx index 2328ada020..859dad6d74 100644 --- a/src/containers/Node/Threads/Threads.tsx +++ b/src/containers/Node/Threads/Threads.tsx @@ -1,6 +1,6 @@ import {ResponseError} from '../../../components/Errors/ResponseError'; -import {LoaderWrapper} from '../../../components/LoaderWrapper/LoaderWrapper'; import {ResizeableDataTable} from '../../../components/ResizeableDataTable/ResizeableDataTable'; +import {TableWithControlsLayout} from '../../../components/TableWithControlsLayout/TableWithControlsLayout'; import {nodeApi} from '../../../store/reducers/node/node'; import {DEFAULT_TABLE_SETTINGS} from '../../../utils/constants'; import {useAutoRefreshInterval} from '../../../utils/hooks'; @@ -11,11 +11,12 @@ import i18n from './i18n'; interface ThreadsProps { nodeId: string; className?: string; + scrollContainerRef: React.RefObject; } const THREADS_COLUMNS_WIDTH_LS_KEY = 'threadsTableColumnsWidth'; -export function Threads({nodeId, className}: ThreadsProps) { +export function Threads({nodeId, className, scrollContainerRef}: ThreadsProps) { const [autoRefreshInterval] = useAutoRefreshInterval(); const { @@ -27,15 +28,18 @@ export function Threads({nodeId, className}: ThreadsProps) { const data = nodeData?.Threads || []; return ( - + {error ? : null} - - + + + +
); } diff --git a/src/containers/Node/i18n/en.json b/src/containers/Node/i18n/en.json index 0762ce7af3..38ea152935 100644 --- a/src/containers/Node/i18n/en.json +++ b/src/containers/Node/i18n/en.json @@ -6,6 +6,7 @@ "tabs.structure": "Structure", "tabs.tablets": "Tablets", "tabs.threads": "Threads", + "tabs.configs": "Configs", "node": "Node", "fqdn": "FQDN", diff --git a/src/containers/Tenant/Diagnostics/Describe/Describe.tsx b/src/containers/Tenant/Diagnostics/Describe/Describe.tsx index 92c3eeb262..906d0291dc 100644 --- a/src/containers/Tenant/Diagnostics/Describe/Describe.tsx +++ b/src/containers/Tenant/Diagnostics/Describe/Describe.tsx @@ -1,5 +1,3 @@ -import {ClipboardButton} from '@gravity-ui/uikit'; - import {ResponseError} from '../../../../components/Errors/ResponseError'; import {JsonViewer} from '../../../../components/JsonViewer/JsonViewer'; import {useUnipikaConvert} from '../../../../components/JsonViewer/unipika/unipika'; @@ -45,12 +43,10 @@ const Describe = ({path, database, databaseFullPath}: IDescribeProps) => {
- } + withClipboardButton={{ + withLabel: false, + copyText: JSON.stringify(currentData), + }} search collapsedInitially /> diff --git a/src/containers/Tenant/Diagnostics/Diagnostics.scss b/src/containers/Tenant/Diagnostics/Diagnostics.scss index cf349c27af..94c229d652 100644 --- a/src/containers/Tenant/Diagnostics/Diagnostics.scss +++ b/src/containers/Tenant/Diagnostics/Diagnostics.scss @@ -26,6 +26,10 @@ } &__page-wrapper { + --ydb-configs-controls-height: 46px; + --ydb-configs-container-height: 100cqh; + --ydb-table-with-controls-layout-controls-height: 44px; + --ydb-table-with-controls-layout-controls-padding-top: 0px; overflow: auto; flex-grow: 1; @@ -37,20 +41,8 @@ margin-top: var(--diagnostics-margin-top); padding: 0 var(--g-spacing-5); - .ydb-table-with-controls-layout { - &__controls { - height: 46px; - padding-top: 0; - } - - &__controls-wrapper { - align-items: flex-start; - } - - .data-table__sticky_moving, - .ydb-paginated-table__head { - top: 46px !important; - } - } + // Use container query units to fix the height to this specific container + // This prevents inheritance issues with percentage-based calculations + container-type: size; } } diff --git a/src/containers/Tenant/Diagnostics/Diagnostics.tsx b/src/containers/Tenant/Diagnostics/Diagnostics.tsx index c7daa945b5..f8eb4fcb73 100644 --- a/src/containers/Tenant/Diagnostics/Diagnostics.tsx +++ b/src/containers/Tenant/Diagnostics/Diagnostics.tsx @@ -7,7 +7,7 @@ import {AutoRefreshControl} from '../../../components/AutoRefreshControl/AutoRef import {DrawerContextProvider} from '../../../components/Drawer/DrawerContext'; import {InternalLink} from '../../../components/InternalLink'; import { - useFeatureFlagsAvailable, + useConfigAvailable, useTopicDataAvailable, } from '../../../store/reducers/capabilities/hooks'; import {TENANT_DIAGNOSTICS_TABS_IDS} from '../../../store/reducers/tenant/constants'; @@ -17,6 +17,7 @@ import {uiFactory} from '../../../uiFactory/uiFactory'; import {cn} from '../../../utils/cn'; import {useScrollPosition, useTypedDispatch, useTypedSelector} from '../../../utils/hooks'; import {useIsViewerUser} from '../../../utils/hooks/useIsUserAllowedToMakeChanges'; +import {Configs} from '../../Configs/Configs'; import {Heatmap} from '../../Heatmap'; import {Nodes} from '../../Nodes/Nodes'; import {Operations} from '../../Operations'; @@ -27,7 +28,6 @@ import {useCurrentSchema} from '../TenantContext'; import {isDatabaseEntityType} from '../utils/schema'; import {AccessRights} from './AccessRights/AccessRights'; -import {Configs} from './Configs/Configs'; import {Consumers} from './Consumers'; import Describe from './Describe/Describe'; import DetailedOverview from './DetailedOverview/DetailedOverview'; @@ -63,15 +63,14 @@ function Diagnostics(props: DiagnosticsProps) { isDatabaseEntityType(type) ? database : '', ); - const hasFeatureFlags = useFeatureFlagsAvailable(); + const hasConfigs = useConfigAvailable(); const hasTopicData = useTopicDataAvailable(); const isViewerUser = useIsViewerUser(); const pages = getPagesByType(type, subType, { - hasFeatureFlags, hasTopicData, isTopLevel: path === database, hasBackups: typeof uiFactory.renderBackups === 'function' && Boolean(controlPlane), - hasConfigs: isViewerUser, + hasConfigs: isViewerUser && hasConfigs, hasAccess: uiFactory.hasAccess, databaseType, }); diff --git a/src/containers/Tenant/Diagnostics/DiagnosticsPages.ts b/src/containers/Tenant/Diagnostics/DiagnosticsPages.ts index 87a4de5a4a..67462186ab 100644 --- a/src/containers/Tenant/Diagnostics/DiagnosticsPages.ts +++ b/src/containers/Tenant/Diagnostics/DiagnosticsPages.ts @@ -16,7 +16,6 @@ type Page = { }; interface GetPagesOptions { - hasFeatureFlags?: boolean; hasTopicData?: boolean; isTopLevel?: boolean; hasBackups?: boolean; @@ -231,12 +230,7 @@ export const getPagesByType = ( const dbContext = isDatabaseEntityType(type) || options?.isTopLevel; const seeded = dbContext ? getDatabasePages(options?.databaseType) : base; - let withFlags = seeded; - if (!options?.hasFeatureFlags) { - withFlags = seeded.filter((p) => p.id !== TENANT_DIAGNOSTICS_TABS_IDS.configs); - } - - return applyFilters(withFlags, type, options); + return applyFilters(seeded, type, options); }; export const useDiagnosticsPageLinkGetter = () => { diff --git a/src/containers/Tenant/Query/QueriesHistory/QueriesHistory.scss b/src/containers/Tenant/Query/QueriesHistory/QueriesHistory.scss index 26a16c55dd..6fa3a97008 100644 --- a/src/containers/Tenant/Query/QueriesHistory/QueriesHistory.scss +++ b/src/containers/Tenant/Query/QueriesHistory/QueriesHistory.scss @@ -1,6 +1,7 @@ @use '../../../../styles/mixins.scss'; .ydb-queries-history { + --ydb-table-with-controls-layout-controls-height: 44px; overflow: auto; height: 100%; @@ -9,14 +10,10 @@ @include mixins.flex-container(); .ydb-table-with-controls-layout__controls { - height: 46px; + height: var(--ydb-table-with-controls-layout-controls-height); padding-top: 0; } - &.ydb-table-with-controls-layout .data-table__sticky_moving { - top: 46px !important; - } - &__search { @include mixins.search(); } diff --git a/src/containers/Tenant/Query/SavedQueries/SavedQueries.scss b/src/containers/Tenant/Query/SavedQueries/SavedQueries.scss index 165d339847..1d9962ba7a 100644 --- a/src/containers/Tenant/Query/SavedQueries/SavedQueries.scss +++ b/src/containers/Tenant/Query/SavedQueries/SavedQueries.scss @@ -3,6 +3,8 @@ .ydb-saved-queries { $block: &; + --ydb-table-with-controls-layout-controls-height: 44px; + overflow: auto; height: 100%; @@ -11,14 +13,10 @@ @include mixins.flex-container(); .ydb-table-with-controls-layout__controls { - height: 46px; + height: var(--ydb-table-with-controls-layout-controls-height); padding-top: 0; } - &.ydb-table-with-controls-layout .data-table__sticky_moving { - top: 46px !important; - } - &__search { @include mixins.search(); } diff --git a/src/services/api/viewer.ts b/src/services/api/viewer.ts index 1826d51ee5..6d9fba9a6f 100644 --- a/src/services/api/viewer.ts +++ b/src/services/api/viewer.ts @@ -522,7 +522,7 @@ export class ViewerAPI extends BaseYdbAPI { ); } - getClusterConfig(database?: string, {concurrentId, signal}: AxiosOptions = {}) { + getFeatureFlags(database?: string, {concurrentId, signal}: AxiosOptions = {}) { return this.get( this.getPath('/viewer/feature_flags'), { @@ -531,6 +531,15 @@ export class ViewerAPI extends BaseYdbAPI { {concurrentId, requestConfig: {signal}}, ); } + getConfig(database?: string, {concurrentId, signal}: AxiosOptions = {}) { + return this.get>( + this.getPath('/viewer/config'), + { + database, + }, + {concurrentId, requestConfig: {signal}}, + ); + } getVDiskBlobIndexStat( { diff --git a/src/store/reducers/capabilities/hooks.ts b/src/store/reducers/capabilities/hooks.ts index 7787636b3c..4fe87b3fae 100644 --- a/src/store/reducers/capabilities/hooks.ts +++ b/src/store/reducers/capabilities/hooks.ts @@ -87,6 +87,16 @@ export const useClusterDashboardAvailable = () => { export const useStreamingAvailable = () => { return useGetFeatureVersion('/viewer/query') >= 8; }; +export const useBaseConfigAvailable = () => { + return useGetFeatureVersion('/viewer/config') >= 1; +}; + +export const useConfigAvailable = () => { + const isBaseConfigsAvailable = useBaseConfigAvailable(); + const isFeaturesAvailable = useFeatureFlagsAvailable(); + return isBaseConfigsAvailable || isFeaturesAvailable; +}; + export const useEditAccessAvailable = () => { return useGetFeatureVersion('/viewer/acl') >= 2 && !uiFactory.hideGrantAccess; }; diff --git a/src/store/reducers/configs.ts b/src/store/reducers/configs.ts new file mode 100644 index 0000000000..51eccf541e --- /dev/null +++ b/src/store/reducers/configs.ts @@ -0,0 +1,34 @@ +import {api} from './api'; + +export const configsApi = api.injectEndpoints({ + endpoints: (build) => ({ + getFeatureFlags: build.query({ + queryFn: async ({database}: {database?: string}, {signal}) => { + try { + const res = await window.api.viewer.getFeatureFlags(database, {signal}); + const db = res.Databases[0]; + + return {data: db.FeatureFlags}; + } catch (error) { + return {error}; + } + }, + providesTags: ['All'], + }), + getConfig: build.query({ + queryFn: async ({database}: {database?: string}, {signal}) => { + try { + const res = await window.api.viewer.getConfig(database, {signal}); + + const {StartupConfigYaml, ...rest} = res; + + return {data: {current: rest, startup: String(StartupConfigYaml)}}; + } catch (error) { + return {error}; + } + }, + providesTags: ['All'], + }), + }), + overrideExisting: 'throw', +}); diff --git a/src/store/reducers/tenant/tenant.ts b/src/store/reducers/tenant/tenant.ts index 395f535d96..9cd40d182e 100644 --- a/src/store/reducers/tenant/tenant.ts +++ b/src/store/reducers/tenant/tenant.ts @@ -108,20 +108,6 @@ export const tenantApi = api.injectEndpoints({ return {clusterName, database}; }, }), - - getClusterConfig: builder.query({ - queryFn: async ({database}: {database: string}, {signal}) => { - try { - const res = await window.api.viewer.getClusterConfig(database, {signal}); - const db = res.Databases[0]; - - return {data: db.FeatureFlags}; - } catch (error) { - return {error}; - } - }, - providesTags: ['All'], - }), }), overrideExisting: 'throw', }); diff --git a/src/styles/mixins.scss b/src/styles/mixins.scss index a991d07f51..5ccd333d18 100644 --- a/src/styles/mixins.scss +++ b/src/styles/mixins.scss @@ -57,9 +57,9 @@ line-height: var(--g-text-header-2-line-height); } -@mixin sticky-top { +@mixin sticky-top($top: 0) { position: sticky; - top: 0; + top: $top; left: 0; background-color: var(--g-color-base-background); @@ -88,12 +88,12 @@ text-overflow: ellipsis; } -@mixin controls() { +@mixin controls($padding-top: var(--g-spacing-4)) { display: flex; align-items: center; gap: 12px; - padding: 16px 0 18px; + padding: $padding-top 0 var(--g-spacing-4); } @mixin search() { diff --git a/src/types/api/capabilities.ts b/src/types/api/capabilities.ts index 769ca52428..728085ab5f 100644 --- a/src/types/api/capabilities.ts +++ b/src/types/api/capabilities.ts @@ -21,6 +21,7 @@ export type Capability = | '/storage/groups' | '/viewer/query' | '/viewer/feature_flags' + | '/viewer/config' | '/viewer/cluster' | '/viewer/nodes' | '/viewer/acl'