Skip to content

Commit a20e325

Browse files
committed
fix: better design
1 parent 0647890 commit a20e325

File tree

9 files changed

+275
-56
lines changed

9 files changed

+275
-56
lines changed

src/containers/Cluster/ClusterInfo/ClusterInfo.scss

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

2121
margin-left: 5px;
2222
}
23-
24-
&__bridge-table {
25-
flex: 0 0 360px; // do not shrink, fixed basis so stats don't take all space
26-
27-
min-width: 360px;
28-
}
2923
}

src/containers/Cluster/ClusterInfo/ClusterInfo.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ export const ClusterInfo = ({
9999
}
100100
return (
101101
<InfoSection>
102-
<Flex gap={10} width="full">
102+
<Flex gap={6} width="full">
103103
<Flex direction="column" gap={2}>
104104
<Text as="div" variant="subheader-2" className={b('section-title')}>
105105
{i18n('title_storage-groups')}{' '}
@@ -112,7 +112,10 @@ export const ClusterInfo = ({
112112
{bridgePiles?.length ? (
113113
<Flex direction="column" gap={2} className={b('bridge-table')}>
114114
<Text as="div" variant="subheader-2" className={b('section-title')}>
115-
{i18n('title_bridge')}
115+
{i18n('title_bridge')}{' '}
116+
<Text variant="subheader-2" color="secondary">
117+
{formatNumber(bridgePiles.length)}
118+
</Text>
116119
</Text>
117120
<BridgeInfoTable piles={bridgePiles} />
118121
</Flex>

src/containers/Cluster/ClusterOverview/ClusterOverview.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
import type {ClusterGroupsStats} from '../../../store/reducers/cluster/types';
1111
import type {AdditionalClusterProps} from '../../../types/additionalProps';
1212
import {isClusterInfoV2, isClusterInfoV5} from '../../../types/api/cluster';
13-
import type {TBridgePile, TClusterInfo} from '../../../types/api/cluster';
13+
import type {TClusterInfo} from '../../../types/api/cluster';
1414
import type {IResponseError} from '../../../types/api/error';
1515
import {valueIsDefined} from '../../../utils';
1616
import {EXPAND_CLUSTER_DASHBOARD} from '../../../utils/constants';
@@ -42,14 +42,15 @@ interface ClusterOverviewProps {
4242
export function ClusterOverview(props: ClusterOverviewProps) {
4343
const [expandDashboard, setExpandDashboard] = useSetting<boolean>(EXPAND_CLUSTER_DASHBOARD);
4444
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;
45+
46+
const bridgePiles = React.useMemo(() => {
47+
if (!bridgeModeEnabled || !isClusterInfoV5(props.cluster)) {
48+
return undefined;
5149
}
52-
}
50+
51+
const {BridgeInfo} = props.cluster;
52+
return BridgeInfo?.Piles?.length ? BridgeInfo.Piles : undefined;
53+
}, [props.cluster, bridgeModeEnabled]);
5354
if (props.error) {
5455
return <ResponseError error={props.error} className={b('error')} />;
5556
}
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+
}
Lines changed: 70 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,93 @@
11
import React from 'react';
22

3-
import type {Column} from '@gravity-ui/react-data-table';
4-
import DataTable from '@gravity-ui/react-data-table';
3+
import {CircleCheckFill, CircleXmarkFill} from '@gravity-ui/icons';
4+
import {DefinitionList, Flex, Icon, Label, Text} from '@gravity-ui/uikit';
55

6-
import {ResizeableDataTable} from '../../../../components/ResizeableDataTable/ResizeableDataTable';
76
import type {TBridgePile} from '../../../../types/api/cluster';
8-
import {DEFAULT_TABLE_SETTINGS, EMPTY_DATA_PLACEHOLDER} from '../../../../utils/constants';
7+
import {cn} from '../../../../utils/cn';
8+
import {EMPTY_DATA_PLACEHOLDER} from '../../../../utils/constants';
99
import {formatNumber} from '../../../../utils/dataFormatters/dataFormatters';
1010
import i18n from '../../i18n';
1111

12+
import './BridgeInfoTable.scss';
13+
14+
const b = cn('bridge-info-table');
15+
1216
interface BridgeInfoTableProps {
1317
piles: TBridgePile[];
14-
collapsed?: boolean;
1518
}
1619

17-
export function BridgeInfoTable({piles}: BridgeInfoTableProps) {
18-
const columns = React.useMemo<Column<TBridgePile>[]>(
20+
interface BridgePileCardProps {
21+
pile: TBridgePile;
22+
}
23+
24+
const BridgePileCard = React.memo(function BridgePileCard({pile}: BridgePileCardProps) {
25+
const renderPrimaryStatus = React.useCallback(() => {
26+
const isPrimary = pile.IsPrimary;
27+
const icon = isPrimary ? CircleCheckFill : CircleXmarkFill;
28+
const text = isPrimary ? i18n('value_yes') : i18n('value_no');
29+
30+
return (
31+
<Flex gap={1} alignItems="center">
32+
<Icon data={icon} size={16} className={b('status-icon', {primary: isPrimary})} />
33+
<Text color="secondary">{text}</Text>
34+
</Flex>
35+
);
36+
}, [pile.IsPrimary]);
37+
38+
const renderStateStatus = React.useCallback(() => {
39+
if (!pile.State) {
40+
return EMPTY_DATA_PLACEHOLDER;
41+
}
42+
43+
const isSynchronized = pile.State.toUpperCase() === 'SYNCHRONIZED';
44+
const theme = isSynchronized ? 'success' : 'info';
45+
46+
return <Label theme={theme}>{pile.State}</Label>;
47+
}, [pile.State]);
48+
49+
const info = React.useMemo(
1950
() => [
2051
{
21-
name: 'Name',
22-
header: i18n('field_name'),
23-
width: 160,
24-
align: DataTable.LEFT,
52+
name: i18n('field_primary'),
53+
content: renderPrimaryStatus(),
2554
},
2655
{
27-
name: 'IsPrimary',
28-
header: i18n('field_primary'),
29-
width: 110,
30-
align: DataTable.LEFT,
31-
render: ({row}) => (row.IsPrimary ? i18n('value_yes') : i18n('value_no')),
56+
name: i18n('field_state'),
57+
content: renderStateStatus(),
3258
},
3359
{
34-
name: 'State',
35-
header: i18n('field_state'),
36-
width: 160,
37-
align: DataTable.LEFT,
38-
},
39-
{
40-
name: 'Nodes',
41-
header: i18n('field_nodes'),
42-
width: 100,
43-
align: DataTable.RIGHT,
44-
render: ({row}) =>
45-
row.Nodes === undefined ? EMPTY_DATA_PLACEHOLDER : formatNumber(row.Nodes),
60+
name: i18n('field_nodes'),
61+
content:
62+
pile.Nodes === undefined ? EMPTY_DATA_PLACEHOLDER : formatNumber(pile.Nodes),
4663
},
4764
],
48-
[],
65+
[renderPrimaryStatus, renderStateStatus, pile.Nodes],
4966
);
5067

5168
return (
52-
<ResizeableDataTable<TBridgePile>
53-
columnsWidthLSKey="bridge-columns-width"
54-
data={piles}
55-
columns={columns}
56-
settings={{...DEFAULT_TABLE_SETTINGS, sortable: false}}
57-
rowKey={(row) => `${row.PileId ?? ''}|${row.Name ?? ''}`}
58-
/>
69+
<Flex direction="column" gap={3} className={b('pile-card')}>
70+
<Text variant="body-2">{pile.Name || EMPTY_DATA_PLACEHOLDER}</Text>
71+
<DefinitionList nameMaxWidth={160}>
72+
{info.map(({name, content}) => (
73+
<DefinitionList.Item key={name} name={name}>
74+
{content}
75+
</DefinitionList.Item>
76+
))}
77+
</DefinitionList>
78+
</Flex>
5979
);
60-
}
80+
});
81+
82+
export const BridgeInfoTable = React.memo(function BridgeInfoTable({piles}: BridgeInfoTableProps) {
83+
const renderedPiles = React.useMemo(
84+
() => piles.map((pile, index) => <BridgePileCard key={pile.PileId ?? index} pile={pile} />),
85+
[piles],
86+
);
87+
88+
return (
89+
<Flex gap={2} className={b()}>
90+
{renderedPiles}
91+
</Flex>
92+
);
93+
});

tests/suites/bridge/bridge.test.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import {expect, test} from '@playwright/test';
22

33
import {backend, nodesPage} from '../../utils/constants';
4+
import {ClusterPage} from '../cluster/ClusterPage';
45
import {ClusterNodesTable, ClusterStorageTable} from '../paginatedTable/paginatedTable';
56
import {StoragePage} from '../storage/StoragePage';
67

7-
import {mockCapabilities, mockNodesWithPile, mockStorageGroupsWithPile} from './mocks';
8+
import {
9+
mockCapabilities,
10+
mockClusterWithBridgePiles,
11+
mockNodesWithPile,
12+
mockStorageGroupsWithPile,
13+
} from './mocks';
814

915
test.describe('Bridge mode - Nodes table', () => {
1016
test('off: no Pile Name column and no group-by option', async ({page}) => {
@@ -85,3 +91,36 @@ test.describe('Bridge mode - Storage groups', () => {
8591
expect(headers.join(' ')).not.toContain('Pile Name');
8692
});
8793
});
94+
95+
test.describe('Bridge mode - Cluster Overview', () => {
96+
test('off: does not show Bridge piles section', async ({page}) => {
97+
await mockCapabilities(page, false);
98+
99+
const clusterPage = new ClusterPage(page);
100+
await clusterPage.goto();
101+
102+
// Bridge piles section should not be visible
103+
expect(await clusterPage.isBridgeSectionVisible()).toBe(false);
104+
});
105+
106+
test('on: shows Bridge piles section with data', async ({page}) => {
107+
await mockCapabilities(page, true);
108+
await mockClusterWithBridgePiles(page);
109+
110+
const clusterPage = new ClusterPage(page);
111+
await clusterPage.goto();
112+
113+
// Bridge piles section should be visible
114+
expect(await clusterPage.isBridgeSectionVisible()).toBe(true);
115+
116+
// Should show pile cards
117+
expect(await clusterPage.getPileCardsCount()).toBe(2);
118+
119+
// Check first pile content
120+
const firstPileContent = await clusterPage.getFirstPileContent();
121+
expect(firstPileContent).toContain('r1');
122+
expect(firstPileContent).toContain('Yes'); // Primary status
123+
expect(firstPileContent).toContain('SYNCHRONIZED');
124+
expect(firstPileContent).toContain('16'); // Nodes count
125+
});
126+
});

tests/suites/bridge/mocks.ts

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ export const mockCapabilities = (page: Page, enabled: boolean) => {
88
body: JSON.stringify({
99
Database: '/local',
1010
Settings: {Cluster: {BridgeModeEnabled: enabled}},
11-
Capabilities: {},
11+
Capabilities: {
12+
'/viewer/cluster': 5, // > 4 to enable cluster dashboard
13+
},
1214
}),
1315
});
1416
});
@@ -53,3 +55,87 @@ export const mockStorageGroupsWithPile = (page: Page) => {
5355
});
5456
});
5557
};
58+
59+
export const mockClusterWithBridgePiles = (page: Page) => {
60+
return page.route(`**/viewer/json/cluster?*`, async (route: Route) => {
61+
await route.fulfill({
62+
status: 200,
63+
contentType: 'application/json',
64+
body: JSON.stringify({
65+
Version: 6,
66+
Domain: '/dev02',
67+
Overall: 'Green',
68+
NodesTotal: 28,
69+
NodesAlive: 28,
70+
NumberOfCpus: 1152,
71+
CoresTotal: 949,
72+
CoresUsed: 1.9579652512078709,
73+
LoadAverage: 10.42,
74+
PoolStats: [
75+
{
76+
Name: 'System',
77+
Usage: 0.0031261495677549389,
78+
Threads: 244,
79+
},
80+
{
81+
Name: 'User',
82+
Usage: 0.0013519195624372728,
83+
Threads: 433,
84+
},
85+
],
86+
MemoryTotal: '2651615580160',
87+
MemoryUsed: '64894066688',
88+
StorageTotal: '102385812766720',
89+
StorageUsed: '12139909611520',
90+
MapStorageTotal: {
91+
SSD: '102385812766720',
92+
},
93+
MapStorageUsed: {
94+
SSD: '12139909611520',
95+
},
96+
DataCenters: ['FAKE', 'KLG', 'VLA'],
97+
MapDataCenters: {
98+
FAKE: 8,
99+
KLG: 1,
100+
VLA: 19,
101+
},
102+
Versions: ['improve-ssl-errors-handling.d28b5d8'],
103+
MapVersions: {
104+
'improve-ssl-errors-handling.d28b5d8': 28,
105+
},
106+
MapNodeStates: {
107+
Green: 28,
108+
},
109+
MapNodeRoles: {
110+
StateStorage: 8,
111+
Bootstrapper: 8,
112+
SchemeBoard: 8,
113+
StateStorageBoard: 8,
114+
Tenant: 20,
115+
Storage: 8,
116+
},
117+
StorageStats: [
118+
{
119+
PDiskFilter: 'Type:SSD',
120+
ErasureSpecies: 'block-4-2',
121+
CurrentGroupsCreated: 16,
122+
CurrentAllocatedSize: '9030951676583',
123+
CurrentAvailableSize: '9845932905808',
124+
AvailableGroupsToCreate: 47,
125+
AvailableSizeToCreate: '49682593436982',
126+
},
127+
],
128+
Hosts: '9',
129+
Tenants: '9',
130+
NetworkUtilization: 0.00087389813662801838,
131+
NetworkWriteThroughput: '1445752',
132+
BridgeInfo: {
133+
Piles: [
134+
{PileId: 1, Name: 'r1', State: 'SYNCHRONIZED', IsPrimary: true, Nodes: 16},
135+
{PileId: 2, Name: 'r2', State: 'READY', IsPrimary: false, Nodes: 12},
136+
],
137+
},
138+
}),
139+
});
140+
});
141+
};

0 commit comments

Comments
 (0)