Skip to content

Commit f39869d

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

File tree

44 files changed

+511
-62
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+511
-62
lines changed

.github/copilot-instructions.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ const handleInputChange = useCallback(
8282
- Follow key format: `<context>_<content>` (e.g., `action_save`, `field_name`)
8383
- Register keysets with `registerKeysets()` using unique component name
8484

85+
### Display Placeholders (MANDATORY)
86+
87+
- ALWAYS use `EMPTY_DATA_PLACEHOLDER` for empty UI values. Do not hardcode em or en dashes (``, ``) as placeholders. Hyphen `-`/dashes may be used as separators in titles/ranges. Before submitting a PR, grep for `` and `` and ensure placeholder usages use `EMPTY_DATA_PLACEHOLDER` from `src/utils/constants.ts`.
88+
8589
### State Management
8690

8791
- Use Redux Toolkit with domain-based organization

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ const [urlParam, setUrlParam] = useQueryParam('sort', SortOrderParam);
297297
- **NEVER** call APIs directly - use `window.api.module.method()`
298298
- **NEVER** mutate state in RTK Query - return new objects/arrays
299299
- **NEVER** hardcode user-facing strings - use i18n
300+
- **ALWAYS** use `EMPTY_DATA_PLACEHOLDER` for empty UI values. Do not hardcode em dashes `` or en dashes `` as placeholders. Hyphen `-` and dashes may be used as separators in titles/ranges. Before submitting, grep the code for ``/`` and ensure placeholders use `EMPTY_DATA_PLACEHOLDER` from `src/utils/constants.ts`.
300301
- **ALWAYS** use `cn()` for classNames: `const b = cn('component-name')`
301302
- **ALWAYS** clear errors on user input
302303
- **ALWAYS** handle loading states in UI

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ const [urlParam, setUrlParam] = useQueryParam('sort', SortOrderParam);
297297
- **NEVER** call APIs directly - use `window.api.module.method()`
298298
- **NEVER** mutate state in RTK Query - return new objects/arrays
299299
- **NEVER** hardcode user-facing strings - use i18n
300+
- **ALWAYS** use `EMPTY_DATA_PLACEHOLDER` for empty UI values. Do not hardcode em dashes `` or en dashes `` as placeholders. Hyphen `-` and dashes may be used as separators in titles/ranges. Before submitting, grep the code for ``/`` and ensure placeholders use `EMPTY_DATA_PLACEHOLDER` from `src/utils/constants.ts`.
300301
- **ALWAYS** use `cn()` for classNames: `const b = cn('component-name')`
301302
- **ALWAYS** clear errors on user input
302303
- **ALWAYS** handle loading states in UI

src/components/NodeHostWrapper/NodeHostWrapper.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {getDefaultNodePath} from '../../containers/Node/NodePages';
22
import type {GetNodeRefFunc, NodeAddress} from '../../types/additionalProps';
33
import type {TNodeInfo, TSystemStateInfo} from '../../types/api/nodes';
4+
import {EMPTY_DATA_PLACEHOLDER} from '../../utils/constants';
45
import {
56
createDeveloperUIInternalPageHref,
67
createDeveloperUILinkWithNodeId,
@@ -30,7 +31,7 @@ export const NodeHostWrapper = ({
3031
statusForIcon = 'SystemState',
3132
}: NodeHostWrapperProps) => {
3233
if (!node.Host) {
33-
return <span></span>;
34+
return EMPTY_DATA_PLACEHOLDER;
3435
}
3536

3637
const status = statusForIcon === 'ConnectStatus' ? node.ConnectStatus : node.SystemState;

src/components/nodesColumns/columns.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,16 @@ export function getRackColumn<T extends {Rack?: string}>(): Column<T> {
110110
width: 100,
111111
};
112112
}
113+
114+
export function getPileNameColumn<T extends {PileName?: string}>(): Column<T> {
115+
return {
116+
name: NODES_COLUMNS_IDS.PileName,
117+
header: i18n('PileName'),
118+
align: DataTable.LEFT,
119+
render: ({row}) => row.PileName || EMPTY_DATA_PLACEHOLDER,
120+
width: 100,
121+
};
122+
}
113123
export function getVersionColumn<T extends {Version?: string}>(): Column<T> {
114124
return {
115125
name: NODES_COLUMNS_IDS.Version,

src/components/nodesColumns/constants.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export const NODES_COLUMNS_IDS = {
3232
Missing: 'Missing',
3333
Tablets: 'Tablets',
3434
PDisks: 'PDisks',
35+
PileName: 'PileName',
3536
} as const;
3637

3738
export type NodesColumnId = ValueOf<typeof NODES_COLUMNS_IDS>;
@@ -130,6 +131,9 @@ export const NODES_COLUMNS_TITLES = {
130131
get PDisks() {
131132
return i18n('pdisks');
132133
},
134+
get PileName() {
135+
return i18n('PileName');
136+
},
133137
} as const satisfies Record<NodesColumnId, string>;
134138

135139
const NODES_COLUMNS_GROUP_BY_TITLES = {
@@ -178,6 +182,9 @@ const NODES_COLUMNS_GROUP_BY_TITLES = {
178182
get PingTime() {
179183
return i18n('ping-time');
180184
},
185+
get PileName() {
186+
return i18n('PileName');
187+
},
181188
} as const satisfies Record<NodesGroupByField, string>;
182189

183190
export function getNodesGroupByFieldTitle(groupByField: NodesGroupByField) {
@@ -213,6 +220,7 @@ export const NODES_COLUMNS_TO_DATA_FIELDS: Record<NodesColumnId, NodesRequiredFi
213220
Missing: ['Missing'],
214221
Tablets: ['Tablets', 'Database'],
215222
PDisks: ['PDisks'],
223+
PileName: ['PileName'],
216224
};
217225

218226
const NODES_COLUMNS_TO_SORT_FIELDS: Record<NodesColumnId, NodesSortValue | undefined> = {
@@ -242,6 +250,7 @@ const NODES_COLUMNS_TO_SORT_FIELDS: Record<NodesColumnId, NodesSortValue | undef
242250
Missing: 'Missing',
243251
Tablets: undefined,
244252
PDisks: undefined,
253+
PileName: undefined,
245254
};
246255

247256
export function getNodesColumnSortField(columnId?: string) {

src/components/nodesColumns/i18n/en.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,9 @@
1818
"sessions": "Sessions",
1919
"missing": "Missing",
2020
"pdisks": "PDisks",
21-
2221
"field_memory-used": "Memory used",
2322
"field_memory-limit": "Memory limit",
24-
23+
"PileName": "Pile Name",
2524
"system-state": "System State",
2625
"connect-status": "Connect Status",
2726
"utilization": "Utilization",
@@ -33,7 +32,6 @@
3332
"ping": "Ping",
3433
"send": "Send",
3534
"receive": "Receive",
36-
3735
"max": "Max",
3836
"min": "Min",
3937
"avg": "Avg",

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: 25 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

@@ -19,12 +20,15 @@ import {getInfo, getStorageGroupStats} from './utils/utils';
1920

2021
import './ClusterInfo.scss';
2122

23+
const GROUPS_SECTION_GAP = 10;
24+
2225
interface ClusterInfoProps {
2326
cluster?: TClusterInfo;
2427
loading?: boolean;
2528
error?: IResponseError | string;
2629
additionalClusterProps?: AdditionalClusterProps;
2730
groupStats?: ClusterGroupsStats;
31+
bridgePiles?: TBridgePile[];
2832
}
2933

3034
export const ClusterInfo = ({
@@ -33,6 +37,7 @@ export const ClusterInfo = ({
3337
error,
3438
additionalClusterProps = {},
3539
groupStats = {},
40+
bridgePiles,
3641
}: ClusterInfoProps) => {
3742
const {info = [], links = []} = additionalClusterProps;
3843

@@ -96,13 +101,25 @@ export const ClusterInfo = ({
96101
}
97102
return (
98103
<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>
104+
<Flex gap={GROUPS_SECTION_GAP} width="full">
105+
<Flex direction="column" gap={2}>
106+
<Text as="div" variant="subheader-2" className={b('section-title')}>
107+
{i18n('title_storage-groups')}{' '}
108+
<Text variant="subheader-2" color="secondary">
109+
{formatNumber(total)}
110+
</Text>
111+
</Text>
112+
<Flex gap={2}>{stats}</Flex>
113+
</Flex>
114+
{bridgePiles?.length ? (
115+
<Flex direction="column" gap={2} className={b('bridge-table')}>
116+
<Text as="div" variant="subheader-2" className={b('section-title')}>
117+
{i18n('title_bridge')}
118+
</Text>
119+
<BridgeInfoTable piles={bridgePiles} />
120+
</Flex>
121+
) : null}
122+
</Flex>
106123
</InfoSection>
107124
);
108125
};

src/containers/Cluster/ClusterOverview/ClusterOverview.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1+
import React from 'react';
2+
13
import {ArrowToggle, Disclosure, Flex, Icon, Text} from '@gravity-ui/uikit';
24

35
import {ResponseError} from '../../../components/Errors/ResponseError';
4-
import {useClusterDashboardAvailable} from '../../../store/reducers/capabilities/hooks';
6+
import {
7+
useBridgeModeEnabled,
8+
useClusterDashboardAvailable,
9+
} from '../../../store/reducers/capabilities/hooks';
510
import type {ClusterGroupsStats} from '../../../store/reducers/cluster/types';
611
import type {AdditionalClusterProps} from '../../../types/additionalProps';
712
import {isClusterInfoV2, isClusterInfoV5} from '../../../types/api/cluster';
8-
import type {TClusterInfo} from '../../../types/api/cluster';
13+
import type {TBridgePile, TClusterInfo} from '../../../types/api/cluster';
914
import type {IResponseError} from '../../../types/api/error';
1015
import {valueIsDefined} from '../../../utils';
1116
import {EXPAND_CLUSTER_DASHBOARD} from '../../../utils/constants';
@@ -36,6 +41,15 @@ interface ClusterOverviewProps {
3641

3742
export function ClusterOverview(props: ClusterOverviewProps) {
3843
const [expandDashboard, setExpandDashboard] = useSetting<boolean>(EXPAND_CLUSTER_DASHBOARD);
44+
const bridgeModeEnabled = useBridgeModeEnabled();
45+
let bridgePiles: TBridgePile[] | undefined;
46+
if (isClusterInfoV5(props.cluster)) {
47+
const {BridgeInfo} = props.cluster;
48+
const shouldShowBridge = bridgeModeEnabled && Boolean(BridgeInfo?.Piles?.length);
49+
if (shouldShowBridge) {
50+
bridgePiles = BridgeInfo?.Piles;
51+
}
52+
}
3953
if (props.error) {
4054
return <ResponseError error={props.error} className={b('error')} />;
4155
}
@@ -67,7 +81,7 @@ export function ClusterOverview(props: ClusterOverviewProps) {
6781
)}
6882
</Disclosure.Summary>
6983
<ClusterDashboard {...props} />
70-
<ClusterInfo {...props} />
84+
<ClusterInfo {...props} bridgePiles={bridgePiles} />
7185
</Disclosure>
7286
</Flex>
7387
);
@@ -93,7 +107,7 @@ function ClusterDoughnuts({cluster, groupStats = {}, loading, collapsed}: Cluste
93107
if (loading) {
94108
return <ClusterDashboardSkeleton collapsed={collapsed} />;
95109
}
96-
const metricsCards = [];
110+
const metricsCards: React.ReactNode[] = [];
97111
if (isClusterInfoV2(cluster)) {
98112
const {CoresUsed, NumberOfCpus, CoresTotal} = cluster;
99113
const total = CoresTotal ?? NumberOfCpus;

0 commit comments

Comments
 (0)