Skip to content

upcoming: [M3-9785, M3-9788, M3-10040] - Added NodeBalacer Table and replace Linodes with Resources and remove "Label" text from Subnet and Linode Column #12232

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4616c72
upcoming: [M3-9785, M3-9788] - Added NodeBalacer Table and replace Li…
hasyed-akamai May 16, 2025
4c0cdbb
fix unit test and e2e
hasyed-akamai May 16, 2025
ce0d7b2
Added changeset: Added NodeBalacer Table under Subnets Table and repl…
hasyed-akamai May 19, 2025
1ce71d6
added unit tests for the `SubnetNodebalancerRow` component
hasyed-akamai May 20, 2025
6d83546
Merge branch 'develop' into M3-9785-M3-9788-add-nodebalancer-table-an…
hasyed-akamai May 20, 2025
1922e5e
fix unit tests
hasyed-akamai May 20, 2025
6756f29
Merge branch 'develop' into M3-9785-M3-9788-add-nodebalancer-table-an…
hasyed-akamai May 20, 2025
24adfb3
added full width for the table rows
hasyed-akamai May 20, 2025
4322931
fix e2e tests
hasyed-akamai May 21, 2025
e3657fa
Merge branch 'develop' into M3-9785-M3-9788-add-nodebalancer-table-an…
hasyed-akamai May 21, 2025
b3d1a5f
Merge branch 'develop' into M3-9785-M3-9788-add-nodebalancer-table-an…
hasyed-akamai May 22, 2025
5a814be
implemented changes suggested by Dajahi
hasyed-akamai May 22, 2025
d95fa86
added a feature flag for the Linodes column and revert back the chang…
hasyed-akamai May 22, 2025
e4aa66a
change changeset description and center loading and error state for s…
hasyed-akamai May 23, 2025
d13e78f
added unit test for `getUniqueResourcesFromSubnets` utils
hasyed-akamai May 23, 2025
6cee89d
added unit test in for UI testing of Resources text
hasyed-akamai May 23, 2025
f7f6019
Merge branch 'develop' into M3-9785-M3-9788-add-nodebalancer-table-an…
hasyed-akamai May 23, 2025
4ebb83a
remove label text from subnet and linode column
hasyed-akamai May 23, 2025
d9062f2
fix unit test for `vpcsubnetstable`
hasyed-akamai May 26, 2025
4bcf611
Merge branch 'develop' into M3-9785-M3-9788-add-nodebalancer-table-an…
hasyed-akamai May 26, 2025
248984e
update error state
hasyed-akamai May 28, 2025
044c845
Merge branch 'develop' into M3-9785-M3-9788-add-nodebalancer-table-an…
hasyed-akamai May 28, 2025
2786be8
align error message
hasyed-akamai May 28, 2025
b6bb539
Merge branch 'develop' into M3-9785-M3-9788-add-nodebalancer-table-an…
hasyed-akamai May 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Add NodeBalancer Table under VPC Subnets Table and rename "Linodes" column to "Resources" ([#12232](https://github.com/linode/manager/pull/12232))
15 changes: 14 additions & 1 deletion packages/manager/src/factories/subnets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Factory } from '@linode/utilities';
import type {
Subnet,
SubnetAssignedLinodeData,
SubnetAssignedNodeBalancerData,
} from '@linode/api-v4/lib/vpcs/types';

// NOTE: Changing to fixed array length for the interfaces and linodes fields of the
Expand All @@ -20,6 +21,12 @@ export const subnetAssignedLinodeDataFactory =
),
});

export const subnetAssignedNodebalancerDataFactory =
Factory.Sync.makeFactory<SubnetAssignedNodeBalancerData>({
id: Factory.each((i) => i),
ipv4_range: Factory.each((i) => `192.168.${i}.0/30`),
});

export const subnetFactory = Factory.Sync.makeFactory<Subnet>({
created: '2023-07-12T16:08:53',
id: Factory.each((i) => i),
Expand All @@ -32,6 +39,12 @@ export const subnetFactory = Factory.Sync.makeFactory<Subnet>({
})
)
),
nodebalancers: [],
nodebalancers: Factory.each((i) =>
Array.from({ length: 3 }, (_, arrIdx) =>
subnetAssignedNodebalancerDataFactory.build({
id: i * 10 + arrIdx,
})
)
),
updated: '2023-07-12T16:08:53',
});
Original file line number Diff line number Diff line change
Expand Up @@ -111,20 +111,20 @@ export const SubnetLinodeRow = (props: Props) => {
)
: _hasUnrecommendedConfiguration(config, subnetId);

if (linodeLoading || !linode) {
if (linodeLoading) {
return (
<TableRow hover={hover}>
<TableCell colSpan={6}>
<TableCell colSpan={6} style={{ textAlign: 'center' }}>
<CircleProgress size="sm" />
</TableCell>
</TableRow>
);
}

if (linodeError) {
if (linodeError || !linode) {
return (
<TableRow data-testid="subnet-linode-row-error" hover={hover}>
<TableCell colSpan={5} style={{ paddingLeft: 24 }}>
<TableCell colSpan={6} style={{ justifyItems: 'center' }}>
<Box alignItems="center" display="flex">
<ErrorOutline
data-qa-error-icon
Expand Down Expand Up @@ -341,7 +341,7 @@ const getIPRangesCellContents = (

export const SubnetLinodeTableRowHead = (
<TableRow>
<TableCell>Linode Label</TableCell>
<TableCell>Linode</TableCell>
<TableCell sx={{ width: '14%' }}>Status</TableCell>
<Hidden smDown>
<TableCell>VPC IPv4</TableCell>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { waitFor, waitForElementToBeRemoved } from '@testing-library/react';
import * as React from 'react';
import { afterAll, afterEach, beforeAll, describe, it } from 'vitest';

import {
firewallFactory,
subnetAssignedNodebalancerDataFactory,
} from 'src/factories';
import { makeResourcePage } from 'src/mocks/serverHandlers';
import { http, HttpResponse, server } from 'src/mocks/testServer';
import {
mockMatchMedia,
renderWithTheme,
wrapWithTableBody,
} from 'src/utilities/testHelpers';

import { SubnetNodeBalancerRow } from './SubnetNodebalancerRow';

const LOADING_TEST_ID = 'circle-progress';

beforeAll(() => mockMatchMedia());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

describe('SubnetNodeBalancerRow', () => {
const nodebalancer = {
id: 123,
label: 'test-nodebalancer',
};

const configs = [
{ nodes_status: { up: 3, down: 1 } },
{ nodes_status: { up: 2, down: 2 } },
];

const firewalls = makeResourcePage(
firewallFactory.buildList(1, { label: 'mock-firewall' })
);

const subnetNodebalancer = subnetAssignedNodebalancerDataFactory.build({
id: nodebalancer.id,
ipv4_range: '192.168.99.0/30',
});

it('renders loading state', async () => {
const { getByTestId } = renderWithTheme(
wrapWithTableBody(
<SubnetNodeBalancerRow
ipv4={subnetNodebalancer.ipv4_range}
nodeBalancerId={subnetNodebalancer.id}
/>
)
);

expect(getByTestId(LOADING_TEST_ID)).toBeInTheDocument();

Check warning on line 55 in packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐶 Avoid destructuring queries from `render` result, use `screen.getByTestId` instead Raw Output: {"ruleId":"testing-library/prefer-screen-queries","severity":1,"message":"Avoid destructuring queries from `render` result, use `screen.getByTestId` instead","line":55,"column":12,"nodeType":"Identifier","messageId":"preferScreenQueries","endLine":55,"endColumn":23}

Check warning on line 55 in packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐶 Don't wrap `getBy*` query with `expect` & presence matchers like `toBeInTheDocument` or `not.toBeNull` as `getBy*` queries fail implicitly when element is not found Raw Output: {"ruleId":"testing-library/prefer-implicit-assert","severity":1,"message":"Don't wrap `getBy*` query with `expect` & presence matchers like `toBeInTheDocument` or `not.toBeNull` as `getBy*` queries fail implicitly when element is not found","line":55,"column":12,"nodeType":"Identifier","messageId":"preferImplicitAssert","endLine":55,"endColumn":23}
await waitForElementToBeRemoved(() => getByTestId(LOADING_TEST_ID));

Check warning on line 56 in packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐶 Prefer using queryBy* when waiting for disappearance Raw Output: {"ruleId":"testing-library/prefer-query-by-disappearance","severity":1,"message":"Prefer using queryBy* when waiting for disappearance","line":56,"column":43,"nodeType":"Identifier","messageId":"preferQueryByDisappearance","endLine":56,"endColumn":54}

Check warning on line 56 in packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐶 Avoid destructuring queries from `render` result, use `screen.getByTestId` instead Raw Output: {"ruleId":"testing-library/prefer-screen-queries","severity":1,"message":"Avoid destructuring queries from `render` result, use `screen.getByTestId` instead","line":56,"column":43,"nodeType":"Identifier","messageId":"preferScreenQueries","endLine":56,"endColumn":54}
});

it('renders nodebalancer row with data', async () => {
server.use(
http.get('*/nodebalancers/:id', () => {
return HttpResponse.json(nodebalancer);
}),
http.get('*/nodebalancers/:id/configs', () => {
return HttpResponse.json(configs);
}),
http.get('*/nodebalancers/:id/firewalls', () => {
return HttpResponse.json(firewalls);
})
);

const { getByText, getByRole } = renderWithTheme(
wrapWithTableBody(
<SubnetNodeBalancerRow
ipv4={subnetNodebalancer.ipv4_range}
nodeBalancerId={nodebalancer.id}
/>
)
);

await waitFor(() => {
expect(getByText(nodebalancer.label)).toBeInTheDocument();

Check warning on line 82 in packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐶 Avoid destructuring queries from `render` result, use `screen.getByText` instead Raw Output: {"ruleId":"testing-library/prefer-screen-queries","severity":1,"message":"Avoid destructuring queries from `render` result, use `screen.getByText` instead","line":82,"column":14,"nodeType":"Identifier","messageId":"preferScreenQueries","endLine":82,"endColumn":23}

Check warning on line 82 in packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐶 Don't wrap `getBy*` query with `expect` & presence matchers like `toBeInTheDocument` or `not.toBeNull` as `getBy*` queries fail implicitly when element is not found Raw Output: {"ruleId":"testing-library/prefer-implicit-assert","severity":1,"message":"Don't wrap `getBy*` query with `expect` & presence matchers like `toBeInTheDocument` or `not.toBeNull` as `getBy*` queries fail implicitly when element is not found","line":82,"column":14,"nodeType":"Identifier","messageId":"preferImplicitAssert","endLine":82,"endColumn":23}
});

expect(getByText(subnetNodebalancer.ipv4_range)).toBeInTheDocument();

Check warning on line 85 in packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐶 Avoid destructuring queries from `render` result, use `screen.getByText` instead Raw Output: {"ruleId":"testing-library/prefer-screen-queries","severity":1,"message":"Avoid destructuring queries from `render` result, use `screen.getByText` instead","line":85,"column":12,"nodeType":"Identifier","messageId":"preferScreenQueries","endLine":85,"endColumn":21}

Check warning on line 85 in packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐶 Don't wrap `getBy*` query with `expect` & presence matchers like `toBeInTheDocument` or `not.toBeNull` as `getBy*` queries fail implicitly when element is not found Raw Output: {"ruleId":"testing-library/prefer-implicit-assert","severity":1,"message":"Don't wrap `getBy*` query with `expect` & presence matchers like `toBeInTheDocument` or `not.toBeNull` as `getBy*` queries fail implicitly when element is not found","line":85,"column":12,"nodeType":"Identifier","messageId":"preferImplicitAssert","endLine":85,"endColumn":21}
expect(getByText('mock-firewall')).toBeInTheDocument();

Check warning on line 86 in packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐶 Avoid destructuring queries from `render` result, use `screen.getByText` instead Raw Output: {"ruleId":"testing-library/prefer-screen-queries","severity":1,"message":"Avoid destructuring queries from `render` result, use `screen.getByText` instead","line":86,"column":12,"nodeType":"Identifier","messageId":"preferScreenQueries","endLine":86,"endColumn":21}

Check warning on line 86 in packages/manager/src/features/VPCs/VPCDetail/SubnetNodebalancerRow.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐶 Don't wrap `getBy*` query with `expect` & presence matchers like `toBeInTheDocument` or `not.toBeNull` as `getBy*` queries fail implicitly when element is not found Raw Output: {"ruleId":"testing-library/prefer-implicit-assert","severity":1,"message":"Don't wrap `getBy*` query with `expect` & presence matchers like `toBeInTheDocument` or `not.toBeNull` as `getBy*` queries fail implicitly when element is not found","line":86,"column":12,"nodeType":"Identifier","messageId":"preferImplicitAssert","endLine":86,"endColumn":21}

const nodebalancerLink = getByRole('link', {
name: nodebalancer.label,
});

expect(nodebalancerLink).toHaveAttribute(
'href',
`/nodebalancers/${nodebalancer.id}/summary`
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import {
useAllNodeBalancerConfigsQuery,
useNodeBalancerQuery,
useNodeBalancersFirewallsQuery,
} from '@linode/queries';
import { Box, CircleProgress, Hidden } from '@linode/ui';
import ErrorOutline from '@mui/icons-material/ErrorOutline';
import { Typography } from '@mui/material';
import * as React from 'react';

import { Link } from 'src/components/Link';
import { StatusIcon } from 'src/components/StatusIcon/StatusIcon';
import { TableCell } from 'src/components/TableCell';
import { TableRow } from 'src/components/TableRow';

interface Props {
hover?: boolean;
ipv4: string;
nodeBalancerId: number;
}

export const SubnetNodeBalancerRow = ({
nodeBalancerId,
hover = false,
ipv4,
}: Props) => {
const {
data: nodebalancer,
error: nodebalancerError,
isLoading: nodebalancerLoading,
} = useNodeBalancerQuery(nodeBalancerId);
const { data: attachedFirewallData } = useNodeBalancersFirewallsQuery(
Number(nodeBalancerId)
);
const { data: configs } = useAllNodeBalancerConfigsQuery(
Number(nodeBalancerId)
);

const firewallLabel = attachedFirewallData?.data[0]?.label;
const firewallId = attachedFirewallData?.data[0]?.id;

const down = configs?.reduce((acc: number, config) => {
return acc + config.nodes_status.down;
}, 0); // add the downtime for each config together

const up = configs?.reduce((acc: number, config) => {
return acc + config.nodes_status.up;
}, 0); // add the uptime for each config together

if (nodebalancerLoading) {
return (
<TableRow hover={hover}>
<TableCell colSpan={6} style={{ textAlign: 'center' }}>
<CircleProgress size="sm" />
</TableCell>
</TableRow>
);
}

if (nodebalancerError || !nodebalancer) {
return (
<TableRow data-testid="subnet-nodebalancer-row-error" hover={hover}>
<TableCell colSpan={6} style={{ justifyItems: 'center' }}>
<Box alignItems="center" display="flex">
<ErrorOutline
data-qa-error-icon
sx={(theme) => ({ color: theme.color.red, marginRight: 1 })}
/>
<Typography>
There was an error loading{' '}
<Link to={`/nodebalancers/${nodeBalancerId}/summary`}>
Nodebalancer {nodeBalancerId}
</Link>
</Typography>
</Box>
</TableCell>
</TableRow>
);
}

return (
<TableRow>
<TableCell>
<Link
className="secondaryLink"
to={`/nodebalancers/${nodebalancer?.id}/summary`}
>
{nodebalancer?.label}
</Link>
</TableCell>
<TableCell statusCell>
<StatusIcon aria-label="Nodebalancer status active" status="active" />
{`${up} up, ${down} down`}
</TableCell>
<TableCell>{ipv4}</TableCell>
<TableCell colSpan={2}>
<Link
accessibleAriaLabel={`Firewall ${firewallLabel}`}
className="secondaryLink"
to={`/firewalls/${firewallId}`}
>
{firewallLabel}
</Link>
</TableCell>
</TableRow>
);
};

export const SubnetNodebalancerTableRowHead = (
<TableRow>
<TableCell>NodeBalancer</TableCell>
<TableCell>Backend Status</TableCell>
<Hidden smDown>
<TableCell>VPC IPv4 Range</TableCell>
</Hidden>
<Hidden smDown>
<TableCell>Firewalls</TableCell>
</Hidden>
<TableCell />
</TableRow>
);
37 changes: 37 additions & 0 deletions packages/manager/src/features/VPCs/VPCDetail/VPCDetail.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,43 @@ describe('VPC Detail Summary section', () => {
getAllByText(vpcFactory1.updated);
});

it('should display number of subnets and resources, region, id, creation and update dates', async () => {
const vpcFactory1 = vpcFactory.build({ id: 1, subnets: [] });
server.use(
http.get('*/vpcs/:vpcId', () => {
return HttpResponse.json(vpcFactory1);
})
);

const { getAllByText, queryByTestId } = await renderWithThemeAndRouter(
<VPCDetail />,
{
flags: { nodebalancerVpc: true },
}
);

const loadingState = queryByTestId(loadingTestId);
if (loadingState) {
await waitForElementToBeRemoved(loadingState);
}

getAllByText('Subnets');
getAllByText('Resources');
getAllByText('0');

getAllByText('Region');
getAllByText('US, Newark, NJ');

getAllByText('VPC ID');
getAllByText(vpcFactory1.id);

getAllByText('Created');
getAllByText(vpcFactory1.created);

getAllByText('Updated');
getAllByText(vpcFactory1.updated);
});

it('should display description if one is provided', async () => {
const vpcFactory1 = vpcFactory.build({
description: `VPC for webserver and database.`,
Expand Down
12 changes: 9 additions & 3 deletions packages/manager/src/features/VPCs/VPCDetail/VPCDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ import { DocumentTitleSegment } from 'src/components/DocumentTitle';
import { EntityHeader } from 'src/components/EntityHeader/EntityHeader';
import { LandingHeader } from 'src/components/LandingHeader';
import { LKE_ENTERPRISE_VPC_WARNING } from 'src/features/Kubernetes/constants';
import { useIsNodebalancerVPCEnabled } from 'src/features/NodeBalancers/utils';
import { VPC_DOCS_LINK, VPC_LABEL } from 'src/features/VPCs/constants';

import {
getIsVPCLKEEnterpriseCluster,
getUniqueLinodesFromSubnets,
getUniqueResourcesFromSubnets,
} from '../utils';
import { VPCDeleteDialog } from '../VPCLanding/VPCDeleteDialog';
import { VPCEditDrawer } from '../VPCLanding/VPCEditDrawer';
Expand Down Expand Up @@ -48,6 +50,8 @@ const VPCDetail = () => {
isLoading,
} = useVPCQuery(Number(vpcId) || -1, Boolean(vpcId));

const flags = useIsNodebalancerVPCEnabled();

const { data: regions } = useRegionsQuery();

const handleEditVPC = (vpc: VPC) => {
Expand Down Expand Up @@ -93,7 +97,9 @@ const VPCDetail = () => {
const regionLabel =
regions?.find((r) => r.id === vpc.region)?.label ?? vpc.region;

const numLinodes = getUniqueLinodesFromSubnets(vpc.subnets);
const numResources = flags.isNodebalancerVPCEnabled
? getUniqueResourcesFromSubnets(vpc.subnets)
: getUniqueLinodesFromSubnets(vpc.subnets);

const summaryData = [
[
Expand All @@ -102,8 +108,8 @@ const VPCDetail = () => {
value: vpc.subnets.length,
},
{
label: 'Linodes',
value: numLinodes,
label: flags.isNodebalancerVPCEnabled ? 'Resources' : 'Linodes',
value: numResources,
},
],
[
Expand Down
Loading
Loading