Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions packages/manager/src/dev-tools/FeatureFlagTool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ const options: { flag: keyof Flags; label: string }[] = [
{ flag: 'apl', label: 'Akamai App Platform' },
{ flag: 'aplGeneralAvailability', label: 'Akamai App Platform GA' },
{ flag: 'aplLkeE', label: 'Akamai App Platform LKE-E' },
{
flag: 'aclpNbMetricsIntegration',
label: 'ACLP NodeBalancer Metrics Integration',
},
{ flag: 'blockStorageEncryption', label: 'Block Storage Encryption (BSE)' },
{ flag: 'blockStorageVolumeLimit', label: 'Block Storage Volume Limit' },
{ flag: 'cloudNat', label: 'Cloud NAT' },
Expand Down
1 change: 1 addition & 0 deletions packages/manager/src/featureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ export interface Flags {
aclpAlerting: AclpAlerting;
aclpAlertServiceTypeConfig: AclpAlertServiceTypeConfig[];
aclpLogs: AclpLogsFlag;
aclpNbMetricsIntegration: boolean;
aclpReadEndpoint: string;
aclpResourceTypeMap: CloudPulseResourceTypeMapFlag[];
aclpServices: Partial<AclpServices>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
useNodeBalancerQuery,
useNodebalancerUpdateMutation,
} from '@linode/queries';
import { CircleProgress, ErrorState, Notice } from '@linode/ui';
import { CircleProgress, ErrorState, NewFeatureChip, Notice } from '@linode/ui';
import { useParams } from '@tanstack/react-router';
import React from 'react';

Expand All @@ -13,12 +13,14 @@ import { TabPanels } from 'src/components/Tabs/TabPanels';
import { Tabs } from 'src/components/Tabs/Tabs';
import { TanStackTabLinkList } from 'src/components/Tabs/TanStackTabLinkList';
import { usePermissions } from 'src/features/IAM/hooks/usePermissions';
import { useFlags } from 'src/hooks/useFlags';
import { useTabs } from 'src/hooks/useTabs';
import { getErrorMap } from 'src/utilities/errorUtils';

import { NodeBalancerConfigurationsWrapper } from './NodeBalancerConfigurationsWrapper';
import { NodeBalancerSettings } from './NodeBalancerSettings';
import { NodeBalancerSummary } from './NodeBalancerSummary/NodeBalancerSummary';
import { NodeBalancerSummaryv2 } from './NodeBalancerSummaryv2/NodeBalancerSummaryv2';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be better to rename it to something clearer with proper casing, like NodeBalancerSummaryV2 OR alternatively move the versioning into the folder structure instead of including it in the component name.


export const NodeBalancerDetail = () => {
const { id } = useParams({
Expand All @@ -31,6 +33,8 @@ export const NodeBalancerDetail = () => {
reset,
} = useNodebalancerUpdateMutation(Number(id));

const { aclpNbMetricsIntegration } = useFlags();

const {
data: nodebalancer,
error,
Expand All @@ -43,7 +47,7 @@ export const NodeBalancerDetail = () => {
nodebalancer?.id
);

const { handleTabChange, tabIndex, tabs } = useTabs([
const { getTabIndex, handleTabChange, tabIndex, tabs } = useTabs([
{
title: 'Summary',
to: '/nodebalancers/$id/summary',
Expand All @@ -52,6 +56,12 @@ export const NodeBalancerDetail = () => {
title: 'Configurations',
to: '/nodebalancers/$id/configurations',
},
{
title: 'Metrics',
to: '/nodebalancers/$id/metrics',
hide: !aclpNbMetricsIntegration,
chip: <NewFeatureChip />,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
chip: <NewFeatureChip />,
chip: getFeatureChip(aclp ?? {}),

Let's use the getFeatureChip utility from the shared package and the aclp flag (used for metrics) to determine the chip here. This will help keep the chips and their behavior consistent with the centralized view.

},
{
title: 'Settings',
to: '/nodebalancers/$id/settings',
Expand All @@ -75,6 +85,9 @@ export const NodeBalancerDetail = () => {
const errorMap = getErrorMap(['label'], updateError);
const labelError = errorMap.label;

const metricsTabIndex = getTabIndex('/nodebalancers/$id/metrics');
const settingsTabIndex = getTabIndex('/nodebalancers/$id/settings');

return (
<React.Fragment>
<LandingHeader
Expand All @@ -101,14 +114,26 @@ export const NodeBalancerDetail = () => {
<React.Suspense fallback={<SuspenseLoader />}>
<TabPanels>
<SafeTabPanel index={0}>
<NodeBalancerSummary />
{!aclpNbMetricsIntegration ? (
<NodeBalancerSummary />
) : (
<NodeBalancerSummaryv2 />
)}
</SafeTabPanel>
<SafeTabPanel index={1}>
<NodeBalancerConfigurationsWrapper />
</SafeTabPanel>
<SafeTabPanel index={2}>
<NodeBalancerSettings />
</SafeTabPanel>
{metricsTabIndex !== null && (
<SafeTabPanel index={metricsTabIndex}>
<Notice text="Metrics tab coming soon.." variant="info" />
</SafeTabPanel>
)}

{settingsTabIndex !== null && (
<SafeTabPanel index={settingsTabIndex}>
<NodeBalancerSettings />
</SafeTabPanel>
)}
</TabPanels>
</React.Suspense>
</Tabs>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import {
convertMegabytesTo,
nodeBalancerConfigFactory,
nodeBalancerFactory,
} from '@linode/utilities';
import React from 'react';

import { firewallFactory } from 'src/factories';
import { renderWithTheme } from 'src/utilities/testHelpers';

import { NodeBalancerDetailBody } from './NodeBalancerDetailBody';

const queryMocks = vi.hoisted(() => ({
useAllNodeBalancerConfigsQuery: vi.fn().mockReturnValue({ data: [] }),
useNodeBalancersFirewallsQuery: vi
.fn()
.mockReturnValue({ data: { data: [] } }),
useParams: vi.fn().mockReturnValue({ id: 1 }),
useRegionsQuery: vi.fn().mockReturnValue({ data: [] }),
}));

vi.mock('@tanstack/react-router', async () => {
const actual = await vi.importActual('@tanstack/react-router');
return {
...actual,
useParams: queryMocks.useParams,
};
});

vi.mock('@linode/queries', async () => {
const actual = await vi.importActual('@linode/queries');
return {
...actual,
useAllNodeBalancerConfigsQuery: queryMocks.useAllNodeBalancerConfigsQuery,
useNodeBalancersFirewallsQuery: queryMocks.useNodeBalancersFirewallsQuery,
useRegionsQuery: queryMocks.useRegionsQuery,
};
});

describe('NodeBalancerDetailBody', () => {
const nodebalancer = nodeBalancerFactory.build({
hostname: 'example.com',
id: 1,
region: 'us-east',
tags: ['tag-1'],
type: 'common',
});

beforeEach(() => {
queryMocks.useAllNodeBalancerConfigsQuery.mockReturnValue({
data: [
nodeBalancerConfigFactory.build({ id: 101, port: 80 }),
nodeBalancerConfigFactory.build({ id: 102, port: 443 }),
],
});
queryMocks.useNodeBalancersFirewallsQuery.mockReturnValue({
data: {
data: [firewallFactory.build({ id: 44, label: 'mock-firewall-1' })],
},
});
queryMocks.useParams.mockReturnValue({ id: 1 });
queryMocks.useRegionsQuery.mockReturnValue({
data: [{ id: 'us-east', label: 'Newark, NJ' }],
});
});

afterEach(() => {
vi.resetAllMocks();
});

it('renders the nodebalancer details, config links, and firewall link', () => {
const { getByRole, getByText } = renderWithTheme(
<NodeBalancerDetailBody nodebalancer={nodebalancer} />
);

expect(getByText('Type')).toBeVisible();
expect(getByText('Basic')).toBeVisible();
expect(getByText('Region')).toBeVisible();
expect(getByText('Newark, NJ')).toBeVisible();
expect(getByText('NodeBalancer ID')).toBeVisible();
expect(getByText(String(nodebalancer.id))).toBeVisible();

expect(getByText('Configuration Ports')).toBeVisible();

const port80Link = getByRole('link', { name: 'Port 80' });
expect(port80Link).toHaveAttribute(
'href',
`/nodebalancers/${nodebalancer.id}/configurations/101`
);

const port443Link = getByRole('link', { name: 'Port 443' });
expect(port443Link).toHaveAttribute(
'href',
`/nodebalancers/${nodebalancer.id}/configurations/102`
);

expect(getByText('Hostname')).toBeVisible();
expect(getByText(nodebalancer.hostname)).toBeVisible();
expect(getByText('Transferred')).toBeVisible();
expect(
getByText(convertMegabytesTo(nodebalancer.transfer.total))
).toBeVisible();

expect(getByText('Firewall')).toBeVisible();
const firewallLink = getByRole('link', {
name: 'Firewall mock-firewall-1',
});
expect(firewallLink).toHaveAttribute('href', '/firewalls/44');
});

it('renders None when there are no configuration ports and hides the firewall section when there is no firewall', () => {
queryMocks.useAllNodeBalancerConfigsQuery.mockReturnValue({
data: [],
});
queryMocks.useNodeBalancersFirewallsQuery.mockReturnValue({
data: {
data: [],
},
});

const { getByText, queryByText } = renderWithTheme(
<NodeBalancerDetailBody nodebalancer={nodebalancer} />
);

expect(getByText('Configuration Ports')).toBeVisible();
expect(getByText('None')).toBeVisible();
expect(queryByText('Firewall')).not.toBeInTheDocument();
});

it('falls back to the raw region id when the region lookup is unavailable', () => {
queryMocks.useRegionsQuery.mockReturnValue({
data: [],
});

const { getByText } = renderWithTheme(
<NodeBalancerDetailBody nodebalancer={nodebalancer} />
);

expect(getByText(nodebalancer.region)).toBeVisible();
});
});
Loading
Loading