Skip to content

Commit f8dbd38

Browse files
committed
feat: add Network section
1 parent 26e1c94 commit f8dbd38

File tree

13 files changed

+375
-12
lines changed

13 files changed

+375
-12
lines changed

src/containers/Tenant/Diagnostics/TenantOverview/MetricsTabs/MetricsTabs.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ export function MetricsTabs({
5757
...queryParams,
5858
[TenantTabsGroups.metricsTab]: TENANT_METRICS_TABS_IDS.memory,
5959
}),
60+
[TENANT_METRICS_TABS_IDS.network]: getTenantPath({
61+
...queryParams,
62+
[TenantTabsGroups.metricsTab]: TENANT_METRICS_TABS_IDS.network,
63+
}),
6064
};
6165

6266
// Use only pools that directly indicate resources available to perform user queries
@@ -131,19 +135,22 @@ export function MetricsTabs({
131135
</Link>
132136
</div>
133137
{showNetworkUtilization && networkStats && networkMetrics && (
134-
<div className={b('link-container')}>
135-
<div className={b('link')}>
138+
<div
139+
className={b('link-container', {
140+
active: metricsTab === TENANT_METRICS_TABS_IDS.network,
141+
})}
142+
>
143+
<Link to={tabLinks.network} className={b('link')}>
136144
<TabCard
137145
label={i18n('cards.network-label')}
138146
sublabel={i18n('context_network-evaluation')}
139147
value={networkMetrics.totalUsed}
140148
limit={networkMetrics.totalLimit}
141149
legendFormatter={formatStorageLegend}
142-
active={false}
143-
clickable={false}
150+
active={metricsTab === TENANT_METRICS_TABS_IDS.network}
144151
helpText={i18n('context_network-description')}
145152
/>
146-
</div>
153+
</Link>
147154
</div>
148155
)}
149156
</Flex>

src/containers/Tenant/Diagnostics/TenantOverview/TabCard/TabCard.scss

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,10 @@
44
&__card-container {
55
padding: var(--g-spacing-4);
66

7+
cursor: pointer;
8+
79
border: 0;
810
border-radius: var(--g-border-radius-s);
9-
}
10-
11-
&__card-container_clickable {
12-
cursor: pointer;
1311

1412
// Smooth hover transition
1513
transition: background-color 0.15s ease;

src/containers/Tenant/Diagnostics/TenantOverview/TabCard/TabCard.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ interface TabCardProps {
1515
limit: number;
1616
active?: boolean;
1717
helpText?: string;
18-
clickable?: boolean;
1918
legendFormatter: (params: {value: number; capacity: number}) => string;
2019
}
2120

@@ -26,7 +25,6 @@ export function TabCard({
2625
limit,
2726
active,
2827
helpText,
29-
clickable = true,
3028
legendFormatter,
3129
}: TabCardProps) {
3230
const {status, percents, legend, fill} = getDiagramValues({
@@ -38,7 +36,7 @@ export function TabCard({
3836
return (
3937
<div className={b({active})}>
4038
<Card
41-
className={b('card-container', {active, clickable})}
39+
className={b('card-container', {active})}
4240
type="container"
4341
view={active ? 'outlined' : 'raised'}
4442
>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.tenant-network {
2+
&__tabs-container {
3+
margin-top: var(--g-spacing-3);
4+
}
5+
6+
&__tab-content {
7+
margin-top: var(--g-spacing-3);
8+
}
9+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import React from 'react';
2+
3+
import {Flex, Tab, TabList, TabProvider} from '@gravity-ui/uikit';
4+
5+
import {TENANT_NETWORK_TABS_IDS} from '../../../../../store/reducers/tenant/constants';
6+
import type {AdditionalNodesProps} from '../../../../../types/additionalProps';
7+
import {cn} from '../../../../../utils/cn';
8+
import i18n from '../i18n';
9+
10+
import {TopNodesByPing} from './TopNodesByPing';
11+
import {TopNodesBySkew} from './TopNodesBySkew';
12+
import {useTenantNetworkQueryParams} from './useTenantNetworkQueryParams';
13+
14+
import './TenantNetwork.scss';
15+
16+
const b = cn('tenant-network');
17+
18+
const networkTabs = [
19+
{id: TENANT_NETWORK_TABS_IDS.ping, title: i18n('title_nodes-by-ping')},
20+
{id: TENANT_NETWORK_TABS_IDS.skew, title: i18n('title_nodes-by-skew')},
21+
];
22+
23+
interface TenantNetworkProps {
24+
tenantName: string;
25+
additionalNodesProps?: AdditionalNodesProps;
26+
}
27+
28+
export function TenantNetwork({tenantName, additionalNodesProps}: TenantNetworkProps) {
29+
const {networkTab, handleNetworkTabChange} = useTenantNetworkQueryParams();
30+
31+
const renderTabContent = () => {
32+
switch (networkTab) {
33+
case TENANT_NETWORK_TABS_IDS.ping: {
34+
return (
35+
<TopNodesByPing
36+
tenantName={tenantName}
37+
additionalNodesProps={additionalNodesProps}
38+
/>
39+
);
40+
}
41+
case TENANT_NETWORK_TABS_IDS.skew: {
42+
return (
43+
<TopNodesBySkew
44+
tenantName={tenantName}
45+
additionalNodesProps={additionalNodesProps}
46+
/>
47+
);
48+
}
49+
default: {
50+
return null;
51+
}
52+
}
53+
};
54+
55+
return (
56+
<Flex direction="column" gap={4} className={b()}>
57+
<Flex direction="column" gap={3} className={b('tabs-container')}>
58+
<TabProvider value={networkTab}>
59+
<TabList size="m">
60+
{networkTabs.map(({id, title}) => {
61+
return (
62+
<Tab key={id} value={id} onClick={() => handleNetworkTabChange(id)}>
63+
{title}
64+
</Tab>
65+
);
66+
})}
67+
</TabList>
68+
</TabProvider>
69+
70+
<Flex direction="column" className={b('tab-content')}>
71+
{renderTabContent()}
72+
</Flex>
73+
</Flex>
74+
</Flex>
75+
);
76+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import React from 'react';
2+
3+
import {ResizeableDataTable} from '../../../../../components/ResizeableDataTable/ResizeableDataTable';
4+
import {NODES_COLUMNS_WIDTH_LS_KEY} from '../../../../../components/nodesColumns/constants';
5+
import {nodesApi} from '../../../../../store/reducers/nodes/nodes';
6+
import {TENANT_DIAGNOSTICS_TABS_IDS} from '../../../../../store/reducers/tenant/constants';
7+
import type {AdditionalNodesProps} from '../../../../../types/additionalProps';
8+
import {TENANT_OVERVIEW_TABLES_LIMIT} from '../../../../../utils/constants';
9+
import {useAutoRefreshInterval, useSearchQuery} from '../../../../../utils/hooks';
10+
import {TenantTabsGroups, getTenantPath} from '../../../TenantPages';
11+
import {TenantOverviewTableLayout} from '../TenantOverviewTableLayout';
12+
import {getSectionTitle} from '../getSectionTitle';
13+
import i18n from '../i18n';
14+
15+
import {getTopNodesByPingColumns} from './columns';
16+
17+
const TENANT_OVERVIEW_TABLES_SETTINGS = {
18+
stripedRows: false,
19+
sortable: false,
20+
};
21+
22+
interface TopNodesByPingProps {
23+
tenantName: string;
24+
additionalNodesProps?: AdditionalNodesProps;
25+
}
26+
27+
export function TopNodesByPing({tenantName, additionalNodesProps}: TopNodesByPingProps) {
28+
const query = useSearchQuery();
29+
const [autoRefreshInterval] = useAutoRefreshInterval();
30+
const [columns, fieldsRequired] = getTopNodesByPingColumns({
31+
getNodeRef: additionalNodesProps?.getNodeRef,
32+
database: tenantName,
33+
});
34+
35+
const {currentData, isFetching, error} = nodesApi.useGetNodesQuery(
36+
{
37+
tenant: tenantName,
38+
type: 'any',
39+
sort: '-PingTime',
40+
limit: TENANT_OVERVIEW_TABLES_LIMIT,
41+
tablets: false,
42+
fieldsRequired: fieldsRequired as any,
43+
},
44+
{pollingInterval: autoRefreshInterval},
45+
);
46+
47+
const loading = isFetching && currentData === undefined;
48+
const topNodes = currentData?.Nodes || [];
49+
50+
const title = getSectionTitle({
51+
entity: i18n('nodes'),
52+
postfix: i18n('by-ping'),
53+
link: getTenantPath({
54+
...query,
55+
[TenantTabsGroups.diagnosticsTab]: TENANT_DIAGNOSTICS_TABS_IDS.nodes,
56+
}),
57+
});
58+
59+
return (
60+
<TenantOverviewTableLayout
61+
title={title}
62+
loading={loading}
63+
error={error}
64+
withData={Boolean(currentData)}
65+
>
66+
<ResizeableDataTable
67+
columnsWidthLSKey={NODES_COLUMNS_WIDTH_LS_KEY}
68+
data={topNodes}
69+
columns={columns}
70+
emptyDataMessage={i18n('top-nodes.empty-data')}
71+
settings={TENANT_OVERVIEW_TABLES_SETTINGS}
72+
/>
73+
</TenantOverviewTableLayout>
74+
);
75+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import React from 'react';
2+
3+
import {ResizeableDataTable} from '../../../../../components/ResizeableDataTable/ResizeableDataTable';
4+
import {NODES_COLUMNS_WIDTH_LS_KEY} from '../../../../../components/nodesColumns/constants';
5+
import {nodesApi} from '../../../../../store/reducers/nodes/nodes';
6+
import {TENANT_DIAGNOSTICS_TABS_IDS} from '../../../../../store/reducers/tenant/constants';
7+
import type {AdditionalNodesProps} from '../../../../../types/additionalProps';
8+
import {TENANT_OVERVIEW_TABLES_LIMIT} from '../../../../../utils/constants';
9+
import {useAutoRefreshInterval, useSearchQuery} from '../../../../../utils/hooks';
10+
import {TenantTabsGroups, getTenantPath} from '../../../TenantPages';
11+
import {TenantOverviewTableLayout} from '../TenantOverviewTableLayout';
12+
import {getSectionTitle} from '../getSectionTitle';
13+
import i18n from '../i18n';
14+
15+
import {getTopNodesBySkewColumns} from './columns';
16+
17+
const TENANT_OVERVIEW_TABLES_SETTINGS = {
18+
stripedRows: false,
19+
sortable: false,
20+
};
21+
22+
interface TopNodesBySkewProps {
23+
tenantName: string;
24+
additionalNodesProps?: AdditionalNodesProps;
25+
}
26+
27+
export function TopNodesBySkew({tenantName, additionalNodesProps}: TopNodesBySkewProps) {
28+
const query = useSearchQuery();
29+
const [autoRefreshInterval] = useAutoRefreshInterval();
30+
const [columns, fieldsRequired] = getTopNodesBySkewColumns({
31+
getNodeRef: additionalNodesProps?.getNodeRef,
32+
database: tenantName,
33+
});
34+
35+
const {currentData, isFetching, error} = nodesApi.useGetNodesQuery(
36+
{
37+
tenant: tenantName,
38+
type: 'any',
39+
sort: '-ClockSkew',
40+
limit: TENANT_OVERVIEW_TABLES_LIMIT,
41+
tablets: false,
42+
fieldsRequired: fieldsRequired as any,
43+
},
44+
{pollingInterval: autoRefreshInterval},
45+
);
46+
47+
const loading = isFetching && currentData === undefined;
48+
const topNodes = currentData?.Nodes || [];
49+
50+
const title = getSectionTitle({
51+
entity: i18n('nodes'),
52+
postfix: i18n('by-skew'),
53+
link: getTenantPath({
54+
...query,
55+
[TenantTabsGroups.diagnosticsTab]: TENANT_DIAGNOSTICS_TABS_IDS.nodes,
56+
}),
57+
});
58+
59+
return (
60+
<TenantOverviewTableLayout
61+
title={title}
62+
loading={loading}
63+
error={error}
64+
withData={Boolean(currentData)}
65+
>
66+
<ResizeableDataTable
67+
columnsWidthLSKey={NODES_COLUMNS_WIDTH_LS_KEY}
68+
data={topNodes}
69+
columns={columns}
70+
emptyDataMessage={i18n('top-nodes.empty-data')}
71+
settings={TENANT_OVERVIEW_TABLES_SETTINGS}
72+
/>
73+
</TenantOverviewTableLayout>
74+
);
75+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import {
2+
getClockSkewColumn,
3+
getDataCenterColumn,
4+
getNetworkHostColumn,
5+
getNodeIdColumn,
6+
getPingTimeColumn,
7+
getRackColumn,
8+
getUptimeColumn,
9+
} from '../../../../../components/nodesColumns/columns';
10+
import {isSortableNodesColumn} from '../../../../../components/nodesColumns/constants';
11+
import type {GetNodesColumnsParams} from '../../../../../components/nodesColumns/types';
12+
import type {NodesPreparedEntity} from '../../../../../store/reducers/nodes/types';
13+
import type {Column} from '../../../../../utils/tableUtils/types';
14+
15+
export function getTopNodesByPingColumns(params: GetNodesColumnsParams) {
16+
const columns: Column<NodesPreparedEntity>[] = [
17+
getNodeIdColumn(),
18+
getNetworkHostColumn(params),
19+
getDataCenterColumn(),
20+
getRackColumn(),
21+
getUptimeColumn(),
22+
getPingTimeColumn(),
23+
];
24+
25+
const fieldsRequired = [
26+
'NodeId',
27+
'Host',
28+
'DataCenter',
29+
'Rack',
30+
'UptimeSeconds',
31+
'PingTimeUs',
32+
'PingTimeMinUs',
33+
'PingTimeMaxUs',
34+
];
35+
36+
return [
37+
columns.map((column) => ({
38+
...column,
39+
sortable: isSortableNodesColumn(column.name),
40+
})),
41+
fieldsRequired,
42+
] as const;
43+
}
44+
45+
export function getTopNodesBySkewColumns(params: GetNodesColumnsParams) {
46+
const columns: Column<NodesPreparedEntity>[] = [
47+
getNodeIdColumn(),
48+
getNetworkHostColumn(params),
49+
getDataCenterColumn(),
50+
getRackColumn(),
51+
getUptimeColumn(),
52+
getClockSkewColumn(),
53+
];
54+
55+
const fieldsRequired = [
56+
'NodeId',
57+
'Host',
58+
'DataCenter',
59+
'Rack',
60+
'UptimeSeconds',
61+
'ClockSkewUs',
62+
'ClockSkewMinUs',
63+
'ClockSkewMaxUs',
64+
];
65+
66+
return [
67+
columns.map((column) => ({
68+
...column,
69+
sortable: isSortableNodesColumn(column.name),
70+
})),
71+
fieldsRequired,
72+
] as const;
73+
}

0 commit comments

Comments
 (0)