Skip to content

Commit b629f46

Browse files
Add failover history filters
Signed-off-by: Adhitya Mamallan <adhitya.mamallan@uber.com>
1 parent 2dd7727 commit b629f46

File tree

6 files changed

+426
-15
lines changed

6 files changed

+426
-15
lines changed
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import { render, screen, userEvent } from '@/test-utils/rtl';
2+
3+
import { type PageQueryParamValues } from '@/hooks/use-page-query-params/use-page-query-params.types';
4+
import { mockDomainPageQueryParamsValues } from '@/views/domain-page/__fixtures__/domain-page-query-params';
5+
import type domainPageQueryParamsConfig from '@/views/domain-page/config/domain-page-query-params.config';
6+
import { mockActiveActiveDomain } from '@/views/shared/active-active/__fixtures__/active-active-domain';
7+
import { type ActiveActiveDomain } from '@/views/shared/active-active/active-active.types';
8+
9+
import { PRIMARY_CLUSTER_SCOPE } from '../../domain-page-failovers/domain-page-failovers.constants';
10+
import DomainPageFailoversFilters from '../domain-page-failovers-filters';
11+
12+
describe(DomainPageFailoversFilters.name, () => {
13+
it('renders both filter comboboxes and reset button', () => {
14+
setup({});
15+
16+
expect(screen.getByText('Cluster Attribute Scope')).toBeInTheDocument();
17+
expect(screen.getByText('Cluster Attribute Value')).toBeInTheDocument();
18+
expect(screen.getByText('Reset filters')).toBeInTheDocument();
19+
});
20+
21+
it('displays cluster attribute scope options including primary and domain scopes', async () => {
22+
const { user } = setup({});
23+
24+
const comboboxes = screen.getAllByRole('combobox');
25+
const scopeCombobox = comboboxes[0];
26+
await user.click(scopeCombobox);
27+
28+
expect(screen.getByText(PRIMARY_CLUSTER_SCOPE)).toBeInTheDocument();
29+
expect(screen.getByText('region')).toBeInTheDocument();
30+
});
31+
32+
it('disables cluster attribute value combobox when scope is primary', () => {
33+
setup({
34+
queryParamsOverrides: {
35+
clusterAttributeScope: PRIMARY_CLUSTER_SCOPE,
36+
},
37+
});
38+
39+
const inputs = screen.getAllByRole('textbox');
40+
expect(inputs[1]).toBeDisabled();
41+
});
42+
43+
it('enables cluster attribute value combobox when scope is not primary', () => {
44+
setup({
45+
queryParamsOverrides: {
46+
clusterAttributeScope: 'region',
47+
},
48+
});
49+
50+
const inputs = screen.getAllByRole('textbox');
51+
expect(inputs[1]).not.toBeDisabled();
52+
});
53+
54+
it('displays cluster attribute values for selected scope', async () => {
55+
const { user } = setup({
56+
queryParamsOverrides: {
57+
clusterAttributeScope: 'region',
58+
},
59+
});
60+
61+
const comboboxes = screen.getAllByRole('combobox');
62+
const valueCombobox = comboboxes[1];
63+
await user.click(valueCombobox);
64+
65+
expect(screen.getByText('region0')).toBeInTheDocument();
66+
expect(screen.getByText('region1')).toBeInTheDocument();
67+
});
68+
69+
it('calls setQueryParams with new scope and resets value when scope changes', async () => {
70+
const { user, mockSetQueryParams } = setup({});
71+
72+
const comboboxes = screen.getAllByRole('combobox');
73+
const scopeCombobox = comboboxes[0];
74+
await user.click(scopeCombobox);
75+
await user.click(screen.getByText('region'));
76+
77+
expect(mockSetQueryParams).toHaveBeenCalledWith({
78+
clusterAttributeScope: 'region',
79+
clusterAttributeValue: undefined,
80+
});
81+
});
82+
83+
it('calls setQueryParams with new value when cluster attribute value changes', async () => {
84+
const { user, mockSetQueryParams } = setup({
85+
queryParamsOverrides: {
86+
clusterAttributeScope: 'region',
87+
},
88+
});
89+
90+
const comboboxes = screen.getAllByRole('combobox');
91+
const valueCombobox = comboboxes[1];
92+
await user.click(valueCombobox);
93+
await user.click(screen.getByText('region0'));
94+
95+
expect(mockSetQueryParams).toHaveBeenCalledWith({
96+
clusterAttributeValue: 'region0',
97+
});
98+
});
99+
100+
it('calls setQueryParams with undefined when clearing scope', async () => {
101+
const { user, mockSetQueryParams } = setup({
102+
queryParamsOverrides: {
103+
clusterAttributeScope: 'region',
104+
clusterAttributeValue: 'region0',
105+
},
106+
});
107+
108+
const comboboxes = screen.getAllByRole('combobox');
109+
const scopeCombobox = comboboxes[0];
110+
await user.click(scopeCombobox);
111+
112+
// Find and click the clear button (BaseUI Combobox clearable)
113+
const clearButtons = screen.getAllByLabelText('Clear value');
114+
await user.click(clearButtons[0]);
115+
116+
expect(mockSetQueryParams).toHaveBeenCalledWith({
117+
clusterAttributeScope: undefined,
118+
clusterAttributeValue: undefined,
119+
});
120+
});
121+
122+
it('calls setQueryParams with undefined when clearing value', async () => {
123+
const { user, mockSetQueryParams } = setup({
124+
queryParamsOverrides: {
125+
clusterAttributeScope: 'region',
126+
clusterAttributeValue: 'region0',
127+
},
128+
});
129+
130+
const comboboxes = screen.getAllByRole('combobox');
131+
const valueCombobox = comboboxes[1];
132+
await user.click(valueCombobox);
133+
134+
// Find and click the clear button
135+
const clearButtons = screen.getAllByLabelText('Clear value');
136+
await user.click(clearButtons[1]);
137+
138+
expect(mockSetQueryParams).toHaveBeenCalledWith({
139+
clusterAttributeValue: undefined,
140+
});
141+
});
142+
143+
it('resets both filters when reset filters button is clicked', async () => {
144+
const { user, mockSetQueryParams } = setup({
145+
queryParamsOverrides: {
146+
clusterAttributeScope: 'region',
147+
clusterAttributeValue: 'region0',
148+
},
149+
});
150+
151+
const resetButton = screen.getByText('Reset filters');
152+
await user.click(resetButton);
153+
154+
expect(mockSetQueryParams).toHaveBeenCalledWith({
155+
clusterAttributeScope: undefined,
156+
clusterAttributeValue: undefined,
157+
});
158+
});
159+
160+
it('displays current scope value in combobox', () => {
161+
setup({
162+
queryParamsOverrides: {
163+
clusterAttributeScope: 'region',
164+
},
165+
});
166+
167+
const inputs = screen.getAllByRole('textbox');
168+
expect(inputs[0]).toHaveValue('region');
169+
});
170+
171+
it('displays current value in cluster attribute value combobox', () => {
172+
setup({
173+
queryParamsOverrides: {
174+
clusterAttributeScope: 'region',
175+
clusterAttributeValue: 'region0',
176+
},
177+
});
178+
179+
const inputs = screen.getAllByRole('textbox');
180+
expect(inputs[1]).toHaveValue('region0');
181+
});
182+
});
183+
184+
function setup({
185+
queryParamsOverrides,
186+
domainDescriptionOverrides,
187+
}: {
188+
queryParamsOverrides?: Partial<
189+
PageQueryParamValues<typeof domainPageQueryParamsConfig>
190+
>;
191+
domainDescriptionOverrides?: Partial<ActiveActiveDomain>;
192+
} = {}) {
193+
const mockSetQueryParams = jest.fn();
194+
195+
const domainDescription = {
196+
...mockActiveActiveDomain,
197+
...domainDescriptionOverrides,
198+
};
199+
200+
const queryParams = {
201+
...mockDomainPageQueryParamsValues,
202+
...queryParamsOverrides,
203+
};
204+
205+
render(
206+
<DomainPageFailoversFilters
207+
domainDescription={domainDescription}
208+
queryParams={queryParams}
209+
setQueryParams={mockSetQueryParams}
210+
/>
211+
);
212+
213+
const user = userEvent.setup();
214+
215+
return { mockSetQueryParams, user };
216+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { type Theme } from 'baseui';
2+
import { styled as createStyled } from 'baseui';
3+
import { type ButtonOverrides } from 'baseui/button';
4+
import type { FormControlOverrides } from 'baseui/form-control/types';
5+
import { type StyleObject } from 'styletron-react';
6+
7+
export const styled = {
8+
FiltersContainer: createStyled('div', ({ $theme }) => ({
9+
display: 'flex',
10+
flexDirection: 'column',
11+
flexWrap: 'wrap',
12+
gap: $theme.sizing.scale500,
13+
[$theme.mediaQuery.medium]: {
14+
flexDirection: 'row',
15+
alignItems: 'flex-end',
16+
},
17+
marginTop: $theme.sizing.scale950,
18+
marginBottom: $theme.sizing.scale900,
19+
})),
20+
FilterContainer: createStyled('div', ({ $theme }) => ({
21+
flexGrow: 1,
22+
flexShrink: 1,
23+
flexBasis: '0',
24+
[$theme.mediaQuery.medium]: {
25+
alignSelf: 'flex-start',
26+
},
27+
})),
28+
};
29+
30+
export const overrides = {
31+
comboboxFormControl: {
32+
Label: {
33+
style: ({ $theme }: { $theme: Theme }): StyleObject => ({
34+
...$theme.typography.LabelXSmall,
35+
}),
36+
},
37+
ControlContainer: {
38+
style: (): StyleObject => ({
39+
margin: '0px',
40+
}),
41+
},
42+
} satisfies FormControlOverrides,
43+
clearFiltersButton: {
44+
Root: {
45+
style: ({ $theme }: { $theme: Theme }): StyleObject => ({
46+
whiteSpace: 'nowrap',
47+
flexGrow: 2,
48+
height: $theme.sizing.scale950,
49+
[$theme.mediaQuery.medium]: {
50+
flexGrow: 0,
51+
alignSelf: 'flex-end',
52+
},
53+
}),
54+
},
55+
} satisfies ButtonOverrides,
56+
};
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { useMemo } from 'react';
2+
3+
import { Button } from 'baseui/button';
4+
import { Combobox } from 'baseui/combobox';
5+
import { FormControl } from 'baseui/form-control';
6+
import { Delete } from 'baseui/icon';
7+
8+
import {
9+
type PageQueryParamSetter,
10+
type PageQueryParamValues,
11+
} from '@/hooks/use-page-query-params/use-page-query-params.types';
12+
import { type ActiveActiveDomain } from '@/views/shared/active-active/active-active.types';
13+
14+
import type domainPageQueryParamsConfig from '../config/domain-page-query-params.config';
15+
import { PRIMARY_CLUSTER_SCOPE } from '../domain-page-failovers/domain-page-failovers.constants';
16+
17+
import { styled, overrides } from './domain-page-failovers-filters.styles';
18+
19+
export default function DomainPageFailoversFilters({
20+
domainDescription,
21+
queryParams,
22+
setQueryParams,
23+
}: {
24+
domainDescription: ActiveActiveDomain;
25+
queryParams: PageQueryParamValues<typeof domainPageQueryParamsConfig>;
26+
setQueryParams: PageQueryParamSetter<typeof domainPageQueryParamsConfig>;
27+
}) {
28+
const { clusterAttributeScope, clusterAttributeValue } = queryParams;
29+
30+
const clusterAttributeScopes = useMemo(
31+
() => [
32+
PRIMARY_CLUSTER_SCOPE,
33+
...Object.keys(
34+
domainDescription.activeClusters.activeClustersByClusterAttribute
35+
),
36+
],
37+
[domainDescription.activeClusters.activeClustersByClusterAttribute]
38+
);
39+
40+
const clusterAttributeValuesForScope = useMemo(() => {
41+
if (
42+
!clusterAttributeScope ||
43+
clusterAttributeScope === PRIMARY_CLUSTER_SCOPE
44+
)
45+
return [];
46+
47+
const activeClustersForScope =
48+
domainDescription.activeClusters.activeClustersByClusterAttribute[
49+
clusterAttributeScope
50+
];
51+
52+
if (!activeClustersForScope) return [];
53+
54+
return Object.keys(activeClustersForScope.clusterAttributes);
55+
}, [
56+
clusterAttributeScope,
57+
domainDescription.activeClusters.activeClustersByClusterAttribute,
58+
]);
59+
60+
return (
61+
<styled.FiltersContainer>
62+
<styled.FilterContainer>
63+
<FormControl
64+
label="Cluster Attribute Scope"
65+
overrides={overrides.comboboxFormControl}
66+
>
67+
<Combobox
68+
size="compact"
69+
clearable
70+
value={clusterAttributeScope ?? ''}
71+
onChange={(nextValue) =>
72+
setQueryParams({
73+
clusterAttributeScope: nextValue === '' ? undefined : nextValue,
74+
clusterAttributeValue: undefined,
75+
})
76+
}
77+
options={clusterAttributeScopes.map((scope) => ({
78+
id: scope,
79+
}))}
80+
mapOptionToString={(option) => option.id}
81+
/>
82+
</FormControl>
83+
</styled.FilterContainer>
84+
<styled.FilterContainer>
85+
<FormControl
86+
label="Cluster Attribute Value"
87+
overrides={overrides.comboboxFormControl}
88+
>
89+
<Combobox
90+
size="compact"
91+
clearable
92+
disabled={clusterAttributeScope === PRIMARY_CLUSTER_SCOPE}
93+
value={clusterAttributeValue ?? ''}
94+
onChange={(nextValue) =>
95+
setQueryParams({
96+
clusterAttributeValue: nextValue === '' ? undefined : nextValue,
97+
})
98+
}
99+
options={clusterAttributeValuesForScope.map((scope) => ({
100+
id: scope,
101+
}))}
102+
mapOptionToString={(option) => option.id}
103+
/>
104+
</FormControl>
105+
</styled.FilterContainer>
106+
<Button
107+
size="compact"
108+
kind="tertiary"
109+
onClick={() =>
110+
setQueryParams({
111+
clusterAttributeScope: undefined,
112+
clusterAttributeValue: undefined,
113+
})
114+
}
115+
startEnhancer={Delete}
116+
overrides={overrides.clearFiltersButton}
117+
>
118+
Reset filters
119+
</Button>
120+
</styled.FiltersContainer>
121+
);
122+
}

0 commit comments

Comments
 (0)