From ea6874d60c8b9b136a882672fe4231edfa0f8b15 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Wed, 12 Nov 2025 15:09:53 +0100 Subject: [PATCH 1/4] failover history table Signed-off-by: Adhitya Mamallan --- .../domain-page-failover-modal.test.tsx | 341 ++++++++++++++++++ .../domain-page-failover-modal.styles.ts | 85 +++++ .../domain-page-failover-modal.tsx | 95 +++++ .../domain-page-failover-modal.types.ts | 7 + 4 files changed, 528 insertions(+) create mode 100644 src/views/domain-page/domain-page-failover-modal/__tests__/domain-page-failover-modal.test.tsx create mode 100644 src/views/domain-page/domain-page-failover-modal/domain-page-failover-modal.styles.ts create mode 100644 src/views/domain-page/domain-page-failover-modal/domain-page-failover-modal.tsx create mode 100644 src/views/domain-page/domain-page-failover-modal/domain-page-failover-modal.types.ts diff --git a/src/views/domain-page/domain-page-failover-modal/__tests__/domain-page-failover-modal.test.tsx b/src/views/domain-page/domain-page-failover-modal/__tests__/domain-page-failover-modal.test.tsx new file mode 100644 index 000000000..4da6d7e41 --- /dev/null +++ b/src/views/domain-page/domain-page-failover-modal/__tests__/domain-page-failover-modal.test.tsx @@ -0,0 +1,341 @@ +import { type ModalProps } from 'baseui/modal'; + +import { render, screen } from '@/test-utils/rtl'; + +import { type FailoverEvent } from '@/route-handlers/list-failover-history/list-failover-history.types'; + +import DomainPageFailoverModal from '../domain-page-failover-modal'; + +jest.mock('baseui/modal', () => ({ + ...jest.requireActual('baseui/modal'), + Modal: ({ isOpen, children }: ModalProps) => + isOpen ? ( +
+ {typeof children === 'function' ? children() : children} +
+ ) : null, +})); + +jest.mock('@/components/formatted-date/formatted-date', () => + jest.fn(({ timestampMs }: { timestampMs: number | null }) => ( +
+ {timestampMs ? new Date(timestampMs).toISOString() : 'No date'} +
+ )) +); + +jest.mock( + '../../domain-page-failover-single-cluster/domain-page-failover-single-cluster', + () => + jest.fn((props: { fromCluster?: string; toCluster?: string }) => + props.fromCluster && props.toCluster ? ( +
+ {`${props.fromCluster} -> ${props.toCluster}`} +
+ ) : null + ) +); + +describe(DomainPageFailoverModal.name, () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders modal when isOpen is true', () => { + const failoverEvent: FailoverEvent = { + id: 'failover-1', + createdTime: { + seconds: '1700000000', + nanos: 0, + }, + failoverType: 'FAILOVER_TYPE_GRACEFUL', + clusterFailovers: [], + }; + + setup({ failoverEvent, isOpen: true }); + + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }); + + it('does not render modal when isOpen is false', () => { + const failoverEvent: FailoverEvent = { + id: 'failover-1', + createdTime: { + seconds: '1700000000', + nanos: 0, + }, + failoverType: 'FAILOVER_TYPE_GRACEFUL', + clusterFailovers: [], + }; + + setup({ failoverEvent, isOpen: false }); + + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); + + it('displays failover ID correctly', () => { + const failoverEvent: FailoverEvent = { + id: 'failover-123', + createdTime: { + seconds: '1700000000', + nanos: 0, + }, + failoverType: 'FAILOVER_TYPE_GRACEFUL', + clusterFailovers: [], + }; + + setup({ failoverEvent, isOpen: true }); + + expect(screen.getByText('ID')).toBeInTheDocument(); + expect(screen.getByText('failover-123')).toBeInTheDocument(); + }); + + it('displays formatted time when createdTime is provided', () => { + const failoverEvent: FailoverEvent = { + id: 'failover-1', + createdTime: { + seconds: '1700000000', + nanos: 0, + }, + failoverType: 'FAILOVER_TYPE_GRACEFUL', + clusterFailovers: [], + }; + + setup({ failoverEvent, isOpen: true }); + + expect(screen.getByText('Time')).toBeInTheDocument(); + expect(screen.getByTestId('formatted-date')).toBeInTheDocument(); + }); + + it('handles null createdTime', () => { + const failoverEvent: FailoverEvent = { + id: 'failover-1', + createdTime: null, + failoverType: 'FAILOVER_TYPE_GRACEFUL', + clusterFailovers: [], + }; + + setup({ failoverEvent, isOpen: true }); + + expect(screen.getByText('Time')).toBeInTheDocument(); + expect(screen.getByTestId('formatted-date')).toBeInTheDocument(); + }); + + it('renders table with cluster failovers when clusterFailovers array is not empty', () => { + 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, isOpen: true }); + + expect(screen.getByText('Scope')).toBeInTheDocument(); + expect(screen.getByText('Attribute')).toBeInTheDocument(); + expect(screen.getByText('Clusters')).toBeInTheDocument(); + expect(screen.getByText('Primary')).toBeInTheDocument(); + expect(screen.getByText('-')).toBeInTheDocument(); + expect( + screen.getByTestId('mock-single-cluster-failover') + ).toBeInTheDocument(); + }); + + it('does not render table when clusterFailovers array is empty', () => { + const failoverEvent: FailoverEvent = { + id: 'failover-1', + createdTime: { + seconds: '1700000000', + nanos: 0, + }, + failoverType: 'FAILOVER_TYPE_GRACEFUL', + clusterFailovers: [], + }; + + setup({ failoverEvent, isOpen: true }); + + expect(screen.queryByText('Scope')).not.toBeInTheDocument(); + expect(screen.queryByText('Attribute')).not.toBeInTheDocument(); + expect(screen.queryByText('Clusters')).not.toBeInTheDocument(); + }); + + it('displays Primary scope and dash for attribute when clusterAttribute is null', () => { + 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, isOpen: true }); + + expect(screen.getByText('Primary')).toBeInTheDocument(); + expect(screen.getByText('-')).toBeInTheDocument(); + }); + + it('displays scope and attribute name when clusterAttribute is provided', () => { + 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, isOpen: true }); + + expect(screen.getByText('city')).toBeInTheDocument(); + expect(screen.getByText('new_york')).toBeInTheDocument(); + }); + + it('renders multiple cluster failovers correctly', () => { + 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, + }, + { + fromCluster: { + activeClusterName: 'cluster-3', + failoverVersion: '3', + }, + toCluster: { + activeClusterName: 'cluster-4', + failoverVersion: '4', + }, + clusterAttribute: { + scope: 'region', + name: 'us-east', + }, + }, + ], + }; + + setup({ failoverEvent, isOpen: true }); + + expect(screen.getByText('Primary')).toBeInTheDocument(); + expect(screen.getByText('region')).toBeInTheDocument(); + expect(screen.getByText('us-east')).toBeInTheDocument(); + const clusterComponents = screen.getAllByTestId( + 'mock-single-cluster-failover' + ); + expect(clusterComponents).toHaveLength(2); + expect(clusterComponents[0]).toHaveTextContent('cluster-1 -> cluster-2'); + expect(clusterComponents[1]).toHaveTextContent('cluster-3 -> cluster-4'); + }); + + it('calls onClose when Close button is clicked', () => { + const failoverEvent: FailoverEvent = { + id: 'failover-1', + createdTime: { + seconds: '1700000000', + nanos: 0, + }, + failoverType: 'FAILOVER_TYPE_GRACEFUL', + clusterFailovers: [], + }; + + const mockOnClose = jest.fn(); + setup({ failoverEvent, isOpen: true, onClose: mockOnClose }); + + const closeButton = screen.getByText('Close'); + closeButton.click(); + + expect(mockOnClose).toHaveBeenCalledTimes(1); + }); + + it('displays modal title correctly', () => { + const failoverEvent: FailoverEvent = { + id: 'failover-1', + createdTime: { + seconds: '1700000000', + nanos: 0, + }, + failoverType: 'FAILOVER_TYPE_GRACEFUL', + clusterFailovers: [], + }; + + setup({ failoverEvent, isOpen: true }); + + expect(screen.getByText('Failover Information')).toBeInTheDocument(); + }); +}); + +function setup({ + failoverEvent, + isOpen = true, + onClose = jest.fn(), +}: { + failoverEvent: FailoverEvent; + isOpen?: boolean; + onClose?: () => void; +}) { + render( + + ); +} diff --git a/src/views/domain-page/domain-page-failover-modal/domain-page-failover-modal.styles.ts b/src/views/domain-page/domain-page-failover-modal/domain-page-failover-modal.styles.ts new file mode 100644 index 000000000..87dc86b2b --- /dev/null +++ b/src/views/domain-page/domain-page-failover-modal/domain-page-failover-modal.styles.ts @@ -0,0 +1,85 @@ +import { styled as createStyled, withStyle, type Theme } from 'baseui'; +import { ModalBody, ModalFooter, type ModalOverrides } from 'baseui/modal'; +import { type TableOverrides } from 'baseui/table-semantic'; +import { type StyleObject } from 'styletron-react'; + +export const overrides = { + modal: { + Close: { + style: ({ $theme }: { $theme: Theme }): StyleObject => ({ + top: $theme.sizing.scale850, + right: $theme.sizing.scale800, + }), + }, + Dialog: { + style: (): StyleObject => ({ + width: '700px', + }), + }, + } satisfies ModalOverrides, + table: { + TableHeadCell: { + style: ({ $theme }: { $theme: Theme }): StyleObject => ({ + ...$theme.typography.LabelXSmall, + paddingTop: $theme.sizing.scale300, + paddingBottom: $theme.sizing.scale300, + paddingLeft: $theme.sizing.scale500, + paddingRight: $theme.sizing.scale500, + }), + }, + TableBodyCell: { + style: ({ $theme }: { $theme: Theme }): StyleObject => ({ + ...$theme.typography.ParagraphXSmall, + paddingTop: $theme.sizing.scale300, + paddingBottom: $theme.sizing.scale300, + paddingLeft: $theme.sizing.scale500, + paddingRight: $theme.sizing.scale500, + }), + }, + } satisfies TableOverrides, +}; + +export const styled = { + ModalHeader: createStyled('div', ({ $theme }: { $theme: Theme }) => ({ + padding: $theme.sizing.scale600, + })), + Title: createStyled('div', ({ $theme }: { $theme: Theme }) => ({ + ...$theme.typography.HeadingSmall, + })), + ModalBody: withStyle(ModalBody, ({ $theme }: { $theme: Theme }) => ({ + padding: $theme.sizing.scale600, + })), + InfoRow: createStyled('div', ({ $theme }: { $theme: Theme }) => ({ + display: 'flex', + gap: $theme.sizing.scale800, + marginBottom: $theme.sizing.scale500, + ':last-child': { + marginBottom: 0, + }, + })), + InfoItem: createStyled('div', { + display: 'flex', + flexDirection: 'column', + }), + InfoLabel: createStyled('div', ({ $theme }: { $theme: Theme }) => ({ + ...$theme.typography.LabelXSmall, + color: $theme.colors.contentSecondary, + marginBottom: $theme.sizing.scale200, + })), + InfoValue: createStyled('div', ({ $theme }: { $theme: Theme }) => ({ + ...$theme.typography.LabelXSmall, + color: $theme.colors.contentPrimary, + })), + TableContainer: createStyled('div', ({ $theme }: { $theme: Theme }) => ({ + marginTop: $theme.sizing.scale600, + maxHeight: '50vh', + overflowY: 'auto', + })), + ModalFooter: withStyle(ModalFooter, ({ $theme }: { $theme: Theme }) => ({ + padding: `${$theme.sizing.scale500} ${$theme.sizing.scale600}`, + borderTop: `1px solid ${$theme.colors.borderOpaque}`, + display: 'flex', + justifyContent: 'flex-end', + gap: $theme.sizing.scale400, + })), +}; diff --git a/src/views/domain-page/domain-page-failover-modal/domain-page-failover-modal.tsx b/src/views/domain-page/domain-page-failover-modal/domain-page-failover-modal.tsx new file mode 100644 index 000000000..b4ac6416b --- /dev/null +++ b/src/views/domain-page/domain-page-failover-modal/domain-page-failover-modal.tsx @@ -0,0 +1,95 @@ +'use client'; +import { useMemo } from 'react'; + +import { Modal, ModalButton } from 'baseui/modal'; +import { Table } from 'baseui/table-semantic'; + +import FormattedDate from '@/components/formatted-date/formatted-date'; +import parseGrpcTimestamp from '@/utils/datetime/parse-grpc-timestamp'; + +import DomainPageFailoverSingleCluster from '../domain-page-failover-single-cluster/domain-page-failover-single-cluster'; + +import { overrides, styled } from './domain-page-failover-modal.styles'; +import { type Props } from './domain-page-failover-modal.types'; + +export default function DomainPageFailoverModal({ + failoverEvent, + isOpen, + onClose, +}: Props) { + const tableRows = useMemo(() => { + return failoverEvent.clusterFailovers.map((clusterFailover) => { + const fromCluster = clusterFailover.fromCluster?.activeClusterName; + const toCluster = clusterFailover.toCluster?.activeClusterName; + const clusters = ( + + ); + + const attribute = clusterFailover.clusterAttribute; + if (attribute === null) { + return { + scope: 'Primary', + attribute: '-', + clusters, + }; + } + + return { + scope: attribute.scope, + attribute: attribute.name, + clusters, + }; + }); + }, [failoverEvent]); + + const formattedTime = useMemo(() => { + if (!failoverEvent.createdTime) return null; + return parseGrpcTimestamp(failoverEvent.createdTime); + }, [failoverEvent]); + + return ( + + + Failover Information + + + + + ID + {failoverEvent.id} + + + Time + + + + + + {tableRows.length > 0 && ( + + + + )} + + + + Close + + + + ); +} diff --git a/src/views/domain-page/domain-page-failover-modal/domain-page-failover-modal.types.ts b/src/views/domain-page/domain-page-failover-modal/domain-page-failover-modal.types.ts new file mode 100644 index 000000000..ca4eadf57 --- /dev/null +++ b/src/views/domain-page/domain-page-failover-modal/domain-page-failover-modal.types.ts @@ -0,0 +1,7 @@ +import { type FailoverEvent } from '@/route-handlers/list-failover-history/list-failover-history.types'; + +export type Props = { + failoverEvent: FailoverEvent; + isOpen: boolean; + onClose: () => void; +}; From 3bec929b4d55b3487bc875cd52a3bc7b01c84476 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Thu, 13 Nov 2025 15:31:50 +0100 Subject: [PATCH 2/4] Add failover history filters Signed-off-by: Adhitya Mamallan --- .../domain-page-failovers-filters.test.tsx | 216 ++++++++++++++++++ .../domain-page-failovers-filters.styles.ts | 56 +++++ .../domain-page-failovers-filters.tsx | 122 ++++++++++ .../__tests__/domain-page-failovers.test.tsx | 19 ++ .../domain-page-failovers.styles.ts | 10 - .../domain-page-failovers.tsx | 18 +- 6 files changed, 426 insertions(+), 15 deletions(-) create mode 100644 src/views/domain-page/domain-page-failovers-filters/__tests__/domain-page-failovers-filters.test.tsx create mode 100644 src/views/domain-page/domain-page-failovers-filters/domain-page-failovers-filters.styles.ts create mode 100644 src/views/domain-page/domain-page-failovers-filters/domain-page-failovers-filters.tsx delete mode 100644 src/views/domain-page/domain-page-failovers/domain-page-failovers.styles.ts diff --git a/src/views/domain-page/domain-page-failovers-filters/__tests__/domain-page-failovers-filters.test.tsx b/src/views/domain-page/domain-page-failovers-filters/__tests__/domain-page-failovers-filters.test.tsx new file mode 100644 index 000000000..abfc5bd7d --- /dev/null +++ b/src/views/domain-page/domain-page-failovers-filters/__tests__/domain-page-failovers-filters.test.tsx @@ -0,0 +1,216 @@ +import { render, screen, userEvent } from '@/test-utils/rtl'; + +import { type PageQueryParamValues } from '@/hooks/use-page-query-params/use-page-query-params.types'; +import { mockDomainPageQueryParamsValues } from '@/views/domain-page/__fixtures__/domain-page-query-params'; +import type domainPageQueryParamsConfig from '@/views/domain-page/config/domain-page-query-params.config'; +import { mockActiveActiveDomain } from '@/views/shared/active-active/__fixtures__/active-active-domain'; +import { type ActiveActiveDomain } from '@/views/shared/active-active/active-active.types'; + +import { PRIMARY_CLUSTER_SCOPE } from '../../domain-page-failovers/domain-page-failovers.constants'; +import DomainPageFailoversFilters from '../domain-page-failovers-filters'; + +describe(DomainPageFailoversFilters.name, () => { + it('renders both filter comboboxes and reset button', () => { + setup({}); + + expect(screen.getByText('Cluster Attribute Scope')).toBeInTheDocument(); + expect(screen.getByText('Cluster Attribute Value')).toBeInTheDocument(); + expect(screen.getByText('Reset filters')).toBeInTheDocument(); + }); + + it('displays cluster attribute scope options including primary and domain scopes', async () => { + const { user } = setup({}); + + const comboboxes = screen.getAllByRole('combobox'); + const scopeCombobox = comboboxes[0]; + await user.click(scopeCombobox); + + expect(screen.getByText(PRIMARY_CLUSTER_SCOPE)).toBeInTheDocument(); + expect(screen.getByText('region')).toBeInTheDocument(); + }); + + it('disables cluster attribute value combobox when scope is primary', () => { + setup({ + queryParamsOverrides: { + clusterAttributeScope: PRIMARY_CLUSTER_SCOPE, + }, + }); + + const inputs = screen.getAllByRole('textbox'); + expect(inputs[1]).toBeDisabled(); + }); + + it('enables cluster attribute value combobox when scope is not primary', () => { + setup({ + queryParamsOverrides: { + clusterAttributeScope: 'region', + }, + }); + + const inputs = screen.getAllByRole('textbox'); + expect(inputs[1]).not.toBeDisabled(); + }); + + it('displays cluster attribute values for selected scope', async () => { + const { user } = setup({ + queryParamsOverrides: { + clusterAttributeScope: 'region', + }, + }); + + const comboboxes = screen.getAllByRole('combobox'); + const valueCombobox = comboboxes[1]; + await user.click(valueCombobox); + + expect(screen.getByText('region0')).toBeInTheDocument(); + expect(screen.getByText('region1')).toBeInTheDocument(); + }); + + it('calls setQueryParams with new scope and resets value when scope changes', async () => { + const { user, mockSetQueryParams } = setup({}); + + const comboboxes = screen.getAllByRole('combobox'); + const scopeCombobox = comboboxes[0]; + await user.click(scopeCombobox); + await user.click(screen.getByText('region')); + + expect(mockSetQueryParams).toHaveBeenCalledWith({ + clusterAttributeScope: 'region', + clusterAttributeValue: undefined, + }); + }); + + it('calls setQueryParams with new value when cluster attribute value changes', async () => { + const { user, mockSetQueryParams } = setup({ + queryParamsOverrides: { + clusterAttributeScope: 'region', + }, + }); + + const comboboxes = screen.getAllByRole('combobox'); + const valueCombobox = comboboxes[1]; + await user.click(valueCombobox); + await user.click(screen.getByText('region0')); + + expect(mockSetQueryParams).toHaveBeenCalledWith({ + clusterAttributeValue: 'region0', + }); + }); + + it('calls setQueryParams with undefined when clearing scope', async () => { + const { user, mockSetQueryParams } = setup({ + queryParamsOverrides: { + clusterAttributeScope: 'region', + clusterAttributeValue: 'region0', + }, + }); + + const comboboxes = screen.getAllByRole('combobox'); + const scopeCombobox = comboboxes[0]; + await user.click(scopeCombobox); + + // Find and click the clear button (BaseUI Combobox clearable) + const clearButtons = screen.getAllByLabelText('Clear value'); + await user.click(clearButtons[0]); + + expect(mockSetQueryParams).toHaveBeenCalledWith({ + clusterAttributeScope: undefined, + clusterAttributeValue: undefined, + }); + }); + + it('calls setQueryParams with undefined when clearing value', async () => { + const { user, mockSetQueryParams } = setup({ + queryParamsOverrides: { + clusterAttributeScope: 'region', + clusterAttributeValue: 'region0', + }, + }); + + const comboboxes = screen.getAllByRole('combobox'); + const valueCombobox = comboboxes[1]; + await user.click(valueCombobox); + + // Find and click the clear button + const clearButtons = screen.getAllByLabelText('Clear value'); + await user.click(clearButtons[1]); + + expect(mockSetQueryParams).toHaveBeenCalledWith({ + clusterAttributeValue: undefined, + }); + }); + + it('resets both filters when reset filters button is clicked', async () => { + const { user, mockSetQueryParams } = setup({ + queryParamsOverrides: { + clusterAttributeScope: 'region', + clusterAttributeValue: 'region0', + }, + }); + + const resetButton = screen.getByText('Reset filters'); + await user.click(resetButton); + + expect(mockSetQueryParams).toHaveBeenCalledWith({ + clusterAttributeScope: undefined, + clusterAttributeValue: undefined, + }); + }); + + it('displays current scope value in combobox', () => { + setup({ + queryParamsOverrides: { + clusterAttributeScope: 'region', + }, + }); + + const inputs = screen.getAllByRole('textbox'); + expect(inputs[0]).toHaveValue('region'); + }); + + it('displays current value in cluster attribute value combobox', () => { + setup({ + queryParamsOverrides: { + clusterAttributeScope: 'region', + clusterAttributeValue: 'region0', + }, + }); + + const inputs = screen.getAllByRole('textbox'); + expect(inputs[1]).toHaveValue('region0'); + }); +}); + +function setup({ + queryParamsOverrides, + domainDescriptionOverrides, +}: { + queryParamsOverrides?: Partial< + PageQueryParamValues + >; + domainDescriptionOverrides?: Partial; +} = {}) { + const mockSetQueryParams = jest.fn(); + + const domainDescription = { + ...mockActiveActiveDomain, + ...domainDescriptionOverrides, + }; + + const queryParams = { + ...mockDomainPageQueryParamsValues, + ...queryParamsOverrides, + }; + + render( + + ); + + const user = userEvent.setup(); + + return { mockSetQueryParams, user }; +} diff --git a/src/views/domain-page/domain-page-failovers-filters/domain-page-failovers-filters.styles.ts b/src/views/domain-page/domain-page-failovers-filters/domain-page-failovers-filters.styles.ts new file mode 100644 index 000000000..c1e832ef7 --- /dev/null +++ b/src/views/domain-page/domain-page-failovers-filters/domain-page-failovers-filters.styles.ts @@ -0,0 +1,56 @@ +import { type Theme } from 'baseui'; +import { styled as createStyled } from 'baseui'; +import { type ButtonOverrides } from 'baseui/button'; +import type { FormControlOverrides } from 'baseui/form-control/types'; +import { type StyleObject } from 'styletron-react'; + +export const styled = { + FiltersContainer: createStyled('div', ({ $theme }) => ({ + display: 'flex', + flexDirection: 'column', + flexWrap: 'wrap', + gap: $theme.sizing.scale500, + [$theme.mediaQuery.medium]: { + flexDirection: 'row', + alignItems: 'flex-end', + }, + marginTop: $theme.sizing.scale950, + marginBottom: $theme.sizing.scale900, + })), + FilterContainer: createStyled('div', ({ $theme }) => ({ + flexGrow: 1, + flexShrink: 1, + flexBasis: '0', + [$theme.mediaQuery.medium]: { + alignSelf: 'flex-start', + }, + })), +}; + +export const overrides = { + comboboxFormControl: { + Label: { + style: ({ $theme }: { $theme: Theme }): StyleObject => ({ + ...$theme.typography.LabelXSmall, + }), + }, + ControlContainer: { + style: (): StyleObject => ({ + margin: '0px', + }), + }, + } satisfies FormControlOverrides, + clearFiltersButton: { + Root: { + style: ({ $theme }: { $theme: Theme }): StyleObject => ({ + whiteSpace: 'nowrap', + flexGrow: 2, + height: $theme.sizing.scale950, + [$theme.mediaQuery.medium]: { + flexGrow: 0, + alignSelf: 'flex-end', + }, + }), + }, + } satisfies ButtonOverrides, +}; diff --git a/src/views/domain-page/domain-page-failovers-filters/domain-page-failovers-filters.tsx b/src/views/domain-page/domain-page-failovers-filters/domain-page-failovers-filters.tsx new file mode 100644 index 000000000..5e9703405 --- /dev/null +++ b/src/views/domain-page/domain-page-failovers-filters/domain-page-failovers-filters.tsx @@ -0,0 +1,122 @@ +import { useMemo } from 'react'; + +import { Button } from 'baseui/button'; +import { Combobox } from 'baseui/combobox'; +import { FormControl } from 'baseui/form-control'; +import { Delete } from 'baseui/icon'; + +import { + type PageQueryParamSetter, + type PageQueryParamValues, +} from '@/hooks/use-page-query-params/use-page-query-params.types'; +import { type ActiveActiveDomain } from '@/views/shared/active-active/active-active.types'; + +import type domainPageQueryParamsConfig from '../config/domain-page-query-params.config'; +import { PRIMARY_CLUSTER_SCOPE } from '../domain-page-failovers/domain-page-failovers.constants'; + +import { styled, overrides } from './domain-page-failovers-filters.styles'; + +export default function DomainPageFailoversFilters({ + domainDescription, + queryParams, + setQueryParams, +}: { + domainDescription: ActiveActiveDomain; + queryParams: PageQueryParamValues; + setQueryParams: PageQueryParamSetter; +}) { + const { clusterAttributeScope, clusterAttributeValue } = queryParams; + + const clusterAttributeScopes = useMemo( + () => [ + PRIMARY_CLUSTER_SCOPE, + ...Object.keys( + domainDescription.activeClusters.activeClustersByClusterAttribute + ), + ], + [domainDescription.activeClusters.activeClustersByClusterAttribute] + ); + + const clusterAttributeValuesForScope = useMemo(() => { + if ( + !clusterAttributeScope || + clusterAttributeScope === PRIMARY_CLUSTER_SCOPE + ) + return []; + + const activeClustersForScope = + domainDescription.activeClusters.activeClustersByClusterAttribute[ + clusterAttributeScope + ]; + + if (!activeClustersForScope) return []; + + return Object.keys(activeClustersForScope.clusterAttributes); + }, [ + clusterAttributeScope, + domainDescription.activeClusters.activeClustersByClusterAttribute, + ]); + + return ( + + + + + setQueryParams({ + clusterAttributeScope: nextValue === '' ? undefined : nextValue, + clusterAttributeValue: undefined, + }) + } + options={clusterAttributeScopes.map((scope) => ({ + id: scope, + }))} + mapOptionToString={(option) => option.id} + /> + + + + + + setQueryParams({ + clusterAttributeValue: nextValue === '' ? undefined : nextValue, + }) + } + options={clusterAttributeValuesForScope.map((scope) => ({ + id: scope, + }))} + mapOptionToString={(option) => option.id} + /> + + + + + ); +} 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 index 5868c44e2..ff0aeae07 100644 --- 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 @@ -100,6 +100,11 @@ jest.mock( ] ); +jest.mock( + '../../domain-page-failovers-filters/domain-page-failovers-filters', + () => jest.fn(() =>
) +); + const mockFailoverEvent: FailoverEvent = { id: 'failover-1', createdTime: { @@ -171,6 +176,20 @@ describe(DomainPageFailovers.name, () => { expect(await screen.findByText('failover-1')).toBeInTheDocument(); }); + + it('renders filters when domain is active-active', async () => { + await setup({ + domainDescription: mockActiveActiveDomain, + failoverResponse: { + failoverEvents: [], + nextPageToken: '', + }, + }); + + expect( + await screen.findByTestId('domain-page-failovers-filters') + ).toBeInTheDocument(); + }); }); async function setup({ 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 deleted file mode 100644 index 54216b184..000000000 --- a/src/views/domain-page/domain-page-failovers/domain-page-failovers.styles.ts +++ /dev/null @@ -1,10 +0,0 @@ -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 868c38ac3..bdb5fa8b0 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 @@ -10,10 +10,9 @@ import domainPageFailoversTableActiveActiveConfig from '../config/domain-page-fa 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 DomainPageFailoversFilters from '../domain-page-failovers-filters/domain-page-failovers-filters'; 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, @@ -25,10 +24,12 @@ export default function DomainPageFailovers({ const isActiveActive = isActiveActiveDomain(domainDescription); - const [{ clusterAttributeScope, clusterAttributeValue }] = usePageQueryParams( + const [queryParams, setQueryParams] = usePageQueryParams( domainPageQueryParamsConfig ); + const { clusterAttributeScope, clusterAttributeValue } = queryParams; + const { filteredFailoverEvents, allFailoverEvents, @@ -50,7 +51,14 @@ export default function DomainPageFailovers({ }); return ( - +
+ {isActiveActive && ( + + )}
0} @@ -68,6 +76,6 @@ export default function DomainPageFailovers({ : domainPageFailoversTableConfig } /> - + ); } From fc48a814aa4748aa68c3345284bd00794c917153 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Fri, 14 Nov 2025 18:37:59 +0100 Subject: [PATCH 3/4] delete modal Signed-off-by: Adhitya Mamallan --- .../domain-page-failover-modal.test.tsx | 341 ------------------ .../domain-page-failover-modal.styles.ts | 85 ----- .../domain-page-failover-modal.tsx | 95 ----- .../domain-page-failover-modal.types.ts | 7 - 4 files changed, 528 deletions(-) delete mode 100644 src/views/domain-page/domain-page-failover-modal/__tests__/domain-page-failover-modal.test.tsx delete mode 100644 src/views/domain-page/domain-page-failover-modal/domain-page-failover-modal.styles.ts delete mode 100644 src/views/domain-page/domain-page-failover-modal/domain-page-failover-modal.tsx delete mode 100644 src/views/domain-page/domain-page-failover-modal/domain-page-failover-modal.types.ts diff --git a/src/views/domain-page/domain-page-failover-modal/__tests__/domain-page-failover-modal.test.tsx b/src/views/domain-page/domain-page-failover-modal/__tests__/domain-page-failover-modal.test.tsx deleted file mode 100644 index 4da6d7e41..000000000 --- a/src/views/domain-page/domain-page-failover-modal/__tests__/domain-page-failover-modal.test.tsx +++ /dev/null @@ -1,341 +0,0 @@ -import { type ModalProps } from 'baseui/modal'; - -import { render, screen } from '@/test-utils/rtl'; - -import { type FailoverEvent } from '@/route-handlers/list-failover-history/list-failover-history.types'; - -import DomainPageFailoverModal from '../domain-page-failover-modal'; - -jest.mock('baseui/modal', () => ({ - ...jest.requireActual('baseui/modal'), - Modal: ({ isOpen, children }: ModalProps) => - isOpen ? ( -
- {typeof children === 'function' ? children() : children} -
- ) : null, -})); - -jest.mock('@/components/formatted-date/formatted-date', () => - jest.fn(({ timestampMs }: { timestampMs: number | null }) => ( -
- {timestampMs ? new Date(timestampMs).toISOString() : 'No date'} -
- )) -); - -jest.mock( - '../../domain-page-failover-single-cluster/domain-page-failover-single-cluster', - () => - jest.fn((props: { fromCluster?: string; toCluster?: string }) => - props.fromCluster && props.toCluster ? ( -
- {`${props.fromCluster} -> ${props.toCluster}`} -
- ) : null - ) -); - -describe(DomainPageFailoverModal.name, () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('renders modal when isOpen is true', () => { - const failoverEvent: FailoverEvent = { - id: 'failover-1', - createdTime: { - seconds: '1700000000', - nanos: 0, - }, - failoverType: 'FAILOVER_TYPE_GRACEFUL', - clusterFailovers: [], - }; - - setup({ failoverEvent, isOpen: true }); - - expect(screen.getByRole('dialog')).toBeInTheDocument(); - }); - - it('does not render modal when isOpen is false', () => { - const failoverEvent: FailoverEvent = { - id: 'failover-1', - createdTime: { - seconds: '1700000000', - nanos: 0, - }, - failoverType: 'FAILOVER_TYPE_GRACEFUL', - clusterFailovers: [], - }; - - setup({ failoverEvent, isOpen: false }); - - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); - }); - - it('displays failover ID correctly', () => { - const failoverEvent: FailoverEvent = { - id: 'failover-123', - createdTime: { - seconds: '1700000000', - nanos: 0, - }, - failoverType: 'FAILOVER_TYPE_GRACEFUL', - clusterFailovers: [], - }; - - setup({ failoverEvent, isOpen: true }); - - expect(screen.getByText('ID')).toBeInTheDocument(); - expect(screen.getByText('failover-123')).toBeInTheDocument(); - }); - - it('displays formatted time when createdTime is provided', () => { - const failoverEvent: FailoverEvent = { - id: 'failover-1', - createdTime: { - seconds: '1700000000', - nanos: 0, - }, - failoverType: 'FAILOVER_TYPE_GRACEFUL', - clusterFailovers: [], - }; - - setup({ failoverEvent, isOpen: true }); - - expect(screen.getByText('Time')).toBeInTheDocument(); - expect(screen.getByTestId('formatted-date')).toBeInTheDocument(); - }); - - it('handles null createdTime', () => { - const failoverEvent: FailoverEvent = { - id: 'failover-1', - createdTime: null, - failoverType: 'FAILOVER_TYPE_GRACEFUL', - clusterFailovers: [], - }; - - setup({ failoverEvent, isOpen: true }); - - expect(screen.getByText('Time')).toBeInTheDocument(); - expect(screen.getByTestId('formatted-date')).toBeInTheDocument(); - }); - - it('renders table with cluster failovers when clusterFailovers array is not empty', () => { - 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, isOpen: true }); - - expect(screen.getByText('Scope')).toBeInTheDocument(); - expect(screen.getByText('Attribute')).toBeInTheDocument(); - expect(screen.getByText('Clusters')).toBeInTheDocument(); - expect(screen.getByText('Primary')).toBeInTheDocument(); - expect(screen.getByText('-')).toBeInTheDocument(); - expect( - screen.getByTestId('mock-single-cluster-failover') - ).toBeInTheDocument(); - }); - - it('does not render table when clusterFailovers array is empty', () => { - const failoverEvent: FailoverEvent = { - id: 'failover-1', - createdTime: { - seconds: '1700000000', - nanos: 0, - }, - failoverType: 'FAILOVER_TYPE_GRACEFUL', - clusterFailovers: [], - }; - - setup({ failoverEvent, isOpen: true }); - - expect(screen.queryByText('Scope')).not.toBeInTheDocument(); - expect(screen.queryByText('Attribute')).not.toBeInTheDocument(); - expect(screen.queryByText('Clusters')).not.toBeInTheDocument(); - }); - - it('displays Primary scope and dash for attribute when clusterAttribute is null', () => { - 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, isOpen: true }); - - expect(screen.getByText('Primary')).toBeInTheDocument(); - expect(screen.getByText('-')).toBeInTheDocument(); - }); - - it('displays scope and attribute name when clusterAttribute is provided', () => { - 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, isOpen: true }); - - expect(screen.getByText('city')).toBeInTheDocument(); - expect(screen.getByText('new_york')).toBeInTheDocument(); - }); - - it('renders multiple cluster failovers correctly', () => { - 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, - }, - { - fromCluster: { - activeClusterName: 'cluster-3', - failoverVersion: '3', - }, - toCluster: { - activeClusterName: 'cluster-4', - failoverVersion: '4', - }, - clusterAttribute: { - scope: 'region', - name: 'us-east', - }, - }, - ], - }; - - setup({ failoverEvent, isOpen: true }); - - expect(screen.getByText('Primary')).toBeInTheDocument(); - expect(screen.getByText('region')).toBeInTheDocument(); - expect(screen.getByText('us-east')).toBeInTheDocument(); - const clusterComponents = screen.getAllByTestId( - 'mock-single-cluster-failover' - ); - expect(clusterComponents).toHaveLength(2); - expect(clusterComponents[0]).toHaveTextContent('cluster-1 -> cluster-2'); - expect(clusterComponents[1]).toHaveTextContent('cluster-3 -> cluster-4'); - }); - - it('calls onClose when Close button is clicked', () => { - const failoverEvent: FailoverEvent = { - id: 'failover-1', - createdTime: { - seconds: '1700000000', - nanos: 0, - }, - failoverType: 'FAILOVER_TYPE_GRACEFUL', - clusterFailovers: [], - }; - - const mockOnClose = jest.fn(); - setup({ failoverEvent, isOpen: true, onClose: mockOnClose }); - - const closeButton = screen.getByText('Close'); - closeButton.click(); - - expect(mockOnClose).toHaveBeenCalledTimes(1); - }); - - it('displays modal title correctly', () => { - const failoverEvent: FailoverEvent = { - id: 'failover-1', - createdTime: { - seconds: '1700000000', - nanos: 0, - }, - failoverType: 'FAILOVER_TYPE_GRACEFUL', - clusterFailovers: [], - }; - - setup({ failoverEvent, isOpen: true }); - - expect(screen.getByText('Failover Information')).toBeInTheDocument(); - }); -}); - -function setup({ - failoverEvent, - isOpen = true, - onClose = jest.fn(), -}: { - failoverEvent: FailoverEvent; - isOpen?: boolean; - onClose?: () => void; -}) { - render( - - ); -} diff --git a/src/views/domain-page/domain-page-failover-modal/domain-page-failover-modal.styles.ts b/src/views/domain-page/domain-page-failover-modal/domain-page-failover-modal.styles.ts deleted file mode 100644 index 87dc86b2b..000000000 --- a/src/views/domain-page/domain-page-failover-modal/domain-page-failover-modal.styles.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { styled as createStyled, withStyle, type Theme } from 'baseui'; -import { ModalBody, ModalFooter, type ModalOverrides } from 'baseui/modal'; -import { type TableOverrides } from 'baseui/table-semantic'; -import { type StyleObject } from 'styletron-react'; - -export const overrides = { - modal: { - Close: { - style: ({ $theme }: { $theme: Theme }): StyleObject => ({ - top: $theme.sizing.scale850, - right: $theme.sizing.scale800, - }), - }, - Dialog: { - style: (): StyleObject => ({ - width: '700px', - }), - }, - } satisfies ModalOverrides, - table: { - TableHeadCell: { - style: ({ $theme }: { $theme: Theme }): StyleObject => ({ - ...$theme.typography.LabelXSmall, - paddingTop: $theme.sizing.scale300, - paddingBottom: $theme.sizing.scale300, - paddingLeft: $theme.sizing.scale500, - paddingRight: $theme.sizing.scale500, - }), - }, - TableBodyCell: { - style: ({ $theme }: { $theme: Theme }): StyleObject => ({ - ...$theme.typography.ParagraphXSmall, - paddingTop: $theme.sizing.scale300, - paddingBottom: $theme.sizing.scale300, - paddingLeft: $theme.sizing.scale500, - paddingRight: $theme.sizing.scale500, - }), - }, - } satisfies TableOverrides, -}; - -export const styled = { - ModalHeader: createStyled('div', ({ $theme }: { $theme: Theme }) => ({ - padding: $theme.sizing.scale600, - })), - Title: createStyled('div', ({ $theme }: { $theme: Theme }) => ({ - ...$theme.typography.HeadingSmall, - })), - ModalBody: withStyle(ModalBody, ({ $theme }: { $theme: Theme }) => ({ - padding: $theme.sizing.scale600, - })), - InfoRow: createStyled('div', ({ $theme }: { $theme: Theme }) => ({ - display: 'flex', - gap: $theme.sizing.scale800, - marginBottom: $theme.sizing.scale500, - ':last-child': { - marginBottom: 0, - }, - })), - InfoItem: createStyled('div', { - display: 'flex', - flexDirection: 'column', - }), - InfoLabel: createStyled('div', ({ $theme }: { $theme: Theme }) => ({ - ...$theme.typography.LabelXSmall, - color: $theme.colors.contentSecondary, - marginBottom: $theme.sizing.scale200, - })), - InfoValue: createStyled('div', ({ $theme }: { $theme: Theme }) => ({ - ...$theme.typography.LabelXSmall, - color: $theme.colors.contentPrimary, - })), - TableContainer: createStyled('div', ({ $theme }: { $theme: Theme }) => ({ - marginTop: $theme.sizing.scale600, - maxHeight: '50vh', - overflowY: 'auto', - })), - ModalFooter: withStyle(ModalFooter, ({ $theme }: { $theme: Theme }) => ({ - padding: `${$theme.sizing.scale500} ${$theme.sizing.scale600}`, - borderTop: `1px solid ${$theme.colors.borderOpaque}`, - display: 'flex', - justifyContent: 'flex-end', - gap: $theme.sizing.scale400, - })), -}; diff --git a/src/views/domain-page/domain-page-failover-modal/domain-page-failover-modal.tsx b/src/views/domain-page/domain-page-failover-modal/domain-page-failover-modal.tsx deleted file mode 100644 index b4ac6416b..000000000 --- a/src/views/domain-page/domain-page-failover-modal/domain-page-failover-modal.tsx +++ /dev/null @@ -1,95 +0,0 @@ -'use client'; -import { useMemo } from 'react'; - -import { Modal, ModalButton } from 'baseui/modal'; -import { Table } from 'baseui/table-semantic'; - -import FormattedDate from '@/components/formatted-date/formatted-date'; -import parseGrpcTimestamp from '@/utils/datetime/parse-grpc-timestamp'; - -import DomainPageFailoverSingleCluster from '../domain-page-failover-single-cluster/domain-page-failover-single-cluster'; - -import { overrides, styled } from './domain-page-failover-modal.styles'; -import { type Props } from './domain-page-failover-modal.types'; - -export default function DomainPageFailoverModal({ - failoverEvent, - isOpen, - onClose, -}: Props) { - const tableRows = useMemo(() => { - return failoverEvent.clusterFailovers.map((clusterFailover) => { - const fromCluster = clusterFailover.fromCluster?.activeClusterName; - const toCluster = clusterFailover.toCluster?.activeClusterName; - const clusters = ( - - ); - - const attribute = clusterFailover.clusterAttribute; - if (attribute === null) { - return { - scope: 'Primary', - attribute: '-', - clusters, - }; - } - - return { - scope: attribute.scope, - attribute: attribute.name, - clusters, - }; - }); - }, [failoverEvent]); - - const formattedTime = useMemo(() => { - if (!failoverEvent.createdTime) return null; - return parseGrpcTimestamp(failoverEvent.createdTime); - }, [failoverEvent]); - - return ( - - - Failover Information - - - - - ID - {failoverEvent.id} - - - Time - - - - - - {tableRows.length > 0 && ( - -
- - )} - - - - Close - - - - ); -} diff --git a/src/views/domain-page/domain-page-failover-modal/domain-page-failover-modal.types.ts b/src/views/domain-page/domain-page-failover-modal/domain-page-failover-modal.types.ts deleted file mode 100644 index ca4eadf57..000000000 --- a/src/views/domain-page/domain-page-failover-modal/domain-page-failover-modal.types.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { type FailoverEvent } from '@/route-handlers/list-failover-history/list-failover-history.types'; - -export type Props = { - failoverEvent: FailoverEvent; - isOpen: boolean; - onClose: () => void; -}; From 2b67689f6a4b303fe98d442119b6758c41e06f46 Mon Sep 17 00:00:00 2001 From: Adhitya Mamallan Date: Mon, 17 Nov 2025 14:39:13 +0100 Subject: [PATCH 4/4] Use placeholder text instead of index Signed-off-by: Adhitya Mamallan --- .../domain-page-failovers-filters.test.tsx | 50 +++++++++++-------- .../domain-page-failovers-filters.tsx | 14 ++++++ 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/src/views/domain-page/domain-page-failovers-filters/__tests__/domain-page-failovers-filters.test.tsx b/src/views/domain-page/domain-page-failovers-filters/__tests__/domain-page-failovers-filters.test.tsx index abfc5bd7d..911fb5192 100644 --- a/src/views/domain-page/domain-page-failovers-filters/__tests__/domain-page-failovers-filters.test.tsx +++ b/src/views/domain-page/domain-page-failovers-filters/__tests__/domain-page-failovers-filters.test.tsx @@ -21,8 +21,9 @@ describe(DomainPageFailoversFilters.name, () => { it('displays cluster attribute scope options including primary and domain scopes', async () => { const { user } = setup({}); - const comboboxes = screen.getAllByRole('combobox'); - const scopeCombobox = comboboxes[0]; + const scopeCombobox = screen.getByPlaceholderText( + 'Scope of cluster attribute' + ); await user.click(scopeCombobox); expect(screen.getByText(PRIMARY_CLUSTER_SCOPE)).toBeInTheDocument(); @@ -36,8 +37,9 @@ describe(DomainPageFailoversFilters.name, () => { }, }); - const inputs = screen.getAllByRole('textbox'); - expect(inputs[1]).toBeDisabled(); + expect( + screen.getByPlaceholderText('Value/name of cluster attribute') + ).toBeDisabled(); }); it('enables cluster attribute value combobox when scope is not primary', () => { @@ -47,8 +49,9 @@ describe(DomainPageFailoversFilters.name, () => { }, }); - const inputs = screen.getAllByRole('textbox'); - expect(inputs[1]).not.toBeDisabled(); + expect( + screen.getByPlaceholderText('Value/name of cluster attribute') + ).not.toBeDisabled(); }); it('displays cluster attribute values for selected scope', async () => { @@ -58,8 +61,9 @@ describe(DomainPageFailoversFilters.name, () => { }, }); - const comboboxes = screen.getAllByRole('combobox'); - const valueCombobox = comboboxes[1]; + const valueCombobox = screen.getByPlaceholderText( + 'Value/name of cluster attribute' + ); await user.click(valueCombobox); expect(screen.getByText('region0')).toBeInTheDocument(); @@ -69,8 +73,9 @@ describe(DomainPageFailoversFilters.name, () => { it('calls setQueryParams with new scope and resets value when scope changes', async () => { const { user, mockSetQueryParams } = setup({}); - const comboboxes = screen.getAllByRole('combobox'); - const scopeCombobox = comboboxes[0]; + const scopeCombobox = screen.getByPlaceholderText( + 'Scope of cluster attribute' + ); await user.click(scopeCombobox); await user.click(screen.getByText('region')); @@ -87,8 +92,9 @@ describe(DomainPageFailoversFilters.name, () => { }, }); - const comboboxes = screen.getAllByRole('combobox'); - const valueCombobox = comboboxes[1]; + const valueCombobox = screen.getByPlaceholderText( + 'Value/name of cluster attribute' + ); await user.click(valueCombobox); await user.click(screen.getByText('region0')); @@ -105,8 +111,9 @@ describe(DomainPageFailoversFilters.name, () => { }, }); - const comboboxes = screen.getAllByRole('combobox'); - const scopeCombobox = comboboxes[0]; + const scopeCombobox = screen.getByPlaceholderText( + 'Scope of cluster attribute' + ); await user.click(scopeCombobox); // Find and click the clear button (BaseUI Combobox clearable) @@ -127,8 +134,9 @@ describe(DomainPageFailoversFilters.name, () => { }, }); - const comboboxes = screen.getAllByRole('combobox'); - const valueCombobox = comboboxes[1]; + const valueCombobox = screen.getByPlaceholderText( + 'Value/name of cluster attribute' + ); await user.click(valueCombobox); // Find and click the clear button @@ -164,8 +172,9 @@ describe(DomainPageFailoversFilters.name, () => { }, }); - const inputs = screen.getAllByRole('textbox'); - expect(inputs[0]).toHaveValue('region'); + expect( + screen.getByPlaceholderText('Scope of cluster attribute') + ).toHaveValue('region'); }); it('displays current value in cluster attribute value combobox', () => { @@ -176,8 +185,9 @@ describe(DomainPageFailoversFilters.name, () => { }, }); - const inputs = screen.getAllByRole('textbox'); - expect(inputs[1]).toHaveValue('region0'); + expect( + screen.getByPlaceholderText('Value/name of cluster attribute') + ).toHaveValue('region0'); }); }); diff --git a/src/views/domain-page/domain-page-failovers-filters/domain-page-failovers-filters.tsx b/src/views/domain-page/domain-page-failovers-filters/domain-page-failovers-filters.tsx index 5e9703405..c834ad95a 100644 --- a/src/views/domain-page/domain-page-failovers-filters/domain-page-failovers-filters.tsx +++ b/src/views/domain-page/domain-page-failovers-filters/domain-page-failovers-filters.tsx @@ -68,6 +68,13 @@ export default function DomainPageFailoversFilters({ size="compact" clearable value={clusterAttributeScope ?? ''} + overrides={{ + Input: { + props: { + placeholder: 'Scope of cluster attribute', + }, + }, + }} onChange={(nextValue) => setQueryParams({ clusterAttributeScope: nextValue === '' ? undefined : nextValue, @@ -91,6 +98,13 @@ export default function DomainPageFailoversFilters({ clearable disabled={clusterAttributeScope === PRIMARY_CLUSTER_SCOPE} value={clusterAttributeValue ?? ''} + overrides={{ + Input: { + props: { + placeholder: 'Value/name of cluster attribute', + }, + }, + }} onChange={(nextValue) => setQueryParams({ clusterAttributeValue: nextValue === '' ? undefined : nextValue,