diff --git a/src/views/domain-page/__fixtures__/domain-page-query-params.ts b/src/views/domain-page/__fixtures__/domain-page-query-params.ts index 993a1ad07..bc65c4601 100644 --- a/src/views/domain-page/__fixtures__/domain-page-query-params.ts +++ b/src/views/domain-page/__fixtures__/domain-page-query-params.ts @@ -23,4 +23,6 @@ export const mockDomainPageQueryParamsValues = { sortColumnArchival: 'startTime', sortOrderArchival: 'DESC', queryArchival: '', + clusterAttributeScope: undefined, + clusterAttributeValue: undefined, } as const satisfies PageQueryParamValues; diff --git a/src/views/domain-page/config/domain-page-failovers-table-active-active.config.ts b/src/views/domain-page/config/domain-page-failovers-table-active-active.config.ts new file mode 100644 index 000000000..8562a614a --- /dev/null +++ b/src/views/domain-page/config/domain-page-failovers-table-active-active.config.ts @@ -0,0 +1,18 @@ +import { createElement } from 'react'; + +import { type FailoverEvent } from '@/route-handlers/list-failover-history/list-failover-history.types'; + +import DomainPageFailoverActiveActive from '../domain-page-failover-active-active/domain-page-failover-active-active'; + +import domainPageFailoversTableConfig from './domain-page-failovers-table.config'; + +const domainPageFailoversTableActiveActiveConfig = [ + ...domainPageFailoversTableConfig.slice(0, 3), + { + ...domainPageFailoversTableConfig[3], + renderCell: (event: FailoverEvent) => + createElement(DomainPageFailoverActiveActive, { failoverEvent: event }), + }, +]; + +export default domainPageFailoversTableActiveActiveConfig; diff --git a/src/views/domain-page/config/domain-page-failovers-table.config.ts b/src/views/domain-page/config/domain-page-failovers-table.config.ts new file mode 100644 index 000000000..f3cf99ef7 --- /dev/null +++ b/src/views/domain-page/config/domain-page-failovers-table.config.ts @@ -0,0 +1,48 @@ +import { createElement } from 'react'; + +import FormattedDate from '@/components/formatted-date/formatted-date'; +import { type TableConfig } from '@/components/table/table.types'; +import { type FailoverEvent } from '@/route-handlers/list-failover-history/list-failover-history.types'; +import parseGrpcTimestamp from '@/utils/datetime/parse-grpc-timestamp'; + +import DomainPageFailoverSingleCluster from '../domain-page-failover-single-cluster/domain-page-failover-single-cluster'; +import { FAILOVER_TYPE_LABEL_MAP } from '../domain-page-failovers/domain-page-failovers.constants'; + +const domainPageFailoversTableConfig = [ + { + name: 'Failover ID', + id: 'failoverId', + width: '35%', + renderCell: (event: FailoverEvent) => event.id, + }, + { + name: 'Time', + id: 'time', + width: '15%', + renderCell: (event: FailoverEvent) => + createElement(FormattedDate, { + timestampMs: event.createdTime + ? parseGrpcTimestamp(event.createdTime) + : null, + }), + }, + { + name: 'Type', + id: 'type', + width: '10%', + renderCell: (event: FailoverEvent) => + FAILOVER_TYPE_LABEL_MAP[event.failoverType], + }, + { + name: 'Failover Information', + id: 'failoverInfo', + width: '40%', + renderCell: (event: FailoverEvent) => + createElement(DomainPageFailoverSingleCluster, { + fromCluster: event.clusterFailovers[0]?.fromCluster?.activeClusterName, + toCluster: event.clusterFailovers[0]?.toCluster?.activeClusterName, + }), + }, +] as const satisfies TableConfig; + +export default domainPageFailoversTableConfig; diff --git a/src/views/domain-page/config/domain-page-query-params.config.ts b/src/views/domain-page/config/domain-page-query-params.config.ts index 645fdb45f..48d551c51 100644 --- a/src/views/domain-page/config/domain-page-query-params.config.ts +++ b/src/views/domain-page/config/domain-page-query-params.config.ts @@ -41,6 +41,9 @@ const domainPageQueryParamsConfig: [ PageQueryParam<'sortColumnArchival', string>, PageQueryParam<'sortOrderArchival', SortOrder>, PageQueryParam<'queryArchival', string>, + // Failovers Tab query params + PageQueryParam<'clusterAttributeScope', string | undefined>, + PageQueryParam<'clusterAttributeValue', string | undefined>, ] = [ { key: 'inputType', @@ -163,6 +166,14 @@ const domainPageQueryParamsConfig: [ queryParamKey: 'aquery', defaultValue: '', }, + { + key: 'clusterAttributeScope', + queryParamKey: 'cs', + }, + { + key: 'clusterAttributeValue', + queryParamKey: 'cv', + }, ] as const; export default domainPageQueryParamsConfig; diff --git a/src/views/domain-page/domain-page-failover-active-active/__tests__/domain-page-failover-active-active.test.tsx b/src/views/domain-page/domain-page-failover-active-active/__tests__/domain-page-failover-active-active.test.tsx new file mode 100644 index 000000000..42691611a --- /dev/null +++ b/src/views/domain-page/domain-page-failover-active-active/__tests__/domain-page-failover-active-active.test.tsx @@ -0,0 +1,311 @@ +import { render, screen } from '@/test-utils/rtl'; + +import * as usePageQueryParamsModule from '@/hooks/use-page-query-params/use-page-query-params'; +import { type FailoverEvent } from '@/route-handlers/list-failover-history/list-failover-history.types'; +import { mockDomainPageQueryParamsValues } from '@/views/domain-page/__fixtures__/domain-page-query-params'; +import { PRIMARY_CLUSTER_SCOPE } from '@/views/domain-page/domain-page-failovers/domain-page-failovers.constants'; + +import DomainPageFailoverActiveActive from '../domain-page-failover-active-active'; + +const mockSetQueryParams = jest.fn(); +jest.mock('@/hooks/use-page-query-params/use-page-query-params', () => + jest.fn(() => [mockDomainPageQueryParamsValues, mockSetQueryParams]) +); + +jest.mock( + '../../domain-page-failover-single-cluster/domain-page-failover-single-cluster', + () => + jest.fn((props: { fromCluster?: string; toCluster?: string }) => ( +
+ {`${props.fromCluster} -> ${props.toCluster}`} +
+ )) +); + +describe(DomainPageFailoverActiveActive.name, () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders cluster failover when matching primary cluster failover is found', () => { + const failoverEvent: FailoverEvent = { + id: 'failover-1', + createdTime: { + seconds: '1700000000', + nanos: 0, + }, + failoverType: 'FAILOVER_TYPE_GRACEFUL', + clusterFailovers: [ + { + fromCluster: { + activeClusterName: 'cluster-1', + failoverVersion: '1', + }, + toCluster: { + activeClusterName: 'cluster-2', + failoverVersion: '2', + }, + clusterAttribute: null, + }, + ], + }; + + setup({ + failoverEvent, + clusterAttributeScope: PRIMARY_CLUSTER_SCOPE, + }); + + expect(screen.getByText('Primary:')).toBeInTheDocument(); + expect( + screen.getByTestId('mock-single-cluster-failover') + ).toBeInTheDocument(); + expect(screen.getByText('cluster-1 -> cluster-2')).toBeInTheDocument(); + expect(screen.getByText('See more')).toBeInTheDocument(); + }); + + it('renders cluster failover when matching non-primary cluster failover is found', () => { + const failoverEvent: FailoverEvent = { + id: 'failover-1', + createdTime: { + seconds: '1700000000', + nanos: 0, + }, + failoverType: 'FAILOVER_TYPE_GRACEFUL', + clusterFailovers: [ + { + fromCluster: { + activeClusterName: 'cluster-1', + failoverVersion: '1', + }, + toCluster: { + activeClusterName: 'cluster-2', + failoverVersion: '2', + }, + clusterAttribute: { + scope: 'city', + name: 'new_york', + }, + }, + ], + }; + + setup({ + failoverEvent, + clusterAttributeScope: 'city', + clusterAttributeValue: 'new_york', + }); + + expect(screen.getByText('city (new_york):')).toBeInTheDocument(); + expect( + screen.getByTestId('mock-single-cluster-failover') + ).toBeInTheDocument(); + expect(screen.getByText('cluster-1 -> cluster-2')).toBeInTheDocument(); + expect(screen.getByText('See more')).toBeInTheDocument(); + }); + + it('does not render cluster failover section when clusterAttributeScope is set but clusterAttributeValue is undefined for non-primary scope', () => { + const failoverEvent: FailoverEvent = { + id: 'failover-1', + createdTime: { + seconds: '1700000000', + nanos: 0, + }, + failoverType: 'FAILOVER_TYPE_GRACEFUL', + clusterFailovers: [ + { + fromCluster: { + activeClusterName: 'cluster-1', + failoverVersion: '1', + }, + toCluster: { + activeClusterName: 'cluster-2', + failoverVersion: '2', + }, + clusterAttribute: { + scope: 'region', + name: 'us-east', + }, + }, + ], + }; + + setup({ + failoverEvent, + clusterAttributeScope: 'region', + }); + + expect( + screen.queryByTestId('mock-single-cluster-failover') + ).not.toBeInTheDocument(); + expect(screen.getByText('See more')).toBeInTheDocument(); + }); + + it('does not render cluster failover section when no matching cluster failover is found', () => { + const failoverEvent: FailoverEvent = { + id: 'failover-1', + createdTime: { + seconds: '1700000000', + nanos: 0, + }, + failoverType: 'FAILOVER_TYPE_GRACEFUL', + clusterFailovers: [ + { + fromCluster: { + activeClusterName: 'cluster-1', + failoverVersion: '1', + }, + toCluster: { + activeClusterName: 'cluster-2', + failoverVersion: '2', + }, + clusterAttribute: { + scope: 'city', + name: 'new_york', + }, + }, + ], + }; + + setup({ + failoverEvent, + clusterAttributeScope: 'city', + clusterAttributeValue: 'los_angeles', + }); + + expect(screen.queryByText('city (los_angeles):')).not.toBeInTheDocument(); + expect( + screen.queryByTestId('mock-single-cluster-failover') + ).not.toBeInTheDocument(); + expect(screen.getByText('See more')).toBeInTheDocument(); + }); + + it('does not render cluster failover section when clusterAttributeScope is undefined', () => { + const failoverEvent: FailoverEvent = { + id: 'failover-1', + createdTime: { + seconds: '1700000000', + nanos: 0, + }, + failoverType: 'FAILOVER_TYPE_GRACEFUL', + clusterFailovers: [ + { + fromCluster: { + activeClusterName: 'cluster-1', + failoverVersion: '1', + }, + toCluster: { + activeClusterName: 'cluster-2', + failoverVersion: '2', + }, + clusterAttribute: null, + }, + ], + }; + + setup({ + failoverEvent, + clusterAttributeScope: undefined, + }); + + expect( + screen.queryByTestId('mock-single-cluster-failover') + ).not.toBeInTheDocument(); + expect(screen.getByText('See more')).toBeInTheDocument(); + }); + + it('renders "See more" button even when no matching cluster failover is found', () => { + const failoverEvent: FailoverEvent = { + id: 'failover-1', + createdTime: { + seconds: '1700000000', + nanos: 0, + }, + failoverType: 'FAILOVER_TYPE_GRACEFUL', + clusterFailovers: [], + }; + + setup({ + failoverEvent, + clusterAttributeScope: PRIMARY_CLUSTER_SCOPE, + }); + + expect( + screen.queryByTestId('mock-single-cluster-failover') + ).not.toBeInTheDocument(); + expect(screen.getByText('See more')).toBeInTheDocument(); + }); + + it('selects the correct cluster failover when multiple cluster failovers exist', () => { + const failoverEvent: FailoverEvent = { + id: 'failover-1', + createdTime: { + seconds: '1700000000', + nanos: 0, + }, + failoverType: 'FAILOVER_TYPE_GRACEFUL', + clusterFailovers: [ + { + fromCluster: { + activeClusterName: 'cluster-1', + failoverVersion: '1', + }, + toCluster: { + activeClusterName: 'cluster-2', + failoverVersion: '2', + }, + clusterAttribute: { + scope: 'city', + name: 'new_york', + }, + }, + { + fromCluster: { + activeClusterName: 'cluster-3', + failoverVersion: '3', + }, + toCluster: { + activeClusterName: 'cluster-4', + failoverVersion: '4', + }, + clusterAttribute: { + scope: 'region', + name: 'us-east', + }, + }, + ], + }; + + setup({ + failoverEvent, + clusterAttributeScope: 'region', + clusterAttributeValue: 'us-east', + }); + + expect(screen.getByText('region (us-east):')).toBeInTheDocument(); + expect( + screen.getByTestId('mock-single-cluster-failover') + ).toBeInTheDocument(); + expect(screen.getByText('cluster-3 -> cluster-4')).toBeInTheDocument(); + }); +}); + +function setup({ + failoverEvent, + clusterAttributeScope, + clusterAttributeValue, +}: { + failoverEvent: FailoverEvent; + clusterAttributeScope?: string; + clusterAttributeValue?: string; +}) { + jest.spyOn(usePageQueryParamsModule, 'default').mockReturnValue([ + { + ...mockDomainPageQueryParamsValues, + clusterAttributeScope, + clusterAttributeValue, + } as typeof mockDomainPageQueryParamsValues, + mockSetQueryParams, + ]); + + render(); +} diff --git a/src/views/domain-page/domain-page-failover-active-active/domain-page-failover-active-active.styles.ts b/src/views/domain-page/domain-page-failover-active-active/domain-page-failover-active-active.styles.ts new file mode 100644 index 000000000..37628c859 --- /dev/null +++ b/src/views/domain-page/domain-page-failover-active-active/domain-page-failover-active-active.styles.ts @@ -0,0 +1,26 @@ +import { styled as createStyled, type Theme } from 'baseui'; + +export const styled = { + FailoverEventContainer: createStyled( + 'div', + ({ $theme }: { $theme: Theme }) => ({ + display: 'flex', + gap: $theme.sizing.scale600, + alignItems: 'baseline', + }) + ), + ClusterFailoverContainer: createStyled( + 'div', + ({ $theme }: { $theme: Theme }) => ({ + display: 'flex', + alignItems: 'baseline', + gap: $theme.sizing.scale300, + }) + ), + ClusterAttributeLabel: createStyled( + 'div', + ({ $theme }: { $theme: Theme }) => ({ + ...$theme.typography.LabelSmall, + }) + ), +}; diff --git a/src/views/domain-page/domain-page-failover-active-active/domain-page-failover-active-active.tsx b/src/views/domain-page/domain-page-failover-active-active/domain-page-failover-active-active.tsx new file mode 100644 index 000000000..c8d5ccc07 --- /dev/null +++ b/src/views/domain-page/domain-page-failover-active-active/domain-page-failover-active-active.tsx @@ -0,0 +1,76 @@ +import { useMemo } from 'react'; + +import { Button } from 'baseui/button'; +import { MdVisibility } from 'react-icons/md'; + +import usePageQueryParams from '@/hooks/use-page-query-params/use-page-query-params'; + +import domainPageQueryParamsConfig from '../config/domain-page-query-params.config'; +import DomainPageFailoverSingleCluster from '../domain-page-failover-single-cluster/domain-page-failover-single-cluster'; +import { PRIMARY_CLUSTER_SCOPE } from '../domain-page-failovers/domain-page-failovers.constants'; +import clusterFailoverMatchesAttribute from '../helpers/cluster-failover-matches-attribute'; + +import { styled } from './domain-page-failover-active-active.styles'; +import { type Props } from './domain-page-failover-active-active.types'; + +export default function DomainPageFailoverActiveActive({ + failoverEvent, +}: Props) { + const [{ clusterAttributeScope, clusterAttributeValue }] = usePageQueryParams( + domainPageQueryParamsConfig + ); + + const clusterFailoverForMaybeSelectedAttribute = useMemo(() => { + if ( + !clusterAttributeScope || + (clusterAttributeScope !== PRIMARY_CLUSTER_SCOPE && + !clusterAttributeValue) + ) + return undefined; + + return failoverEvent.clusterFailovers.find((clusterFailover) => + clusterFailoverMatchesAttribute( + clusterFailover, + clusterAttributeScope, + clusterAttributeValue + ) + ); + }, [ + clusterAttributeScope, + clusterAttributeValue, + failoverEvent.clusterFailovers, + ]); + + return ( + + {clusterFailoverForMaybeSelectedAttribute && ( + + + {clusterAttributeScope === PRIMARY_CLUSTER_SCOPE + ? 'Primary:' + : `${clusterAttributeScope} (${clusterAttributeValue}):`} + + + + )} + + + ); +} diff --git a/src/views/domain-page/domain-page-failover-active-active/domain-page-failover-active-active.types.ts b/src/views/domain-page/domain-page-failover-active-active/domain-page-failover-active-active.types.ts new file mode 100644 index 000000000..300e642c9 --- /dev/null +++ b/src/views/domain-page/domain-page-failover-active-active/domain-page-failover-active-active.types.ts @@ -0,0 +1,5 @@ +import { type FailoverEvent } from '@/route-handlers/list-failover-history/list-failover-history.types'; + +export type Props = { + failoverEvent: FailoverEvent; +}; diff --git a/src/views/domain-page/domain-page-failover-single-cluster/__tests__/domain-page-failover-single-cluster.test.tsx b/src/views/domain-page/domain-page-failover-single-cluster/__tests__/domain-page-failover-single-cluster.test.tsx new file mode 100644 index 000000000..849cb6b0f --- /dev/null +++ b/src/views/domain-page/domain-page-failover-single-cluster/__tests__/domain-page-failover-single-cluster.test.tsx @@ -0,0 +1,28 @@ +import { render, screen } from '@/test-utils/rtl'; + +import DomainPageFailoverSingleCluster from '../domain-page-failover-single-cluster'; + +describe(DomainPageFailoverSingleCluster.name, () => { + it('renders fromCluster and toCluster with arrow', () => { + setup({ fromCluster: 'cluster-1', toCluster: 'cluster-2' }); + + expect(screen.getByText(/cluster-1/)).toBeInTheDocument(); + expect(screen.getByText(/cluster-2/)).toBeInTheDocument(); + }); + + it('returns null when fromCluster is missing', () => { + setup({ toCluster: 'cluster-2' }); + + expect(screen.queryByText(/cluster-2/)).not.toBeInTheDocument(); + }); + + it('returns null when toCluster is missing', () => { + setup({ fromCluster: 'cluster-1' }); + + expect(screen.queryByText(/cluster-1/)).not.toBeInTheDocument(); + }); +}); + +function setup(props: { fromCluster?: string; toCluster?: string }) { + render(); +} diff --git a/src/views/domain-page/domain-page-failover-single-cluster/domain-page-failover-single-cluster.styles.ts b/src/views/domain-page/domain-page-failover-single-cluster/domain-page-failover-single-cluster.styles.ts new file mode 100644 index 000000000..b6ba98489 --- /dev/null +++ b/src/views/domain-page/domain-page-failover-single-cluster/domain-page-failover-single-cluster.styles.ts @@ -0,0 +1,17 @@ +import { type Theme } from 'baseui'; + +import type { + StyletronCSSObject, + StyletronCSSObjectOf, +} from '@/hooks/use-styletron-classes'; + +const cssStylesObj = { + failoverContainer: (theme: Theme) => ({ + display: 'flex', + gap: theme.sizing.scale400, + alignItems: 'center', + }), +} satisfies StyletronCSSObject; + +export const cssStyles: StyletronCSSObjectOf = + cssStylesObj; diff --git a/src/views/domain-page/domain-page-failover-single-cluster/domain-page-failover-single-cluster.tsx b/src/views/domain-page/domain-page-failover-single-cluster/domain-page-failover-single-cluster.tsx new file mode 100644 index 000000000..3466dc29e --- /dev/null +++ b/src/views/domain-page/domain-page-failover-single-cluster/domain-page-failover-single-cluster.tsx @@ -0,0 +1,23 @@ +import { MdArrowForward } from 'react-icons/md'; + +import useStyletronClasses from '@/hooks/use-styletron-classes'; + +import { cssStyles } from './domain-page-failover-single-cluster.styles'; +import { type Props } from './domain-page-failover-single-cluster.types'; + +export default function DomainPageFailoverSingleCluster({ + fromCluster, + toCluster, +}: Props) { + const { cls, theme } = useStyletronClasses(cssStyles); + + if (!fromCluster || !toCluster) return null; + + return ( +
+ {fromCluster} + + {toCluster} +
+ ); +} diff --git a/src/views/domain-page/domain-page-failover-single-cluster/domain-page-failover-single-cluster.types.ts b/src/views/domain-page/domain-page-failover-single-cluster/domain-page-failover-single-cluster.types.ts new file mode 100644 index 000000000..c5714aeae --- /dev/null +++ b/src/views/domain-page/domain-page-failover-single-cluster/domain-page-failover-single-cluster.types.ts @@ -0,0 +1,4 @@ +export type Props = { + fromCluster?: string; + toCluster?: string; +}; diff --git a/src/views/domain-page/domain-page-failovers/__tests__/domain-page-failovers.test.tsx b/src/views/domain-page/domain-page-failovers/__tests__/domain-page-failovers.test.tsx new file mode 100644 index 000000000..5868c44e2 --- /dev/null +++ b/src/views/domain-page/domain-page-failovers/__tests__/domain-page-failovers.test.tsx @@ -0,0 +1,227 @@ +import React, { Suspense } from 'react'; + +import { HttpResponse } from 'msw'; + +import { render, screen } from '@/test-utils/rtl'; + +import { type Props as LoaderProps } from '@/components/table/table-infinite-scroll-loader/table-infinite-scroll-loader.types'; +import { type DescribeDomainResponse } from '@/route-handlers/describe-domain/describe-domain.types'; +import { + type FailoverEvent, + type ListFailoverHistoryResponse, +} from '@/route-handlers/list-failover-history/list-failover-history.types'; +import { mockDomainDescription } from '@/views/domain-page/__fixtures__/domain-description'; +import { mockDomainPageQueryParamsValues } from '@/views/domain-page/__fixtures__/domain-page-query-params'; +import { mockActiveActiveDomain } from '@/views/shared/active-active/__fixtures__/active-active-domain'; + +import DomainPageFailovers from '../domain-page-failovers'; + +const mockSetQueryParams = jest.fn(); +jest.mock('@/hooks/use-page-query-params/use-page-query-params', () => + jest.fn(() => [mockDomainPageQueryParamsValues, mockSetQueryParams]) +); + +jest.mock( + '@/components/table/table-infinite-scroll-loader/table-infinite-scroll-loader', + () => + jest.fn((props: LoaderProps) => ( + + )) +); + +jest.mock('../../config/domain-page-failovers-table.config', () => [ + { + name: 'Failover ID', + id: 'failoverId', + width: '35%', + renderCell: (event: FailoverEvent) =>
{event.id}
, + }, + { + name: 'Time', + id: 'time', + width: '15%', + renderCell: (event: FailoverEvent) => ( +
{event.createdTime?.seconds || 'No date'}
+ ), + }, + { + name: 'Type', + id: 'type', + width: '10%', + renderCell: (event: FailoverEvent) =>
{event.failoverType}
, + }, + { + name: 'Failover Information', + id: 'failoverInfo', + width: '40%', + renderCell: (event: FailoverEvent) => ( +
+ {event.clusterFailovers[0]?.fromCluster?.activeClusterName} + {` -> `} + {event.clusterFailovers[0]?.toCluster?.activeClusterName} +
+ ), + }, +]); + +jest.mock( + '../../config/domain-page-failovers-table-active-active.config', + () => [ + { + name: 'Failover ID', + id: 'failoverId', + width: '35%', + renderCell: (event: FailoverEvent) =>
{event.id}
, + }, + { + name: 'Time', + id: 'time', + width: '15%', + renderCell: (event: FailoverEvent) => ( +
{event.createdTime?.seconds || 'No date'}
+ ), + }, + { + name: 'Type', + id: 'type', + width: '10%', + renderCell: (event: FailoverEvent) =>
{event.failoverType}
, + }, + { + name: 'Failover Information', + id: 'failoverInfo', + width: '40%', + renderCell: (event: FailoverEvent) => ( +
Active Active: {event.id}
+ ), + }, + ] +); + +const mockFailoverEvent: FailoverEvent = { + id: 'failover-1', + createdTime: { + seconds: '1700000000', + nanos: 0, + }, + failoverType: 'FAILOVER_TYPE_GRACEFUL', + clusterFailovers: [ + { + fromCluster: { + activeClusterName: 'cluster-1', + failoverVersion: '1', + }, + toCluster: { + activeClusterName: 'cluster-2', + failoverVersion: '2', + }, + clusterAttribute: null, + }, + ], +}; + +describe(DomainPageFailovers.name, () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders table with column headers', async () => { + await setup({}); + + expect(await screen.findByText('Failover ID')).toBeInTheDocument(); + expect(screen.getByText('Time')).toBeInTheDocument(); + expect(screen.getByText('Type')).toBeInTheDocument(); + expect(screen.getByText('Failover Information')).toBeInTheDocument(); + }); + + it('renders failover events in table', async () => { + await setup({ + failoverResponse: { + failoverEvents: [mockFailoverEvent], + nextPageToken: '', + }, + }); + + expect(await screen.findByText('failover-1')).toBeInTheDocument(); + expect(screen.getByText('FAILOVER_TYPE_GRACEFUL')).toBeInTheDocument(); + }); + + it('does not render data rows when no failover events', async () => { + await setup({ + failoverResponse: { + failoverEvents: [], + nextPageToken: '', + }, + }); + + await screen.findByText('Failover ID'); + expect(screen.queryByText('failover-1')).not.toBeInTheDocument(); + }); + + it('renders table with active-active config when domain is active-active', async () => { + await setup({ + domainDescription: mockActiveActiveDomain, + failoverResponse: { + failoverEvents: [mockFailoverEvent], + nextPageToken: '', + }, + }); + + expect(await screen.findByText('failover-1')).toBeInTheDocument(); + }); +}); + +async function setup({ + domain = 'mock-domain', + cluster = 'mock-cluster', + domainDescription = mockDomainDescription, + failoverResponse = { + failoverEvents: [], + nextPageToken: '', + }, + failoverError = false, +}: { + domain?: string; + cluster?: string; + domainDescription?: typeof mockDomainDescription; + failoverResponse?: ListFailoverHistoryResponse; + failoverError?: boolean; +}) { + render( + Loading...}> + + , + { + endpointsMocks: [ + { + path: '/api/domains/:domain/:cluster', + httpMethod: 'GET', + mockOnce: false, + jsonResponse: domainDescription satisfies DescribeDomainResponse, + }, + { + path: '/api/domains/:domain/:cluster/failovers', + httpMethod: 'GET', + mockOnce: false, + ...(failoverError + ? { + httpResolver: () => { + return HttpResponse.json( + { message: 'Failed to fetch failover history' }, + { status: 500 } + ); + }, + } + : { + jsonResponse: + failoverResponse satisfies ListFailoverHistoryResponse, + }), + }, + ], + } + ); + + await screen.findByText('Failover ID'); +} diff --git a/src/views/domain-page/domain-page-failovers/domain-page-failovers.styles.ts b/src/views/domain-page/domain-page-failovers/domain-page-failovers.styles.ts new file mode 100644 index 000000000..54216b184 --- /dev/null +++ b/src/views/domain-page/domain-page-failovers/domain-page-failovers.styles.ts @@ -0,0 +1,10 @@ +import { styled as createStyled, type Theme } from 'baseui'; + +export const styled = { + FailoversTableContainer: createStyled( + 'div', + ({ $theme }: { $theme: Theme }) => ({ + paddingTop: $theme.sizing.scale950, + }) + ), +}; diff --git a/src/views/domain-page/domain-page-failovers/domain-page-failovers.tsx b/src/views/domain-page/domain-page-failovers/domain-page-failovers.tsx index 019602f20..868c38ac3 100644 --- a/src/views/domain-page/domain-page-failovers/domain-page-failovers.tsx +++ b/src/views/domain-page/domain-page-failovers/domain-page-failovers.tsx @@ -1,8 +1,73 @@ 'use client'; import React from 'react'; +import Table from '@/components/table/table'; +import usePageQueryParams from '@/hooks/use-page-query-params/use-page-query-params'; +import isActiveActiveDomain from '@/views/shared/active-active/helpers/is-active-active-domain'; +import useSuspenseDomainDescription from '@/views/shared/hooks/use-domain-description/use-suspense-domain-description'; + +import domainPageFailoversTableActiveActiveConfig from '../config/domain-page-failovers-table-active-active.config'; +import domainPageFailoversTableConfig from '../config/domain-page-failovers-table.config'; +import domainPageQueryParamsConfig from '../config/domain-page-query-params.config'; import { type DomainPageTabContentProps } from '../domain-page-content/domain-page-content.types'; +import useDomainFailoverHistory from '../hooks/use-domain-failover-history/use-domain-failover-history'; + +import { styled } from './domain-page-failovers.styles'; + +export default function DomainPageFailovers({ + domain, + cluster, +}: DomainPageTabContentProps) { + const { data: domainDescription } = useSuspenseDomainDescription({ + domain, + cluster, + }); + + const isActiveActive = isActiveActiveDomain(domainDescription); + + const [{ clusterAttributeScope, clusterAttributeValue }] = usePageQueryParams( + domainPageQueryParamsConfig + ); + + const { + filteredFailoverEvents, + allFailoverEvents, + isLoading, + error, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } = useDomainFailoverHistory({ + domainName: domain, + domainId: domainDescription.id, + cluster, + ...(isActiveActive + ? { + clusterAttributeScope, + clusterAttributeValue, + } + : {}), + }); -export default function DomainPageFailovers(_: DomainPageTabContentProps) { - return
WIP: Domain Page Failovers Tab
; + return ( + + 0} + endMessageProps={{ + kind: 'infinite-scroll', + hasData: allFailoverEvents.length > 0, + error, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + }} + columns={ + isActiveActive + ? domainPageFailoversTableActiveActiveConfig + : domainPageFailoversTableConfig + } + /> + + ); }