Skip to content

Commit 01176b5

Browse files
committed
upcoming: [M3-9785, M3-9788] - Added NodeBalacer Table and replace Linodes with Resources
1 parent b23ebe6 commit 01176b5

File tree

7 files changed

+271
-40
lines changed

7 files changed

+271
-40
lines changed
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import {
2+
useAllNodeBalancerConfigsQuery,
3+
useNodeBalancerQuery,
4+
useNodeBalancersFirewallsQuery,
5+
} from '@linode/queries';
6+
import { Box, CircleProgress, Hidden } from '@linode/ui';
7+
import ErrorOutline from '@mui/icons-material/ErrorOutline';
8+
import { Typography } from '@mui/material';
9+
import * as React from 'react';
10+
11+
import { Link } from 'src/components/Link';
12+
import { StatusIcon } from 'src/components/StatusIcon/StatusIcon';
13+
import { TableCell } from 'src/components/TableCell';
14+
import { TableRow } from 'src/components/TableRow';
15+
16+
interface Props {
17+
hover?: boolean;
18+
ipv4: string;
19+
nodeBalancerId: number;
20+
}
21+
22+
export const SubnetNodeBalancerRow = ({
23+
nodeBalancerId,
24+
hover = false,
25+
ipv4,
26+
}: Props) => {
27+
const {
28+
data: nodebalancer,
29+
error: nodebalancerError,
30+
isLoading: nodebalancerLoading,
31+
} = useNodeBalancerQuery(nodeBalancerId);
32+
const { data: attachedFirewallData } = useNodeBalancersFirewallsQuery(
33+
Number(nodeBalancerId)
34+
);
35+
const { data: configs } = useAllNodeBalancerConfigsQuery(
36+
Number(nodeBalancerId)
37+
);
38+
39+
const firewallLabel = attachedFirewallData?.data[0]?.label;
40+
const firewallId = attachedFirewallData?.data[0]?.id;
41+
42+
const down = configs?.reduce((acc: number, config) => {
43+
return acc + config.nodes_status.down;
44+
}, 0); // add the downtime for each config together
45+
46+
const up = configs?.reduce((acc: number, config) => {
47+
return acc + config.nodes_status.up;
48+
}, 0); // add the uptime for each config together
49+
50+
if (nodebalancerLoading || !nodebalancer) {
51+
return (
52+
<TableRow hover={hover}>
53+
<TableCell colSpan={6}>
54+
<CircleProgress size="sm" />
55+
</TableCell>
56+
</TableRow>
57+
);
58+
}
59+
60+
if (nodebalancerError) {
61+
return (
62+
<TableRow data-testid="subnet-nodebalancer-row-error" hover={hover}>
63+
<TableCell colSpan={5} style={{ paddingLeft: 24 }}>
64+
<Box alignItems="center" display="flex">
65+
<ErrorOutline
66+
data-qa-error-icon
67+
sx={(theme) => ({ color: theme.color.red, marginRight: 1 })}
68+
/>
69+
<Typography>
70+
There was an error loading{' '}
71+
<Link to={`/nodebalancers/${nodebalancer?.id}/summary`}>
72+
Nodebalancer {nodeBalancerId}
73+
</Link>
74+
</Typography>
75+
</Box>
76+
</TableCell>
77+
</TableRow>
78+
);
79+
}
80+
81+
return (
82+
<TableRow key={nodeBalancerId}>
83+
<TableCell>
84+
<Link
85+
className="secondaryLink"
86+
to={`/nodebalancers/${nodebalancer?.id}/summary`}
87+
>
88+
{nodebalancer?.label}
89+
</Link>
90+
</TableCell>
91+
<TableCell statusCell>
92+
<StatusIcon
93+
aria-label={`Nodebalancer status active`}
94+
status={'active'}
95+
/>
96+
{`${up} up, ${down} down`}
97+
</TableCell>
98+
<TableCell>{ipv4}</TableCell>
99+
<TableCell>
100+
<Link
101+
accessibleAriaLabel={`Firewall ${firewallLabel}`}
102+
className="secondaryLink"
103+
to={`/firewalls/${firewallId}`}
104+
>
105+
{firewallLabel}
106+
</Link>
107+
</TableCell>
108+
</TableRow>
109+
);
110+
};
111+
112+
export const SubnetNodebalancerTableRowHead = (
113+
<TableRow>
114+
<TableCell>NodeBalancer</TableCell>
115+
<TableCell>Backend Status</TableCell>
116+
<Hidden smDown>
117+
<TableCell>VPC IPv4 Range</TableCell>
118+
</Hidden>
119+
<Hidden smDown>
120+
<TableCell>Firewalls</TableCell>
121+
</Hidden>
122+
<TableCell />
123+
</TableRow>
124+
);

packages/manager/src/features/VPCs/VPCDetail/VPCDetail.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ const VPCDetail = () => {
101101
value: vpc.subnets.length,
102102
},
103103
{
104-
label: 'Linodes',
104+
label: 'Resources',
105105
value: numLinodes,
106106
},
107107
],

packages/manager/src/features/VPCs/VPCDetail/VPCSubnetsTable.test.tsx

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ describe('VPC Subnets table', () => {
7878
getByText('Subnet IP Range');
7979
getByText(subnet.ipv4!);
8080

81-
getByText('Linodes');
82-
getByText(subnet.linodes.length);
81+
getByText('Resources');
82+
getByText(subnet.linodes.length + subnet.nodebalancers.length);
8383

8484
const actionMenuButton = getAllByRole('button')[4];
8585
await userEvent.click(actionMenuButton);
@@ -155,6 +155,71 @@ describe('VPC Subnets table', () => {
155155
getByText('Firewalls');
156156
});
157157

158+
it('should display no nodeBalancers text if there are no nodeBalancers associated with the subnet', async () => {
159+
const subnet = subnetFactory.build();
160+
161+
server.use(
162+
http.get('*/vpcs/:vpcId/subnets', () => {
163+
return HttpResponse.json(makeResourcePage([subnet]));
164+
}),
165+
http.get('*/networking/firewalls/settings', () => {
166+
return HttpResponse.json(firewallSettingsFactory.build());
167+
})
168+
);
169+
170+
const { getAllByRole, getByText, queryByTestId } =
171+
await renderWithThemeAndRouter(
172+
<VPCSubnetsTable
173+
isVPCLKEEnterpriseCluster={false}
174+
vpcId={2}
175+
vpcRegion=""
176+
/>,
177+
{ flags: { nodebalancerVpc: true } }
178+
);
179+
180+
const loadingState = queryByTestId(loadingTestId);
181+
if (loadingState) {
182+
await waitForElementToBeRemoved(loadingState);
183+
}
184+
185+
const expandTableButton = getAllByRole('button')[3];
186+
await userEvent.click(expandTableButton);
187+
getByText('No NodeBalancers');
188+
});
189+
190+
it('should show Nodebalancer table head data when table is expanded', async () => {
191+
const subnet = subnetFactory.build();
192+
server.use(
193+
http.get('*/vpcs/:vpcId/subnets', () => {
194+
return HttpResponse.json(makeResourcePage([subnet]));
195+
}),
196+
http.get('*/networking/firewalls/settings', () => {
197+
return HttpResponse.json(firewallSettingsFactory.build());
198+
})
199+
);
200+
const { getAllByRole, getByText, queryByTestId } =
201+
await renderWithThemeAndRouter(
202+
<VPCSubnetsTable
203+
isVPCLKEEnterpriseCluster={false}
204+
vpcId={3}
205+
vpcRegion=""
206+
/>,
207+
{ flags: { nodebalancerVpc: true } }
208+
);
209+
210+
const loadingState = queryByTestId(loadingTestId);
211+
if (loadingState) {
212+
await waitForElementToBeRemoved(loadingState);
213+
}
214+
215+
const expandTableButton = getAllByRole('button')[3];
216+
await userEvent.click(expandTableButton);
217+
218+
getByText('NodeBalancer');
219+
getByText('Backend Status');
220+
getByText('VPC IPv4 Range');
221+
});
222+
158223
it('should disable Create Subnet button if the VPC is associated with a LKE-E cluster', async () => {
159224
server.use(
160225
http.get('*/networking/firewalls/settings', () => {

packages/manager/src/features/VPCs/VPCDetail/VPCSubnetsTable.tsx

Lines changed: 66 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { TableRow } from 'src/components/TableRow';
2626
import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty';
2727
import { TableSortCell } from 'src/components/TableSortCell';
2828
import { PowerActionsDialog } from 'src/features/Linodes/PowerActionsDialogOrDrawer';
29+
import { useIsNodebalancerVPCEnabled } from 'src/features/NodeBalancers/utils';
2930
import { SubnetActionMenu } from 'src/features/VPCs/VPCDetail/SubnetActionMenu';
3031
import { useOrderV2 } from 'src/hooks/useOrderV2';
3132
import { usePaginationV2 } from 'src/hooks/usePaginationV2';
@@ -37,6 +38,10 @@ import { SubnetCreateDrawer } from './SubnetCreateDrawer';
3738
import { SubnetDeleteDialog } from './SubnetDeleteDialog';
3839
import { SubnetEditDrawer } from './SubnetEditDrawer';
3940
import { SubnetLinodeRow, SubnetLinodeTableRowHead } from './SubnetLinodeRow';
41+
import {
42+
SubnetNodeBalancerRow,
43+
SubnetNodebalancerTableRowHead,
44+
} from './SubnetNodebalancerRow';
4045
import { SubnetUnassignLinodesDrawer } from './SubnetUnassignLinodesDrawer';
4146

4247
import type { Linode } from '@linode/api-v4/lib/linodes/types';
@@ -81,6 +86,8 @@ export const VPCSubnetsTable = (props: Props) => {
8186
});
8287
const { query } = search;
8388

89+
const flags = useIsNodebalancerVPCEnabled();
90+
8491
const pagination = usePaginationV2({
8592
currentRoute: VPC_DETAILS_ROUTE,
8693
preferenceKey,
@@ -286,7 +293,7 @@ export const VPCSubnetsTable = (props: Props) => {
286293
</Hidden>
287294
<TableCell sx={{ width: '18%' }}>Subnet IP Range</TableCell>
288295
<Hidden smDown>
289-
<TableCell sx={{ width: '10%' }}>Linodes</TableCell>
296+
<TableCell sx={{ width: '10%' }}>Resources</TableCell>
290297
</Hidden>
291298
<TableCell />
292299
</TableRow>
@@ -301,7 +308,9 @@ export const VPCSubnetsTable = (props: Props) => {
301308
</Hidden>
302309
<TableCell>{subnet.ipv4}</TableCell>
303310
<Hidden smDown>
304-
<TableCell>{subnet.linodes.length}</TableCell>
311+
<TableCell>
312+
{subnet.linodes.length + subnet.nodebalancers.length}
313+
</TableCell>
305314
</Hidden>
306315
<TableCell actionCell>
307316
<SubnetActionMenu
@@ -319,34 +328,61 @@ export const VPCSubnetsTable = (props: Props) => {
319328
);
320329

321330
const InnerTable = (
322-
<Table aria-label="Linode" size="small" striped={false}>
323-
<TableHead
324-
style={{
325-
color: theme.tokens.color.Neutrals.White,
326-
fontSize: '.875rem',
327-
}}
328-
>
329-
{SubnetLinodeTableRowHead}
330-
</TableHead>
331-
<TableBody>
332-
{subnet.linodes.length > 0 ? (
333-
subnet.linodes.map((linodeInfo) => (
334-
<SubnetLinodeRow
335-
handlePowerActionsLinode={handlePowerActionsLinode}
336-
handleUnassignLinode={handleSubnetUnassignLinode}
337-
isVPCLKEEnterpriseCluster={isVPCLKEEnterpriseCluster}
338-
key={linodeInfo.id}
339-
linodeId={linodeInfo.id}
340-
subnet={subnet}
341-
subnetId={subnet.id}
342-
subnetInterfaces={linodeInfo.interfaces}
343-
/>
344-
))
345-
) : (
346-
<TableRowEmpty colSpan={6} message={'No Linodes'} />
347-
)}
348-
</TableBody>
349-
</Table>
331+
<>
332+
<Table aria-label="Linode" size="small" striped={false}>
333+
<TableHead
334+
style={{
335+
color: theme.tokens.color.Neutrals.White,
336+
fontSize: '.875rem',
337+
}}
338+
>
339+
{SubnetLinodeTableRowHead}
340+
</TableHead>
341+
<TableBody>
342+
{subnet.linodes.length > 0 ? (
343+
subnet.linodes.map((linodeInfo) => (
344+
<SubnetLinodeRow
345+
handlePowerActionsLinode={handlePowerActionsLinode}
346+
handleUnassignLinode={handleSubnetUnassignLinode}
347+
isVPCLKEEnterpriseCluster={isVPCLKEEnterpriseCluster}
348+
key={linodeInfo.id}
349+
linodeId={linodeInfo.id}
350+
subnet={subnet}
351+
subnetId={subnet.id}
352+
subnetInterfaces={linodeInfo.interfaces}
353+
/>
354+
))
355+
) : (
356+
<TableRowEmpty colSpan={6} message={'No Linodes'} />
357+
)}
358+
</TableBody>
359+
</Table>
360+
{flags.isNodebalancerVPCEnabled && (
361+
<Table aria-label="NodeBalancers" size="small" striped={false}>
362+
<TableHead
363+
style={{
364+
color: theme.tokens.color.Neutrals.White,
365+
fontSize: '.875rem',
366+
}}
367+
>
368+
{SubnetNodebalancerTableRowHead}
369+
</TableHead>
370+
<TableBody>
371+
{subnet.nodebalancers?.length > 0 ? (
372+
subnet.nodebalancers.map((nb) => (
373+
<SubnetNodeBalancerRow
374+
ipv4={nb.ipv4_range}
375+
key={nb.id}
376+
nodeBalancerId={nb.id}
377+
/>
378+
))
379+
) : (
380+
<TableRowEmpty colSpan={6} message="No NodeBalancers" />
381+
)}
382+
</TableBody>
383+
</Table>
384+
)}
385+
</>
350386
);
351387

352388
return {

packages/manager/src/features/VPCs/VPCLanding/VPCLanding.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ const VPCLanding = () => {
168168
</Hidden>
169169
<TableCell>Subnets</TableCell>
170170
<Hidden mdDown>
171-
<TableCell>Linodes</TableCell>
171+
<TableCell>Resources</TableCell>
172172
</Hidden>
173173
<TableCell />
174174
</TableRow>

packages/manager/src/features/VPCs/VPCLanding/VPCRow.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ import { getRestrictedResourceText } from 'src/features/Account/utils';
1010
import { LKE_ENTERPRISE_VPC_WARNING } from 'src/features/Kubernetes/constants';
1111
import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted';
1212

13-
import { getIsVPCLKEEnterpriseCluster } from '../utils';
13+
import {
14+
getIsVPCLKEEnterpriseCluster,
15+
getUniqueLinodesFromSubnets,
16+
} from '../utils';
1417

1518
import type { VPC } from '@linode/api-v4/lib/vpcs/types';
1619
import type { Action } from 'src/components/ActionMenu/ActionMenu';
@@ -26,10 +29,7 @@ export const VPCRow = ({ handleDeleteVPC, handleEditVPC, vpc }: Props) => {
2629
const { data: regions } = useRegionsQuery();
2730

2831
const regionLabel = regions?.find((r) => r.id === vpc.region)?.label ?? '';
29-
const numLinodes = subnets.reduce(
30-
(acc, subnet) => acc + subnet.linodes.length,
31-
0
32-
);
32+
const numLinodes = getUniqueLinodesFromSubnets(vpc.subnets);
3333

3434
const isVPCReadOnly = useIsResourceRestricted({
3535
grantLevel: 'read_only',

0 commit comments

Comments
 (0)