diff --git a/src/containers/Cluster/ClusterOverview/components/BridgeInfoTable.tsx b/src/containers/Cluster/ClusterOverview/components/BridgeInfoTable.tsx index 1939437dde..0ee5ea69fe 100644 --- a/src/containers/Cluster/ClusterOverview/components/BridgeInfoTable.tsx +++ b/src/containers/Cluster/ClusterOverview/components/BridgeInfoTable.tsx @@ -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'; @@ -13,6 +14,27 @@ import './BridgeInfoTable.scss'; const b = cn('bridge-info-table'); +function getBridgePileStateTheme(state?: string): NonNullable { + if (!state) { + return 'unknown'; + } + + switch (state.toUpperCase()) { + 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[]; } @@ -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 ( - - - {text} - - ); - }, [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 ; }, [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 ( - - - {text} - - ); - }, [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(), @@ -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 ( diff --git a/src/types/api/cluster.ts b/src/types/api/cluster.ts index 4c49245c88..addb698bc9 100644 --- a/src/types/api/cluster.ts +++ b/src/types/api/cluster.ts @@ -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; } diff --git a/tests/suites/bridge/bridge.test.ts b/tests/suites/bridge/bridge.test.ts index 35c4ca6211..b69486377c 100644 --- a/tests/suites/bridge/bridge.test.ts +++ b/tests/suites/bridge/bridge.test.ts @@ -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 }); }); diff --git a/tests/suites/bridge/mocks.ts b/tests/suites/bridge/mocks.ts index 1f61e53fc5..e8c0874ed7 100644 --- a/tests/suites/bridge/mocks.ts +++ b/tests/suites/bridge/mocks.ts @@ -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({ @@ -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({ @@ -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, }, ],