Skip to content

Commit f476783

Browse files
committed
feat: Bridge Piles
1 parent 252c898 commit f476783

File tree

10 files changed

+186
-14
lines changed

10 files changed

+186
-14
lines changed

src/containers/Cluster/ClusterInfo/ClusterInfo.scss

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,15 @@
2020

2121
margin-left: 5px;
2222
}
23+
24+
&__details-layout {
25+
align-items: flex-start;
26+
gap: var(--g-spacing-6);
27+
}
28+
29+
&__bridge-table {
30+
flex: 0 0 360px; // do not shrink, fixed basis so stats don't take all space
31+
32+
min-width: 360px;
33+
}
2334
}

src/containers/Cluster/ClusterInfo/ClusterInfo.tsx

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import {InfoViewerSkeleton} from '../../../components/InfoViewerSkeleton/InfoVie
77
import {LinkWithIcon} from '../../../components/LinkWithIcon/LinkWithIcon';
88
import type {ClusterGroupsStats} from '../../../store/reducers/cluster/types';
99
import type {AdditionalClusterProps} from '../../../types/additionalProps';
10-
import type {TClusterInfo} from '../../../types/api/cluster';
10+
import type {TBridgePile, TClusterInfo} from '../../../types/api/cluster';
1111
import type {IResponseError} from '../../../types/api/error';
1212
import {formatNumber} from '../../../utils/dataFormatters/dataFormatters';
13+
import {BridgeInfoTable} from '../ClusterOverview/components/BridgeInfoTable';
1314
import i18n from '../i18n';
1415
import {getTotalStorageGroupsUsed} from '../utils';
1516

@@ -25,6 +26,7 @@ interface ClusterInfoProps {
2526
error?: IResponseError | string;
2627
additionalClusterProps?: AdditionalClusterProps;
2728
groupStats?: ClusterGroupsStats;
29+
bridgePiles?: TBridgePile[];
2830
}
2931

3032
export const ClusterInfo = ({
@@ -33,6 +35,7 @@ export const ClusterInfo = ({
3335
error,
3436
additionalClusterProps = {},
3537
groupStats = {},
38+
bridgePiles,
3639
}: ClusterInfoProps) => {
3740
const {info = [], links = []} = additionalClusterProps;
3841

@@ -96,13 +99,25 @@ export const ClusterInfo = ({
9699
}
97100
return (
98101
<InfoSection>
99-
<Text as="div" variant="subheader-2" className={b('section-title')}>
100-
{i18n('title_storage-groups')}{' '}
101-
<Text variant="subheader-2" color="secondary">
102-
{formatNumber(total)}
103-
</Text>
104-
</Text>
105-
<Flex gap={2}>{stats}</Flex>
102+
<Flex gap={6} width="full">
103+
<Flex direction="column" gap={2}>
104+
<Text as="div" variant="subheader-2" className={b('section-title')}>
105+
{i18n('title_storage-groups')}{' '}
106+
<Text variant="subheader-2" color="secondary">
107+
{formatNumber(total)}
108+
</Text>
109+
</Text>
110+
<Flex gap={2}>{stats}</Flex>
111+
</Flex>
112+
{bridgePiles?.length ? (
113+
<Flex direction="column" gap={2} className={b('bridge-table')}>
114+
<Text as="div" variant="subheader-2" className={b('section-title')}>
115+
{i18n('title_bridge')}
116+
</Text>
117+
<BridgeInfoTable piles={bridgePiles} />
118+
</Flex>
119+
) : null}
120+
</Flex>
106121
</InfoSection>
107122
);
108123
};

src/containers/Cluster/ClusterOverview/ClusterOverview.tsx

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import {ArrowToggle, Disclosure, Flex, Icon, Text} from '@gravity-ui/uikit';
22

33
import {ResponseError} from '../../../components/Errors/ResponseError';
4-
import {useClusterDashboardAvailable} from '../../../store/reducers/capabilities/hooks';
4+
import {
5+
useBridgeMockEnabled,
6+
useBridgeModeEnabled,
7+
useClusterDashboardAvailable,
8+
} from '../../../store/reducers/capabilities/hooks';
59
import type {ClusterGroupsStats} from '../../../store/reducers/cluster/types';
610
import type {AdditionalClusterProps} from '../../../types/additionalProps';
711
import {isClusterInfoV2, isClusterInfoV5} from '../../../types/api/cluster';
8-
import type {TClusterInfo} from '../../../types/api/cluster';
12+
import type {TBridgePile, TClusterInfo} from '../../../types/api/cluster';
913
import type {IResponseError} from '../../../types/api/error';
1014
import {valueIsDefined} from '../../../utils';
1115
import {EXPAND_CLUSTER_DASHBOARD} from '../../../utils/constants';
@@ -36,6 +40,18 @@ interface ClusterOverviewProps {
3640

3741
export function ClusterOverview(props: ClusterOverviewProps) {
3842
const [expandDashboard, setExpandDashboard] = useSetting<boolean>(EXPAND_CLUSTER_DASHBOARD);
43+
const bridgeModeEnabled = useBridgeModeEnabled();
44+
const bridgeMockEnabled = useBridgeMockEnabled();
45+
let bridgePiles: TBridgePile[] | undefined;
46+
if (isClusterInfoV5(props.cluster)) {
47+
const {BridgeInfo} = props.cluster;
48+
const shouldShowBridge =
49+
(bridgeModeEnabled || bridgeMockEnabled) &&
50+
(Boolean(BridgeInfo?.Piles?.length) || bridgeMockEnabled);
51+
if (shouldShowBridge) {
52+
bridgePiles = BridgeInfo?.Piles ?? getMockBridgePiles();
53+
}
54+
}
3955
if (props.error) {
4056
return <ResponseError error={props.error} className={b('error')} />;
4157
}
@@ -67,7 +83,7 @@ export function ClusterOverview(props: ClusterOverviewProps) {
6783
)}
6884
</Disclosure.Summary>
6985
<ClusterDashboard {...props} />
70-
<ClusterInfo {...props} />
86+
<ClusterInfo {...props} bridgePiles={bridgePiles} />
7187
</Disclosure>
7288
</Flex>
7389
);
@@ -93,7 +109,7 @@ function ClusterDoughnuts({cluster, groupStats = {}, loading, collapsed}: Cluste
93109
if (loading) {
94110
return <ClusterDashboardSkeleton collapsed={collapsed} />;
95111
}
96-
const metricsCards = [];
112+
const metricsCards = [] as React.ReactNode[];
97113
if (isClusterInfoV2(cluster)) {
98114
const {CoresUsed, NumberOfCpus, CoresTotal} = cluster;
99115
const total = CoresTotal ?? NumberOfCpus;
@@ -148,3 +164,10 @@ function ClusterDoughnuts({cluster, groupStats = {}, loading, collapsed}: Cluste
148164

149165
return metricsCards;
150166
}
167+
168+
function getMockBridgePiles(): TBridgePile[] {
169+
return [
170+
{PileId: 1, Name: 'r1', State: 'SYNCHRONIZED', IsPrimary: true, Nodes: 16},
171+
{PileId: 2, Name: 'r2', State: 'SYNCHRONIZED', IsPrimary: false, Nodes: 16},
172+
];
173+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import React from 'react';
2+
3+
import type {Column} from '@gravity-ui/react-data-table';
4+
import DataTable from '@gravity-ui/react-data-table';
5+
6+
import {ResizeableDataTable} from '../../../../components/ResizeableDataTable/ResizeableDataTable';
7+
import type {TBridgePile} from '../../../../types/api/cluster';
8+
import {DEFAULT_TABLE_SETTINGS} from '../../../../utils/constants';
9+
import {formatNumber} from '../../../../utils/dataFormatters/dataFormatters';
10+
import i18n from '../../i18n';
11+
12+
interface BridgeInfoTableProps {
13+
piles: TBridgePile[];
14+
collapsed?: boolean;
15+
}
16+
17+
export function BridgeInfoTable({piles}: BridgeInfoTableProps) {
18+
const columns = React.useMemo<Column<TBridgePile>[]>(
19+
() => [
20+
{
21+
name: 'Name',
22+
header: i18n('label_name'),
23+
width: 160,
24+
align: DataTable.LEFT,
25+
},
26+
{
27+
name: 'IsPrimary',
28+
header: i18n('label_primary'),
29+
width: 110,
30+
align: DataTable.LEFT,
31+
render: ({row}) => (row.IsPrimary ? i18n('value_yes') : i18n('value_no')),
32+
},
33+
{
34+
name: 'State',
35+
header: i18n('label_state'),
36+
width: 160,
37+
align: DataTable.LEFT,
38+
},
39+
{
40+
name: 'Nodes',
41+
header: i18n('label_nodes'),
42+
width: 100,
43+
align: DataTable.RIGHT,
44+
render: ({row}) => (row.Nodes === undefined ? '—' : formatNumber(row.Nodes)),
45+
},
46+
],
47+
[],
48+
);
49+
50+
return (
51+
<ResizeableDataTable<TBridgePile>
52+
columnsWidthLSKey="bridge-columns-width"
53+
data={piles}
54+
columns={columns}
55+
settings={{...DEFAULT_TABLE_SETTINGS, sortable: false}}
56+
rowKey={(row) => `${row.PileId ?? ''}|${row.Name ?? ''}`}
57+
/>
58+
);
59+
}

src/containers/Cluster/i18n/en.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,15 @@
1717
"title_network": "Network",
1818
"title_links": "Links",
1919
"title_details": "Details",
20+
"title_bridge": "Bridge",
2021
"label_overview": "Overview",
2122
"label_load": "Load",
23+
"label_name": "Name",
24+
"label_primary": "Primary",
25+
"label_state": "State",
26+
"label_nodes": "Nodes",
27+
"value_yes": "Yes",
28+
"value_no": "No",
2229
"context_of": "of",
2330
"context_cpu": "CPU load",
2431
"context_memory": "Memory used",

src/store/reducers/capabilities/capabilities.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ export const selectGraphShardExists = createSelector(
7272
selectDatabaseCapabilities(state, database).data?.Settings?.Database?.GraphShardExists,
7373
);
7474

75+
export const selectBridgeModeEnabled = createSelector(
76+
(state: RootState) => state,
77+
(_state: RootState, database?: string) => database,
78+
(state, database) =>
79+
selectDatabaseCapabilities(state, database).data?.Settings?.Cluster?.BridgeModeEnabled,
80+
);
81+
7582
export async function queryCapability(
7683
capability: Capability,
7784
database: string | undefined,

src/store/reducers/capabilities/hooks.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import {StringParam, useQueryParam} from 'use-query-params';
2+
13
import type {Capability, MetaCapability, SecuritySetting} from '../../../types/api/capabilities';
24
import {useTypedSelector} from '../../../utils/hooks';
35
import {useDatabaseFromQuery} from '../../../utils/hooks/useDatabaseFromQuery';
46

57
import {
68
capabilitiesApi,
9+
selectBridgeModeEnabled,
710
selectCapabilityVersion,
811
selectDatabaseCapabilities,
912
selectGraphShardExists,
@@ -105,6 +108,21 @@ export const useGraphShardExists = () => {
105108
return useTypedSelector((state) => selectGraphShardExists(state, database));
106109
};
107110

111+
export const useBridgeModeEnabled = () => {
112+
const database = useDatabaseFromQuery();
113+
const [mockBridge] = useQueryParam('mockBridge', StringParam);
114+
const isMockEnabled =
115+
(mockBridge ?? '').toLowerCase() === '1' || (mockBridge ?? '').toLowerCase() === 'true';
116+
117+
const enabled = useTypedSelector((state) => selectBridgeModeEnabled(state, database));
118+
return Boolean(enabled || isMockEnabled);
119+
};
120+
121+
export const useBridgeMockEnabled = () => {
122+
const [mockBridge] = useQueryParam('mockBridge', StringParam);
123+
return (mockBridge ?? '').toLowerCase() === '1' || (mockBridge ?? '').toLowerCase() === 'true';
124+
};
125+
108126
export const useClusterWithoutAuthInUI = () => {
109127
return useGetSecuritySetting('UseLoginProvider') === false;
110128
};

src/store/reducers/cluster/cluster.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import {skipToken} from '@reduxjs/toolkit/query';
55
import type {ClusterTab} from '../../../containers/Cluster/utils';
66
import {clusterTabsIds, isClusterTab} from '../../../containers/Cluster/utils';
77
import {parseTraceFields} from '../../../services/parsers/parseMetaCluster';
8-
import {isClusterInfoV2} from '../../../types/api/cluster';
9-
import type {TClusterInfo} from '../../../types/api/cluster';
8+
import {isClusterInfoV2, isClusterInfoV5} from '../../../types/api/cluster';
9+
import type {TClusterInfo, TClusterInfoV5} from '../../../types/api/cluster';
1010
import type {TTabletStateInfo} from '../../../types/api/tablet';
1111
import {CLUSTER_DEFAULT_TITLE, DEFAULT_CLUSTER_TAB_KEY} from '../../../utils/constants';
1212
import {useClusterNameFromQuery} from '../../../utils/hooks/useDatabaseFromQuery';
@@ -207,3 +207,14 @@ export const selectClusterTabletsWithFqdn = createSelector(
207207
});
208208
},
209209
);
210+
211+
export const selectBridgeInfo = createSelector(
212+
(state: RootState) => state,
213+
(_state: RootState, clusterName?: string) => clusterName,
214+
(state, clusterName) => {
215+
const info = selectClusterInfo(state, clusterName)?.clusterData;
216+
return isClusterInfoV5(info as TClusterInfo)
217+
? (info as TClusterInfoV5).BridgeInfo
218+
: undefined;
219+
},
220+
);

src/types/api/capabilities.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ export interface CapabilitiesResponse {
88
Database?: {
99
GraphShardExists?: boolean;
1010
};
11+
Cluster?: {
12+
BridgeModeEnabled?: boolean;
13+
};
1114
};
1215
}
1316

src/types/api/cluster.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export interface TClusterInfoV5 extends TClusterInfoV2 {
7979
NetworkUtilization?: number;
8080
/** value is uint64 */
8181
NetworkWriteThroughput?: string;
82+
BridgeInfo?: TBridgeInfo;
8283
}
8384

8485
export type TClusterInfo = TClusterInfoV1 | TClusterInfoV2 | TClusterInfoV5;
@@ -95,3 +96,20 @@ function isClusterParticularVersionOrHigher(info: TClusterInfo | undefined, vers
9596
info && 'Version' in info && typeof info.Version === 'number' && info.Version >= version,
9697
);
9798
}
99+
100+
export interface TBridgePile {
101+
/** unique pile identifier */
102+
PileId?: number;
103+
/** pile name, e.g., r1 */
104+
Name?: string;
105+
/** pile state (string from backend, e.g., SYNCHRONIZED) */
106+
State?: string;
107+
/** whether this pile is primary */
108+
IsPrimary?: boolean;
109+
/** number of nodes in the pile */
110+
Nodes?: number;
111+
}
112+
113+
export interface TBridgeInfo {
114+
Piles?: TBridgePile[];
115+
}

0 commit comments

Comments
 (0)