diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/MetricsTabs/MetricsTabs.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/MetricsTabs/MetricsTabs.tsx index d98331f3d1..1933857312 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/MetricsTabs/MetricsTabs.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/MetricsTabs/MetricsTabs.tsx @@ -15,6 +15,7 @@ import {useSetting, useTypedSelector} from '../../../../../utils/hooks'; import {calculateMetricAggregates} from '../../../../../utils/metrics'; import { formatCoresLegend, + formatSpeedLegend, formatStorageLegend, } from '../../../../../utils/metrics/formatMetricLegend'; import {TenantTabsGroups, getTenantPath} from '../../../TenantPages'; @@ -57,6 +58,10 @@ export function MetricsTabs({ ...queryParams, [TenantTabsGroups.metricsTab]: TENANT_METRICS_TABS_IDS.memory, }), + [TENANT_METRICS_TABS_IDS.network]: getTenantPath({ + ...queryParams, + [TenantTabsGroups.metricsTab]: TENANT_METRICS_TABS_IDS.network, + }), }; // Use only pools that directly indicate resources available to perform user queries @@ -131,19 +136,22 @@ export function MetricsTabs({ {showNetworkUtilization && networkStats && networkMetrics && ( -
-
+
+ -
+
)} diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TabCard/TabCard.scss b/src/containers/Tenant/Diagnostics/TenantOverview/TabCard/TabCard.scss index ece0f273fd..49edda49e8 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TabCard/TabCard.scss +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TabCard/TabCard.scss @@ -4,12 +4,10 @@ &__card-container { padding: var(--g-spacing-4); + cursor: pointer; + border: 0; border-radius: var(--g-border-radius-s); - } - - &__card-container_clickable { - cursor: pointer; // Smooth hover transition transition: background-color 0.15s ease; diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TabCard/TabCard.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TabCard/TabCard.tsx index 81288b2889..3a1ad2efd3 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TabCard/TabCard.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TabCard/TabCard.tsx @@ -15,7 +15,6 @@ interface TabCardProps { limit: number; active?: boolean; helpText?: string; - clickable?: boolean; legendFormatter: (params: {value: number; capacity: number}) => string; } @@ -26,7 +25,6 @@ export function TabCard({ limit, active, helpText, - clickable = true, legendFormatter, }: TabCardProps) { const {status, percents, legend, fill} = getDiagramValues({ @@ -38,7 +36,7 @@ export function TabCard({ return (
diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantNetwork/TenantNetwork.scss b/src/containers/Tenant/Diagnostics/TenantOverview/TenantNetwork/TenantNetwork.scss new file mode 100644 index 0000000000..9a33d054ec --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantNetwork/TenantNetwork.scss @@ -0,0 +1,9 @@ +.tenant-network { + &__tabs-container { + margin-top: var(--g-spacing-3); + } + + &__tab-content { + margin-top: var(--g-spacing-3); + } +} diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantNetwork/TenantNetwork.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantNetwork/TenantNetwork.tsx new file mode 100644 index 0000000000..32db2b9866 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantNetwork/TenantNetwork.tsx @@ -0,0 +1,74 @@ +import {Flex, Tab, TabList, TabProvider} from '@gravity-ui/uikit'; + +import {TENANT_NETWORK_TABS_IDS} from '../../../../../store/reducers/tenant/constants'; +import type {AdditionalNodesProps} from '../../../../../types/additionalProps'; +import {cn} from '../../../../../utils/cn'; +import i18n from '../i18n'; + +import {TopNodesByPing} from './TopNodesByPing'; +import {TopNodesBySkew} from './TopNodesBySkew'; +import {useTenantNetworkQueryParams} from './useTenantNetworkQueryParams'; + +import './TenantNetwork.scss'; + +const b = cn('tenant-network'); + +const networkTabs = [ + {id: TENANT_NETWORK_TABS_IDS.ping, title: i18n('title_nodes-by-ping')}, + {id: TENANT_NETWORK_TABS_IDS.skew, title: i18n('title_nodes-by-skew')}, +]; + +interface TenantNetworkProps { + tenantName: string; + additionalNodesProps?: AdditionalNodesProps; +} + +export function TenantNetwork({tenantName, additionalNodesProps}: TenantNetworkProps) { + const {networkTab, handleNetworkTabChange} = useTenantNetworkQueryParams(); + + const renderTabContent = () => { + switch (networkTab) { + case TENANT_NETWORK_TABS_IDS.ping: { + return ( + + ); + } + case TENANT_NETWORK_TABS_IDS.skew: { + return ( + + ); + } + default: { + return null; + } + } + }; + + return ( + + + + + {networkTabs.map(({id, title}) => { + return ( + handleNetworkTabChange(id)}> + {title} + + ); + })} + + + + + {renderTabContent()} + + + + ); +} diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantNetwork/TopNodesByPing.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantNetwork/TopNodesByPing.tsx new file mode 100644 index 0000000000..d2f3de552d --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantNetwork/TopNodesByPing.tsx @@ -0,0 +1,73 @@ +import {ResizeableDataTable} from '../../../../../components/ResizeableDataTable/ResizeableDataTable'; +import {NODES_COLUMNS_WIDTH_LS_KEY} from '../../../../../components/nodesColumns/constants'; +import {nodesApi} from '../../../../../store/reducers/nodes/nodes'; +import {TENANT_DIAGNOSTICS_TABS_IDS} from '../../../../../store/reducers/tenant/constants'; +import type {AdditionalNodesProps} from '../../../../../types/additionalProps'; +import {TENANT_OVERVIEW_TABLES_LIMIT} from '../../../../../utils/constants'; +import {useAutoRefreshInterval, useSearchQuery} from '../../../../../utils/hooks'; +import {TenantTabsGroups, getTenantPath} from '../../../TenantPages'; +import {TenantOverviewTableLayout} from '../TenantOverviewTableLayout'; +import {getSectionTitle} from '../getSectionTitle'; +import i18n from '../i18n'; + +import {getTopNodesByPingColumns} from './columns'; + +const TENANT_OVERVIEW_TABLES_SETTINGS = { + stripedRows: false, + sortable: false, +}; + +interface TopNodesByPingProps { + tenantName: string; + additionalNodesProps?: AdditionalNodesProps; +} + +export function TopNodesByPing({tenantName, additionalNodesProps}: TopNodesByPingProps) { + const query = useSearchQuery(); + const [autoRefreshInterval] = useAutoRefreshInterval(); + const [columns, fieldsRequired] = getTopNodesByPingColumns({ + getNodeRef: additionalNodesProps?.getNodeRef, + database: tenantName, + }); + + const {currentData, isFetching, error} = nodesApi.useGetNodesQuery( + { + tenant: tenantName, + type: 'any', + sort: '-PingTime', + limit: TENANT_OVERVIEW_TABLES_LIMIT, + tablets: false, + fieldsRequired: fieldsRequired as any, + }, + {pollingInterval: autoRefreshInterval}, + ); + + const loading = isFetching && currentData === undefined; + const topNodes = currentData?.Nodes || []; + + const title = getSectionTitle({ + entity: i18n('nodes'), + postfix: i18n('by-ping'), + link: getTenantPath({ + ...query, + [TenantTabsGroups.diagnosticsTab]: TENANT_DIAGNOSTICS_TABS_IDS.nodes, + }), + }); + + return ( + + + + ); +} diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantNetwork/TopNodesBySkew.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantNetwork/TopNodesBySkew.tsx new file mode 100644 index 0000000000..aef8d220d1 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantNetwork/TopNodesBySkew.tsx @@ -0,0 +1,73 @@ +import {ResizeableDataTable} from '../../../../../components/ResizeableDataTable/ResizeableDataTable'; +import {NODES_COLUMNS_WIDTH_LS_KEY} from '../../../../../components/nodesColumns/constants'; +import {nodesApi} from '../../../../../store/reducers/nodes/nodes'; +import {TENANT_DIAGNOSTICS_TABS_IDS} from '../../../../../store/reducers/tenant/constants'; +import type {AdditionalNodesProps} from '../../../../../types/additionalProps'; +import {TENANT_OVERVIEW_TABLES_LIMIT} from '../../../../../utils/constants'; +import {useAutoRefreshInterval, useSearchQuery} from '../../../../../utils/hooks'; +import {TenantTabsGroups, getTenantPath} from '../../../TenantPages'; +import {TenantOverviewTableLayout} from '../TenantOverviewTableLayout'; +import {getSectionTitle} from '../getSectionTitle'; +import i18n from '../i18n'; + +import {getTopNodesBySkewColumns} from './columns'; + +const TENANT_OVERVIEW_TABLES_SETTINGS = { + stripedRows: false, + sortable: false, +}; + +interface TopNodesBySkewProps { + tenantName: string; + additionalNodesProps?: AdditionalNodesProps; +} + +export function TopNodesBySkew({tenantName, additionalNodesProps}: TopNodesBySkewProps) { + const query = useSearchQuery(); + const [autoRefreshInterval] = useAutoRefreshInterval(); + const [columns, fieldsRequired] = getTopNodesBySkewColumns({ + getNodeRef: additionalNodesProps?.getNodeRef, + database: tenantName, + }); + + const {currentData, isFetching, error} = nodesApi.useGetNodesQuery( + { + tenant: tenantName, + type: 'any', + sort: '-ClockSkew', + limit: TENANT_OVERVIEW_TABLES_LIMIT, + tablets: false, + fieldsRequired: fieldsRequired as any, + }, + {pollingInterval: autoRefreshInterval}, + ); + + const loading = isFetching && currentData === undefined; + const topNodes = currentData?.Nodes || []; + + const title = getSectionTitle({ + entity: i18n('nodes'), + postfix: i18n('by-skew'), + link: getTenantPath({ + ...query, + [TenantTabsGroups.diagnosticsTab]: TENANT_DIAGNOSTICS_TABS_IDS.nodes, + }), + }); + + return ( + + + + ); +} diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantNetwork/columns.ts b/src/containers/Tenant/Diagnostics/TenantOverview/TenantNetwork/columns.ts new file mode 100644 index 0000000000..5937e0623a --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantNetwork/columns.ts @@ -0,0 +1,55 @@ +import { + getClockSkewColumn, + getNetworkHostColumn, + getNodeIdColumn, + getPingTimeColumn, + getUptimeColumn, +} from '../../../../../components/nodesColumns/columns'; +import { + NODES_COLUMNS_TO_DATA_FIELDS, + isSortableNodesColumn, +} from '../../../../../components/nodesColumns/constants'; +import type {GetNodesColumnsParams} from '../../../../../components/nodesColumns/types'; +import type {NodesPreparedEntity} from '../../../../../store/reducers/nodes/types'; +import {getRequiredDataFields} from '../../../../../utils/tableUtils/getRequiredDataFields'; +import type {Column} from '../../../../../utils/tableUtils/types'; + +export function getTopNodesByPingColumns(params: GetNodesColumnsParams) { + const columns: Column[] = [ + getNodeIdColumn(), + getNetworkHostColumn(params), + getUptimeColumn(), + getPingTimeColumn(), + ]; + + const columnsIds = columns.map((column) => column.name); + const fieldsRequired = getRequiredDataFields(columnsIds, NODES_COLUMNS_TO_DATA_FIELDS); + + return [ + columns.map((column) => ({ + ...column, + sortable: isSortableNodesColumn(column.name), + })), + fieldsRequired, + ] as const; +} + +export function getTopNodesBySkewColumns(params: GetNodesColumnsParams) { + const columns: Column[] = [ + getNodeIdColumn(), + getNetworkHostColumn(params), + getUptimeColumn(), + getClockSkewColumn(), + ]; + + const columnsIds = columns.map((column) => column.name); + const fieldsRequired = getRequiredDataFields(columnsIds, NODES_COLUMNS_TO_DATA_FIELDS); + + return [ + columns.map((column) => ({ + ...column, + sortable: isSortableNodesColumn(column.name), + })), + fieldsRequired, + ] as const; +} diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantNetwork/useTenantNetworkQueryParams.ts b/src/containers/Tenant/Diagnostics/TenantOverview/TenantNetwork/useTenantNetworkQueryParams.ts new file mode 100644 index 0000000000..39e747fff0 --- /dev/null +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantNetwork/useTenantNetworkQueryParams.ts @@ -0,0 +1,30 @@ +import {StringParam, useQueryParams} from 'use-query-params'; + +import {TENANT_NETWORK_TABS_IDS} from '../../../../../store/reducers/tenant/constants'; +import type {TenantNetworkTab} from '../../../../../store/reducers/tenant/types'; + +export function useTenantNetworkQueryParams() { + const [queryParams, setQueryParams] = useQueryParams({ + networkTab: StringParam, + }); + + // Parse and validate networkTab with fallback to ping + const networkTab: TenantNetworkTab = (() => { + if (!queryParams.networkTab) { + return TENANT_NETWORK_TABS_IDS.ping; + } + const validTabs = Object.values(TENANT_NETWORK_TABS_IDS) as string[]; + return validTabs.includes(queryParams.networkTab) + ? (queryParams.networkTab as TenantNetworkTab) + : TENANT_NETWORK_TABS_IDS.ping; + })(); + + const handleNetworkTabChange = (value: TenantNetworkTab) => { + setQueryParams({networkTab: value}, 'replaceIn'); + }; + + return { + networkTab, + handleNetworkTabChange, + }; +} diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx index 2e375c84be..b3a3967aab 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx @@ -18,6 +18,7 @@ import {HealthcheckPreview} from './Healthcheck/HealthcheckPreview'; import {MetricsTabs} from './MetricsTabs/MetricsTabs'; import {TenantCpu} from './TenantCpu/TenantCpu'; import {TenantMemory} from './TenantMemory/TenantMemory'; +import {TenantNetwork} from './TenantNetwork/TenantNetwork'; import {TenantStorage} from './TenantStorage/TenantStorage'; import {b} from './utils'; @@ -134,6 +135,14 @@ export function TenantOverview({ /> ); } + case TENANT_METRICS_TABS_IDS.network: { + return ( + + ); + } } }; diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json b/src/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json index 8f6e250338..5059b37fa0 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json +++ b/src/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json @@ -14,6 +14,8 @@ "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", @@ -21,20 +23,21 @@ "cards.storage-label": "Storage", "cards.memory-label": "Memory used", "cards.network-label": "Network", - "cards.network-note": "Network usage is the average outgoing bandwidth usage across all nodes in the database", + "cards.network-note": "Network throughput is the average outgoing bandwidth usage across all nodes in the database", "context_cpu-load": "CPU load", "context_storage-groups": "Storage: {{count}} groups", "context_memory-used": "Memory used", - "context_network-evaluation": "Network evaluation", + "context_network-usage": "Network usage", "context_cpu-description": "CPU load is calculated as the cumulative usage across all actor system pools in the database", "context_memory-description": "Memory usage is the total memory consumed by all processes in the database", "context_storage-description": "Storage usage shows how much data is stored in the database including user data and indexes", - "context_network-description": "Network usage is the average outgoing bandwidth usage across all nodes in the database", + "context_network-description": "Network throughput is the average outgoing bandwidth usage across all nodes in the database", "charts.queries-per-second": "Queries per second", "charts.queries-latency": "Queries latencies {{percentile}}", "charts.cpu-usage": "CPU usage by pool", "charts.storage-usage": "Tablet storage usage", "charts.memory-usage": "Memory usage", + "charts.network-utilization": "Network utilization", "title_storage-details": "Storage Details", "storage.tablet-storage-title": "Tablet storage", "storage.tablet-storage-description": "Size of user data and indexes stored in schema objects (tables, topics, etc.)", @@ -47,9 +50,13 @@ "title_top-queries": "Top Queries", "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", + "title_nodes-by-skew": "Nodes By Clock Skew", "action_by-load": "By Load", "action_by-pool-usage": "By Pool Usage", "title_memory-details": "Memory Details", "field_memory-usage": "Memory usage", - "context_capacity-usage": "{{value}} of {{capacity}}" + "context_capacity-usage": "{{value}} of {{capacity}}", + "network-stats-unavailable.title": "Network Statistics Unavailable", + "network-stats-unavailable.description": "Network ping and clock skew statistics require a newer backend version. Please upgrade your YDB installation to access these features." } diff --git a/src/store/reducers/tenant/constants.ts b/src/store/reducers/tenant/constants.ts index 267dfc3e12..08aa7f79e9 100644 --- a/src/store/reducers/tenant/constants.ts +++ b/src/store/reducers/tenant/constants.ts @@ -42,6 +42,7 @@ export const TENANT_METRICS_TABS_IDS = { cpu: 'cpu', storage: 'storage', memory: 'memory', + network: 'network', } as const; export const TENANT_CPU_TABS_IDS = { @@ -59,3 +60,8 @@ export const TENANT_STORAGE_TABS_IDS = { tables: 'tables', groups: 'groups', } as const; + +export const TENANT_NETWORK_TABS_IDS = { + ping: 'ping', + skew: 'skew', +} as const; diff --git a/src/store/reducers/tenant/types.ts b/src/store/reducers/tenant/types.ts index cf5b85b154..e52eb72def 100644 --- a/src/store/reducers/tenant/types.ts +++ b/src/store/reducers/tenant/types.ts @@ -11,6 +11,7 @@ import { import type { TENANT_DIAGNOSTICS_TABS_IDS, TENANT_METRICS_TABS_IDS, + TENANT_NETWORK_TABS_IDS, TENANT_QUERY_TABS_ID, TENANT_SUMMARY_TABS_IDS, } from './constants'; @@ -37,6 +38,7 @@ export type TenantMetricsTab = ValueOf; export type TenantCpuTab = ValueOf; export type TenantNodesMode = ValueOf; export type TenantStorageTab = ValueOf; +export type TenantNetworkTab = ValueOf; export interface TenantState { tenantPage: TenantPage; diff --git a/src/utils/metrics/formatMetricLegend.ts b/src/utils/metrics/formatMetricLegend.ts index f1f885f0cc..8554e075b3 100644 --- a/src/utils/metrics/formatMetricLegend.ts +++ b/src/utils/metrics/formatMetricLegend.ts @@ -1,3 +1,4 @@ +import {formatBytes, getBytesSizeUnit} from '../bytesParsers'; import { formatNumber, formatNumericValues, @@ -25,3 +26,27 @@ export function formatCoresLegend({value, capacity}: MetricFormatParams): string } return `${formatted[0]} ${i18n('context_of')} ${formatted[1]} ${i18n('context_cores')}`; } + +export function formatSpeedLegend({value, capacity}: MetricFormatParams): string { + // Determine unit based on capacity + const unit = getBytesSizeUnit(capacity); + + // Format used value without units + const usedSpeed = formatBytes({ + value, + size: unit, + precision: 2, + withSpeedLabel: false, + withSizeLabel: false, + }); + + // Format limit with speed units + const limitSpeed = formatBytes({ + value: capacity, + size: unit, + precision: 2, + withSpeedLabel: true, + }); + + return `${usedSpeed} ${i18n('context_of')} ${limitSpeed}`; +}