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, }; }); };