Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React from 'react';

import {CircleCheckFill, CircleXmarkFill} from '@gravity-ui/icons';
import {DefinitionList, Flex, Icon, Label, Text} from '@gravity-ui/uikit';
import {DefinitionList, Flex, Label, Text} from '@gravity-ui/uikit';
import type {LabelProps} from '@gravity-ui/uikit';

import type {TBridgePile} from '../../../../types/api/cluster';
import {BridgePileState} from '../../../../types/api/cluster';
import {cn} from '../../../../utils/cn';
import {EMPTY_DATA_PLACEHOLDER} from '../../../../utils/constants';
import {formatNumber} from '../../../../utils/dataFormatters/dataFormatters';
Expand All @@ -13,6 +14,27 @@ import './BridgeInfoTable.scss';

const b = cn('bridge-info-table');

function getBridgePileStateTheme(state?: string): NonNullable<LabelProps['theme']> {
if (!state) {
return 'unknown';
}

switch (state.toUpperCase()) {
Copy link

Copilot AI Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using the BridgePileState enum values directly instead of converting to uppercase and comparing strings. This would be more type-safe and maintainable: switch (state as BridgePileState) {

Suggested change
switch (state.toUpperCase()) {
// Helper to map string to BridgePileState enum value
function toBridgePileState(state?: string): BridgePileState | undefined {
if (!state) {
return undefined;
}
// Normalize and match to enum
const normalized = state.trim().toUpperCase();
switch (normalized) {
case BridgePileState.PRIMARY:
return BridgePileState.PRIMARY;
case BridgePileState.PROMOTE:
return BridgePileState.PROMOTE;
case BridgePileState.SYNCHRONIZED:
return BridgePileState.SYNCHRONIZED;
case BridgePileState.NOT_SYNCHRONIZED:
return BridgePileState.NOT_SYNCHRONIZED;
case BridgePileState.SUSPENDED:
return BridgePileState.SUSPENDED;
case BridgePileState.DISCONNECTED:
return BridgePileState.DISCONNECTED;
case BridgePileState.UNSPECIFIED:
return BridgePileState.UNSPECIFIED;
default:
return undefined;
}
}
function getBridgePileStateTheme(state?: string): NonNullable<LabelProps['theme']> {
const pileState = toBridgePileState(state);
if (!pileState) {
return 'unknown';
}
switch (pileState) {

Copilot uses AI. Check for mistakes.
case BridgePileState.PRIMARY:
case BridgePileState.PROMOTE:
case BridgePileState.SYNCHRONIZED:
return 'success'; // Green - healthy states
case BridgePileState.NOT_SYNCHRONIZED:
return 'warning'; // Yellow - needs attention
case BridgePileState.SUSPENDED:
case BridgePileState.DISCONNECTED:
return 'danger'; // Red - critical states
case BridgePileState.UNSPECIFIED:
default:
return 'unknown'; // Purple - unknown state
}
}

interface BridgeInfoTableProps {
piles: TBridgePile[];
}
Expand All @@ -22,57 +44,17 @@ interface BridgePileCardProps {
}

const BridgePileCard = React.memo(function BridgePileCard({pile}: BridgePileCardProps) {
const renderPrimaryStatus = React.useCallback(() => {
const isPrimary = pile.IsPrimary;
const icon = isPrimary ? CircleCheckFill : CircleXmarkFill;
const text = isPrimary ? i18n('value_yes') : i18n('value_no');

return (
<Flex gap={1} alignItems="center">
<Icon data={icon} size={16} className={b('status-icon', {primary: isPrimary})} />
<Text color="secondary">{text}</Text>
</Flex>
);
}, [pile.IsPrimary]);

const renderStateStatus = React.useCallback(() => {
if (!pile.State) {
return EMPTY_DATA_PLACEHOLDER;
}

const isSynchronized = pile.State.toUpperCase() === 'SYNCHRONIZED';
const theme = isSynchronized ? 'success' : 'info';

const theme = getBridgePileStateTheme(pile.State);
return <Label theme={theme}>{pile.State}</Label>;
}, [pile.State]);

const renderBeingPromotedStatus = React.useCallback(() => {
const isBeingPromoted = pile.IsBeingPromoted;
const icon = isBeingPromoted ? CircleCheckFill : CircleXmarkFill;
const text = isBeingPromoted ? i18n('value_yes') : i18n('value_no');

return (
<Flex gap={1} alignItems="center">
<Icon
data={icon}
size={16}
className={b('status-icon', {primary: isBeingPromoted})}
/>
<Text color="secondary">{text}</Text>
</Flex>
);
}, [pile.IsBeingPromoted]);

const info = React.useMemo(
() => [
{
name: i18n('field_primary'),
content: renderPrimaryStatus(),
},
{
name: i18n('field_being-promoted'),
content: renderBeingPromotedStatus(),
},
{
name: i18n('field_state'),
content: renderStateStatus(),
Expand All @@ -83,7 +65,7 @@ const BridgePileCard = React.memo(function BridgePileCard({pile}: BridgePileCard
pile.Nodes === undefined ? EMPTY_DATA_PLACEHOLDER : formatNumber(pile.Nodes),
},
],
[renderPrimaryStatus, renderBeingPromotedStatus, renderStateStatus, pile.Nodes],
[renderStateStatus, pile.Nodes],
);

return (
Expand Down
16 changes: 11 additions & 5 deletions src/types/api/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,17 +97,23 @@ function isClusterParticularVersionOrHigher(info: TClusterInfo | undefined, vers
);
}

export enum BridgePileState {
UNSPECIFIED = 'UNSPECIFIED',
PRIMARY = 'PRIMARY',
PROMOTE = 'PROMOTE',
SYNCHRONIZED = 'SYNCHRONIZED',
NOT_SYNCHRONIZED = 'NOT_SYNCHRONIZED',
SUSPENDED = 'SUSPENDED',
DISCONNECTED = 'DISCONNECTED',
}

export interface TBridgePile {
/** unique pile identifier */
PileId?: number;
/** pile name, e.g., r1 */
Name?: string;
/** pile state (string from backend, e.g., SYNCHRONIZED) */
/** pile state from backend */
State?: string;
/** whether this pile is primary */
IsPrimary?: boolean;
/** whether this pile is being promoted to primary */
IsBeingPromoted?: boolean;
/** number of nodes in the pile */
Nodes?: number;
}
Expand Down
4 changes: 1 addition & 3 deletions tests/suites/bridge/bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,7 @@ test.describe('Bridge mode - Cluster Overview', () => {
// Check first pile content
const firstPileContent = await clusterPage.getFirstPileContent();
expect(firstPileContent).toContain('r1');
expect(firstPileContent).toContain('Yes'); // Primary status
expect(firstPileContent).toContain('No'); // Being Promoted status (false for first pile)
expect(firstPileContent).toContain('SYNCHRONIZED');
expect(firstPileContent).toContain('PRIMARY'); // State
expect(firstPileContent).toContain('16'); // Nodes count
});
});
63 changes: 57 additions & 6 deletions tests/suites/bridge/mocks.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type {Page, Route} from '@playwright/test';

import {BridgePileState} from '../../../src/types/api/cluster';

export const mockCapabilities = (page: Page, enabled: boolean) => {
return page.route(`**/viewer/capabilities`, async (route: Route) => {
await route.fulfill({
Expand Down Expand Up @@ -56,6 +58,59 @@ export const mockStorageGroupsWithPile = (page: Page) => {
});
};

export const mockClusterWithAllBridgePileStates = (page: Page) => {
return page.route(`**/viewer/json/cluster?*`, async (route: Route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
Version: 6,
Domain: '/dev02',
BridgeInfo: {
Piles: [
{
PileId: 1,
Name: 'primary-pile',
State: BridgePileState.PRIMARY,
Nodes: 16,
},
{
PileId: 2,
Name: 'promoting-pile',
State: BridgePileState.PROMOTE,
Nodes: 12,
},
{
PileId: 3,
Name: 'sync-pile',
State: BridgePileState.SYNCHRONIZED,
Nodes: 8,
},
{
PileId: 4,
Name: 'not-sync-pile',
State: BridgePileState.NOT_SYNCHRONIZED,
Nodes: 4,
},
{
PileId: 5,
Name: 'suspended-pile',
State: BridgePileState.SUSPENDED,
Nodes: 6,
},
{
PileId: 6,
Name: 'disconnected-pile',
State: BridgePileState.DISCONNECTED,
Nodes: 0,
},
],
},
}),
});
});
};

export const mockClusterWithBridgePiles = (page: Page) => {
return page.route(`**/viewer/json/cluster?*`, async (route: Route) => {
await route.fulfill({
Expand Down Expand Up @@ -134,17 +189,13 @@ export const mockClusterWithBridgePiles = (page: Page) => {
{
PileId: 1,
Name: 'r1',
State: 'SYNCHRONIZED',
IsPrimary: true,
IsBeingPromoted: false,
State: BridgePileState.PRIMARY,
Nodes: 16,
},
{
PileId: 2,
Name: 'r2',
State: 'READY',
IsPrimary: false,
IsBeingPromoted: true,
State: BridgePileState.SYNCHRONIZED,
Nodes: 12,
},
],
Expand Down
Loading