diff --git a/src/components/FixedHeightQuery/FixedHeightQuery.scss b/src/components/FixedHeightQuery/FixedHeightQuery.scss index 02fa7124df..e61f2cad7d 100644 --- a/src/components/FixedHeightQuery/FixedHeightQuery.scss +++ b/src/components/FixedHeightQuery/FixedHeightQuery.scss @@ -23,7 +23,7 @@ height: 100% !important; margin: 0 !important; - padding: var(--g-spacing-2) !important; + padding: 0 !important; white-space: pre-wrap !important; text-overflow: ellipsis !important; @@ -39,4 +39,8 @@ word-break: break-word !important; } } + .ydb-syntax-highlighter__copy { + top: 0; + right: 0; + } } diff --git a/src/components/FixedHeightQuery/FixedHeightQuery.tsx b/src/components/FixedHeightQuery/FixedHeightQuery.tsx index 1eefe60f90..3af165a8fd 100644 --- a/src/components/FixedHeightQuery/FixedHeightQuery.tsx +++ b/src/components/FixedHeightQuery/FixedHeightQuery.tsx @@ -7,14 +7,17 @@ import './FixedHeightQuery.scss'; const b = cn('ydb-fixed-height-query'); -const FIXED_PADDING = 8; +const FIXED_PADDING = 0; const LINE_HEIGHT = 20; +type FixedHeightQueryMode = 'fixed' | 'max'; + interface FixedHeightQueryProps { value?: string; lines?: number; hasClipboardButton?: boolean; clipboardButtonAlwaysVisible?: boolean; + mode?: FixedHeightQueryMode; } export const FixedHeightQuery = ({ @@ -22,18 +25,21 @@ export const FixedHeightQuery = ({ lines = 4, hasClipboardButton, clipboardButtonAlwaysVisible, + mode = 'fixed', }: FixedHeightQueryProps) => { const heightValue = `${lines * LINE_HEIGHT + FIXED_PADDING}px`; // Remove empty lines from the beginning (lines with only whitespace are considered empty) const trimmedValue = value.replace(/^(\s*\n)+/, ''); + const heightStyle = mode === 'fixed' ? {height: heightValue} : {maxHeight: heightValue}; + return (
void; +} + +export function SeeAllButton({to, className, onClick}: SeeAllButtonProps) { + return ( + + + {i18n('action_see-all')} + + + + ); +} diff --git a/src/components/SeeAllButton/i18n/en.json b/src/components/SeeAllButton/i18n/en.json new file mode 100644 index 0000000000..6bbc8ea1b9 --- /dev/null +++ b/src/components/SeeAllButton/i18n/en.json @@ -0,0 +1,3 @@ +{ + "action_see-all": "See all" +} diff --git a/src/components/SeeAllButton/i18n/index.ts b/src/components/SeeAllButton/i18n/index.ts new file mode 100644 index 0000000000..9ebba16c89 --- /dev/null +++ b/src/components/SeeAllButton/i18n/index.ts @@ -0,0 +1,7 @@ +import {registerKeysets} from '../../../utils/i18n'; + +import en from './en.json'; + +const COMPONENT = 'ydb-see-all-button'; + +export default registerKeysets(COMPONENT, {en}); diff --git a/src/components/ShardsTable/columns.tsx b/src/components/ShardsTable/columns.tsx index f555a0f4b0..ab6f9538ce 100644 --- a/src/components/ShardsTable/columns.tsx +++ b/src/components/ShardsTable/columns.tsx @@ -80,7 +80,7 @@ export const getCpuCoresColumn: GetShardsColumn = () => { ); }, - align: DataTable.RIGHT, + align: DataTable.LEFT, width: 110, resizeMinWidth: 110, }; diff --git a/src/components/SyntaxHighlighter/YDBSyntaxHighlighter.tsx b/src/components/SyntaxHighlighter/YDBSyntaxHighlighter.tsx index 28b4b47003..52382ccbc9 100644 --- a/src/components/SyntaxHighlighter/YDBSyntaxHighlighter.tsx +++ b/src/components/SyntaxHighlighter/YDBSyntaxHighlighter.tsx @@ -1,5 +1,6 @@ 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'; @@ -27,6 +28,7 @@ interface ClipboardButtonOptions { alwaysVisible?: boolean; copyText?: string; withLabel?: boolean; + size?: ButtonSize; } export type WithClipboardButtonProp = ClipboardButtonOptions | boolean; @@ -58,28 +60,22 @@ export function YDBSyntaxHighlighter({ registerLangAndUpdateKey(); }, [language]); + const clipboardButtonProps = + typeof withClipboardButton === 'object' ? withClipboardButton : undefined; + const renderCopyButton = () => { if (withClipboardButton) { return (
e.stopPropagation()}> - {typeof withClipboardButton === 'object' && - withClipboardButton.withLabel === false - ? null - : i18n('copy')} + {clipboardButtonProps?.withLabel === false ? null : i18n('copy')}
); diff --git a/src/components/SyntaxHighlighter/themes.ts b/src/components/SyntaxHighlighter/themes.ts index 3c5204731d..c0886419d4 100644 --- a/src/components/SyntaxHighlighter/themes.ts +++ b/src/components/SyntaxHighlighter/themes.ts @@ -7,12 +7,14 @@ export const lightTransparent = { ...materialLight['pre[class*="language-"]'], background: 'transparent', margin: 0, + lineHeight: '15px', }, 'code[class*="language-"]': { ...materialLight['code[class*="language-"]'], background: 'transparent', color: 'var(--g-color-text-primary)', whiteSpace: 'pre-wrap' as const, + fontSize: '13px', }, comment: { color: '#969896', @@ -49,12 +51,14 @@ export const darkTransparent = { ...vscDarkPlus['pre[class*="language-"]'], background: 'transparent', margin: 0, + lineHeight: '15px', }, 'code[class*="language-"]': { ...vscDarkPlus['code[class*="language-"]'], background: 'transparent', color: 'var(--g-color-text-primary)', whiteSpace: 'pre-wrap' as const, + fontSize: '13px', }, comment: { color: '#969896', @@ -91,10 +95,12 @@ const dark: Record = { ...darkTransparent['pre[class*="language-"]'], background: vscDarkPlus['pre[class*="language-"]'].background, scrollbarColor: `var(--g-color-scroll-handle) transparent`, + lineHeight: '15px', }, 'code[class*="language-"]': { ...darkTransparent['code[class*="language-"]'], whiteSpace: 'pre', + fontSize: '13px', }, }; @@ -104,10 +110,12 @@ const light: Record = { ...lightTransparent['pre[class*="language-"]'], background: 'var(--g-color-base-misc-light)', scrollbarColor: `var(--g-color-scroll-handle) transparent`, + lineHeight: '15px', }, 'code[class*="language-"]': { ...lightTransparent['code[class*="language-"]'], whiteSpace: 'pre', + fontSize: '13px', }, }; diff --git a/src/containers/Tenant/Diagnostics/Diagnostics.tsx b/src/containers/Tenant/Diagnostics/Diagnostics.tsx index 6ccb76e6eb..55a25031fa 100644 --- a/src/containers/Tenant/Diagnostics/Diagnostics.tsx +++ b/src/containers/Tenant/Diagnostics/Diagnostics.tsx @@ -16,7 +16,7 @@ import {setDiagnosticsTab} from '../../../store/reducers/tenant/tenant'; import type {AdditionalNodesProps, AdditionalTenantsProps} from '../../../types/additionalProps'; import {uiFactory} from '../../../uiFactory/uiFactory'; import {cn} from '../../../utils/cn'; -import {useTypedDispatch, useTypedSelector} from '../../../utils/hooks'; +import {useScrollPosition, useTypedDispatch, useTypedSelector} from '../../../utils/hooks'; import {Heatmap} from '../../Heatmap'; import {Nodes} from '../../Nodes/Nodes'; import {Operations} from '../../Operations'; @@ -200,6 +200,12 @@ function Diagnostics(props: DiagnosticsProps) { ); }; + useScrollPosition( + containerRef, + `tenant-diagnostics-${tenantName}-${activeTab?.id}`, + activeTab?.id === TENANT_DIAGNOSTICS_TABS_IDS.overview, + ); + return (
{activeTab ? ( diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/StatsWrapper/StatsWrapper.scss b/src/containers/Tenant/Diagnostics/TenantOverview/StatsWrapper/StatsWrapper.scss new file mode 100644 index 0000000000..47cf8a3374 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TenantOverview/StatsWrapper/StatsWrapper.scss @@ -0,0 +1,14 @@ +.ydb-stats-wrapper { + overflow: auto; + + padding: var(--g-spacing-4); + + border: 1px solid var(--g-color-line-generic); + border-radius: var(--g-border-radius-xs); + + &__header { + position: sticky; + top: 0; + left: 0; + } +} diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/StatsWrapper/StatsWrapper.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/StatsWrapper/StatsWrapper.tsx new file mode 100644 index 0000000000..765ef92286 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TenantOverview/StatsWrapper/StatsWrapper.tsx @@ -0,0 +1,41 @@ +import {Flex, Text} from '@gravity-ui/uikit'; + +import {SeeAllButton} from '../../../../../components/SeeAllButton/SeeAllButton'; +import {cn} from '../../../../../utils/cn'; + +const b = cn('ydb-stats-wrapper'); + +import './StatsWrapper.scss'; + +interface StatsWrapperProps { + children: React.ReactNode; + className?: string; + title: string; + description?: string; + allEntitiesLink?: string; + onAllEntitiesClick?: () => void; +} + +export function StatsWrapper({ + children, + className, + title, + description, + allEntitiesLink, + onAllEntitiesClick, +}: StatsWrapperProps) { + return ( + + + + {title} + {description && {description}} + + {allEntitiesLink && ( + + )} + + {children} + + ); +} diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TenantCpu.scss b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TenantCpu.scss deleted file mode 100644 index 048627b86e..0000000000 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TenantCpu.scss +++ /dev/null @@ -1,21 +0,0 @@ -@use '../../../../../styles/mixins.scss'; - -.tenant-cpu { - &__tabs-container { - margin-top: var(--g-spacing-3); - } - - &__tab-content { - margin-top: var(--g-spacing-3); - } - - &__all-nodes-link { - display: flex; - align-items: center; - gap: var(--g-spacing-1); - - margin-right: var(--g-spacing-2); - - @include mixins.body-1-typography(); - } -} diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TenantCpu.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TenantCpu.tsx index 3a85c28655..b04026f366 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TenantCpu.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TenantCpu.tsx @@ -1,17 +1,11 @@ -import React from 'react'; +import {Flex} from '@gravity-ui/uikit'; -import {ArrowRight} from '@gravity-ui/icons'; -import {Flex, Icon, SegmentedRadioGroup, Tab, TabList, TabProvider} from '@gravity-ui/uikit'; - -import {InternalLink} from '../../../../../components/InternalLink'; -import { - TENANT_CPU_NODES_MODE_IDS, - TENANT_CPU_TABS_IDS, - TENANT_DIAGNOSTICS_TABS_IDS, -} from '../../../../../store/reducers/tenant/constants'; +import {setTopQueriesFilters} from '../../../../../store/reducers/executeTopQueries/executeTopQueries'; +import {TENANT_DIAGNOSTICS_TABS_IDS} from '../../../../../store/reducers/tenant/constants'; import type {AdditionalNodesProps} from '../../../../../types/additionalProps'; -import {cn} from '../../../../../utils/cn'; +import {useTypedDispatch} from '../../../../../utils/hooks'; import {useDiagnosticsPageLinkGetter} from '../../../Diagnostics/DiagnosticsPages'; +import {StatsWrapper} from '../StatsWrapper/StatsWrapper'; import {TenantDashboard} from '../TenantDashboard/TenantDashboard'; import i18n from '../i18n'; @@ -20,17 +14,6 @@ import {TopNodesByLoad} from './TopNodesByLoad'; import {TopQueries} from './TopQueries'; import {TopShards} from './TopShards'; import {cpuDashboardConfig} from './cpuDashboardConfig'; -import {useTenantCpuQueryParams} from './useTenantCpuQueryParams'; - -import './TenantCpu.scss'; - -const b = cn('tenant-cpu'); - -const cpuTabs = [ - {id: TENANT_CPU_TABS_IDS.nodes, title: i18n('title_top-nodes')}, - {id: TENANT_CPU_TABS_IDS.shards, title: i18n('title_top-shards')}, - {id: TENANT_CPU_TABS_IDS.queries, title: i18n('title_top-queries')}, -]; interface TenantCpuProps { tenantName: string; @@ -38,88 +21,40 @@ interface TenantCpuProps { } export function TenantCpu({tenantName, additionalNodesProps}: TenantCpuProps) { - const {cpuTab, nodesMode, handleCpuTabChange, handleNodesModeChange} = - useTenantCpuQueryParams(); + const dispatch = useTypedDispatch(); const getDiagnosticsPageLink = useDiagnosticsPageLinkGetter(); - const renderNodesContent = () => { - const nodesModeControl = ( - - - {i18n('action_by-load')} - - - {i18n('action_by-pool-usage')} - - - ); + const allNodesLink = getDiagnosticsPageLink(TENANT_DIAGNOSTICS_TABS_IDS.nodes); + const topShardsLink = getDiagnosticsPageLink(TENANT_DIAGNOSTICS_TABS_IDS.topShards); + const topQueriesLink = getDiagnosticsPageLink(TENANT_DIAGNOSTICS_TABS_IDS.topQueries); - const allNodesButton = ( - - {i18n('action_all-nodes')} - - - ); - - const nodesComponent = - nodesMode === TENANT_CPU_NODES_MODE_IDS.load ? ( + return ( + + + - ) : ( + + - ); - - return ( - - - {nodesModeControl} - {allNodesButton} - - {nodesComponent} - - ); - }; - - const renderTabContent = () => { - switch (cpuTab) { - case TENANT_CPU_TABS_IDS.nodes: - return renderNodesContent(); - case TENANT_CPU_TABS_IDS.shards: - return ; - case TENANT_CPU_TABS_IDS.queries: - return ; - default: - return null; - } - }; - - return ( - - - -
- - - {cpuTabs.map(({id, title}) => { - return ( - handleCpuTabChange(id)}> - {title} - - ); - })} - - - -
{renderTabContent()}
-
-
+
+ + + + + dispatch(setTopQueriesFilters({from: undefined, to: undefined})) + } + > + + +
); } diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx index afa9f2b982..f33b1dd43f 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopQueries.tsx @@ -1,108 +1,53 @@ -import React from 'react'; - -import {useHistory, useLocation} from 'react-router-dom'; - -import {ResizeableDataTable} from '../../../../../components/ResizeableDataTable/ResizeableDataTable'; -import {parseQuery} from '../../../../../routes'; -import { - setTopQueriesFilters, - topQueriesApi, -} from '../../../../../store/reducers/executeTopQueries/executeTopQueries'; -import {changeUserInput, setIsDirty} from '../../../../../store/reducers/query/query'; -import { - TENANT_DIAGNOSTICS_TABS_IDS, - TENANT_PAGE, - TENANT_PAGES_IDS, - TENANT_QUERY_TABS_ID, -} from '../../../../../store/reducers/tenant/constants'; +import {topQueriesApi} from '../../../../../store/reducers/executeTopQueries/executeTopQueries'; import { TENANT_OVERVIEW_TABLES_LIMIT, TENANT_OVERVIEW_TABLES_SETTINGS, } from '../../../../../utils/constants'; -import {useAutoRefreshInterval, useTypedDispatch} from '../../../../../utils/hooks'; -import {useChangeInputWithConfirmation} from '../../../../../utils/hooks/withConfirmation/useChangeInputWithConfirmation'; +import {useAutoRefreshInterval} from '../../../../../utils/hooks'; import {parseQueryErrorToString} from '../../../../../utils/query'; -import {TenantTabsGroups, getTenantPath} from '../../../TenantPages'; +import {QueriesTableWithDrawer} from '../../TopQueries/QueriesTableWithDrawer'; import {getTenantOverviewTopQueriesColumns} from '../../TopQueries/columns/columns'; import {TOP_QUERIES_COLUMNS_WIDTH_LS_KEY} from '../../TopQueries/columns/constants'; +import {useTopQueriesSort} from '../../TopQueries/hooks/useTopQueriesSort'; import {TenantOverviewTableLayout} from '../TenantOverviewTableLayout'; -import {getSectionTitle} from '../getSectionTitle'; -import i18n from '../i18n'; -import {b} from '../utils'; interface TopQueriesProps { tenantName: string; } -export function TopQueries({tenantName}: TopQueriesProps) { - const dispatch = useTypedDispatch(); - const location = useLocation(); - const history = useHistory(); - - const query = parseQuery(location); +const columns = getTenantOverviewTopQueriesColumns(); +export function TopQueries({tenantName}: TopQueriesProps) { const [autoRefreshInterval] = useAutoRefreshInterval(); - const columns = React.useMemo(() => { - return getTenantOverviewTopQueriesColumns(); - }, []); + const {backendSort} = useTopQueriesSort(); const {currentData, isFetching, error} = topQueriesApi.useGetTopQueriesQuery( - {database: tenantName, timeFrame: 'hour', limit: TENANT_OVERVIEW_TABLES_LIMIT}, + { + database: tenantName, + timeFrame: 'hour', + limit: TENANT_OVERVIEW_TABLES_LIMIT, + sortOrder: backendSort, + }, {pollingInterval: autoRefreshInterval}, ); const loading = isFetching && currentData === undefined; const data = currentData?.resultSets?.[0]?.result || []; - const applyRowClick = React.useCallback( - (row: any) => { - const {QueryText: input} = row; - - dispatch(changeUserInput({input})); - dispatch(setIsDirty(false)); - - const queryParams = parseQuery(location); - - const queryPath = getTenantPath({ - ...queryParams, - [TENANT_PAGE]: TENANT_PAGES_IDS.query, - [TenantTabsGroups.queryTab]: TENANT_QUERY_TABS_ID.newQuery, - }); - - history.push(queryPath); - }, - [dispatch, history, location], - ); - - const handleRowClick = useChangeInputWithConfirmation(applyRowClick); - - const title = getSectionTitle({ - entity: i18n('queries'), - postfix: i18n('by-cpu-time', {executionPeriod: i18n('executed-last-hour')}), - onClick: () => { - dispatch(setTopQueriesFilters({from: undefined, to: undefined})); - }, - link: getTenantPath({ - ...query, - [TenantTabsGroups.diagnosticsTab]: TENANT_DIAGNOSTICS_TABS_IDS.topQueries, - }), - }); - return ( - b('top-queries-row')} - settings={TENANT_OVERVIEW_TABLES_SETTINGS} + tableSettings={TENANT_OVERVIEW_TABLES_SETTINGS} + drawerId="tenant-overview-query-details" + storageKey="tenant-overview-queries-drawer-width" /> ); diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopShards.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopShards.tsx index 92c4b04125..0e7a0c9459 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopShards.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopShards.tsx @@ -1,18 +1,10 @@ -import {useLocation} from 'react-router-dom'; - import {useComponent} from '../../../../../components/ComponentsProvider/ComponentsProvider'; import type {TopShardsColumnId} from '../../../../../components/ShardsTable/constants'; -import {parseQuery} from '../../../../../routes'; -import {TENANT_DIAGNOSTICS_TABS_IDS} from '../../../../../store/reducers/tenant/constants'; import {topShardsApi} from '../../../../../store/reducers/tenantOverview/topShards/tenantOverviewTopShards'; import {TENANT_OVERVIEW_TABLES_SETTINGS} from '../../../../../utils/constants'; import {useAutoRefreshInterval} from '../../../../../utils/hooks'; import {parseQueryErrorToString} from '../../../../../utils/query'; -import {TenantTabsGroups, getTenantPath} from '../../../TenantPages'; import {TenantOverviewTableLayout} from '../TenantOverviewTableLayout'; -import {getSectionTitle} from '../getSectionTitle'; -import i18n from '../i18n'; - const columnsIds: TopShardsColumnId[] = ['TabletId', 'Path', 'CPUCores']; interface TopShardsProps { @@ -23,10 +15,6 @@ interface TopShardsProps { export const TopShards = ({tenantName, path}: TopShardsProps) => { const ShardsTable = useComponent('ShardsTable'); - const location = useLocation(); - - const query = parseQuery(location); - const [autoRefreshInterval] = useAutoRefreshInterval(); const {currentData, isFetching, error} = topShardsApi.useGetTopShardsQuery( @@ -37,18 +25,8 @@ export const TopShards = ({tenantName, path}: TopShardsProps) => { const loading = isFetching && currentData === undefined; const data = currentData?.resultSets?.[0]?.result || []; - const title = getSectionTitle({ - entity: i18n('shards'), - postfix: i18n('by-cpu-usage'), - link: getTenantPath({ - ...query, - [TenantTabsGroups.diagnosticsTab]: TENANT_DIAGNOSTICS_TABS_IDS.topShards, - }), - }); - return ( { - setQueryParams({cpuTab: value}, 'replaceIn'); - }; - - const handleNodesModeChange = (value: TenantNodesMode) => { - setQueryParams({nodesMode: value}, 'replaceIn'); - }; - - return { - cpuTab, - nodesMode, - handleCpuTabChange, - handleNodesModeChange, - }; -} diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json b/src/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json index 5059b37fa0..292498fec3 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json +++ b/src/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json @@ -1,24 +1,14 @@ { - "no-data": "No data", - "no-pools-data": "No pools data", "top-nodes.empty-data": "No such nodes", - "top-groups.empty-data": "No such groups", "top": "Top", "nodes": "nodes", - "shards": "shards", "groups": "groups", - "queries": "queries", - "tables": "tables", - "by-pools-usage": "by pools usage", - "by-cpu-time": "by cpu time, {{executionPeriod}}", - "by-cpu-usage": "by cpu usage", - "by-load": "by load", "by-memory": "by memory", "by-ping": "by ping time", "by-skew": "by clock skew", "by-usage": "by usage", - "by-size": "by size", - "action_all-nodes": "All Nodes", + "title_top-nodes-load": "Top nodes by load", + "title_top-nodes-pool": "Top nodes by pools usage", "cards.cpu-label": "CPU load", "cards.storage-label": "Storage", "cards.memory-label": "Memory used", @@ -43,11 +33,8 @@ "storage.tablet-storage-description": "Size of user data and indexes stored in schema objects (tables, topics, etc.)", "storage.db-storage-title": "Database storage", "storage.db-storage-description": "Size of data stored in distributed storage with all overheads for redundancy", - "executed-last-hour": "executed in the last hour", - "column-header.process": "Process", - "title_top-nodes": "Top Nodes", - "title_top-shards": "Top Shards", - "title_top-queries": "Top Queries", + "title_top-shards": "Top shards by CPU usage", + "title_top-queries": "Top queries by CPU usage", "title_top-tables-by-size": "Top Tables By Size", "title_top-groups-by-usage": "Top Groups By Usage", "title_nodes-by-ping": "Nodes By Ping Time", diff --git a/src/containers/Tenant/Diagnostics/TopQueries/QueriesTableWithDrawer.tsx b/src/containers/Tenant/Diagnostics/TopQueries/QueriesTableWithDrawer.tsx new file mode 100644 index 0000000000..6916cee1d3 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TopQueries/QueriesTableWithDrawer.tsx @@ -0,0 +1,116 @@ +import React from 'react'; + +import type {Column, Settings, SortOrder} from '@gravity-ui/react-data-table'; +import {isEqual} from 'lodash'; + +import {DrawerWrapper} from '../../../../components/Drawer'; +import type {DrawerControl} from '../../../../components/Drawer/Drawer'; +import {ResizeableDataTable} from '../../../../components/ResizeableDataTable/ResizeableDataTable'; +import type {KeyValueRow} from '../../../../types/api/query'; +import {cn} from '../../../../utils/cn'; + +import {QueryDetailsDrawerContent} from './QueryDetails/QueryDetailsDrawerContent'; +import i18n from './i18n'; +import {TOP_QUERIES_TABLE_SETTINGS} from './utils'; + +const b = cn('kv-top-queries'); + +interface SimpleTableWithDrawerProps { + columns: Column[]; + data: KeyValueRow[]; + loading?: boolean; + onRowClick?: ( + row: KeyValueRow | null, + index?: number, + event?: React.MouseEvent, + ) => void; + columnsWidthLSKey?: string; + emptyDataMessage?: string; + sortOrder?: SortOrder | SortOrder[]; + onSort?: (sortOrder: SortOrder | SortOrder[] | undefined) => void; + selectedRow?: KeyValueRow | null | undefined; + onSelectedRowChange?: (row: KeyValueRow | null | undefined) => void; + drawerControls?: DrawerControl[]; + drawerId?: string; + storageKey?: string; + tableSettings?: Settings; +} + +export function QueriesTableWithDrawer({ + columns, + data, + loading, + onRowClick, + columnsWidthLSKey, + emptyDataMessage, + sortOrder, + onSort, + selectedRow: externalSelectedRow, + onSelectedRowChange, + drawerControls, + drawerId = 'query-details', + storageKey = 'kv-top-queries-drawer-width', + tableSettings = TOP_QUERIES_TABLE_SETTINGS, +}: SimpleTableWithDrawerProps) { + const [internalSelectedRow, setInternalSelectedRow] = React.useState< + KeyValueRow | null | undefined + >(undefined); + + const selectedRow = + externalSelectedRow === undefined ? internalSelectedRow : externalSelectedRow; + const setSelectedRow = onSelectedRowChange || setInternalSelectedRow; + + const handleCloseDetails = React.useCallback(() => { + setSelectedRow(undefined); + }, [setSelectedRow]); + + const isDrawerVisible = selectedRow !== undefined; + + const handleRowClick = React.useCallback( + ( + row: KeyValueRow | null, + index?: number, + event?: React.MouseEvent, + ) => { + event?.stopPropagation(); + setSelectedRow(row); + onRowClick?.(row, index, event); + }, + [onRowClick, setSelectedRow], + ); + + const renderDrawerContent = React.useCallback( + () => , + [selectedRow, handleCloseDetails], + ); + + const defaultDrawerControls: DrawerControl[] = React.useMemo(() => [{type: 'close'}], []); + const finalDrawerControls = drawerControls || defaultDrawerControls; + + return ( + + b('row', {active: isEqual(row, selectedRow)})} + sortOrder={sortOrder} + onSort={onSort} + /> + + ); +} diff --git a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx index bebf6d9756..d1382e4cf4 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/RunningQueriesData.tsx @@ -2,12 +2,8 @@ import React from 'react'; import type {Column} from '@gravity-ui/react-data-table'; import {TableColumnSetup} from '@gravity-ui/uikit'; -import {isEqual} from 'lodash'; -import {DrawerWrapper} from '../../../../components/Drawer'; -import type {DrawerControl} from '../../../../components/Drawer/Drawer'; import {ResponseError} from '../../../../components/Errors/ResponseError'; -import {ResizeableDataTable} from '../../../../components/ResizeableDataTable/ResizeableDataTable'; import {Search} from '../../../../components/Search'; import {TableWithControlsLayout} from '../../../../components/TableWithControlsLayout/TableWithControlsLayout'; import {topQueriesApi} from '../../../../store/reducers/executeTopQueries/executeTopQueries'; @@ -17,7 +13,7 @@ import {useAutoRefreshInterval, useTypedSelector} from '../../../../utils/hooks' import {useSelectedColumns} from '../../../../utils/hooks/useSelectedColumns'; import {parseQueryErrorToString} from '../../../../utils/query'; -import {QueryDetailsDrawerContent} from './QueryDetails/QueryDetailsDrawerContent'; +import {QueriesTableWithDrawer} from './QueriesTableWithDrawer'; import {getRunningQueriesColumns} from './columns/columns'; import { DEFAULT_RUNNING_QUERIES_COLUMNS, @@ -45,9 +41,6 @@ export const RunningQueriesData = ({ }: RunningQueriesDataProps) => { const [autoRefreshInterval] = useAutoRefreshInterval(); const filters = useTypedSelector((state) => state.executeTopQueries); - // Internal state for selected row - // null is reserved for not found state - const [selectedRow, setSelectedRow] = React.useState(undefined); // Get columns for running queries const columns: Column[] = React.useMemo(() => { @@ -76,86 +69,43 @@ export const RunningQueriesData = ({ const rows = currentData?.resultSets?.[0]?.result; - const isDrawerVisible = selectedRow !== undefined; - - const handleCloseDetails = React.useCallback(() => { - setSelectedRow(undefined); - }, [setSelectedRow]); - - const renderDrawerContent = React.useCallback( - () => , - [selectedRow, handleCloseDetails], - ); - - const onRowClick = React.useCallback( - ( - row: KeyValueRow | null, - _index?: number, - event?: React.MouseEvent, - ) => { - event?.stopPropagation(); - setSelectedRow(row); - }, - [setSelectedRow], - ); - const inputRef = React.useRef(null); - React.useEffect(() => { - if (isDrawerVisible) { - inputRef.current?.blur(); - } - }, [isDrawerVisible]); - - const drawerControls: DrawerControl[] = React.useMemo(() => [{type: 'close'}], []); - return ( - - - - {renderQueryModeControl()} - - - - - {error ? : null} - - b('row', {active: isEqual(row, selectedRow)})} - sortOrder={tableSort} - onSort={handleTableSort} - /> - - - + + + {renderQueryModeControl()} + + + + + {error ? : null} + + + + ); }; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx index 70be0027d1..17c5483874 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/TopQueriesData.tsx @@ -2,14 +2,11 @@ import React from 'react'; import type {Column} from '@gravity-ui/react-data-table'; import {Select, TableColumnSetup} from '@gravity-ui/uikit'; -import {isEqual} from 'lodash'; import type {DateRangeValues} from '../../../../components/DateRange'; import {DateRange} from '../../../../components/DateRange'; -import {DrawerWrapper} from '../../../../components/Drawer'; import type {DrawerControl} from '../../../../components/Drawer/Drawer'; import {ResponseError} from '../../../../components/Errors/ResponseError'; -import {ResizeableDataTable} from '../../../../components/ResizeableDataTable/ResizeableDataTable'; import {Search} from '../../../../components/Search'; import {TableWithControlsLayout} from '../../../../components/TableWithControlsLayout/TableWithControlsLayout'; import {topQueriesApi} from '../../../../store/reducers/executeTopQueries/executeTopQueries'; @@ -20,7 +17,7 @@ import {useAutoRefreshInterval, useTypedSelector} from '../../../../utils/hooks' import {useSelectedColumns} from '../../../../utils/hooks/useSelectedColumns'; import {parseQueryErrorToString} from '../../../../utils/query'; -import {QueryDetailsDrawerContent} from './QueryDetails/QueryDetailsDrawerContent'; +import {QueriesTableWithDrawer} from './QueriesTableWithDrawer'; import {getTopQueriesColumns} from './columns/columns'; import { DEFAULT_TOP_QUERIES_COLUMNS, @@ -106,10 +103,6 @@ export const TopQueriesData = ({ // Use custom hook to handle scrolling to selected row useScrollToSelected({selectedRow, rows, reactListRef}); - const handleCloseDetails = React.useCallback(() => { - setSelectedRow(undefined); - }, [setSelectedRow]); - const isDrawerVisible = selectedRow !== undefined; const getTopQueryUrl = React.useCallback(() => { @@ -119,23 +112,6 @@ export const TopQueriesData = ({ return ''; }, [selectedRow]); - const renderDrawerContent = React.useCallback( - () => , - [selectedRow, handleCloseDetails], - ); - - const onRowClick = React.useCallback( - ( - row: KeyValueRow | null, - _index?: number, - event?: React.MouseEvent, - ) => { - event?.stopPropagation(); - setSelectedRow(row); - }, - [setSelectedRow], - ); - const inputRef = React.useRef(null); React.useEffect(() => { @@ -150,63 +126,54 @@ export const TopQueriesData = ({ ); return ( - - - - {renderQueryModeControl()} - + + + + + + {error ? : null} + + + + ); }; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/columns/columns.tsx b/src/containers/Tenant/Diagnostics/TopQueries/columns/columns.tsx index 089047a1f5..182dbc4345 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/columns/columns.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/columns/columns.tsx @@ -2,7 +2,6 @@ import DataTable from '@gravity-ui/react-data-table'; import type {Column, OrderType} from '@gravity-ui/react-data-table'; import {FixedHeightQuery} from '../../../../../components/FixedHeightQuery/FixedHeightQuery'; -import {YDBSyntaxHighlighter} from '../../../../../components/SyntaxHighlighter/YDBSyntaxHighlighter'; import type {KeyValueRow} from '../../../../../types/api/query'; import {cn} from '../../../../../utils/cn'; import {formatDateTime, formatNumber} from '../../../../../utils/dataFormatters/dataFormatters'; @@ -39,6 +38,22 @@ const queryTextColumn: Column = { width: 500, }; +const queryTextColumnWithMaxHeight: Column = { + name: QUERIES_COLUMNS_IDS.QueryText, + header: QUERIES_COLUMNS_TITLES.QueryText, + render: ({row}) => ( +
+ +
+ ), + width: 500, +}; + const endTimeColumn: Column = { name: QUERIES_COLUMNS_IDS.EndTime, header: QUERIES_COLUMNS_TITLES.EndTime, @@ -71,21 +86,6 @@ const userSIDColumn: Column = { width: 120, }; -const oneLineQueryTextColumn: Column = { - name: QUERIES_COLUMNS_IDS.OneLineQueryText, - header: QUERIES_COLUMNS_TITLES.OneLineQueryText, - render: ({row}) => ( - - ), - width: 500, -}; - const queryHashColumn: Column = { name: QUERIES_COLUMNS_IDS.QueryHash, header: QUERIES_COLUMNS_TITLES.QueryHash, @@ -144,7 +144,7 @@ export function getTopQueriesColumns() { } export function getTenantOverviewTopQueriesColumns() { - return [queryHashColumn, oneLineQueryTextColumn, cpuTimeUsColumn]; + return [queryHashColumn, queryTextColumnWithMaxHeight, cpuTimeUsColumn]; } export function getRunningQueriesColumns() { const columns = [ diff --git a/src/store/reducers/tenant/constants.ts b/src/store/reducers/tenant/constants.ts index 08aa7f79e9..73ff9f79b9 100644 --- a/src/store/reducers/tenant/constants.ts +++ b/src/store/reducers/tenant/constants.ts @@ -45,17 +45,6 @@ export const TENANT_METRICS_TABS_IDS = { network: 'network', } as const; -export const TENANT_CPU_TABS_IDS = { - nodes: 'nodes', - shards: 'shards', - queries: 'queries', -} as const; - -export const TENANT_CPU_NODES_MODE_IDS = { - load: 'load', - pools: 'pools', -} as const; - export const TENANT_STORAGE_TABS_IDS = { tables: 'tables', groups: 'groups', diff --git a/src/store/reducers/tenant/types.ts b/src/store/reducers/tenant/types.ts index e52eb72def..eb4df29063 100644 --- a/src/store/reducers/tenant/types.ts +++ b/src/store/reducers/tenant/types.ts @@ -2,12 +2,7 @@ import {z} from 'zod'; import type {ValueOf} from '../../../types/common'; -import { - TENANT_CPU_NODES_MODE_IDS, - TENANT_CPU_TABS_IDS, - TENANT_PAGES_IDS, - TENANT_STORAGE_TABS_IDS, -} from './constants'; +import {TENANT_PAGES_IDS, TENANT_STORAGE_TABS_IDS} from './constants'; import type { TENANT_DIAGNOSTICS_TABS_IDS, TENANT_METRICS_TABS_IDS, @@ -23,20 +18,11 @@ export const tenantStorageTabSchema = z .nativeEnum(TENANT_STORAGE_TABS_IDS) .catch(TENANT_STORAGE_TABS_IDS.tables); -export const tenantCpuTabSchema = z - .nativeEnum(TENANT_CPU_TABS_IDS) - .catch(TENANT_CPU_TABS_IDS.nodes); - -export const tenantNodesModeSchema = z - .nativeEnum(TENANT_CPU_NODES_MODE_IDS) - .catch(TENANT_CPU_NODES_MODE_IDS.load); - export type TenantQueryTab = ValueOf; export type TenantDiagnosticsTab = ValueOf; export type TenantSummaryTab = ValueOf; export type TenantMetricsTab = ValueOf; -export type TenantCpuTab = ValueOf; -export type TenantNodesMode = ValueOf; + export type TenantStorageTab = ValueOf; export type TenantNetworkTab = ValueOf; diff --git a/src/styles/mixins.scss b/src/styles/mixins.scss index b044c429b0..8bd35440b2 100644 --- a/src/styles/mixins.scss +++ b/src/styles/mixins.scss @@ -132,6 +132,12 @@ border-spacing: 0; border-collapse: separate; } + + tbody > tr:last-child { + td { + border-bottom: unset; + } + } } // DataTable with moving head is actually made of two separate tables diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 07978d373d..2192fd15fe 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -97,7 +97,7 @@ export const SECTION_IDS = { ABOUT: 'aboutSettingsSection', } as const; -export const TENANT_OVERVIEW_TABLES_LIMIT = 5; +export const TENANT_OVERVIEW_TABLES_LIMIT = 3; export const EMPTY_DATA_PLACEHOLDER = '—'; diff --git a/src/utils/hooks/index.ts b/src/utils/hooks/index.ts index e6bc7cd24c..5b4a4f572b 100644 --- a/src/utils/hooks/index.ts +++ b/src/utils/hooks/index.ts @@ -9,3 +9,4 @@ export * from './useAutoRefreshInterval'; export * from './useEventHandler'; export * from './useDelayed'; export * from './useAclSyntax'; +export * from './useScrollPosition'; diff --git a/src/utils/hooks/useScrollPosition.ts b/src/utils/hooks/useScrollPosition.ts new file mode 100644 index 0000000000..4e321c4643 --- /dev/null +++ b/src/utils/hooks/useScrollPosition.ts @@ -0,0 +1,120 @@ +import React from 'react'; + +import {debounce} from 'lodash'; +import {useHistory} from 'react-router-dom'; + +const DEBOUNCE_DELAY = 100; + +/** + * Hook for saving and restoring scroll position on navigation + * Uses sessionStorage to persist scroll position across browser history navigation + * Only restores position when navigating via browser back/forward buttons + */ +export function useScrollPosition( + elementRef: React.RefObject, + key: string, + shouldRestore: boolean, +) { + const history = useHistory(); + + // Save scroll position to sessionStorage + const saveScrollPosition = React.useCallback( + (scrollTop: number) => { + try { + sessionStorage.setItem(`scroll-${key}`, String(scrollTop)); + } catch (error) { + // Ignore sessionStorage errors (e.g., in private mode) + console.warn('Failed to save scroll position:', error); + } + }, + [key], + ); + + // Clear scroll position from sessionStorage + const clearScrollPosition = React.useCallback(() => { + try { + sessionStorage.removeItem(`scroll-${key}`); + } catch (error) { + // Ignore sessionStorage errors + console.warn('Failed to clear scroll position:', error); + } + }, [key]); + + // Restore scroll position from sessionStorage + const restoreScrollPosition = React.useCallback(() => { + if (!elementRef.current) { + return; + } + + try { + const savedPosition = sessionStorage.getItem(`scroll-${key}`); + if (savedPosition !== null) { + const scrollTop = parseInt(savedPosition, 10); + if (!isNaN(scrollTop)) { + elementRef.current.scrollTop = scrollTop; + } + } + } catch (error) { + // Ignore sessionStorage errors + console.warn('Failed to restore scroll position:', error); + } + }, [elementRef, key]); + + // Create debounced save function + const debouncedSaveScrollPosition = React.useMemo( + () => + debounce((scrollTop: number) => { + saveScrollPosition(scrollTop); + }, DEBOUNCE_DELAY), + [saveScrollPosition], + ); + + // Handle scroll events with debouncing + const handleScroll = React.useCallback(() => { + if (!elementRef.current) { + return; + } + + debouncedSaveScrollPosition(elementRef.current.scrollTop); + }, [elementRef, debouncedSaveScrollPosition]); + + // Detect if navigation is via browser back/forward + const isHistoryNavigation = React.useMemo(() => { + // Check if this is a browser back/forward navigation + // React Router v5 doesn't provide direct way to detect this, + // but we can use history.action to determine the type of navigation + return history.action === 'POP'; + }, [history.action]); + + // Handle location changes + React.useLayoutEffect(() => { + let timeoutId: number | undefined; + if (isHistoryNavigation && shouldRestore) { + // This is a browser back/forward navigation - restore position with timeout to ensure that content rendered + timeoutId = window.setTimeout(restoreScrollPosition, 100); + } else { + // This is a regular navigation (tab click, page reload) - clear position + clearScrollPosition(); + } + return () => clearTimeout(timeoutId); + }, [ + isHistoryNavigation, + restoreScrollPosition, + clearScrollPosition, + elementRef, + shouldRestore, + ]); + + React.useEffect(() => { + const element = elementRef.current; + if (!element) { + return; + } + + element.addEventListener('scroll', handleScroll, {passive: true}); + return () => { + element.removeEventListener('scroll', handleScroll); + debouncedSaveScrollPosition.cancel(); + }; + }, [handleScroll, debouncedSaveScrollPosition, elementRef]); +} diff --git a/tests/suites/tenant/diagnostics/tabs/queries.test.ts b/tests/suites/tenant/diagnostics/tabs/queries.test.ts index 4af7a659be..e1985405e2 100644 --- a/tests/suites/tenant/diagnostics/tabs/queries.test.ts +++ b/tests/suites/tenant/diagnostics/tabs/queries.test.ts @@ -269,7 +269,7 @@ test.describe('Diagnostics Queries tab', async () => { // Get the number of rows and select a row that requires scrolling (should be 100 from mock) const rowCount = await diagnostics.table.getRowCount(); - expect(rowCount).toBe(8); // Verify we have the expected 100 rows from mock + expect(rowCount).toBe(9); // Verify we have the expected 100 rows from mock // Target a row further down that requires scrolling const targetRowIndex = 8;