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..911fb5192 --- /dev/null +++ b/src/views/domain-page/domain-page-failovers-filters/__tests__/domain-page-failovers-filters.test.tsx @@ -0,0 +1,226 @@ +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 scopeCombobox = screen.getByPlaceholderText( + 'Scope of cluster attribute' + ); + 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, + }, + }); + + expect( + screen.getByPlaceholderText('Value/name of cluster attribute') + ).toBeDisabled(); + }); + + it('enables cluster attribute value combobox when scope is not primary', () => { + setup({ + queryParamsOverrides: { + clusterAttributeScope: 'region', + }, + }); + + expect( + screen.getByPlaceholderText('Value/name of cluster attribute') + ).not.toBeDisabled(); + }); + + it('displays cluster attribute values for selected scope', async () => { + const { user } = setup({ + queryParamsOverrides: { + clusterAttributeScope: 'region', + }, + }); + + const valueCombobox = screen.getByPlaceholderText( + 'Value/name of cluster attribute' + ); + 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 scopeCombobox = screen.getByPlaceholderText( + 'Scope of cluster attribute' + ); + 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 valueCombobox = screen.getByPlaceholderText( + 'Value/name of cluster attribute' + ); + 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 scopeCombobox = screen.getByPlaceholderText( + 'Scope of cluster attribute' + ); + 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 valueCombobox = screen.getByPlaceholderText( + 'Value/name of cluster attribute' + ); + 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', + }, + }); + + expect( + screen.getByPlaceholderText('Scope of cluster attribute') + ).toHaveValue('region'); + }); + + it('displays current value in cluster attribute value combobox', () => { + setup({ + queryParamsOverrides: { + clusterAttributeScope: 'region', + clusterAttributeValue: 'region0', + }, + }); + + expect( + screen.getByPlaceholderText('Value/name of cluster attribute') + ).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..c834ad95a --- /dev/null +++ b/src/views/domain-page/domain-page-failovers-filters/domain-page-failovers-filters.tsx @@ -0,0 +1,136 @@ +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 } /> - + ); }