Skip to content

Commit 830b05e

Browse files
feat: Failover Version table for Active-Active domains (#1052)
* Add expandable table for showing failover versions per cluster attribute, for active-active domains. * Moved domain-page-metadata-failover-version logic into config Signed-off-by: Adhitya Mamallan <[email protected]>
1 parent 5f52059 commit 830b05e

9 files changed

+296
-45
lines changed

src/views/domain-page/config/domain-page-metadata-extended-table.config.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { createElement } from 'react';
22

3+
import isActiveActiveDomain from '@/views/shared/active-active/helpers/is-active-active-domain';
4+
35
import DomainPageMetadataClusters from '../domain-page-metadata-clusters/domain-page-metadata-clusters';
46
import DomainPageMetadataDescription from '../domain-page-metadata-description/domain-page-metadata-description';
5-
import DomainPageMetadataFailoverVersion from '../domain-page-metadata-failover-version/domain-page-metadata-failover-version';
7+
import DomainPageMetadataFailoverVersionActiveActive from '../domain-page-metadata-failover-version-active-active/domain-page-metadata-failover-version-active-active';
68
import { type MetadataItem } from '../domain-page-metadata-table/domain-page-metadata-table.types';
79
import DomainPageMetadataViewJson from '../domain-page-metadata-view-json/domain-page-metadata-view-json';
810
import getClusterOperationMode from '../helpers/get-cluster-operation-mode';
@@ -55,7 +57,12 @@ const domainPageMetadataExtendedTableConfig = [
5557
description: 'The failover version of the domain',
5658
kind: 'simple',
5759
getValue: ({ domainDescription }) =>
58-
createElement(DomainPageMetadataFailoverVersion, domainDescription),
60+
isActiveActiveDomain(domainDescription)
61+
? createElement(
62+
DomainPageMetadataFailoverVersionActiveActive,
63+
domainDescription
64+
)
65+
: domainDescription.failoverVersion,
5966
},
6067
{
6168
key: 'describeDomainJson',

src/views/domain-page/config/domain-page-metadata-table.config.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import { createElement } from 'react';
2+
13
import type { ListTableItem } from '@/components/list-table/list-table.types';
4+
import isActiveActiveDomain from '@/views/shared/active-active/helpers/is-active-active-domain';
25

36
import DomainPageMetadataClusters from '../domain-page-metadata-clusters/domain-page-metadata-clusters';
4-
import DomainPageMetadataFailoverVersion from '../domain-page-metadata-failover-version/domain-page-metadata-failover-version';
7+
import DomainPageMetadataFailoverVersionActiveActive from '../domain-page-metadata-failover-version-active-active/domain-page-metadata-failover-version-active-active';
58
import { type DomainDescription } from '../domain-page.types';
69
import getClusterOperationMode from '../helpers/get-cluster-operation-mode';
710

@@ -31,7 +34,13 @@ const domainPageMetadataTableConfig: Array<ListTableItem<DomainDescription>> = [
3134
{
3235
key: 'failoverVersion',
3336
label: 'Failover version',
34-
renderValue: DomainPageMetadataFailoverVersion,
37+
renderValue: (domainDescription: DomainDescription) =>
38+
isActiveActiveDomain(domainDescription)
39+
? createElement(
40+
DomainPageMetadataFailoverVersionActiveActive,
41+
domainDescription
42+
)
43+
: domainDescription.failoverVersion,
3544
},
3645
];
3746

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { render, screen, userEvent } from '@/test-utils/rtl';
2+
3+
import { type ClusterAttributeScope } from '@/__generated__/proto-ts/uber/cadence/api/v1/ClusterAttributeScope';
4+
import { mockActiveActiveDomain } from '@/views/shared/active-active/__fixtures__/active-active-domain';
5+
import { type ActiveActiveDomain } from '@/views/shared/active-active/active-active.types';
6+
7+
import DomainPageMetadataFailoverVersionActiveActive from '../domain-page-metadata-failover-version-active-active';
8+
9+
describe(DomainPageMetadataFailoverVersionActiveActive.name, () => {
10+
it('renders the table with headers', () => {
11+
setup();
12+
13+
expect(screen.getByText('Scope')).toBeInTheDocument();
14+
expect(screen.getByText('Attribute')).toBeInTheDocument();
15+
expect(screen.getByText('Active Cluster')).toBeInTheDocument();
16+
expect(screen.getByText('Failover Version')).toBeInTheDocument();
17+
});
18+
19+
it('renders default entry with domain data', () => {
20+
setup();
21+
22+
expect(screen.getByText('default')).toBeInTheDocument();
23+
expect(screen.getByText('-')).toBeInTheDocument();
24+
expect(screen.getByText('cluster0')).toBeInTheDocument();
25+
expect(screen.getByText('2')).toBeInTheDocument();
26+
});
27+
28+
it('renders multiple failover version entries', () => {
29+
setup({ attributeCount: 2 });
30+
31+
// Default entry
32+
expect(screen.getByText('default')).toBeInTheDocument();
33+
expect(screen.getByText('cluster0')).toBeInTheDocument();
34+
expect(screen.getByText('2')).toBeInTheDocument();
35+
36+
// Additional entries
37+
expect(screen.getByText('scope1')).toBeInTheDocument();
38+
expect(screen.getByText('attribute1')).toBeInTheDocument();
39+
expect(screen.getByText('cluster_1')).toBeInTheDocument();
40+
expect(screen.getByText('1000')).toBeInTheDocument();
41+
42+
expect(screen.getByText('scope2')).toBeInTheDocument();
43+
expect(screen.getByText('attribute2')).toBeInTheDocument();
44+
expect(screen.getByText('cluster_2')).toBeInTheDocument();
45+
expect(screen.getByText('1001')).toBeInTheDocument();
46+
});
47+
48+
it('does not show "Show all" button when entries are less than or equal to MAX_ATTRIBUTES_COUNT_TRUNCATED', () => {
49+
// 3 additional attributes + 1 default = 4 total (exactly at the limit)
50+
setup({ attributeCount: 3 });
51+
52+
expect(screen.queryByText(/Show all/)).not.toBeInTheDocument();
53+
});
54+
55+
it('shows "Show all" button when entries exceed MAX_ATTRIBUTES_COUNT_TRUNCATED', () => {
56+
// 5 additional attributes + 1 default = 6 total (exceeds limit of 4)
57+
setup({ attributeCount: 5 });
58+
59+
expect(screen.getByText('Show all (6)')).toBeInTheDocument();
60+
});
61+
62+
it('truncates entries when not expanded', () => {
63+
setup({ attributeCount: 5 });
64+
65+
// Should show first 4 entries only (default + first 3 attributes)
66+
expect(screen.getByText('default')).toBeInTheDocument();
67+
expect(screen.getByText('scope1')).toBeInTheDocument();
68+
expect(screen.getByText('scope2')).toBeInTheDocument();
69+
expect(screen.getByText('scope3')).toBeInTheDocument();
70+
71+
// Should not show 5th and 6th entries
72+
expect(screen.queryByText('scope4')).not.toBeInTheDocument();
73+
expect(screen.queryByText('scope5')).not.toBeInTheDocument();
74+
});
75+
76+
it('expands to show all entries when "Show all" button is clicked', async () => {
77+
const { user } = setup({ attributeCount: 5 });
78+
79+
const showAllButton = screen.getByText('Show all (6)');
80+
await user.click(showAllButton);
81+
82+
// Now all entries should be visible
83+
expect(screen.getByText('default')).toBeInTheDocument();
84+
expect(screen.getByText('scope1')).toBeInTheDocument();
85+
expect(screen.getByText('scope2')).toBeInTheDocument();
86+
expect(screen.getByText('scope3')).toBeInTheDocument();
87+
expect(screen.getByText('scope4')).toBeInTheDocument();
88+
expect(screen.getByText('scope5')).toBeInTheDocument();
89+
90+
// Button text should change
91+
expect(screen.getByText('Show less')).toBeInTheDocument();
92+
expect(screen.queryByText('Show all (6)')).not.toBeInTheDocument();
93+
});
94+
95+
it('collapses to show truncated entries when "Show less" button is clicked', async () => {
96+
const { user } = setup({ attributeCount: 5 });
97+
98+
// Expand first
99+
const showAllButton = screen.getByText('Show all (6)');
100+
await user.click(showAllButton);
101+
102+
// Then collapse
103+
const showLessButton = screen.getByText('Show less');
104+
await user.click(showLessButton);
105+
106+
// Should show first 4 entries only
107+
expect(screen.getByText('default')).toBeInTheDocument();
108+
expect(screen.getByText('scope1')).toBeInTheDocument();
109+
expect(screen.getByText('scope2')).toBeInTheDocument();
110+
expect(screen.getByText('scope3')).toBeInTheDocument();
111+
112+
// Should not show 5th and 6th entries
113+
expect(screen.queryByText('scope4')).not.toBeInTheDocument();
114+
expect(screen.queryByText('scope5')).not.toBeInTheDocument();
115+
116+
// Button text should change back
117+
expect(screen.getByText('Show all (6)')).toBeInTheDocument();
118+
expect(screen.queryByText('Show less')).not.toBeInTheDocument();
119+
});
120+
});
121+
122+
function setup({
123+
attributeCount = 0,
124+
}: {
125+
attributeCount?: number;
126+
} = {}) {
127+
const user = userEvent.setup();
128+
129+
const activeClustersByClusterAttribute: Record<
130+
string,
131+
ClusterAttributeScope
132+
> = {};
133+
134+
// Create additional attributes beyond the default one
135+
for (let i = 0; i < attributeCount; i++) {
136+
const scope = `scope${i + 1}`;
137+
activeClustersByClusterAttribute[scope] = {
138+
clusterAttributes: {
139+
[`attribute${i + 1}`]: {
140+
activeClusterName: `cluster_${i + 1}`,
141+
failoverVersion: `${1000 + i}`,
142+
},
143+
},
144+
};
145+
}
146+
147+
const domain: ActiveActiveDomain = {
148+
...mockActiveActiveDomain,
149+
activeClusters: {
150+
regionToCluster: {},
151+
activeClustersByClusterAttribute,
152+
},
153+
};
154+
155+
render(<DomainPageMetadataFailoverVersionActiveActive {...domain} />);
156+
157+
return { user };
158+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const MAX_ATTRIBUTES_COUNT_TRUNCATED = 4;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { styled as createStyled, type Theme } from 'baseui';
2+
import { type TableOverrides } from 'baseui/table-semantic';
3+
import { type StyleObject } from 'styletron-react';
4+
5+
export const overrides = {
6+
table: {
7+
TableHeadCell: {
8+
style: ({ $theme }: { $theme: Theme }): StyleObject => ({
9+
...$theme.typography.LabelXSmall,
10+
paddingTop: $theme.sizing.scale300,
11+
paddingBottom: $theme.sizing.scale300,
12+
paddingLeft: $theme.sizing.scale500,
13+
paddingRight: $theme.sizing.scale500,
14+
}),
15+
},
16+
TableBodyCell: {
17+
style: ({ $theme }: { $theme: Theme }): StyleObject => ({
18+
...$theme.typography.ParagraphXSmall,
19+
paddingTop: $theme.sizing.scale300,
20+
paddingBottom: $theme.sizing.scale300,
21+
paddingLeft: $theme.sizing.scale500,
22+
paddingRight: $theme.sizing.scale500,
23+
}),
24+
},
25+
} satisfies TableOverrides,
26+
};
27+
28+
export const styled = {
29+
FailoverVersionsContainer: createStyled('div', ({ $theme }) => ({
30+
display: 'flex',
31+
flexDirection: 'column',
32+
alignItems: 'flex-start',
33+
gap: $theme.sizing.scale500,
34+
})),
35+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { useMemo, useState } from 'react';
2+
3+
import { Button } from 'baseui/button';
4+
import { Table } from 'baseui/table-semantic';
5+
import { MdAdd, MdHorizontalRule } from 'react-icons/md';
6+
7+
import { type ActiveActiveDomain } from '@/views/shared/active-active/active-active.types';
8+
9+
import { MAX_ATTRIBUTES_COUNT_TRUNCATED } from './domain-page-metadata-failover-version-active-active.constants';
10+
import {
11+
overrides,
12+
styled,
13+
} from './domain-page-metadata-failover-version-active-active.styles';
14+
import { type FailoverVersionEntryActiveActive } from './domain-page-metadata-failover-version-active-active.types';
15+
16+
export default function DomainPageMetadataFailoverVersionActiveActive(
17+
domain: ActiveActiveDomain
18+
) {
19+
const [showAll, setShowAll] = useState(false);
20+
21+
const allEntries = useMemo<Array<FailoverVersionEntryActiveActive>>(() => {
22+
return [
23+
{
24+
scope: 'default',
25+
attribute: '-',
26+
cluster: domain.activeClusterName,
27+
version: domain.failoverVersion,
28+
},
29+
...Object.entries(
30+
domain.activeClusters.activeClustersByClusterAttribute
31+
).flatMap(([scope, { clusterAttributes }]) =>
32+
Object.entries(clusterAttributes).map(
33+
([attribute, activeClusterInfo]) => ({
34+
scope,
35+
attribute,
36+
cluster: activeClusterInfo.activeClusterName,
37+
version: activeClusterInfo.failoverVersion,
38+
})
39+
)
40+
),
41+
];
42+
}, [
43+
domain.activeClusters.activeClustersByClusterAttribute,
44+
domain.activeClusterName,
45+
domain.failoverVersion,
46+
]);
47+
48+
const entriesToShow = useMemo(() => {
49+
return showAll
50+
? allEntries
51+
: allEntries.slice(0, MAX_ATTRIBUTES_COUNT_TRUNCATED);
52+
}, [allEntries, showAll]);
53+
54+
return (
55+
<styled.FailoverVersionsContainer>
56+
<Table
57+
size="compact"
58+
divider="clean"
59+
overrides={overrides.table}
60+
columns={['Scope', 'Attribute', 'Active Cluster', 'Failover Version']}
61+
data={entriesToShow.map(Object.values)}
62+
/>
63+
{allEntries.length > MAX_ATTRIBUTES_COUNT_TRUNCATED && (
64+
<Button
65+
kind="secondary"
66+
size="mini"
67+
shape="pill"
68+
startEnhancer={showAll ? <MdHorizontalRule /> : <MdAdd />}
69+
onClick={() => setShowAll((v) => !v)}
70+
>
71+
{showAll ? 'Show less' : `Show all (${allEntries.length})`}
72+
</Button>
73+
)}
74+
</styled.FailoverVersionsContainer>
75+
);
76+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export type FailoverVersionEntryActiveActive = {
2+
scope: string;
3+
attribute: string;
4+
cluster: string;
5+
version: string;
6+
};

src/views/domain-page/domain-page-metadata-failover-version/__tests__/domain-page-metadata-failover-version.test.tsx

Lines changed: 0 additions & 27 deletions
This file was deleted.

src/views/domain-page/domain-page-metadata-failover-version/domain-page-metadata-failover-version.tsx

Lines changed: 0 additions & 14 deletions
This file was deleted.

0 commit comments

Comments
 (0)