Skip to content

Commit 5050b3a

Browse files
authored
upcoming: [UIE-9562] - Implement Listeners Table in Network LoadBalancer Detail page (#13123)
* upcoming: [UIE-9562] - Implement Listeners Table in Network LoadBalancer Detail page * Added changeset: Implement Listeners Table in Network LoadBalancer Detail page * PR feedback * PR feedback @bnussman-akamai * PR feedback @tvijay-akamai * fixed failing unit test
1 parent 2b16491 commit 5050b3a

File tree

8 files changed

+319
-31
lines changed

8 files changed

+319
-31
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Upcoming Features
3+
---
4+
5+
Implement Listeners Table in Network LoadBalancer Detail page ([#13123](https://github.com/linode/manager/pull/13123))

packages/manager/src/features/NetworkLoadBalancers/NetworkLoadBalancersDetail/NetworkLoadBalancerDetailBody.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export const NetworkLoadBalancerDetailBody = (
7272
{` (ID: ${lkeCluster.id})`}
7373
</>
7474
) : (
75-
'N/A'
75+
'None'
7676
)}
7777
</Box>
7878
<Box>

packages/manager/src/features/NetworkLoadBalancers/NetworkLoadBalancersDetail/NetworkLoadBalancersDetail.test.tsx

Lines changed: 83 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { regionFactory } from '@linode/utilities';
22
import * as React from 'react';
33

4-
import { networkLoadBalancerFactory } from 'src/factories/networkLoadBalancer';
4+
import {
5+
networkLoadBalancerFactory,
6+
networkLoadBalancerListenerFactory,
7+
} from 'src/factories/networkLoadBalancer';
58
import { formatDate } from 'src/utilities/formatDate';
69
import { mockMatchMedia, renderWithTheme } from 'src/utilities/testHelpers';
710

@@ -12,6 +15,7 @@ const queryMocks = vi.hoisted(() => ({
1215
useParams: vi.fn().mockReturnValue({}),
1316
useProfile: vi.fn().mockReturnValue({}),
1417
useRegionQuery: vi.fn().mockReturnValue({}),
18+
useNetworkLoadBalancerListenersQuery: vi.fn().mockReturnValue({}),
1519
}));
1620

1721
vi.mock('@linode/queries', async () => {
@@ -21,6 +25,8 @@ vi.mock('@linode/queries', async () => {
2125
useProfile: queryMocks.useProfile,
2226
useRegionQuery: queryMocks.useRegionQuery,
2327
useNetworkLoadBalancerQuery: queryMocks.useNetworkLoadBalancerQuery,
28+
useNetworkLoadBalancerListenersQuery:
29+
queryMocks.useNetworkLoadBalancerListenersQuery,
2430
};
2531
});
2632

@@ -55,7 +61,7 @@ describe('NetworkLoadBalancersDetail', () => {
5561

5662
const { getByTestId } = renderWithTheme(<NetworkLoadBalancersDetail />);
5763

58-
expect(getByTestId('circle-progress')).toBeInTheDocument();
64+
expect(getByTestId('circle-progress')).toBeVisible();
5965
});
6066

6167
it('renders an error state', () => {
@@ -68,7 +74,7 @@ describe('NetworkLoadBalancersDetail', () => {
6874

6975
expect(
7076
getByText('There was a problem retrieving your NLB. Please try again.')
71-
).toBeInTheDocument();
77+
).toBeVisible();
7278
});
7379

7480
it('renders the NLB details', () => {
@@ -80,28 +86,28 @@ describe('NetworkLoadBalancersDetail', () => {
8086

8187
const { getByText } = renderWithTheme(<NetworkLoadBalancersDetail />);
8288

83-
expect(getByText('ACTIVE')).toBeInTheDocument();
89+
expect(getByText('ACTIVE')).toBeVisible();
8490

85-
expect(getByText('Virtual IP (IPv4)')).toBeInTheDocument();
86-
expect(getByText(nlbFactory.address_v4)).toBeInTheDocument();
91+
expect(getByText('Virtual IP (IPv4)')).toBeVisible();
92+
expect(getByText(nlbFactory.address_v4)).toBeVisible();
8793

88-
expect(getByText('Virtual IP (IPv6)')).toBeInTheDocument();
89-
expect(getByText(nlbFactory.address_v6)).toBeInTheDocument();
94+
expect(getByText('Virtual IP (IPv6)')).toBeVisible();
95+
expect(getByText(nlbFactory.address_v6)).toBeVisible();
9096

91-
expect(getByText('Region')).toBeInTheDocument();
92-
expect(getByText('US, Newark, NJ')).toBeInTheDocument();
97+
expect(getByText('Region')).toBeVisible();
98+
expect(getByText('US, Newark, NJ')).toBeVisible();
9399

94-
expect(getByText('LKE-E Cluster')).toBeInTheDocument();
95-
expect(getByText('N/A')).toBeInTheDocument();
100+
expect(getByText('LKE-E Cluster')).toBeVisible();
101+
expect(getByText('None')).toBeVisible();
96102

97-
expect(getByText('Network Load Balancer ID')).toBeInTheDocument();
98-
expect(getByText(nlbFactory.id)).toBeInTheDocument();
103+
expect(getByText('Network Load Balancer ID')).toBeVisible();
104+
expect(getByText(nlbFactory.id)).toBeVisible();
99105

100-
expect(getByText('Created')).toBeInTheDocument();
101-
expect(getByText(formatDate(nlbFactory.created))).toBeInTheDocument();
106+
expect(getByText('Created')).toBeVisible();
107+
expect(getByText(formatDate(nlbFactory.created))).toBeVisible();
102108

103-
expect(getByText('Updated')).toBeInTheDocument();
104-
expect(getByText(formatDate(nlbFactory.updated))).toBeInTheDocument();
109+
expect(getByText('Updated')).toBeVisible();
110+
expect(getByText(formatDate(nlbFactory.updated))).toBeVisible();
105111
});
106112

107113
it('renders LKE Details if the NLB is associated with an LKE cluster', () => {
@@ -115,12 +121,68 @@ describe('NetworkLoadBalancersDetail', () => {
115121

116122
const { getByText } = renderWithTheme(<NetworkLoadBalancersDetail />);
117123

118-
expect(getByText('LKE-E Cluster')).toBeInTheDocument();
119-
expect(getByText(nlbFactory.lke_cluster!.label)).toBeInTheDocument();
124+
expect(getByText('LKE-E Cluster')).toBeVisible();
125+
expect(getByText(nlbFactory.lke_cluster!.label)).toBeVisible();
120126
expect(
121127
getByText(`(ID: ${nlbFactory.lke_cluster!.id})`, {
122128
exact: false,
123129
})
124-
).toBeInTheDocument();
130+
).toBeVisible();
131+
});
132+
133+
it('renders a Listeners table', () => {
134+
const listenerFactory = networkLoadBalancerListenerFactory.build();
135+
const nlbFactory = networkLoadBalancerFactory.build({
136+
listeners: [listenerFactory],
137+
});
138+
queryMocks.useNetworkLoadBalancerQuery.mockReturnValue({
139+
isLoading: false,
140+
data: nlbFactory,
141+
});
142+
143+
queryMocks.useNetworkLoadBalancerListenersQuery.mockReturnValue({
144+
isLoading: false,
145+
data: { data: [listenerFactory], results: 1, page: 1, page_size: 25 },
146+
});
147+
148+
const { getByText, getByTestId, getByRole } = renderWithTheme(
149+
<NetworkLoadBalancersDetail />
150+
);
151+
152+
const link = getByRole('link', { name: `${listenerFactory.label}` });
153+
expect(link).toHaveAttribute(
154+
'href',
155+
`/netloadbalancers/${nlbFactory.id}/listeners/${listenerFactory.id}`
156+
);
157+
expect(getByText('Listeners (1)')).toBeVisible();
158+
expect(getByTestId('nlb-listeners-table')).toBeVisible();
159+
expect(getByText(listenerFactory.label)).toBeVisible();
160+
expect(getByText(listenerFactory.port)).toBeVisible();
161+
expect(getByText(listenerFactory.protocol.toUpperCase())).toBeVisible();
162+
expect(getByText(listenerFactory.id)).toBeVisible();
163+
});
164+
165+
it('renders an empty Listeners table if there are no listeners', () => {
166+
const nlbFactory = networkLoadBalancerFactory.build({
167+
listeners: [],
168+
});
169+
queryMocks.useNetworkLoadBalancerQuery.mockReturnValue({
170+
isLoading: false,
171+
data: nlbFactory,
172+
});
173+
queryMocks.useNetworkLoadBalancerListenersQuery.mockReturnValue({
174+
isLoading: false,
175+
data: { data: [], results: 0 },
176+
});
177+
178+
const { getByText, getByTestId } = renderWithTheme(
179+
<NetworkLoadBalancersDetail />
180+
);
181+
182+
expect(getByText('Listeners (0)')).toBeVisible();
183+
expect(getByTestId('nlb-listeners-table')).toBeVisible();
184+
expect(
185+
getByText('No Listeners are defined for this Network Load Balancer')
186+
).toBeVisible();
125187
});
126188
});

packages/manager/src/features/NetworkLoadBalancers/NetworkLoadBalancersDetail/NetworkLoadBalancersDetail.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { LandingHeader } from 'src/components/LandingHeader';
1010
import { NLB_API_DOCS_LINK } from '../constants';
1111
import { NetworkLoadBalancerDetailBody } from './NetworkLoadBalancerDetailBody';
1212
import { NetworkLoadBalancerDetailHeader } from './NetworkLoadBalancerDetailHeader';
13+
import { NetworkLoadBalancersListenerTable } from './NetworkLoadBalancersListenerTable';
1314

1415
const NetworkLoadBalancersDetail = () => {
1516
const params = useParams({ strict: false });
@@ -33,7 +34,7 @@ const NetworkLoadBalancersDetail = () => {
3334

3435
return (
3536
<>
36-
<DocumentTitleSegment segment="Network Load Balancer" />
37+
<DocumentTitleSegment segment={`${nlb.label} | Network Load Balancer`} />
3738
<LandingHeader
3839
breadcrumbProps={{
3940
labelOptions: { noCap: true },
@@ -64,6 +65,7 @@ const NetworkLoadBalancersDetail = () => {
6465
header={<NetworkLoadBalancerDetailHeader status={nlb.status} />}
6566
noBodyBottomBorder={true}
6667
/>
68+
<NetworkLoadBalancersListenerTable nlbId={nlb.id} />
6769
</>
6870
);
6971
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import {
2+
useNetworkLoadBalancerListenerQuery,
3+
useNetworkLoadBalancerQuery,
4+
} from '@linode/queries';
5+
import { CircleProgress, ErrorState, Notice } from '@linode/ui';
6+
import { useParams } from '@tanstack/react-router';
7+
import * as React from 'react';
8+
9+
import { LandingHeader } from 'src/components/LandingHeader';
10+
11+
export const NetworkLoadBalancersListenerDetail = () => {
12+
const params = useParams({ strict: false });
13+
const { id: nlbId, listenerId } = params;
14+
15+
const {
16+
data: nlb,
17+
error,
18+
isLoading,
19+
} = useNetworkLoadBalancerQuery(Number(nlbId) || -1, true);
20+
21+
const {
22+
data: listener,
23+
error: listenerError,
24+
isLoading: listenerLoading,
25+
} = useNetworkLoadBalancerListenerQuery(
26+
Number(nlbId) || -1,
27+
Number(listenerId) || -1,
28+
true
29+
);
30+
31+
if (isLoading || listenerLoading) {
32+
return <CircleProgress />;
33+
}
34+
35+
if (!nlb || error || !listener || listenerError) {
36+
return (
37+
<ErrorState errorText="There was a problem retrieving your NLB. Please try again." />
38+
);
39+
}
40+
41+
return (
42+
<>
43+
<LandingHeader
44+
breadcrumbProps={{
45+
labelOptions: { noCap: true },
46+
crumbOverrides: [
47+
{
48+
label: 'Network Load Balancer',
49+
position: 1,
50+
},
51+
{
52+
label: nlb.label,
53+
position: 2,
54+
linkTo: `/netloadbalancers/$id/listeners`,
55+
noCap: true,
56+
},
57+
],
58+
pathname: `/netloadbalancers/${nlbId}/${listenerId}`,
59+
}}
60+
docsLabel="Docs"
61+
docsLink={
62+
'https://techdocs.akamai.com/linode-api/changelog/network-load-balancers'
63+
}
64+
title={listener.label}
65+
/>
66+
<Notice variant="info">Listener Detail is coming soon...</Notice>
67+
</>
68+
);
69+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { createLazyRoute } from '@tanstack/react-router';
2+
3+
import { NetworkLoadBalancersListenerDetail } from './NetworkLoadBalancersListenerDetail';
4+
5+
export const NetworkLoadBalancersListenerDetailLazyRoute = createLazyRoute(
6+
'/netloadbalancers/$id/listeners/$listenerId'
7+
)({
8+
component: NetworkLoadBalancersListenerDetail,
9+
});
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { useNetworkLoadBalancerListenersQuery } from '@linode/queries';
2+
import { CircleProgress, Stack, Typography, useTheme } from '@linode/ui';
3+
import * as React from 'react';
4+
5+
import { Link } from 'src/components/Link';
6+
import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter';
7+
import { Table } from 'src/components/Table';
8+
import { TableBody } from 'src/components/TableBody';
9+
import { TableCell } from 'src/components/TableCell';
10+
import { TableHead } from 'src/components/TableHead';
11+
import { TableRow } from 'src/components/TableRow';
12+
import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty';
13+
import { TableRowError } from 'src/components/TableRowError/TableRowError';
14+
import { usePaginationV2 } from 'src/hooks/usePaginationV2';
15+
16+
interface ListenersTableProps {
17+
nlbId: number;
18+
}
19+
20+
export const NetworkLoadBalancersListenerTable = (
21+
props: ListenersTableProps
22+
) => {
23+
const { nlbId } = props;
24+
const theme = useTheme();
25+
26+
const pagination = usePaginationV2({
27+
currentRoute: '/netloadbalancers/$id/listeners',
28+
preferenceKey: 'netloadbalancers-listeners',
29+
});
30+
31+
const {
32+
data: nlbListeners,
33+
error,
34+
isLoading,
35+
} = useNetworkLoadBalancerListenersQuery(nlbId, {
36+
page: pagination.page,
37+
page_size: pagination.pageSize,
38+
});
39+
40+
if (isLoading) {
41+
return <CircleProgress />;
42+
}
43+
44+
return (
45+
<Stack>
46+
<Stack
47+
direction="row"
48+
gap={theme.spacingFunction(20)}
49+
paddingBottom={theme.spacingFunction(20)}
50+
paddingTop={theme.spacingFunction(40)}
51+
sx={(theme) => ({
52+
[theme.breakpoints.up('lg')]: {
53+
paddingLeft: 0,
54+
},
55+
})}
56+
>
57+
<Typography
58+
sx={{
59+
alignItems: 'center',
60+
display: 'inline-flex',
61+
}}
62+
variant="h3"
63+
>
64+
Listeners ({nlbListeners?.results ?? 0})
65+
</Typography>
66+
</Stack>
67+
<Table data-testid="nlb-listeners-table">
68+
<TableHead>
69+
<TableRow>
70+
<TableCell>Listener Label</TableCell>
71+
<TableCell>Port</TableCell>
72+
<TableCell>Protocol</TableCell>
73+
<TableCell>ID</TableCell>
74+
</TableRow>
75+
</TableHead>
76+
<TableBody>
77+
{error && <TableRowError colSpan={4} message={error[0].reason} />}
78+
{nlbListeners?.data.length === 0 && (
79+
<TableRowEmpty
80+
colSpan={4}
81+
message="No Listeners are defined for this Network Load Balancer"
82+
/>
83+
)}
84+
{nlbListeners?.data.map(({ id, label, port, protocol }) => (
85+
<TableRow data-testid={`nlb-listener-row-${id}`} key={id}>
86+
<TableCell>
87+
<Link
88+
accessibleAriaLabel={label}
89+
to={`/netloadbalancers/${nlbId}/listeners/${id}`}
90+
>
91+
{label}
92+
</Link>
93+
</TableCell>
94+
<TableCell>{port}</TableCell>
95+
<TableCell>{protocol.toUpperCase()}</TableCell>
96+
<TableCell>{id}</TableCell>
97+
</TableRow>
98+
))}
99+
</TableBody>
100+
</Table>
101+
<PaginationFooter
102+
count={nlbListeners?.results || 0}
103+
handlePageChange={pagination.handlePageChange}
104+
handleSizeChange={pagination.handlePageSizeChange}
105+
page={pagination.page}
106+
pageSize={pagination.pageSize}
107+
/>
108+
</Stack>
109+
);
110+
};

0 commit comments

Comments
 (0)