From c0a86e980bc165949215337eb6046b09ab8b770c Mon Sep 17 00:00:00 2001 From: Ganesh Revanakar Date: Thu, 12 Feb 2026 17:30:41 +0530 Subject: [PATCH 1/4] change: [UIE-9888] - Display front end IP and backend VPCs for nodebalancers --- packages/api-v4/src/nodebalancers/types.ts | 5 +- packages/manager/src/factories/vpcs.ts | 13 +- .../NodeBalancerSummary/SummaryPanel.test.tsx | 36 ++- .../NodeBalancerSummary/SummaryPanel.tsx | 229 +++++++++++------- .../NodeBalancersLanding/NodeBalancerVPC.tsx | 21 +- .../NodeBalancersLanding.test.tsx | 2 +- .../NodeBalancersLanding.tsx | 2 +- .../VPCDetail/SubnetNodebalancerRow.test.tsx | 54 +++-- .../VPCs/VPCDetail/SubnetNodebalancerRow.tsx | 98 +++++++- .../VPCs/VPCDetail/VPCSubnetsTable.test.tsx | 5 +- .../VPCs/VPCDetail/VPCSubnetsTable.tsx | 2 +- packages/manager/src/mocks/mockState.ts | 1 + .../presets/crud/handlers/nodebalancers.ts | 30 +++ .../mocks/presets/crud/seeds/nodebalancers.ts | 9 + packages/manager/src/mocks/types.ts | 2 + .../utilities/src/factories/nodebalancer.ts | 43 +++- 16 files changed, 410 insertions(+), 142 deletions(-) diff --git a/packages/api-v4/src/nodebalancers/types.ts b/packages/api-v4/src/nodebalancers/types.ts index 77d577d33c6..4ee00d3070c 100644 --- a/packages/api-v4/src/nodebalancers/types.ts +++ b/packages/api-v4/src/nodebalancers/types.ts @@ -10,7 +10,7 @@ type UDPStickiness = 'none' | 'session' | 'source_ip'; export type Stickiness = TCPStickiness | UDPStickiness; -type NodeBalancerType = 'common' | 'premium'; +type NodeBalancerType = 'common' | 'premium' | 'premium_40GB'; export interface LKEClusterInfo { id: number; @@ -33,6 +33,8 @@ export interface NodeBalancer { */ client_udp_sess_throttle?: number; created: string; + frontend_address_type: 'public' | 'vpc'; + frontend_vpc_subnet_id: null | number; hostname: string; id: number; ipv4: string; @@ -145,6 +147,7 @@ export interface NodeBalancerVpcConfig { ipv4_range: null | string; ipv6_range: null | string; nodebalancer_id: number; + purpose: 'backend' | 'frontend'; subnet_id: number; vpc_id: number; } diff --git a/packages/manager/src/factories/vpcs.ts b/packages/manager/src/factories/vpcs.ts index fa282366490..e8b4f5474dd 100644 --- a/packages/manager/src/factories/vpcs.ts +++ b/packages/manager/src/factories/vpcs.ts @@ -8,7 +8,18 @@ export const vpcFactory = Factory.Sync.makeFactory({ id: Factory.each((i) => i), label: Factory.each((i) => `vpc-${i}`), region: 'us-east', - subnets: [], + subnets: [ + { + id: 1, + label: 'subnet-1', + ipv4: '10.0.0.0/24', + ipv6: [], + linodes: [], + nodebalancers: [], + created: '2025-10-24T14:09:38', + updated: '2025-10-24T14:09:38', + }, + ], updated: '2023-07-12T16:08:53', }); diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.test.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.test.tsx index 5bf5b5559f5..e8318187c8b 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.test.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.test.tsx @@ -1,7 +1,7 @@ import { nodeBalancerConfigFactory, - nodeBalancerConfigVPCFactory, nodeBalancerFactory, + nodeBalancerVPCFactory, } from '@linode/utilities'; import { waitFor } from '@testing-library/react'; import * as React from 'react'; @@ -52,7 +52,7 @@ vi.mock('@linode/queries', async () => { }); const nodeBalancerDetails = 'NodeBalancer Details'; -const nbVpcConfig = nodeBalancerConfigVPCFactory.build(); +const nbVpcConfig = nodeBalancerVPCFactory.build(); describe('SummaryPanel', () => { beforeEach(() => { @@ -110,8 +110,8 @@ describe('SummaryPanel', () => { expect(getByText('Host Name:')).toBeVisible(); expect(getByText('example.com')).toBeVisible(); expect(getByText('Region:')).toBeVisible(); - // Type should not display for non-premium NBs - expect(queryByText('Type:')).not.toBeInTheDocument(); + // Type should be visible and default to Basic since the NB is not premium + expect(getByText('Basic')).toBeVisible(); // Cluster should not display for if the NB is not associated with LKE or LKE-E expect(queryByText('Cluster:')).not.toBeInTheDocument(); @@ -120,11 +120,11 @@ describe('SummaryPanel', () => { expect(getByText('mock-firewall-1')).toBeVisible(); // IP Address panel - expect(getByText('IP Addresses')).toBeVisible(); + expect(getByText('Frontend Configuration')).toBeVisible(); expect(getByText('0.0.0.0')).toBeVisible(); // VPC Details Panel - expect(getByText('VPC')).toBeVisible(); + expect(getByText('Backend Configuration - VPC')).toBeVisible(); expect(getByText('Subnets:')).toBeVisible(); expect(getByText(`${nbVpcConfig.ipv4_range}`)).toBeVisible(); @@ -133,6 +133,18 @@ describe('SummaryPanel', () => { expect(getByText('Add a tag')).toBeVisible(); }); + it('displays type: Basic if the nodebalancer is non premium', () => { + queryMocks.useNodeBalancerQuery.mockReturnValue({ + data: nodeBalancerFactory.build({ type: 'common' }), + }); + + const { container } = renderWithTheme(); + + expect(container.querySelector('[data-qa-type]')).toHaveTextContent( + 'Type: Basic' + ); + }); + it('displays type: premium if the nodebalancer is premium', () => { queryMocks.useNodeBalancerQuery.mockReturnValue({ data: nodeBalancerFactory.build({ type: 'premium' }), @@ -145,6 +157,18 @@ describe('SummaryPanel', () => { ); }); + it('displays type: Enterprise if the nodebalancer is premium_40GB', () => { + queryMocks.useNodeBalancerQuery.mockReturnValue({ + data: nodeBalancerFactory.build({ type: 'premium_40GB' }), + }); + + const { container } = renderWithTheme(); + + expect(container.querySelector('[data-qa-type]')).toHaveTextContent( + 'Type: Enterprise' + ); + }); + it('displays link to cluster if it exists', () => { queryMocks.useNodeBalancerQuery.mockReturnValue({ data: nodeBalancerFactory.build({ diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx index 8b46e8a6541..06de0d821e8 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx @@ -50,21 +50,26 @@ export const SummaryPanel = () => { flags.isNodebalancerVPCEnabled ); + // "/nodebalancers/:id/vpcs" returns both frontend and backend VPC configs, + // but we only want to display the backend configs. + const nbBackendVpcConfigs = + vpcConfig?.data.filter((v) => v.purpose === 'backend') ?? []; + const { data: vpcDetails } = useVPCQuery( - Number(vpcConfig?.data[0]?.vpc_id) || -1, - Boolean(vpcConfig?.data[0]?.vpc_id) + Number(nbBackendVpcConfigs[0]?.vpc_id) || -1, + Boolean(nbBackendVpcConfigs[0]?.vpc_id) ); - const nbVPCConfigs = vpcConfig?.data ?? []; const subnets = vpcDetails?.subnets ?? []; - const mergedSubnets = nbVPCConfigs.map((config) => { + const mergedSubnets = nbBackendVpcConfigs.map((config) => { const subnet = subnets.find((s) => s.id === config.subnet_id); return { id: config.subnet_id, label: subnet?.label ?? `Subnet ${config.subnet_id}`, ipv4Range: config.ipv4_range, + ipv6Range: config.ipv6_range, }; }); @@ -103,36 +108,14 @@ export const SummaryPanel = () => { NodeBalancer Details - {nodebalancer.type === 'premium' && ( - - - Type: - Premium - - - )} - {nodebalancer.lke_cluster && ( - - - Cluster: - {clusterStatus === 'error' ? ( - <> - - {nodebalancer.lke_cluster.label} - - (deleted) - - ) : ( - - {nodebalancer.lke_cluster.label} - - )} - - - )} + + + Type: + {nodebalancer.type === 'common' && 'Basic'} + {nodebalancer.type === 'premium' && 'Premium'} + {nodebalancer.type === 'premium_40GB' && 'Enterprise'} + + Ports: @@ -166,7 +149,7 @@ export const SummaryPanel = () => { Host Name: - {nodebalancer.hostname} + {nodebalancer.hostname || 'None'} @@ -176,82 +159,145 @@ export const SummaryPanel = () => { - {displayFirewallLink && ( + {nodebalancer.lke_cluster && ( - Firewall + LKE Cluster - - - {linkText} - - + + + Cluster: + {clusterStatus === 'error' ? ( + <> + + {nodebalancer.lke_cluster.label} + + (deleted) + + ) : ( + + {nodebalancer.lke_cluster.label} + + )} + + )} - IP Addresses + Frontend Configuration + + + Type: + {nodebalancer.frontend_address_type === 'public' + ? 'Public' + : nodebalancer.frontend_address_type === 'vpc' + ? 'VPC' + : ''} + + {nodebalancer?.ipv4 && ( - + + + )} {nodebalancer?.ipv6 && ( - + + + )} - {flags.isNodebalancerVPCEnabled && Boolean(vpcConfig?.data.length) && ( - - - VPC - - - - VPC:{' '} - {vpcConfig?.data.map((vpc, i) => ( - - + + Backend Configuration - VPC + + + + VPC:{' '} + {nbBackendVpcConfigs?.map((vpc, i) => ( + + + {vpcDetails?.label} + + {i < nbBackendVpcConfigs.length - 1 ? ', ' : ''} + + ))} + + + + + Subnets: + + + {mergedSubnets.map((subnet) => ( + + - {vpcDetails?.label} - - {i < vpcConfig.data.length - 1 ? ', ' : ''} + {`${subnet.label}`} + + + {subnet.ipv4Range && ( + + + + )} + {subnet.ipv6Range && ( + + + + )} + ))} - - - - - Subnets: - - - {mergedSubnets.map((subnet) => ( - - - {`${subnet.label}:`} - - - {subnet.ipv4Range} - - - ))} - + + + )} + {displayFirewallLink && ( + + + Firewall + + + + {linkText} + + )} @@ -289,7 +335,6 @@ const StyledSummarySection = styled(Paper, { })(({ theme }) => ({ height: '93%', marginBottom: theme.spacing(2), - minHeight: '160px', padding: theme.spacing(2.5), })); diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerVPC.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerVPC.tsx index c28242278cc..0edfb75d39c 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerVPC.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerVPC.tsx @@ -15,28 +15,33 @@ export const NodeBalancerVPC = ({ nodeBalancerId }: Props) => { const { data: vpcConfig, isLoading: isVPCConfigLoading } = useNodeBalancerVPCConfigsBetaQuery(nodeBalancerId, Boolean(nodeBalancerId)); + // NodeBalancerVPCConfigsBetaQuery returns both frontend and backend VPC configs, + // but we only want to display the backend configs. + const nbBackendVpcConfigs = + vpcConfig?.data.filter((v) => v.purpose === 'backend') ?? []; + const { data: vpcDetails, isLoading: isVPCDetailsLoading } = useVPCQuery( - Number(vpcConfig?.data[0]?.vpc_id), - Boolean(vpcConfig?.data[0]?.vpc_id) + Number(nbBackendVpcConfigs[0]?.vpc_id), + Boolean(nbBackendVpcConfigs[0]?.vpc_id) ); if (isVPCConfigLoading || isVPCDetailsLoading) { return ; } - if (vpcConfig?.data?.length === 0) { + if (nbBackendVpcConfigs.length === 0) { return 'None'; } - return vpcConfig?.data.map(({ vpc_id: vpcId }, i) => ( - + return nbBackendVpcConfigs.map((vpc, i) => ( + {vpcDetails?.label} - {i < vpcConfig.data.length - 1 ? ', ' : ''} + {i < nbBackendVpcConfigs.length - 1 ? ', ' : ''} )); }; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.test.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.test.tsx index 72934f64d7a..70049b834a2 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.test.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.test.tsx @@ -89,7 +89,7 @@ describe('NodeBalancersLanding', () => { expect(getByText('Backend Status')).toBeVisible(); expect(getByText('Transferred')).toBeVisible(); expect(getByText('Ports')).toBeVisible(); - expect(getByText('IP Address')).toBeVisible(); + expect(getByText('Frontend IP')).toBeVisible(); expect(getByText('Region')).toBeVisible(); }); diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx index 5b88ac4b80c..bd0d96fd660 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx @@ -126,7 +126,7 @@ export const NodeBalancersLanding = () => { Transferred Ports - IP Address + Frontend IP ({ useAllNodeBalancerConfigsQuery: vi.fn().mockReturnValue({}), useNodeBalancerQuery: vi.fn().mockReturnValue({}), + useNodeBalancerVPCConfigsBetaQuery: vi.fn().mockReturnValue({}), useNodeBalancersFirewallsQuery: vi.fn().mockReturnValue({}), })); @@ -29,6 +28,8 @@ vi.mock('@linode/queries', async () => { ...actual, useAllNodeBalancerConfigsQuery: queryMocks.useAllNodeBalancerConfigsQuery, useNodeBalancerQuery: queryMocks.useNodeBalancerQuery, + useNodeBalancerVPCConfigsBetaQuery: + queryMocks.useNodeBalancerVPCConfigsBetaQuery, useNodeBalancersFirewallsQuery: queryMocks.useNodeBalancersFirewallsQuery, }; }); @@ -41,6 +42,8 @@ describe('SubnetNodeBalancerRow', () => { label: 'test-nodebalancer', }; + const subnetId = 456; + const configs = [ { nodes_status: { up: 3, down: 1 } }, { nodes_status: { up: 2, down: 2 } }, @@ -50,10 +53,22 @@ describe('SubnetNodeBalancerRow', () => { firewallFactory.buildList(1, { label: 'mock-firewall' }) ); - const subnetNodebalancer = subnetAssignedNodebalancerDataFactory.build({ - id: nodebalancer.id, - ipv4_range: '192.168.99.0/30', - }); + const vpcConfigs = makeResourcePage([ + nodeBalancerVPCFactory.build({ + ipv4_range: '192.168.1.0/30', + ipv6_range: '2001:db8::1/64', + nodebalancer_id: nodebalancer.id, + purpose: 'frontend', + subnet_id: subnetId, + }), + nodeBalancerVPCFactory.build({ + ipv4_range: '192.168.2.0/30', + ipv6_range: '2001:db8::2/64', + nodebalancer_id: nodebalancer.id, + purpose: 'backend', + subnet_id: subnetId, + }), + ]); it('renders loading state', async () => { queryMocks.useNodeBalancerQuery.mockReturnValue({ @@ -62,8 +77,8 @@ describe('SubnetNodeBalancerRow', () => { const { getByTestId } = renderWithTheme( wrapWithTableBody( ) ); @@ -83,22 +98,33 @@ describe('SubnetNodeBalancerRow', () => { queryMocks.useNodeBalancersFirewallsQuery.mockReturnValue({ data: firewalls, }); + queryMocks.useNodeBalancerVPCConfigsBetaQuery.mockReturnValue({ + data: vpcConfigs, + }); const { getByText, getByRole } = renderWithTheme( wrapWithTableBody( ) ); await waitFor(() => { - expect(getByText(nodebalancer.label)).toBeInTheDocument(); + getByText(nodebalancer.label); }); - expect(getByText(subnetNodebalancer.ipv4_range)).toBeInTheDocument(); - expect(getByText('mock-firewall')).toBeInTheDocument(); + // Frontend IPv4 range + getByText('192.168.1.0/30'); + // Frontend IPv6 range + getByText('2001:db8::1/64'); + // Backend IPv4 range + getByText('192.168.2.0/30'); + // Backend IPv6 range + getByText('2001:db8::2/64'); + // Firewall + getByText('mock-firewall'); const nodebalancerLink = getByRole('link', { name: nodebalancer.label, diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.tsx index 5246002a51e..ddb09a9f28d 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.tsx @@ -2,6 +2,7 @@ import { useAllNodeBalancerConfigsQuery, useNodeBalancerQuery, useNodeBalancersFirewallsQuery, + useNodeBalancerVPCConfigsBetaQuery, } from '@linode/queries'; import { Box, CircleProgress, Typography } from '@linode/ui'; import ErrorOutline from '@mui/icons-material/ErrorOutline'; @@ -11,17 +12,24 @@ import { Link } from 'src/components/Link'; import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; -import type { APIError, Firewall, NodeBalancerConfig } from '@linode/api-v4'; +const LOADING_TEXT = 'Loading...'; + +import type { + APIError, + Firewall, + NodeBalancerConfig, + NodeBalancerVpcConfig, +} from '@linode/api-v4'; interface Props { hover?: boolean; - ipv4: string; nodeBalancerId: number; + subnetId: number; } export const SubnetNodeBalancerRow = ({ nodeBalancerId, hover = false, - ipv4, + subnetId, }: Props) => { const { data: nodebalancer, @@ -39,13 +47,30 @@ export const SubnetNodeBalancerRow = ({ error, } = useNodeBalancersFirewallsQuery(Number(nodeBalancerId)); + const { + data: vpcConfigs, + error: vpcConfigsError, + isLoading: isVpcConfigsLoading, + } = useNodeBalancerVPCConfigsBetaQuery( + Number(nodeBalancerId), + Boolean(nodeBalancerId) + ); + + const frontendVpcConfig = vpcConfigs?.data.find( + (config) => config.purpose === 'frontend' && config.subnet_id === subnetId + ); + + const backendVpcConfig = vpcConfigs?.data.find( + (config) => config.purpose === 'backend' && config.subnet_id === subnetId + ); + const getNodebalancerStatus = ( data: NodeBalancerConfig[], loading: boolean, error?: APIError[] ): React.JSX.Element | string => { if (loading) { - return 'Loading...'; + return LOADING_TEXT; } if (error) { @@ -67,13 +92,34 @@ export const SubnetNodeBalancerRow = ({ ); }; + const getVpcIpCellString = ( + data: NodeBalancerVpcConfig | undefined, + ipProperty: 'ipv4_range' | 'ipv6_range', + loading: boolean, + error?: APIError[] + ): React.JSX.Element | string => { + if (loading) { + return LOADING_TEXT; + } + + if (error) { + return 'Error retrieving IP'; + } + + if (!data || data.subnet_id !== subnetId) { + return '-'; + } + + return data[ipProperty] ?? '-'; + }; + const getFirewallsCellString = ( data: Firewall[], loading: boolean, error?: APIError[] ): React.JSX.Element | string => { if (loading) { - return 'Loading...'; + return LOADING_TEXT; } if (error) { @@ -149,7 +195,38 @@ export const SubnetNodeBalancerRow = ({ configsError ?? undefined )} - {ipv4} + + {getVpcIpCellString( + frontendVpcConfig, + 'ipv4_range', + isVpcConfigsLoading, + vpcConfigsError ?? undefined + )} + + + {getVpcIpCellString( + frontendVpcConfig, + 'ipv6_range', + isVpcConfigsLoading, + vpcConfigsError ?? undefined + )} + + + {getVpcIpCellString( + backendVpcConfig, + 'ipv4_range', + isVpcConfigsLoading, + vpcConfigsError ?? undefined + )} + + + {getVpcIpCellString( + backendVpcConfig, + 'ipv6_range', + isVpcConfigsLoading, + vpcConfigsError ?? undefined + )} + {getFirewallsCellString( attachedFirewallData?.data ?? [], @@ -163,9 +240,12 @@ export const SubnetNodeBalancerRow = ({ export const SubnetNodebalancerTableRowHead = ( - NodeBalancer - Backend Status - VPC IPv4 Range + NodeBalancer + Backend Status + Frontend IPv4 Ranges + Frontend IPv6 Ranges + Backend IPv4 Ranges + Backend IPv6 Ranges Firewalls ); diff --git a/packages/manager/src/features/VPCs/VPCDetail/VPCSubnetsTable.test.tsx b/packages/manager/src/features/VPCs/VPCDetail/VPCSubnetsTable.test.tsx index 5959af08e95..7b09fdb2c20 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/VPCSubnetsTable.test.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/VPCSubnetsTable.test.tsx @@ -288,7 +288,10 @@ describe('VPC Subnets table', () => { await findByText('NodeBalancer'); await findByText('Backend Status'); - await findByText('VPC IPv4 Range'); + await findByText('Frontend IPv4 Ranges'); + await findByText('Frontend IPv6 Ranges'); + await findByText('Backend IPv4 Ranges'); + await findByText('Backend IPv6 Ranges'); } ); diff --git a/packages/manager/src/features/VPCs/VPCDetail/VPCSubnetsTable.tsx b/packages/manager/src/features/VPCs/VPCDetail/VPCSubnetsTable.tsx index ed27397248d..fe49e01abee 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/VPCSubnetsTable.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/VPCSubnetsTable.tsx @@ -388,9 +388,9 @@ export const VPCSubnetsTable = (props: Props) => { {subnet.nodebalancers.map((nb) => ( ))} diff --git a/packages/manager/src/mocks/mockState.ts b/packages/manager/src/mocks/mockState.ts index c3acbcf1c4f..66be91b6f58 100644 --- a/packages/manager/src/mocks/mockState.ts +++ b/packages/manager/src/mocks/mockState.ts @@ -43,6 +43,7 @@ export const emptyStore: MockState = { linodeIps: [], linodes: [], locks: [], + nodeBalancerVPCs: [], nodeBalancerConfigNodes: [], nodeBalancerConfigs: [], nodeBalancers: [], diff --git a/packages/manager/src/mocks/presets/crud/handlers/nodebalancers.ts b/packages/manager/src/mocks/presets/crud/handlers/nodebalancers.ts index f9ecf041997..810300363b7 100644 --- a/packages/manager/src/mocks/presets/crud/handlers/nodebalancers.ts +++ b/packages/manager/src/mocks/presets/crud/handlers/nodebalancers.ts @@ -24,6 +24,7 @@ import type { NodeBalancerConfig, NodeBalancerConfigNode, NodeBalancerStats, + NodeBalancerVpcConfig, PriceType, } from '@linode/api-v4'; import type { StrictResponse } from 'msw'; @@ -102,6 +103,35 @@ export const getNodeBalancers = (mockState: MockState) => [ } ), + http.get( + '*/v4beta/nodebalancers/:id/vpcs', + async ({ + params, + request, + }): Promise< + StrictResponse< + APIErrorResponse | APIPaginatedResponse + > + > => { + const nodeBalancerId = Number(params.id); + const nodeBalancer = await mswDB.get('nodeBalancers', nodeBalancerId); + const nodeBalancerVPCs = await mswDB.getAll('nodeBalancerVPCs'); + + if (!nodeBalancer || !nodeBalancerVPCs) { + return makeNotFoundResponse(); + } + + const configs = nodeBalancerVPCs.filter( + (config) => config.nodebalancer_id === nodeBalancerId + ); + + return makePaginatedResponse({ + data: configs, + request, + }); + } + ), + http.get( '*/v4/nodebalancers/:id/configs/:configId', async ({ diff --git a/packages/manager/src/mocks/presets/crud/seeds/nodebalancers.ts b/packages/manager/src/mocks/presets/crud/seeds/nodebalancers.ts index 9756ba4edfe..0d79e941f30 100644 --- a/packages/manager/src/mocks/presets/crud/seeds/nodebalancers.ts +++ b/packages/manager/src/mocks/presets/crud/seeds/nodebalancers.ts @@ -2,6 +2,7 @@ import { nodeBalancerConfigFactory, nodeBalancerConfigNodeFactory, nodeBalancerFactory, + nodeBalancerVPCFactory, } from '@linode/utilities'; import { getSeedsCountMap } from 'src/dev-tools/utils'; @@ -32,6 +33,13 @@ export const nodeBalancerSeeder: MockSeeder = { ), }); + const nodeBalancerVPCSeeds = seedWithUniqueIds<'nodeBalancerVPCs'>({ + dbEntities: await mswDB.getAll('nodeBalancerVPCs'), + seedEntities: nodeBalancerSeeds.map((nb) => + nodeBalancerVPCFactory.build({ nodebalancer_id: nb.id }) + ), + }); + const nodeBalancerConfigNodeSeeds = seedWithUniqueIds<'nodeBalancerConfigNodes'>({ dbEntities: await mswDB.getAll('nodeBalancerConfigNodes'), @@ -54,6 +62,7 @@ export const nodeBalancerSeeder: MockSeeder = { nodeBalancerConfigs: mockState.nodeBalancerConfigs.concat( nodeBalancerConfigSeeds ), + nodeBalancerVPCs: mockState.nodeBalancerVPCs.concat(nodeBalancerVPCSeeds), nodeBalancers: mockState.nodeBalancers.concat(nodeBalancerSeeds), }; diff --git a/packages/manager/src/mocks/types.ts b/packages/manager/src/mocks/types.ts index 67f3fd53ff6..14b8887ec69 100644 --- a/packages/manager/src/mocks/types.ts +++ b/packages/manager/src/mocks/types.ts @@ -26,6 +26,7 @@ import type { NodeBalancer, NodeBalancerConfig, NodeBalancerConfigNode, + NodeBalancerVpcConfig, Notification, PlacementGroup, Region, @@ -232,6 +233,7 @@ export interface MockState { locks: ResourceLock[]; nodeBalancerConfigNodes: NodeBalancerConfigNode[]; nodeBalancerConfigs: NodeBalancerConfig[]; + nodeBalancerVPCs: NodeBalancerVpcConfig[]; nodeBalancers: NodeBalancer[]; notificationQueue: Notification[]; placementGroups: PlacementGroup[]; diff --git a/packages/utilities/src/factories/nodebalancer.ts b/packages/utilities/src/factories/nodebalancer.ts index 77c2fa122ae..8dcc6a16488 100644 --- a/packages/utilities/src/factories/nodebalancer.ts +++ b/packages/utilities/src/factories/nodebalancer.ts @@ -12,10 +12,18 @@ import type { export const nodeBalancerFactory = Factory.Sync.makeFactory({ client_conn_throttle: 0, created: '2019-12-12T00:00:00', + frontend_address_type: Factory.each((i) => { + if (i % 2 === 0) { + return 'vpc'; + } else { + return 'public'; + } + }), + frontend_vpc_subnet_id: null, hostname: 'example.com', id: Factory.each((id) => id), ipv4: '0.0.0.0', - ipv6: null, + ipv6: '2600:3c11:e954:1::1', label: Factory.each((i) => `nodebalancer-id-${i}`), region: 'us-east', tags: [], @@ -25,8 +33,28 @@ export const nodeBalancerFactory = Factory.Sync.makeFactory({ total: 0, }, updated: '2019-12-13T00:00:00', - lke_cluster: null, - type: 'common', + lke_cluster: Factory.each((i) => { + if (i % 2 === 0) { + return { + id: 1, + type: 'lkecluster', + label: 'cluster-1', + url: '/v4/lke/clusters/1', + }; + } else { + return null; + } + }), + type: Factory.each((i) => { + if (i === 1) { + return 'premium_40GB'; + } + if (i % 2 === 0) { + return 'premium'; + } else { + return 'common'; + } + }), }); export const nodeBalancerConfigFactory = @@ -67,14 +95,15 @@ export const nodeBalancerConfigNodeFactory = vpc_config_id: null, }); -export const nodeBalancerConfigVPCFactory = +export const nodeBalancerVPCFactory = Factory.Sync.makeFactory({ id: Factory.each((i) => i), ipv4_range: Factory.each((i) => `192.168.${i}.0/30`), ipv6_range: null, - nodebalancer_id: Factory.each((i) => 1000 + i), - subnet_id: Factory.each((i) => 2000 + i), - vpc_id: Factory.each((i) => 3000 + i), + nodebalancer_id: Factory.each((i) => i), + subnet_id: Factory.each((i) => i), + vpc_id: Factory.each((i) => i), + purpose: 'backend', }); export const nodeBalancerStatsFactory = From 91b3343f8644dc7d71d0df91b0cf31340b72c6c2 Mon Sep 17 00:00:00 2001 From: Ganesh Revanakar Date: Thu, 12 Feb 2026 21:45:18 +0530 Subject: [PATCH 2/4] Added changeset: UIE-9888: Display front end IP and backend VPCs for Nodebalancer --- .../pr-13394-changed-1770903849543.md | 5 ++ .../pr-13394-changed-1770903888397.md | 5 ++ packages/manager/src/factories/vpcs.ts | 13 +--- .../NodeBalancerSummary/SummaryPanel.test.tsx | 2 +- .../NodeBalancerSummary/SummaryPanel.tsx | 2 +- .../VPCs/VPCDetail/SubnetNodebalancerRow.tsx | 63 +++---------------- 6 files changed, 21 insertions(+), 69 deletions(-) create mode 100644 packages/api-v4/.changeset/pr-13394-changed-1770903849543.md create mode 100644 packages/manager/.changeset/pr-13394-changed-1770903888397.md diff --git a/packages/api-v4/.changeset/pr-13394-changed-1770903849543.md b/packages/api-v4/.changeset/pr-13394-changed-1770903849543.md new file mode 100644 index 00000000000..b5ebcae1586 --- /dev/null +++ b/packages/api-v4/.changeset/pr-13394-changed-1770903849543.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Changed +--- + +UIE-9888: Added new fields in NodeBalancer details object and NodeBalancerVPC object as per API changes ([#13394](https://github.com/linode/manager/pull/13394)) diff --git a/packages/manager/.changeset/pr-13394-changed-1770903888397.md b/packages/manager/.changeset/pr-13394-changed-1770903888397.md new file mode 100644 index 00000000000..d3223a0ac18 --- /dev/null +++ b/packages/manager/.changeset/pr-13394-changed-1770903888397.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +UIE-9888: Display front end IP and backend VPCs for Nodebalancer ([#13394](https://github.com/linode/manager/pull/13394)) diff --git a/packages/manager/src/factories/vpcs.ts b/packages/manager/src/factories/vpcs.ts index e8b4f5474dd..fa282366490 100644 --- a/packages/manager/src/factories/vpcs.ts +++ b/packages/manager/src/factories/vpcs.ts @@ -8,18 +8,7 @@ export const vpcFactory = Factory.Sync.makeFactory({ id: Factory.each((i) => i), label: Factory.each((i) => `vpc-${i}`), region: 'us-east', - subnets: [ - { - id: 1, - label: 'subnet-1', - ipv4: '10.0.0.0/24', - ipv6: [], - linodes: [], - nodebalancers: [], - created: '2025-10-24T14:09:38', - updated: '2025-10-24T14:09:38', - }, - ], + subnets: [], updated: '2023-07-12T16:08:53', }); diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.test.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.test.tsx index e8318187c8b..5d524fb8437 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.test.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.test.tsx @@ -51,7 +51,7 @@ vi.mock('@linode/queries', async () => { }; }); -const nodeBalancerDetails = 'NodeBalancer Details'; +const nodeBalancerDetails = 'Details'; const nbVpcConfig = nodeBalancerVPCFactory.build(); describe('SummaryPanel', () => { diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx index 06de0d821e8..396b76e6cc4 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx @@ -106,7 +106,7 @@ export const SummaryPanel = () => { - NodeBalancer Details + Details diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.tsx index ddb09a9f28d..91671f72cb0 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.tsx @@ -1,5 +1,4 @@ import { - useAllNodeBalancerConfigsQuery, useNodeBalancerQuery, useNodeBalancersFirewallsQuery, useNodeBalancerVPCConfigsBetaQuery, @@ -14,12 +13,7 @@ import { TableRow } from 'src/components/TableRow'; const LOADING_TEXT = 'Loading...'; -import type { - APIError, - Firewall, - NodeBalancerConfig, - NodeBalancerVpcConfig, -} from '@linode/api-v4'; +import type { APIError, Firewall, NodeBalancerVpcConfig } from '@linode/api-v4'; interface Props { hover?: boolean; nodeBalancerId: number; @@ -36,11 +30,6 @@ export const SubnetNodeBalancerRow = ({ error: nodebalancerError, isLoading: nodebalancerLoading, } = useNodeBalancerQuery(nodeBalancerId); - const { - data: configs, - isLoading: isConfigsLoading, - error: configsError, - } = useAllNodeBalancerConfigsQuery(Number(nodeBalancerId)); const { data: attachedFirewallData, isLoading, @@ -64,34 +53,6 @@ export const SubnetNodeBalancerRow = ({ (config) => config.purpose === 'backend' && config.subnet_id === subnetId ); - const getNodebalancerStatus = ( - data: NodeBalancerConfig[], - loading: boolean, - error?: APIError[] - ): React.JSX.Element | string => { - if (loading) { - return LOADING_TEXT; - } - - if (error) { - return 'Error retrieving Status'; - } - - const down = data?.reduce((acc: number, config) => { - return acc + config.nodes_status.down; - }, 0); - - const up = data?.reduce((acc: number, config) => { - return acc + config.nodes_status.up; - }, 0); - - return ( - <> - {up} up - {down} down - - ); - }; - const getVpcIpCellString = ( data: NodeBalancerVpcConfig | undefined, ipProperty: 'ipv4_range' | 'ipv6_range', @@ -107,10 +68,10 @@ export const SubnetNodeBalancerRow = ({ } if (!data || data.subnet_id !== subnetId) { - return '-'; + return '—'; } - return data[ipProperty] ?? '-'; + return data[ipProperty] ?? '—'; }; const getFirewallsCellString = ( @@ -188,13 +149,6 @@ export const SubnetNodeBalancerRow = ({ {nodebalancer?.label} - - {getNodebalancerStatus( - configs ?? [], - isConfigsLoading, - configsError ?? undefined - )} - {getVpcIpCellString( frontendVpcConfig, @@ -240,12 +194,11 @@ export const SubnetNodeBalancerRow = ({ export const SubnetNodebalancerTableRowHead = ( - NodeBalancer - Backend Status - Frontend IPv4 Ranges - Frontend IPv6 Ranges - Backend IPv4 Ranges + NodeBalancer + Frontend IPv4 + Frontend IPv6 + Backend IPv4 Ranges Backend IPv6 Ranges - Firewalls + Firewall ); From 88ca56f5ee3771666416955dcef6056ee94a0dc9 Mon Sep 17 00:00:00 2001 From: Ganesh Revanakar Date: Mon, 16 Feb 2026 16:50:01 +0530 Subject: [PATCH 3/4] fix unit test failure --- .../NodeBalancerSummary/SummaryPanel.tsx | 196 +++++++++--------- .../NodeBalancersLanding/NodeBalancerVPC.tsx | 24 +-- .../VPCDetail/SubnetNodebalancerRow.test.tsx | 9 - .../VPCs/VPCDetail/VPCSubnetsTable.test.tsx | 5 +- 4 files changed, 109 insertions(+), 125 deletions(-) diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx index 396b76e6cc4..b2a0d16552f 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx @@ -51,27 +51,26 @@ export const SummaryPanel = () => { ); // "/nodebalancers/:id/vpcs" returns both frontend and backend VPC configs, - // but we only want to display the backend configs. - const nbBackendVpcConfigs = - vpcConfig?.data.filter((v) => v.purpose === 'backend') ?? []; + // but we only want to display the backend configs and + // a nodebalancer can have only one backend VPC. + const nbBackendVpcConfig = + vpcConfig?.data.filter((v) => v.purpose === 'backend')[0] ?? null; const { data: vpcDetails } = useVPCQuery( - Number(nbBackendVpcConfigs[0]?.vpc_id) || -1, - Boolean(nbBackendVpcConfigs[0]?.vpc_id) + Number(nbBackendVpcConfig?.vpc_id) || -1, + Boolean(nbBackendVpcConfig?.vpc_id) ); const subnets = vpcDetails?.subnets ?? []; - const mergedSubnets = nbBackendVpcConfigs.map((config) => { - const subnet = subnets.find((s) => s.id === config.subnet_id); + const subnet = subnets.find((s) => s.id === nbBackendVpcConfig?.subnet_id); - return { - id: config.subnet_id, - label: subnet?.label ?? `Subnet ${config.subnet_id}`, - ipv4Range: config.ipv4_range, - ipv6Range: config.ipv6_range, - }; - }); + const subnetWithConfigData = { + id: nbBackendVpcConfig?.subnet_id, + label: subnet?.label ?? `Subnet ${nbBackendVpcConfig?.subnet_id}`, + ipv4Range: nbBackendVpcConfig?.ipv4_range, + ipv6Range: nbBackendVpcConfig?.ipv6_range, + }; // If we can't get the cluster (status === 'error'), we can assume it's been deleted const { status: clusterStatus } = useKubernetesClusterQuery({ @@ -191,99 +190,94 @@ export const SummaryPanel = () => { Frontend Configuration - - - Type: - {nodebalancer.frontend_address_type === 'public' - ? 'Public' - : nodebalancer.frontend_address_type === 'vpc' - ? 'VPC' - : ''} - - - - {nodebalancer?.ipv4 && ( - - - - )} - {nodebalancer?.ipv6 && ( - - - - )} - + + Type: + {nodebalancer.frontend_address_type === 'public' + ? 'Public' + : nodebalancer.frontend_address_type === 'vpc' + ? 'VPC' + : ''} + - - {flags.isNodebalancerVPCEnabled && - Boolean(nbBackendVpcConfigs?.length) && ( - - - Backend Configuration - VPC - + + {nodebalancer?.ipv4 && ( - - VPC:{' '} - {nbBackendVpcConfigs?.map((vpc, i) => ( - - - {vpcDetails?.label} - - {i < nbBackendVpcConfigs.length - 1 ? ', ' : ''} - - ))} - + + )} + {nodebalancer?.ipv6 && ( - - Subnets: - - - {mergedSubnets.map((subnet) => ( - - + + )} + + + {flags.isNodebalancerVPCEnabled && nbBackendVpcConfig && ( + + + Backend Configuration - VPC + + + + VPC:{' '} + {nbBackendVpcConfig && ( + + - {`${subnet.label}`} - - - {subnet.ipv4Range && ( - - - - )} - {subnet.ipv6Range && ( - - - - )} - + {vpcDetails?.label} + - ))} - - - )} + )} + + + + + Subnets: + + + {subnetWithConfigData && ( + + + {`${subnetWithConfigData.label}`} + + + {subnetWithConfigData.ipv4Range && ( + + + + )} + {subnetWithConfigData.ipv6Range && ( + + + + )} + + + )} + + + )} {displayFirewallLink && ( diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerVPC.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerVPC.tsx index 0edfb75d39c..b54badd44ff 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerVPC.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerVPC.tsx @@ -16,32 +16,32 @@ export const NodeBalancerVPC = ({ nodeBalancerId }: Props) => { useNodeBalancerVPCConfigsBetaQuery(nodeBalancerId, Boolean(nodeBalancerId)); // NodeBalancerVPCConfigsBetaQuery returns both frontend and backend VPC configs, - // but we only want to display the backend configs. - const nbBackendVpcConfigs = - vpcConfig?.data.filter((v) => v.purpose === 'backend') ?? []; + // but we only want to display the backend configs and + // a nodebalancer can have only one backend VPC. + const nbBackendVpcConfig = + vpcConfig?.data.filter((v) => v.purpose === 'backend')[0] ?? null; const { data: vpcDetails, isLoading: isVPCDetailsLoading } = useVPCQuery( - Number(nbBackendVpcConfigs[0]?.vpc_id), - Boolean(nbBackendVpcConfigs[0]?.vpc_id) + Number(nbBackendVpcConfig?.vpc_id), + Boolean(nbBackendVpcConfig?.vpc_id) ); if (isVPCConfigLoading || isVPCDetailsLoading) { return ; } - if (nbBackendVpcConfigs.length === 0) { + if (!nbBackendVpcConfig) { return 'None'; } - return nbBackendVpcConfigs.map((vpc, i) => ( - + return ( + {vpcDetails?.label} - {i < nbBackendVpcConfigs.length - 1 ? ', ' : ''} - )); + ); }; diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.test.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.test.tsx index c3a08368d1a..bf1ad23f8b9 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.test.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.test.tsx @@ -26,7 +26,6 @@ vi.mock('@linode/queries', async () => { const actual = await vi.importActual('@linode/queries'); return { ...actual, - useAllNodeBalancerConfigsQuery: queryMocks.useAllNodeBalancerConfigsQuery, useNodeBalancerQuery: queryMocks.useNodeBalancerQuery, useNodeBalancerVPCConfigsBetaQuery: queryMocks.useNodeBalancerVPCConfigsBetaQuery, @@ -44,11 +43,6 @@ describe('SubnetNodeBalancerRow', () => { const subnetId = 456; - const configs = [ - { nodes_status: { up: 3, down: 1 } }, - { nodes_status: { up: 2, down: 2 } }, - ]; - const firewalls = makeResourcePage( firewallFactory.buildList(1, { label: 'mock-firewall' }) ); @@ -92,9 +86,6 @@ describe('SubnetNodeBalancerRow', () => { queryMocks.useNodeBalancerQuery.mockReturnValue({ data: nodebalancer, }); - queryMocks.useAllNodeBalancerConfigsQuery.mockReturnValue({ - data: configs, - }); queryMocks.useNodeBalancersFirewallsQuery.mockReturnValue({ data: firewalls, }); diff --git a/packages/manager/src/features/VPCs/VPCDetail/VPCSubnetsTable.test.tsx b/packages/manager/src/features/VPCs/VPCDetail/VPCSubnetsTable.test.tsx index 7b09fdb2c20..d885bc3bb61 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/VPCSubnetsTable.test.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/VPCSubnetsTable.test.tsx @@ -287,9 +287,8 @@ describe('VPC Subnets table', () => { await userEvent.click(expandTableButton); await findByText('NodeBalancer'); - await findByText('Backend Status'); - await findByText('Frontend IPv4 Ranges'); - await findByText('Frontend IPv6 Ranges'); + await findByText('Frontend IPv4'); + await findByText('Frontend IPv6'); await findByText('Backend IPv4 Ranges'); await findByText('Backend IPv6 Ranges'); } From 4b162b1693f8481def93ae635e0c3a55e218f330 Mon Sep 17 00:00:00 2001 From: Ganesh Revanakar Date: Wed, 18 Feb 2026 00:53:41 +0530 Subject: [PATCH 4/4] Addressed review comments --- .../pr-13394-changed-1770903849543.md | 2 +- .../pr-13394-changed-1770903888397.md | 2 +- .../NodeBalancerSummary/SummaryPanel.test.tsx | 87 +++++++++++-------- .../NodeBalancerSummary/SummaryPanel.tsx | 24 +++-- .../VPCs/VPCDetail/SubnetNodebalancerRow.tsx | 4 +- 5 files changed, 68 insertions(+), 51 deletions(-) diff --git a/packages/api-v4/.changeset/pr-13394-changed-1770903849543.md b/packages/api-v4/.changeset/pr-13394-changed-1770903849543.md index b5ebcae1586..41fcf552f53 100644 --- a/packages/api-v4/.changeset/pr-13394-changed-1770903849543.md +++ b/packages/api-v4/.changeset/pr-13394-changed-1770903849543.md @@ -2,4 +2,4 @@ "@linode/api-v4": Changed --- -UIE-9888: Added new fields in NodeBalancer details object and NodeBalancerVPC object as per API changes ([#13394](https://github.com/linode/manager/pull/13394)) +New fields in the NodeBalancer details object and NodeBalancerVPC object to align with recent API updates ([#13394](https://github.com/linode/manager/pull/13394)) diff --git a/packages/manager/.changeset/pr-13394-changed-1770903888397.md b/packages/manager/.changeset/pr-13394-changed-1770903888397.md index d3223a0ac18..8e3f95c2a77 100644 --- a/packages/manager/.changeset/pr-13394-changed-1770903888397.md +++ b/packages/manager/.changeset/pr-13394-changed-1770903888397.md @@ -2,4 +2,4 @@ "@linode/manager": Changed --- -UIE-9888: Display front end IP and backend VPCs for Nodebalancer ([#13394](https://github.com/linode/manager/pull/13394)) +Display front end IP and backend VPCs for Nodebalancer ([#13394](https://github.com/linode/manager/pull/13394)) diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.test.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.test.tsx index 5d524fb8437..6ea7fc427be 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.test.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.test.tsx @@ -13,12 +13,10 @@ import { renderWithTheme } from 'src/utilities/testHelpers'; import { SummaryPanel } from './SummaryPanel'; const queryMocks = vi.hoisted(() => ({ - useAllNodeBalancerConfigsQuery: vi.fn().mockReturnValue({ data: undefined }), - useNodeBalancerQuery: vi.fn().mockReturnValue({ data: undefined }), - useNodeBalancersFirewallsQuery: vi.fn().mockReturnValue({ data: undefined }), - useNodeBalancerVPCConfigsBetaQuery: vi - .fn() - .mockReturnValue({ data: undefined }), + useAllNodeBalancerConfigsQuery: vi.fn().mockReturnValue({ data: null }), + useNodeBalancerQuery: vi.fn().mockReturnValue({ data: null }), + useNodeBalancersFirewallsQuery: vi.fn().mockReturnValue({ data: null }), + useNodeBalancerVPCConfigsBetaQuery: vi.fn().mockReturnValue({ data: null }), useParams: vi.fn().mockReturnValue({}), userPermissions: vi.fn(() => ({ data: { @@ -79,7 +77,7 @@ describe('SummaryPanel', () => { it('does not render anything if there is no nodebalancer', () => { queryMocks.useAllNodeBalancerConfigsQuery.mockReturnValue({ - data: undefined, + data: null, }); const { queryByText } = renderWithTheme(); @@ -88,7 +86,7 @@ describe('SummaryPanel', () => { it('does not render anything if there are no configs', () => { queryMocks.useNodeBalancerQuery.mockReturnValue({ - data: undefined, + data: null, }); const { queryByText } = renderWithTheme(); @@ -104,7 +102,7 @@ describe('SummaryPanel', () => { expect(getByText(nodeBalancerDetails)).toBeVisible(); expect(getByText('Ports:')).toBeVisible(); expect(getByText('Backend Status:')).toBeVisible(); - expect(getByText('0 up, 2 down')); + expect(getByText('0 up, 2 down')).toBeVisible(); expect(getByText('Transferred:')).toBeVisible(); expect(getByText('0 bytes')).toBeVisible(); expect(getByText('Host Name:')).toBeVisible(); @@ -138,11 +136,14 @@ describe('SummaryPanel', () => { data: nodeBalancerFactory.build({ type: 'common' }), }); - const { container } = renderWithTheme(); - - expect(container.querySelector('[data-qa-type]')).toHaveTextContent( - 'Type: Basic' - ); + const { getByText } = renderWithTheme(); + const typeElement = getByText((_, element) => { + return ( + !!element?.hasAttribute('data-qa-type') && + element?.textContent === 'Type: Basic' + ); + }); + expect(typeElement).toBeVisible(); }); it('displays type: premium if the nodebalancer is premium', () => { @@ -150,11 +151,15 @@ describe('SummaryPanel', () => { data: nodeBalancerFactory.build({ type: 'premium' }), }); - const { container } = renderWithTheme(); + const { getByText } = renderWithTheme(); - expect(container.querySelector('[data-qa-type]')).toHaveTextContent( - 'Type: Premium' - ); + const typeElement = getByText((_, element) => { + return ( + !!element?.hasAttribute('data-qa-type') && + element?.textContent === 'Type: Premium' + ); + }); + expect(typeElement).toBeVisible(); }); it('displays type: Enterprise if the nodebalancer is premium_40GB', () => { @@ -162,14 +167,18 @@ describe('SummaryPanel', () => { data: nodeBalancerFactory.build({ type: 'premium_40GB' }), }); - const { container } = renderWithTheme(); + const { getByText } = renderWithTheme(); - expect(container.querySelector('[data-qa-type]')).toHaveTextContent( - 'Type: Enterprise' - ); + const typeElement = getByText((_, element) => { + return ( + !!element?.hasAttribute('data-qa-type') && + element?.textContent === 'Type: Enterprise' + ); + }); + expect(typeElement).toBeVisible(); }); - it('displays link to cluster if it exists', () => { + it('displays link to cluster if it exists', async () => { queryMocks.useNodeBalancerQuery.mockReturnValue({ data: nodeBalancerFactory.build({ lke_cluster: { @@ -181,11 +190,19 @@ describe('SummaryPanel', () => { }), }); - const { container, getByText } = renderWithTheme(); + server.use( + http.get('*/lke/clusters/:clusterId', () => { + return HttpResponse.json({ id: 1, label: 'lke-123' }); + }) + ); + + const { getByText } = renderWithTheme(); expect(getByText('Cluster:')).toBeVisible(); - const clusterLink = container.querySelector('[data-qa-cluster] a'); - expect(clusterLink).toHaveTextContent('lke-123'); + const clusterLink = await waitFor(() => { + return getByText('lke-123'); + }); + expect(clusterLink).toBeVisible(); expect(clusterLink).toHaveAttribute( 'href', '/kubernetes/clusters/1/summary' @@ -210,16 +227,18 @@ describe('SummaryPanel', () => { }) ); - const { container } = renderWithTheme(); + const { getByText } = renderWithTheme(); - await waitFor(() => { - const clusterLink = container.querySelector('[data-qa-cluster]'); - expect(clusterLink).toHaveTextContent('Cluster: lke-123 (deleted)'); - expect(clusterLink).not.toHaveAttribute( - 'href', - '/kubernetes/clusters/1/summary' - ); + const clusterElement = await waitFor(() => { + return getByText((_, element) => { + return ( + !!element?.hasAttribute('data-qa-cluster') && + element?.textContent === 'Cluster: lke-123 (deleted)' + ); + }); }); + expect(clusterElement).toBeVisible(); + expect(clusterElement).not.toHaveAttribute('href'); }); it('should disable "Add a tag" if user does not have permission', () => { diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx index b2a0d16552f..2ef256ac4c2 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx @@ -54,7 +54,7 @@ export const SummaryPanel = () => { // but we only want to display the backend configs and // a nodebalancer can have only one backend VPC. const nbBackendVpcConfig = - vpcConfig?.data.filter((v) => v.purpose === 'backend')[0] ?? null; + vpcConfig?.data.find((v) => v.purpose === 'backend') ?? null; const { data: vpcDetails } = useVPCQuery( Number(nbBackendVpcConfig?.vpc_id) || -1, @@ -190,7 +190,7 @@ export const SummaryPanel = () => { Frontend Configuration - + Type: {nodebalancer.frontend_address_type === 'public' ? 'Public' @@ -224,17 +224,15 @@ export const SummaryPanel = () => { VPC:{' '} - {nbBackendVpcConfig && ( - - - {vpcDetails?.label} - - - )} + + + {vpcDetails?.label} + + diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.tsx index 91671f72cb0..85153ff9821 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.tsx @@ -11,9 +11,9 @@ import { Link } from 'src/components/Link'; import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; -const LOADING_TEXT = 'Loading...'; - import type { APIError, Firewall, NodeBalancerVpcConfig } from '@linode/api-v4'; + +const LOADING_TEXT = 'Loading...'; interface Props { hover?: boolean; nodeBalancerId: number;