Skip to content

Commit a4b737b

Browse files
authored
feat: 2dc (ydb-platform#2699)
1 parent 7734edb commit a4b737b

File tree

45 files changed

+721
-90
lines changed

Some content is hidden

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

45 files changed

+721
-90
lines changed

.github/copilot-instructions.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
# Copilot Instructions
2+
3+
## Internationalization (i18n)
4+
5+
- Do NOT hardcode user-facing strings.
6+
- ALWAYS use the component i18n keysets.
7+
- For key naming, see `i18n-naming-ruleset.md` in the repo root.
8+
19
# GitHub Copilot Instructions for YDB Embedded UI
210

311
> **Note**: This file contains project-specific instructions for GitHub Copilot code review and assistance.
@@ -82,6 +90,10 @@ const handleInputChange = useCallback(
8290
- Follow key format: `<context>_<content>` (e.g., `action_save`, `field_name`)
8391
- Register keysets with `registerKeysets()` using unique component name
8492

93+
### Display Placeholders (MANDATORY)
94+
95+
- 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`.
96+
8597
### State Management
8698

8799
- Use Redux Toolkit with domain-based organization

AGENTS.md

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -232,21 +232,7 @@ Uses BEM naming convention with `cn()` utility from `utils/cn`. Create a block f
232232

233233
### Internationalization (i18n)
234234

235-
All user-facing text must be internationalized using the i18n system. Follow the naming rules from `i18n-naming-ruleset.md`:
236-
237-
- **Component Structure**: Each component has an `i18n/` folder with `en.json` and `index.ts`
238-
- **Registration**: Use `registerKeysets()` with a unique component name
239-
- **Key Format**: Follow `<context>_<content>` pattern (e.g., `action_save`, `field_name`, `alert_error`)
240-
- **Context Prefixes**:
241-
- `action_` - buttons, links, menu items
242-
- `field_` - form fields, table columns
243-
- `title_` - page/section titles
244-
- `alert_` - notifications, errors
245-
- `context_` - descriptions, hints
246-
- `confirm_` - confirmation dialogs
247-
- `value_` - status values, options
248-
- **NEVER** use hardcoded strings in UI components
249-
- **ALWAYS** create i18n entries for all user-visible text
235+
See `i18n-naming-ruleset.md` in the repo root for all i18n conventions (naming and usage).
250236

251237
### Performance Considerations
252238

@@ -297,6 +283,7 @@ const [urlParam, setUrlParam] = useQueryParam('sort', SortOrderParam);
297283
- **NEVER** call APIs directly - use `window.api.module.method()`
298284
- **NEVER** mutate state in RTK Query - return new objects/arrays
299285
- **NEVER** hardcode user-facing strings - use i18n
286+
- **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`.
300287
- **ALWAYS** use `cn()` for classNames: `const b = cn('component-name')`
301288
- **ALWAYS** clear errors on user input
302289
- **ALWAYS** handle loading states in UI

CLAUDE.md

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -232,21 +232,7 @@ Uses BEM naming convention with `cn()` utility from `utils/cn`. Create a block f
232232

233233
### Internationalization (i18n)
234234

235-
All user-facing text must be internationalized using the i18n system. Follow the naming rules from `i18n-naming-ruleset.md`:
236-
237-
- **Component Structure**: Each component has an `i18n/` folder with `en.json` and `index.ts`
238-
- **Registration**: Use `registerKeysets()` with a unique component name
239-
- **Key Format**: Follow `<context>_<content>` pattern (e.g., `action_save`, `field_name`, `alert_error`)
240-
- **Context Prefixes**:
241-
- `action_` - buttons, links, menu items
242-
- `field_` - form fields, table columns
243-
- `title_` - page/section titles
244-
- `alert_` - notifications, errors
245-
- `context_` - descriptions, hints
246-
- `confirm_` - confirmation dialogs
247-
- `value_` - status values, options
248-
- **NEVER** use hardcoded strings in UI components
249-
- **ALWAYS** create i18n entries for all user-visible text
235+
See `i18n-naming-ruleset.md` in the repo root for all i18n conventions (naming and usage).
250236

251237
### Performance Considerations
252238

@@ -297,6 +283,7 @@ const [urlParam, setUrlParam] = useQueryParam('sort', SortOrderParam);
297283
- **NEVER** call APIs directly - use `window.api.module.method()`
298284
- **NEVER** mutate state in RTK Query - return new objects/arrays
299285
- **NEVER** hardcode user-facing strings - use i18n
286+
- **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`.
300287
- **ALWAYS** use `cn()` for classNames: `const b = cn('component-name')`
301288
- **ALWAYS** clear errors on user input
302289
- **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('field_pile-name'),
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('field_pile-name');
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('field_pile-name');
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+
"field_pile-name": "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.tsx

Lines changed: 26 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,28 @@ 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 variant="subheader-2" color="secondary">
117+
{formatNumber(bridgePiles.length)}
118+
</Text>
119+
</Text>
120+
<BridgeInfoTable piles={bridgePiles} />
121+
</Flex>
122+
) : null}
123+
</Flex>
106124
</InfoSection>
107125
);
108126
};

src/containers/Cluster/ClusterOverview/ClusterOverview.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
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';
@@ -36,6 +41,16 @@ interface ClusterOverviewProps {
3641

3742
export function ClusterOverview(props: ClusterOverviewProps) {
3843
const [expandDashboard, setExpandDashboard] = useSetting<boolean>(EXPAND_CLUSTER_DASHBOARD);
44+
const bridgeModeEnabled = useBridgeModeEnabled();
45+
46+
const bridgePiles = React.useMemo(() => {
47+
if (!bridgeModeEnabled || !isClusterInfoV5(props.cluster)) {
48+
return undefined;
49+
}
50+
51+
const {BridgeInfo} = props.cluster;
52+
return BridgeInfo?.Piles?.length ? BridgeInfo.Piles : undefined;
53+
}, [props.cluster, bridgeModeEnabled]);
3954
if (props.error) {
4055
return <ResponseError error={props.error} className={b('error')} />;
4156
}
@@ -67,7 +82,7 @@ export function ClusterOverview(props: ClusterOverviewProps) {
6782
)}
6883
</Disclosure.Summary>
6984
<ClusterDashboard {...props} />
70-
<ClusterInfo {...props} />
85+
<ClusterInfo {...props} bridgePiles={bridgePiles} />
7186
</Disclosure>
7287
</Flex>
7388
);
@@ -93,7 +108,7 @@ function ClusterDoughnuts({cluster, groupStats = {}, loading, collapsed}: Cluste
93108
if (loading) {
94109
return <ClusterDashboardSkeleton collapsed={collapsed} />;
95110
}
96-
const metricsCards = [];
111+
const metricsCards: React.ReactNode[] = [];
97112
if (isClusterInfoV2(cluster)) {
98113
const {CoresUsed, NumberOfCpus, CoresTotal} = cluster;
99114
const total = CoresTotal ?? NumberOfCpus;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
@use '../../../../styles/mixins.scss';
2+
3+
.bridge-info-table {
4+
height: 100%;
5+
6+
&__pile-card {
7+
--g-definition-list-item-gap: var(--g-spacing-3);
8+
9+
width: 347px;
10+
padding: var(--g-spacing-3) var(--g-spacing-4);
11+
12+
border-radius: var(--g-border-radius-s);
13+
background-color: var(--g-color-base-generic-ultralight);
14+
15+
@include mixins.body-2-typography();
16+
}
17+
18+
&__status-icon {
19+
&_primary {
20+
color: var(--g-color-text-positive);
21+
}
22+
23+
&:not(&_primary) {
24+
color: var(--g-color-private-white-250);
25+
}
26+
}
27+
}

0 commit comments

Comments
 (0)