Skip to content

Commit 0f461e9

Browse files
committed
Pull request #101: upcoming: [UIE-10692, UIE-10693, UIE-10694, UIE-10695] – Add new Summary and Metrics sections in the NodeBalancer Details
Merge in FEE/cloud-manager from feature/UIE-10692-add-lke-ip-sections-to-nb-detail-page to develop Squashed commit of the following: commit 3cf8034f325e2cba0679901087ff8fc0a27fd48b Author: hrao <hrao@akamai.com> Date: Wed Apr 22 14:49:05 2026 +0530 fix LKEClusterInfo failing test commit d3611d97142894d1372d289f4adb801c52fc376d Author: hrao <hrao@akamai.com> Date: Wed Apr 22 14:43:37 2026 +0530 remove ipv4 optional chaining commit fd2dae93794d206e2840291b291d38ebdf486d45 Author: hrao <hrao@akamai.com> Date: Wed Apr 22 14:40:19 2026 +0530 PR Feedback commit 3d54b4a2aa2819da3ce3975a9472fbc02d3a7e29 Author: hrao <hrao@akamai.com> Date: Wed Apr 22 11:29:05 2026 +0530 fix failing test commit f40f3c7244b6d8edbfabde12ea569a4259388000 Author: hrao <hrao@akamai.com> Date: Wed Apr 22 10:39:04 2026 +0530 Add changeset commit 8b51a9ee3c307c3fbcb84c68ad7ebef28328ce53 Merge: 3ac6869499 89611ae Author: hrao <hrao@akamai.com> Date: Wed Apr 22 10:33:55 2026 +0530 Merge branch 'develop' into feature/UIE-10692-add-lke-ip-sections-to-nb-detail-page commit 3ac6869499823c2974ddd85ca44c2cf4f0cf2c7e Author: hrao <hrao@akamai.com> Date: Tue Apr 21 18:54:18 2026 +0530 fix failing tests commit 5c82ec50261214a4f486eb8f3ece514979c7d1dc Merge: 653b5495c7 46b907f Author: hrao <hrao@akamai.com> Date: Tue Apr 21 16:25:55 2026 +0530 Merge branch 'develop' into feature/UIE-10692-add-lke-ip-sections-to-nb-detail-page commit 653b5495c7da79457e567cc1ad835ac54fa61741 Author: hrao <hrao@akamai.com> Date: Tue Apr 21 16:24:36 2026 +0530 add support for cloudpulse metrics commit abf372a3c4427e382b58948019fef1baee516439 Author: hrao <hrao@akamai.com> Date: Mon Apr 20 13:45:10 2026 +0530 updated designs commit 84bdabc8a971de1919fa7b5f5776bda1bcc7e322 Author: hrao <hrao@akamai.com> Date: Fri Apr 17 13:28:51 2026 +0530 upcoming: [UIE-10692, UIE-10693, UIE-10694] – Add LKE Cluster Info, Frotend and Backend Configuration sections in the NodeBalancer Details' summary tab commit 0bf8fcb1249f6ab3f6179e90616a2dddcae5cf87 Author: hrao <hrao@akamai.com> Date: Fri Apr 17 12:31:16 2026 +0530 upcoming: [UIE-10690, UIE-10691] – Create new Summary section in the NodeBalancer Details tab
1 parent d890ad9 commit 0f461e9

10 files changed

Lines changed: 551 additions & 1 deletion

File tree

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+
Add new Summary and Metrics sections in the NodeBalancer Details page ([#101](https://git.source.akamai.com/projects/FEE/repos/cloud-manager/pull-requests/101))

packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { useTabs } from 'src/hooks/useTabs';
1919
import { getErrorMap } from 'src/utilities/errorUtils';
2020

2121
import { NodeBalancerConfigurationsWrapper } from './NodeBalancerConfigurationsWrapper';
22+
import { NodeBalancerMetrics } from './NodeBalancerMetrics';
2223
import { NodeBalancerSettings } from './NodeBalancerSettings';
2324
import { NodeBalancerSummary } from './NodeBalancerSummary/NodeBalancerSummary';
2425
import { NodeBalancerSummaryV2 } from './NodeBalancerSummaryV2/NodeBalancerSummaryV2';
@@ -126,7 +127,7 @@ export const NodeBalancerDetail = () => {
126127
</SafeTabPanel>
127128
{metricsTabIndex !== null && (
128129
<SafeTabPanel index={metricsTabIndex}>
129-
<Notice text="Metrics tab coming soon.." variant="info" />
130+
<NodeBalancerMetrics />
130131
</SafeTabPanel>
131132
)}
132133

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { useParams } from '@tanstack/react-router';
2+
import * as React from 'react';
3+
4+
import { CloudPulseDashboardWithFilters } from 'src/features/CloudPulse/Dashboard/CloudPulseDashboardWithFilters';
5+
6+
export const NodeBalancerMetrics = () => {
7+
const { id } = useParams({ from: '/nodebalancers/$id/metrics' });
8+
9+
const nodeBalancerDashboardId = 3;
10+
return (
11+
<CloudPulseDashboardWithFilters
12+
dashboardId={nodeBalancerDashboardId}
13+
resource={id}
14+
/>
15+
);
16+
};
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import React from 'react';
2+
3+
import { subnetFactory, vpcFactory } from 'src/factories';
4+
import { renderWithTheme } from 'src/utilities/testHelpers';
5+
6+
import { BackendConfigurationVPC } from './BackendConfigVPC';
7+
8+
const queryMocks = vi.hoisted(() => ({
9+
useNodeBalancerVPCConfigsBetaQuery: vi.fn().mockReturnValue({ data: null }),
10+
useParams: vi.fn().mockReturnValue({ id: 1 }),
11+
useVPCQuery: vi.fn().mockReturnValue({ data: null }),
12+
}));
13+
14+
vi.mock('@tanstack/react-router', async () => {
15+
const actual = await vi.importActual('@tanstack/react-router');
16+
return {
17+
...actual,
18+
useParams: queryMocks.useParams,
19+
};
20+
});
21+
22+
vi.mock('@linode/queries', async () => {
23+
const actual = await vi.importActual('@linode/queries');
24+
return {
25+
...actual,
26+
useNodeBalancerVPCConfigsBetaQuery:
27+
queryMocks.useNodeBalancerVPCConfigsBetaQuery,
28+
useVPCQuery: queryMocks.useVPCQuery,
29+
};
30+
});
31+
32+
describe('BackendConfigurationVPC', () => {
33+
beforeEach(() => {
34+
queryMocks.useParams.mockReturnValue({ id: 1 });
35+
queryMocks.useNodeBalancerVPCConfigsBetaQuery.mockReturnValue({
36+
data: {
37+
data: [],
38+
},
39+
});
40+
queryMocks.useVPCQuery.mockReturnValue({ data: null });
41+
});
42+
43+
afterEach(() => {
44+
vi.resetAllMocks();
45+
});
46+
47+
it('renders the backend VPC link, subnet label, and configured IP ranges', () => {
48+
const subnet = subnetFactory.build({
49+
id: 401,
50+
label: 'private-subnet',
51+
});
52+
53+
queryMocks.useNodeBalancerVPCConfigsBetaQuery.mockReturnValue({
54+
data: {
55+
data: [
56+
{
57+
id: 7,
58+
ipv4_range: '10.0.0.0/24',
59+
ipv6_range: '2600:3c11:e41c:1::/56',
60+
purpose: 'backend',
61+
subnet_id: subnet.id,
62+
vpc_id: 301,
63+
},
64+
],
65+
},
66+
});
67+
queryMocks.useVPCQuery.mockReturnValue({
68+
data: vpcFactory.build({
69+
id: 301,
70+
label: 'prod-vpc',
71+
subnets: [subnet],
72+
}),
73+
});
74+
75+
const { getByRole, getByText, getByTestId } = renderWithTheme(
76+
<BackendConfigurationVPC />
77+
);
78+
79+
expect(getByText('Backend Configuration - VPC')).toBeVisible();
80+
expect(getByRole('link', { name: 'VPC prod-vpc' })).toHaveAttribute(
81+
'href',
82+
'/vpcs/301'
83+
);
84+
expect(getByText('private-subnet')).toBeVisible();
85+
expect(getByTestId('vpc-ipv4-label')).toHaveTextContent('IPv4 Range');
86+
expect(getByTestId('vpc-ipv6-label')).toHaveTextContent('IPv6 Range');
87+
expect(getByText('10.0.0.0/24')).toBeVisible();
88+
expect(getByText('2600:3c11:e41c:1::/56')).toBeVisible();
89+
});
90+
91+
it('falls back to the subnet id when the subnet details are unavailable', () => {
92+
queryMocks.useNodeBalancerVPCConfigsBetaQuery.mockReturnValue({
93+
data: {
94+
data: [
95+
{
96+
id: 8,
97+
ipv4_range: '10.1.0.0/24',
98+
ipv6_range: null,
99+
purpose: 'backend',
100+
subnet_id: 999,
101+
vpc_id: 302,
102+
},
103+
],
104+
},
105+
});
106+
queryMocks.useVPCQuery.mockReturnValue({
107+
data: vpcFactory.build({
108+
id: 302,
109+
label: 'staging-vpc',
110+
subnets: [],
111+
}),
112+
});
113+
114+
const { getByText } = renderWithTheme(<BackendConfigurationVPC />);
115+
116+
expect(getByText('Subnet 999')).toBeVisible();
117+
expect(getByText('10.1.0.0/24')).toBeVisible();
118+
});
119+
});
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import {
2+
useNodeBalancerVPCConfigsBetaQuery,
3+
useVPCQuery,
4+
} from '@linode/queries';
5+
import { Box, Divider, Paper, Stack, Typography } from '@linode/ui';
6+
import { useTheme } from '@mui/material/styles';
7+
import { useParams } from '@tanstack/react-router';
8+
import * as React from 'react';
9+
10+
import { Link } from 'src/components/Link';
11+
import { IPAddress } from 'src/features/Linodes/LinodesLanding/IPAddress';
12+
13+
import { StyledIPBox } from './FrontendConfiguration';
14+
15+
export const BackendConfigurationVPC = () => {
16+
const theme = useTheme();
17+
18+
const { id } = useParams({
19+
from: '/nodebalancers/$id/summary',
20+
});
21+
const { data: vpcConfig } = useNodeBalancerVPCConfigsBetaQuery(
22+
Number(id),
23+
true
24+
);
25+
// "/nodebalancers/:id/vpcs" returns both frontend and backend VPC configs,
26+
// but we only want to display the backend configs and
27+
// a nodebalancer can have only one backend VPC.
28+
const nbBackendVpcConfig =
29+
vpcConfig?.data.find((v) => v.purpose === 'backend') ?? null;
30+
31+
const { data: vpcDetails } = useVPCQuery(
32+
Number(nbBackendVpcConfig?.vpc_id) || -1,
33+
Boolean(nbBackendVpcConfig?.vpc_id)
34+
);
35+
36+
if (!nbBackendVpcConfig) {
37+
return null;
38+
}
39+
40+
const subnets = vpcDetails?.subnets ?? [];
41+
42+
const subnet = subnets.find((s) => s.id === nbBackendVpcConfig?.subnet_id);
43+
44+
const subnetWithConfigData = {
45+
id: nbBackendVpcConfig?.subnet_id,
46+
label: subnet?.label ?? `Subnet ${nbBackendVpcConfig?.subnet_id}`,
47+
ipv4Range: nbBackendVpcConfig?.ipv4_range,
48+
ipv6Range: nbBackendVpcConfig?.ipv6_range,
49+
};
50+
51+
return (
52+
<Paper
53+
sx={(theme) => ({
54+
padding: `${theme.spacingFunction(24)}`,
55+
})}
56+
>
57+
<Stack spacing={2}>
58+
<Typography data-qa-title sx={{ mb: 2 }} variant="h2">
59+
Backend Configuration - VPC
60+
</Typography>
61+
<Stack
62+
direction="row"
63+
divider={<Divider flexItem orientation="vertical" />}
64+
spacing={1}
65+
>
66+
<Typography>
67+
<strong>VPC:</strong>{' '}
68+
<React.Fragment key={nbBackendVpcConfig.id}>
69+
<Link
70+
accessibleAriaLabel={`VPC ${vpcDetails?.label}`}
71+
className="secondaryLink"
72+
style={{ marginLeft: theme.spacingFunction(8) }}
73+
to={`/vpcs/${nbBackendVpcConfig.vpc_id}`}
74+
>
75+
{vpcDetails?.label}
76+
</Link>
77+
</React.Fragment>
78+
</Typography>
79+
<Typography>
80+
<strong>Subnet:</strong>{' '}
81+
<span
82+
style={{
83+
wordBreak: 'break-word',
84+
marginLeft: theme.spacingFunction(8),
85+
}}
86+
>
87+
{`${subnetWithConfigData.label}`}
88+
</span>
89+
</Typography>
90+
</Stack>
91+
{subnetWithConfigData.ipv4Range && (
92+
<StyledIPBox>
93+
<Typography component="span" data-testid="vpc-ipv4-label">
94+
<strong>IPv4 Range:</strong>
95+
</Typography>
96+
<Box>
97+
<IPAddress
98+
ips={[subnetWithConfigData?.ipv4Range]}
99+
isHovered={true}
100+
showMore
101+
/>
102+
</Box>
103+
</StyledIPBox>
104+
)}
105+
{subnetWithConfigData.ipv6Range && (
106+
<StyledIPBox>
107+
<Typography component="span" data-testid="vpc-ipv6-label">
108+
<strong>IPv6 Range:</strong>
109+
</Typography>
110+
<Box>
111+
<IPAddress
112+
ips={[subnetWithConfigData.ipv6Range]}
113+
isHovered={true}
114+
showMore
115+
/>
116+
</Box>
117+
</StyledIPBox>
118+
)}
119+
</Stack>
120+
</Paper>
121+
);
122+
};
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { nodeBalancerFactory } from '@linode/utilities';
2+
import React from 'react';
3+
4+
import { renderWithTheme } from 'src/utilities/testHelpers';
5+
6+
import { FrontendConfiguration } from './FrontendConfiguration';
7+
8+
describe('FrontendConfiguration', () => {
9+
it('renders the public frontend type and both frontend IP addresses', () => {
10+
const nodebalancer = nodeBalancerFactory.build({
11+
frontend_address_type: 'public',
12+
ipv4: '192.0.2.10',
13+
ipv6: '2001:db8::10',
14+
});
15+
16+
const { getByText, getByTestId } = renderWithTheme(
17+
<FrontendConfiguration nodebalancer={nodebalancer} />
18+
);
19+
20+
expect(getByText('Frontend Configuration')).toBeVisible();
21+
expect(getByText('Type:')).toBeVisible();
22+
expect(getByText('Public')).toBeVisible();
23+
expect(getByTestId('ipv4-label')).toHaveTextContent('IPv4 Address');
24+
expect(getByTestId('ipv6-label')).toHaveTextContent('IPv6 Address');
25+
expect(getByText('192.0.2.10')).toBeVisible();
26+
expect(getByText('2001:db8::10')).toBeVisible();
27+
});
28+
29+
it('renders the VPC frontend type and hides the IPv6 section when no IPv6 address exists', () => {
30+
const nodebalancer = nodeBalancerFactory.build({
31+
frontend_address_type: 'vpc',
32+
ipv4: '192.0.2.20',
33+
ipv6: null,
34+
});
35+
36+
const { getByText, queryByTestId } = renderWithTheme(
37+
<FrontendConfiguration nodebalancer={nodebalancer} />
38+
);
39+
40+
expect(getByText('VPC')).toBeVisible();
41+
expect(getByText('192.0.2.20')).toBeVisible();
42+
expect(queryByTestId('ipv6-label')).not.toBeInTheDocument();
43+
});
44+
});

0 commit comments

Comments
 (0)