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}`;
+}