diff --git a/packages/api-v4/.changeset/pr-11727-upcoming-features-1740523371376.md b/packages/api-v4/.changeset/pr-11727-upcoming-features-1740523371376.md
new file mode 100644
index 00000000000..acab0df136d
--- /dev/null
+++ b/packages/api-v4/.changeset/pr-11727-upcoming-features-1740523371376.md
@@ -0,0 +1,5 @@
+---
+"@linode/api-v4": Upcoming Features
+---
+
+Update Linode Config types for Linode Interfaces ([#11727](https://github.com/linode/manager/pull/11727))
diff --git a/packages/api-v4/src/linodes/types.ts b/packages/api-v4/src/linodes/types.ts
index f5128bc5bc1..557504d6341 100644
--- a/packages/api-v4/src/linodes/types.ts
+++ b/packages/api-v4/src/linodes/types.ts
@@ -226,8 +226,8 @@ export interface Config {
created: string;
updated: string;
initrd: string | null;
- // If a Linode is using the new Linode Interfaces, this field will no longer be present.
- interfaces?: Interface[];
+ // If a Linode is using new Linode Interfaces, the interfaces in the Config object will be returned as null.
+ interfaces: Interface[] | null;
}
// ----------------------------------------------------------
@@ -430,7 +430,7 @@ export interface LinodeConfigCreationData {
updatedb_disabled: boolean;
distro: boolean;
modules_dep: boolean;
- network: boolean;
+ network?: boolean;
devtmpfs_automount: boolean;
};
root_device: string;
diff --git a/packages/manager/.changeset/pr-11727-upcoming-features-1740523339141.md b/packages/manager/.changeset/pr-11727-upcoming-features-1740523339141.md
new file mode 100644
index 00000000000..309aae00843
--- /dev/null
+++ b/packages/manager/.changeset/pr-11727-upcoming-features-1740523339141.md
@@ -0,0 +1,5 @@
+---
+"@linode/manager": Upcoming Features
+---
+
+Hide Networking sections from Linode Configurations page for Linodes with new interfaces ([#11727](https://github.com/linode/manager/pull/11727))
diff --git a/packages/manager/cypress/support/intercepts/configs.ts b/packages/manager/cypress/support/intercepts/configs.ts
index ca54fe8b6d5..3a6396050e9 100644
--- a/packages/manager/cypress/support/intercepts/configs.ts
+++ b/packages/manager/cypress/support/intercepts/configs.ts
@@ -170,6 +170,6 @@ export const mockCreateLinodeConfigInterfaces = (
return cy.intercept(
'POST',
apiMatcher(`linode/instances/${linodeId}/configs/${config.id}/interfaces`),
- config.interfaces
+ config.interfaces ?? undefined
);
};
diff --git a/packages/manager/src/factories/linodeConfigs.ts b/packages/manager/src/factories/linodeConfigs.ts
index 08c64f8f2d9..339952ce57b 100644
--- a/packages/manager/src/factories/linodeConfigs.ts
+++ b/packages/manager/src/factories/linodeConfigs.ts
@@ -1,11 +1,11 @@
-import { Config } from '@linode/api-v4/lib/linodes/types';
import Factory from 'src/factories/factoryProxy';
-
import {
LinodeConfigInterfaceFactory,
LinodeConfigInterfaceFactoryWithVPC,
} from 'src/factories/linodeConfigInterfaceFactory';
+import type { Config } from '@linode/api-v4';
+
const generateRandomId = () => Math.floor(Math.random() * 10000);
const publicInterface = LinodeConfigInterfaceFactory.build({
diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/ConfigRow.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/ConfigRow.tsx
index 445fa48bf57..c051e63a984 100644
--- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/ConfigRow.tsx
+++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/ConfigRow.tsx
@@ -1,9 +1,3 @@
-import {
- Config,
- Devices,
- DiskDevice,
- VolumeDevice,
-} from '@linode/api-v4/lib/linodes';
import { styled } from '@mui/material/styles';
import * as React from 'react';
@@ -11,12 +5,22 @@ import { TableCell } from 'src/components/TableCell';
import { TableRow } from 'src/components/TableRow';
import { API_MAX_PAGE_SIZE } from 'src/constants';
import { useAllLinodeDisksQuery } from 'src/queries/linodes/disks';
-import { useLinodeKernelQuery } from 'src/queries/linodes/linodes';
+import {
+ useLinodeKernelQuery,
+ useLinodeQuery,
+} from 'src/queries/linodes/linodes';
import { useLinodeVolumesQuery } from 'src/queries/volumes/volumes';
import { InterfaceListItem } from './InterfaceListItem';
import { ConfigActionMenu } from './LinodeConfigActionMenu';
+import type {
+ Config,
+ Devices,
+ DiskDevice,
+ VolumeDevice,
+} from '@linode/api-v4/lib/linodes';
+
interface Props {
config: Config;
linodeId: number;
@@ -27,13 +31,13 @@ interface Props {
}
export const isDiskDevice = (
- device: VolumeDevice | DiskDevice
+ device: DiskDevice | VolumeDevice
): device is DiskDevice => {
return 'disk_id' in device;
};
const isVolumeDevice = (
- device: VolumeDevice | DiskDevice
+ device: DiskDevice | VolumeDevice
): device is VolumeDevice => {
return 'volume_id' in device;
};
@@ -41,6 +45,8 @@ const isVolumeDevice = (
export const ConfigRow = React.memo((props: Props) => {
const { config, linodeId, onBoot, onDelete, onEdit, readOnly } = props;
+ const { data: linode } = useLinodeQuery(linodeId);
+
const { data: kernel } = useLinodeKernelQuery(config.kernel);
const { data: disks } = useAllLinodeDisksQuery(linodeId);
@@ -110,9 +116,11 @@ export const ConfigRow = React.memo((props: Props) => {
{config.label} – {kernel?.label ?? config.kernel}
{deviceLabels}
-
- {interfaces.length > 0 ? InterfaceList : defaultInterfaceLabel}
-
+ {linode?.interface_generation !== 'linode' && (
+
+ {interfaces.length > 0 ? InterfaceList : defaultInterfaceLabel}
+
+ )}
({
+ useFlags: vi.fn().mockReturnValue({}),
+ useLinodeQuery: vi.fn().mockReturnValue({}),
+}));
+
+vi.mock('src/queries/linodes/linodes', async () => {
+ const actual = await vi.importActual('src/queries/linodes/linodes');
+ return {
+ ...actual,
+ useLinodeQuery: queryMocks.useLinodeQuery,
+ };
+});
+
+vi.mock('src/hooks/useFlags', () => {
+ const actual = vi.importActual('src/hooks/useFlags');
+ return {
+ ...actual,
+ useFlags: queryMocks.useFlags,
+ };
+});
+
describe('LinodeConfigDialog', () => {
describe('padInterface helper method', () => {
it('should return a list of the correct length unchanged', () => {
@@ -171,4 +193,37 @@ describe('LinodeConfigDialog', () => {
await findByDisplayValue('VPC');
await findByDisplayValue('Public Internet');
});
+
+ it('should hide the Network Interfaces section if Linode uses new interfaces', () => {
+ const props = {
+ isReadOnly: false,
+ linodeId: 1,
+ onClose: vi.fn(),
+ };
+
+ const linode = linodeFactory.build({ interface_generation: 'linode' });
+
+ queryMocks.useLinodeQuery.mockReturnValue({
+ data: linode,
+ });
+
+ queryMocks.useFlags.mockReturnValue({
+ linodeInterfaces: { enabled: true },
+ });
+
+ const { queryByLabelText } = renderWithTheme(
+
+ );
+
+ expect(
+ queryByLabelText('Primary Interface (Default Route)')
+ ).not.toBeInTheDocument();
+ expect(queryByLabelText('eth0')).not.toBeInTheDocument();
+ expect(queryByLabelText('eth1')).not.toBeInTheDocument();
+ expect(queryByLabelText('eth2')).not.toBeInTheDocument();
+ });
});
diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx
index 97b5915eea3..a2ff1ea90e2 100644
--- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx
+++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx
@@ -15,9 +15,10 @@ import {
Toggle,
TooltipIcon,
Typography,
+ omitProps,
} from '@linode/ui';
-import { useTheme } from '@mui/material/styles';
import Grid from '@mui/material/Grid2';
+import { useTheme } from '@mui/material/styles';
import { useQueryClient } from '@tanstack/react-query';
import { useFormik } from 'formik';
import { useSnackbar } from 'notistack';
@@ -52,6 +53,7 @@ import {
handleFieldErrors,
handleGeneralErrors,
} from 'src/utilities/formikErrorUtils';
+import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes';
import { scrollErrorIntoViewV2 } from 'src/utilities/scrollErrorIntoViewV2';
import { InterfaceSelect } from '../LinodeSettings/InterfaceSelect';
@@ -80,7 +82,7 @@ interface Helpers {
devtmpfs_automount: boolean;
distro: boolean;
modules_dep: boolean;
- network: boolean;
+ network?: boolean;
updatedb_disabled: boolean;
}
@@ -93,7 +95,7 @@ interface EditableFields {
devices: DevicesAsStrings;
helpers: Helpers;
initrd: null | string;
- interfaces: ExtendedInterface[];
+ interfaces?: ExtendedInterface[] | null;
kernel?: string;
label: string;
memory_limit?: number;
@@ -140,18 +142,18 @@ const defaultInterfaceList = padInterfaceList([
},
]);
-const defaultFieldsValues: EditableFields = {
+const baseHelpers: Helpers = {
+ devtmpfs_automount: true,
+ distro: true,
+ modules_dep: true,
+ updatedb_disabled: true,
+};
+
+const baseFieldValues: EditableFields = {
comments: '',
devices: {},
- helpers: {
- devtmpfs_automount: true,
- distro: true,
- modules_dep: true,
- network: true,
- updatedb_disabled: true,
- },
+ helpers: baseHelpers,
initrd: '',
- interfaces: defaultInterfaceList,
kernel: 'linode/latest-64bit',
label: '',
memory_limit: 0,
@@ -162,6 +164,12 @@ const defaultFieldsValues: EditableFields = {
virt_mode: 'paravirt' as VirtMode,
};
+const defaultLegacyInterfaceFieldValues: EditableFields = {
+ ...baseFieldValues,
+ helpers: { ...baseHelpers, network: true },
+ interfaces: defaultInterfaceList,
+};
+
const pathsOptions = [
{ label: '/dev/sda', value: '/dev/sda' },
{ label: '/dev/sdb', value: '/dev/sdb' },
@@ -173,7 +181,7 @@ const pathsOptions = [
{ label: '/dev/sdh', value: '/dev/sdh' },
];
-const interfacesToState = (interfaces?: Interface[]) => {
+const interfacesToState = (interfaces?: Interface[] | null) => {
if (!interfaces || interfaces.length === 0) {
return defaultInterfaceList;
}
@@ -203,7 +211,7 @@ const interfacesToState = (interfaces?: Interface[]) => {
return padInterfaceList(interfacesPayload);
};
-const interfacesToPayload = (interfaces?: ExtendedInterface[]) => {
+const interfacesToPayload = (interfaces?: ExtendedInterface[] | null) => {
if (!interfaces || interfaces.length === 0) {
return [];
}
@@ -242,6 +250,8 @@ export const LinodeConfigDialog = (props: Props) => {
const { data: linode } = useLinodeQuery(linodeId, open);
+ const { isLinodeInterfacesEnabled } = useIsLinodeInterfacesEnabled();
+
const { enqueueSnackbar } = useSnackbar();
const virtModeCaptionId = React.useId();
@@ -266,6 +276,9 @@ export const LinodeConfigDialog = (props: Props) => {
config?.id ?? -1
);
+ const isLinodeInterface = linode?.interface_generation === 'linode';
+ const isLegacyConfigInterface = !isLinodeInterface;
+
const theme = useTheme();
const regions = useRegionsQuery().data ?? [];
@@ -290,7 +303,9 @@ export const LinodeConfigDialog = (props: Props) => {
);
const { resetForm, setFieldValue, values, ...formik } = useFormik({
- initialValues: defaultFieldsValues,
+ initialValues: isLinodeInterface
+ ? baseFieldValues
+ : defaultLegacyInterfaceFieldValues,
onSubmit: (values) => onSubmit(values),
validate: (values) => {
onValidate(values);
@@ -318,12 +333,11 @@ export const LinodeConfigDialog = (props: Props) => {
virt_mode,
} = state;
- return {
+ const baseValuesToReturn = {
comments,
devices: createDevicesFromStrings(devices),
helpers,
initrd: initrd !== '' ? initrd : null,
- interfaces: interfacesToPayload(interfaces),
kernel,
label,
/** if the user did not toggle the limit radio button, send a value of 0 */
@@ -332,6 +346,12 @@ export const LinodeConfigDialog = (props: Props) => {
run_level,
virt_mode,
};
+ return isLinodeInterface
+ ? baseValuesToReturn
+ : {
+ ...baseValuesToReturn,
+ interfaces: interfacesToPayload(interfaces),
+ };
};
// This validation runs BEFORE Yup schema validation. This validation logic
@@ -341,22 +361,24 @@ export const LinodeConfigDialog = (props: Props) => {
const errors: any = {};
const { interfaces } = values;
- const eth1 = interfaces[1];
- const eth2 = interfaces[2];
+ if (interfaces) {
+ const eth1 = interfaces[1];
+ const eth2 = interfaces[2];
- if (eth1?.purpose === 'none' && eth2.purpose !== 'none') {
- errors.interfaces =
- 'You cannot assign an interface to eth2 without an interface assigned to eth1.';
- return errors;
- }
-
- // The API field is called "label" and thus the Validation package error
- // message is "Label is required." Our field in Cloud is called "VLAN".
- interfaces.forEach((thisInterface, idx) => {
- if (thisInterface.purpose === 'vlan' && !thisInterface.label) {
- errors[`interfaces[${idx}].label`] = 'VLAN is required.';
+ if (eth1?.purpose === 'none' && eth2.purpose !== 'none') {
+ errors.interfaces =
+ 'You cannot assign an interface to eth2 without an interface assigned to eth1.';
+ return errors;
}
- });
+
+ // The API field is called "label" and thus the Validation package error
+ // message is "Label is required." Our field in Cloud is called "VLAN".
+ interfaces.forEach((thisInterface, idx) => {
+ if (thisInterface.purpose === 'vlan' && !thisInterface.label) {
+ errors[`interfaces[${idx}].label`] = 'VLAN is required.';
+ }
+ });
+ }
return errors;
};
@@ -365,7 +387,6 @@ export const LinodeConfigDialog = (props: Props) => {
formik.setSubmitting(true);
const configData = convertStateToData(values) as LinodeConfigCreationData;
-
// If Finnix was selected, make sure it gets sent as a number in the payload, not a string.
if (Number(configData.initrd) === finnixDiskID) {
configData.initrd = finnixDiskID;
@@ -488,32 +509,53 @@ export const LinodeConfigDialog = (props: Props) => {
)
);
+ const baseValues = {
+ comments: config.comments,
+ devices,
+ helpers: isLinodeInterface
+ ? omitProps(config.helpers, ['network'])
+ : config.helpers,
+ initrd: initrdFromConfig,
+ kernel: config.kernel,
+ label: config.label,
+ memory_limit: config.memory_limit,
+ root_device: config.root_device,
+ run_level: config.run_level,
+ setMemoryLimit: (config.memory_limit !== 0
+ ? 'set_limit'
+ : 'no_limit') as MemoryLimit,
+ useCustomRoot: isUsingCustomRoot(config.root_device),
+ virt_mode: config.virt_mode,
+ };
+
resetForm({
- values: {
- comments: config.comments,
- devices,
- helpers: config.helpers,
- initrd: initrdFromConfig,
- interfaces: interfacesToState(config.interfaces),
- kernel: config.kernel,
- label: config.label,
- memory_limit: config.memory_limit,
- root_device: config.root_device,
- run_level: config.run_level,
- setMemoryLimit:
- config.memory_limit !== 0 ? 'set_limit' : 'no_limit',
- useCustomRoot: isUsingCustomRoot(config.root_device),
- virt_mode: config.virt_mode,
- },
+ values: isLinodeInterface
+ ? baseValues
+ : {
+ ...baseValues,
+ interfaces: interfacesToState(config.interfaces),
+ },
});
} else {
// Create mode; make sure loading/error states are cleared.
- resetForm({ values: defaultFieldsValues });
+ resetForm({
+ values: isLinodeInterface
+ ? baseFieldValues
+ : defaultLegacyInterfaceFieldValues,
+ });
setUseCustomRoot(false);
setDeviceCounter(deviceCounterDefault);
}
}
- }, [open, config, initrdFromConfig, resetForm, queryClient]);
+ }, [
+ open,
+ linode,
+ isLinodeInterface,
+ config,
+ initrdFromConfig,
+ resetForm,
+ queryClient,
+ ]);
const generalError = formik.status?.generalError;
@@ -592,9 +634,9 @@ export const LinodeConfigDialog = (props: Props) => {
value: null,
});
- const interfacesWithoutPlaceholderInterfaces = values.interfaces.filter(
+ const interfacesWithoutPlaceholderInterfaces = (values.interfaces?.filter(
(i) => i.purpose !== 'none'
- ) as Interface[];
+ ) ?? []) as Interface[];
const primaryInterfaceOptions = interfacesWithoutPlaceholderInterfaces.map(
(networkInterface, idx) => ({
@@ -953,142 +995,167 @@ export const LinodeConfigDialog = (props: Props) => {
- Networking
-
+ {(isLegacyConfigInterface ||
+ (isLinodeInterface && isLinodeInterfacesEnabled)) && (
+ Networking
+ )}
+ {isLegacyConfigInterface && (
+
+ )}
- {formik.errors.interfaces && (
-
+ {isLinodeInterface && isLinodeInterfacesEnabled && (
+ <>
+ ({ marginTop: theme.spacing(2) })}>
+ Go to{' '}
+ Network{' '}
+ to view your Linode's Network interfaces.
+
+
+
+ >
)}
- <>
- i.purpose === 'public' || i.purpose === 'vpc'
+ {isLegacyConfigInterface && (
+ <>
+ {formik.errors.interfaces && (
+
)}
- onChange={(_, selected) => {
- const updatedInterfaces = [...values.interfaces];
-
- for (let i = 0; i < updatedInterfaces.length; i++) {
- if (selected && selected.value === i) {
- updatedInterfaces[i].primary = true;
- } else {
- updatedInterfaces[i].primary = false;
- }
- }
+ <>
+ i.purpose === 'public' || i.purpose === 'vpc'
+ )}
+ onChange={(_, selected) => {
+ const updatedInterfaces = [
+ ...(values.interfaces ?? []),
+ ];
+
+ for (let i = 0; i < updatedInterfaces.length; i++) {
+ if (selected && selected.value === i) {
+ updatedInterfaces[i].primary = true;
+ } else {
+ updatedInterfaces[i].primary = false;
+ }
+ }
- formik.setValues({
- ...values,
- interfaces: updatedInterfaces,
- });
- }}
- value={
- primaryInterfaceIndex !== null
- ? primaryInterfaceOptions[primaryInterfaceIndex]
- : null
- }
- autoHighlight
- data-testid="primary-interface-dropdown"
- disabled={isReadOnly}
- label="Primary Interface (Default Route)"
- options={primaryInterfaceOptions}
- placeholder="None"
- />
-
- >
- {values.interfaces.map((thisInterface, idx) => {
- const thisInterfaceIPRanges: ExtendedIP[] = (
- thisInterface.ip_ranges ?? []
- ).map((ip_range, index) => {
- // Display a more user-friendly error to the user as opposed to, for example, "interfaces[1].ip_ranges[1] is invalid"
- // @ts-expect-error this form intentionally breaks formik's error type
- const errorString: string = formik.errors[
- `interfaces[${idx}].ip_ranges[${index}]`
- ]?.includes('is invalid')
- ? 'Invalid IP range'
- : // @ts-expect-error this form intentionally breaks formik's error type
- formik.errors[`interfaces[${idx}].ip_ranges[${index}]`];
-
- return {
- address: ip_range,
- error: errorString,
- };
- });
-
- return (
-
- {unrecommendedConfigNoticeSelector({
- _interface: thisInterface,
- primaryInterfaceIndex,
- thisIndex: idx,
- values,
- })}
- {
- handleInterfaceChange(idx, newInterface);
- }}
- nattedIPv4Address={
- thisInterface.ipv4?.nat_1_1 ?? undefined
+ value={
+ primaryInterfaceIndex !== null
+ ? primaryInterfaceOptions[primaryInterfaceIndex]
+ : null
}
- additionalIPv4RangesForVPC={thisInterfaceIPRanges}
- ipamAddress={thisInterface.ipam_address}
- key={`eth${idx}-interface`}
- label={thisInterface.label}
- purpose={thisInterface.purpose}
- readOnly={isReadOnly}
- region={linode?.region}
- regionHasVLANs={regionHasVLANS}
- regionHasVPCs={regionHasVPCs}
- slotNumber={idx}
- subnetId={thisInterface.subnet_id}
- vpcIPv4={thisInterface.ipv4?.vpc ?? undefined}
- vpcId={thisInterface.vpc_id}
+ autoHighlight
+ data-testid="primary-interface-dropdown"
+ disabled={isReadOnly}
+ label="Primary Interface (Default Route)"
+ options={primaryInterfaceOptions}
+ placeholder="None"
/>
-
- );
- })}
-
+
+ >
+ {values.interfaces?.map((thisInterface, idx) => {
+ const thisInterfaceIPRanges: ExtendedIP[] = (
+ thisInterface.ip_ranges ?? []
+ ).map((ip_range, index) => {
+ // Display a more user-friendly error to the user as opposed to, for example, "interfaces[1].ip_ranges[1] is invalid"
+ // @ts-expect-error this form intentionally breaks formik's error type
+ const errorString: string = formik.errors[
+ `interfaces[${idx}].ip_ranges[${index}]`
+ ]?.includes('is invalid')
+ ? 'Invalid IP range'
+ : // @ts-expect-error this form intentionally breaks formik's error type
+ formik.errors[
+ `interfaces[${idx}].ip_ranges[${index}]`
+ ];
+
+ return {
+ address: ip_range,
+ error: errorString,
+ };
+ });
+ return (
+
+ {unrecommendedConfigNoticeSelector({
+ _interface: thisInterface,
+ primaryInterfaceIndex,
+ thisIndex: idx,
+ values,
+ })}
+ {
+ handleInterfaceChange(idx, newInterface);
+ }}
+ nattedIPv4Address={
+ thisInterface.ipv4?.nat_1_1 ?? undefined
+ }
+ additionalIPv4RangesForVPC={thisInterfaceIPRanges}
+ ipamAddress={thisInterface.ipam_address}
+ key={`eth${idx}-interface`}
+ label={thisInterface.label}
+ purpose={thisInterface.purpose}
+ readOnly={isReadOnly}
+ region={linode?.region}
+ regionHasVLANs={regionHasVLANS}
+ regionHasVPCs={regionHasVPCs}
+ slotNumber={idx}
+ subnetId={thisInterface.subnet_id}
+ vpcIPv4={thisInterface.ipv4?.vpc ?? undefined}
+ vpcId={thisInterface.vpc_id}
+ />
+
+ );
+ })}
+ >
+ )}
+
Filesystem/Boot Helpers
@@ -1145,25 +1212,28 @@ export const LinodeConfigDialog = (props: Props) => {
name="helpers.devtmpfs_automount"
/>
-
- Automatically configure static networking
-
- (more info)
-
- >
- }
- checked={values.helpers.network}
- disabled={isReadOnly}
- onChange={formik.handleChange}
- />
- }
- label="Auto-configure networking"
- name="helpers.network"
- />
+ {isLegacyConfigInterface && (
+
+ Automatically configure static networking
+
+ {' '}
+ (more info)
+
+ >
+ }
+ checked={values.helpers.network}
+ disabled={isReadOnly}
+ onChange={formik.handleChange}
+ />
+ }
+ label="Auto-configure networking"
+ name="helpers.network"
+ />
+ )}
@@ -1246,13 +1316,14 @@ export const unrecommendedConfigNoticeSelector = ({
const vpcInterface = _interface.purpose === 'vpc';
const nattedIPv4Address = Boolean(_interface.ipv4?.nat_1_1);
- const filteredInterfaces = values.interfaces.filter(
- (_interface) => _interface.purpose !== 'none'
- );
+ const filteredInterfaces =
+ values.interfaces?.filter((_interface) => _interface.purpose !== 'none') ??
+ [];
// Edge case: users w/ ability to have multiple VPC interfaces. Scenario 1 & 2 notices not helpful if that's done
const primaryInterfaceIsVPC =
primaryInterfaceIndex !== null &&
+ values.interfaces &&
values.interfaces[primaryInterfaceIndex].purpose === 'vpc';
/*
diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigs.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigs.test.tsx
new file mode 100644
index 00000000000..016d2085188
--- /dev/null
+++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigs.test.tsx
@@ -0,0 +1,56 @@
+import React from 'react';
+
+import { linodeFactory } from 'src/factories';
+import 'src/mocks/testServer';
+import { renderWithTheme } from 'src/utilities/testHelpers';
+
+import LinodeConfigs from './LinodeConfigs';
+
+const queryMocks = vi.hoisted(() => ({
+ useFlags: vi.fn().mockReturnValue({}),
+ useLinodeQuery: vi.fn().mockReturnValue({}),
+}));
+
+vi.mock('src/queries/linodes/linodes', async () => {
+ const actual = await vi.importActual('src/queries/linodes/linodes');
+ return {
+ ...actual,
+ useLinodeQuery: queryMocks.useLinodeQuery,
+ };
+});
+
+vi.mock('src/hooks/useFlags', () => {
+ const actual = vi.importActual('src/hooks/useFlags');
+ return {
+ ...actual,
+ useFlags: queryMocks.useFlags,
+ };
+});
+
+describe('LinodeConfigs', () => {
+ it('should show the Network Interfaces column for legacy config Linodes', () => {
+ queryMocks.useLinodeQuery.mockReturnValue({
+ data: linodeFactory.build,
+ });
+
+ const { queryByText } = renderWithTheme();
+
+ expect(queryByText('Network Interfaces')).toBeVisible();
+ });
+
+ it('should hide the Network Interfaces column for new Linode interface Linodes', () => {
+ const linode = linodeFactory.build({ interface_generation: 'linode' });
+
+ queryMocks.useLinodeQuery.mockReturnValue({
+ data: linode,
+ });
+
+ queryMocks.useFlags.mockReturnValue({
+ linodeInterfaces: { enabled: true },
+ });
+
+ const { queryByText } = renderWithTheme();
+
+ expect(queryByText('Network Interfaces')).not.toBeInTheDocument();
+ });
+});
diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigs.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigs.tsx
index 008833cf951..e27b7df1d92 100644
--- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigs.tsx
+++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigs.tsx
@@ -15,6 +15,7 @@ import { TableHead } from 'src/components/TableHead';
import { TableRow } from 'src/components/TableRow';
import { TableSortCell } from 'src/components/TableSortCell';
import { useAllLinodeConfigsQuery } from 'src/queries/linodes/configs';
+import { useLinodeQuery } from 'src/queries/linodes/linodes';
import { useGrants } from 'src/queries/profile/profile';
import { sendLinodeConfigurationDocsEvent } from 'src/utilities/analytics/customEventAnalytics';
@@ -30,6 +31,10 @@ const LinodeConfigs = () => {
const id = Number(linodeId);
+ const { data: linode } = useLinodeQuery(id);
+
+ const isLegacyConfigInterface = linode?.interface_generation !== 'linode';
+
const configsPanel = React.useRef();
const { data: grants } = useGrants();
@@ -131,19 +136,21 @@ const LinodeConfigs = () => {
Disks
-
- Network Interfaces
-
+ {isLegacyConfigInterface && (
+
+ Network Interfaces
+
+ )}
diff --git a/packages/manager/src/utilities/configs.ts b/packages/manager/src/utilities/configs.ts
index 4c03ae33121..71dc0f7b09c 100644
--- a/packages/manager/src/utilities/configs.ts
+++ b/packages/manager/src/utilities/configs.ts
@@ -9,13 +9,14 @@ export const manuallySetVPCConfigInterfacesToActive = (
return configs.map((config) => {
return {
...config,
- interfaces: config.interfaces?.map((linodeInterface) => {
- if (linodeInterface.purpose === 'vpc') {
- return { ...linodeInterface, active: true };
- } else {
- return linodeInterface;
- }
- }),
+ interfaces:
+ config.interfaces?.map((linodeInterface) => {
+ if (linodeInterface.purpose === 'vpc') {
+ return { ...linodeInterface, active: true };
+ } else {
+ return linodeInterface;
+ }
+ }) ?? null,
};
});
};