Skip to content

Commit

Permalink
feat: [M3-7121 & M3-7122] - Support VPC in Linode Add/Edit Config dia…
Browse files Browse the repository at this point in the history
…log (linode#9709)
  • Loading branch information
hana-akamai authored and abailly-akamai committed Oct 17, 2023
1 parent e7118dd commit e777479
Show file tree
Hide file tree
Showing 17 changed files with 750 additions and 181 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Support VPCs in Add/Edit Linode Config dialog ([#9709](https://github.com/linode/manager/pull/9709))
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {

import { InterfaceSelect } from '../LinodesDetail/LinodeSettings/InterfaceSelect';

// @TODO Delete this file when VPC is released
// @TODO VPC: Delete this file when VPC is released

interface Props {
handleVLANChange: (updatedInterface: Interface) => void;
Expand Down Expand Up @@ -105,14 +105,16 @@ export const AttachVLAN = React.memo((props: Props) => {
.
</Typography>
<InterfaceSelect
errors={{
ipamError,
labelError,
}}
handleChange={(newInterface: Interface) =>
handleVLANChange(newInterface)
}
fromAddonsPanel
ipamAddress={ipamAddress}
ipamError={ipamError}
label={vlanLabel}
labelError={labelError}
purpose="vlan"
readOnly={readOnly || !regionSupportsVLANs || false}
region={region}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,7 @@ export class LinodeCreate extends React.PureComponent<
}
assignPublicIPv4Address={this.props.assignPublicIPv4Address}
autoassignIPv4WithinVPC={this.props.autoassignIPv4WithinVPC}
from="linodeCreate"
handleSelectVPC={this.props.setSelectedVPC}
handleSubnetChange={this.props.handleSubnetChange}
handleVPCIPv4Change={this.props.handleVPCIPv4Change}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,16 @@ export const VLANAccordion = React.memo((props: Props) => {
.
</Typography>
<InterfaceSelect
errors={{
ipamError,
labelError,
}}
handleChange={(newInterface: Interface) =>
handleVLANChange(newInterface)
}
fromAddonsPanel
ipamAddress={ipamAddress}
ipamError={ipamError}
label={vlanLabel}
labelError={labelError}
purpose="vlan"
readOnly={readOnly || !regionSupportsVLANs || false}
region={region}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { makeResourcePage } from 'src/mocks/serverHandlers';
import { rest, server } from 'src/mocks/testServer';
import { mockMatchMedia, renderWithTheme } from 'src/utilities/testHelpers';

import { VPCPanel } from './VPCPanel';
import { VPCPanel, VPCPanelProps } from './VPCPanel';

const queryClient = new QueryClient();

Expand All @@ -19,6 +19,7 @@ afterEach(() => {
const props = {
assignPublicIPv4Address: false,
autoassignIPv4WithinVPC: true,
from: 'linodeCreate' as VPCPanelProps['from'],
handleSelectVPC: jest.fn(),
handleSubnetChange: jest.fn(),
handleVPCIPv4Change: jest.fn(),
Expand Down
107 changes: 76 additions & 31 deletions packages/manager/src/features/Linodes/LinodesCreate/VPCPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import Stack from '@mui/material/Stack';
import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import * as React from 'react';

import { Box } from 'src/components/Box';
Expand All @@ -23,20 +25,23 @@ import scrollErrorIntoView from 'src/utilities/scrollErrorIntoView';
import { StyledCreateLink } from './LinodeCreate.styles';
import { REGION_CAVEAT_HELPER_TEXT } from './constants';

interface VPCPanelProps {
export interface VPCPanelProps {
assignPublicIPv4Address: boolean;
autoassignIPv4WithinVPC: boolean;
from: 'linodeConfig' | 'linodeCreate';
handleSelectVPC: (vpcId: number) => void;
handleSubnetChange: (subnetId: number) => void;
handleVPCIPv4Change: (IPv4: string) => void;
publicIPv4Error?: string;
region: string | undefined;
selectedSubnetId: number | undefined;
selectedVPCId: number | undefined;
selectedSubnetId: null | number | undefined;
selectedVPCId: null | number | undefined;
subnetError?: string;
toggleAssignPublicIPv4Address: () => void;
toggleAutoassignIPv4WithinVPCEnabled: () => void;
vpcIPv4AddressOfLinode: string | undefined;
vpcIPv4Error?: string;
vpcIdError?: string;
}

const ERROR_GROUP_STRING = 'vpc-errors';
Expand All @@ -45,9 +50,11 @@ export const VPCPanel = (props: VPCPanelProps) => {
const {
assignPublicIPv4Address,
autoassignIPv4WithinVPC,
from,
handleSelectVPC,
handleSubnetChange,
handleVPCIPv4Change,
publicIPv4Error,
region,
selectedSubnetId,
selectedVPCId,
Expand All @@ -56,8 +63,12 @@ export const VPCPanel = (props: VPCPanelProps) => {
toggleAutoassignIPv4WithinVPCEnabled,
vpcIPv4AddressOfLinode,
vpcIPv4Error,
vpcIdError,
} = props;

const theme = useTheme();
const isSmallBp = useMediaQuery(theme.breakpoints.down('sm'));

const flags = useFlags();
const { account } = useAccountManagement();

Expand Down Expand Up @@ -104,10 +115,15 @@ export const VPCPanel = (props: VPCPanelProps) => {
: accumulator;
}, []);

vpcDropdownOptions.unshift({
label: 'None',
value: -1,
});
const fromLinodeCreate = from === 'linodeCreate';
const fromLinodeConfig = from === 'linodeConfig';

if (fromLinodeCreate) {
vpcDropdownOptions.unshift({
label: 'None',
value: -1,
});
}

const subnetDropdownOptions: Item[] =
vpcs
Expand All @@ -121,27 +137,46 @@ export const VPCPanel = (props: VPCPanelProps) => {
? getAPIErrorOrDefault(error, 'Unable to load VPCs')[0].reason
: undefined;

const mainCopyVPC =
vpcDropdownOptions.length <= 1
? 'Allow Linode to communicate in an isolated environment.'
: 'Assign this Linode to an existing VPC.';
const getMainCopyVPC = () => {
if (fromLinodeConfig) {
return null;
}

const copy =
vpcDropdownOptions.length <= 1
? 'Allow Linode to communicate in an isolated environment.'
: 'Assign this Linode to an existing VPC.';

return (
<>
{/* @TODO VPC: Update link */}
{copy} <Link to="">Learn more</Link>.
</>
);
};

return (
<Paper
sx={(theme) => ({
...(fromLinodeCreate && {
marginTop: theme.spacing(3),
}),
...(fromLinodeConfig && {
padding: 0,
}),
})}
data-testid="vpc-panel"
sx={(theme) => ({ marginTop: theme.spacing(3) })}
>
<Typography
sx={(theme) => ({ marginBottom: theme.spacing(2) })}
variant="h2"
>
VPC
</Typography>
<Stack>
<Typography>
{/* @TODO VPC: Update link */}
{mainCopyVPC} <Link to="">Learn more</Link>.
{fromLinodeCreate && (
<Typography
sx={(theme) => ({ marginBottom: theme.spacing(2) })}
variant="h2"
>
VPC
</Typography>
)}
<Stack>
<Typography>{getMainCopyVPC()}</Typography>
<Select
onChange={(selectedVPC: Item<number, string>) => {
handleSelectVPC(selectedVPC.value);
Expand All @@ -152,15 +187,15 @@ export const VPCPanel = (props: VPCPanelProps) => {
value={vpcDropdownOptions.find(
(option) => option.value === selectedVPCId
)}
defaultValue={vpcDropdownOptions[0]}
defaultValue={fromLinodeConfig ? null : vpcDropdownOptions[0]} // If we're in the Config dialog, there is no "None" option at index 0
disabled={!regionSupportsVPCs}
errorText={vpcError}
errorText={vpcIdError ?? vpcError}
isClearable={false}
isLoading={isLoading}
label="Assign VPC"
label={from === 'linodeCreate' ? 'Assign VPC' : 'VPC'}
noOptionsMessage={() => 'Create a VPC to assign to this Linode.'}
options={vpcDropdownOptions}
placeholder={''}
placeholder={'Select a VPC'}
/>
{vpcDropdownOptions.length <= 1 && regionSupportsVPCs && (
<Typography sx={(theme) => ({ paddingTop: theme.spacing(1.5) })}>
Expand All @@ -169,9 +204,11 @@ export const VPCPanel = (props: VPCPanelProps) => {
</Typography>
)}

<StyledCreateLink to={`${APP_ROOT}/vpcs/create`}>
Create VPC
</StyledCreateLink>
{from === 'linodeCreate' && (
<StyledCreateLink to={`${APP_ROOT}/vpcs/create`}>
Create VPC
</StyledCreateLink>
)}

{selectedVPCId !== -1 && regionSupportsVPCs && (
<Stack data-testid="subnet-and-additional-options-section">
Expand Down Expand Up @@ -214,7 +251,7 @@ export const VPCPanel = (props: VPCPanelProps) => {
flexDirection="row"
sx={{}}
>
<Typography noWrap>
<Typography noWrap={!isSmallBp && from === 'linodeConfig'}>
Auto-assign a VPC IPv4 address for this Linode in the VPC
</Typography>
<TooltipIcon
Expand Down Expand Up @@ -245,7 +282,6 @@ export const VPCPanel = (props: VPCPanelProps) => {
})}
alignItems="center"
display="flex"
flexDirection="row"
>
<FormControlLabel
control={
Expand All @@ -268,6 +304,15 @@ export const VPCPanel = (props: VPCPanelProps) => {
</Box>
}
/>
{assignPublicIPv4Address && publicIPv4Error && (
<Typography
sx={(theme) => ({
color: theme.color.red,
})}
>
{publicIPv4Error}
</Typography>
)}
</Box>
</Stack>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Config, Interface } from '@linode/api-v4/lib/linodes';
import { Config } from '@linode/api-v4/lib/linodes';
import { styled } from '@mui/material/styles';
import * as React from 'react';

Expand All @@ -9,6 +9,7 @@ import { useAllLinodeDisksQuery } from 'src/queries/linodes/disks';
import { useLinodeKernelQuery } from 'src/queries/linodes/linodes';
import { useLinodeVolumesQuery } from 'src/queries/volumes';

import { InterfaceListItem } from './InterfaceListItem';
import { ConfigActionMenu } from './LinodeConfigActionMenu';

interface Props {
Expand Down Expand Up @@ -59,7 +60,7 @@ export const ConfigRow = React.memo((props: Props) => {
return undefined;
}
return (
<li style={{ paddingBottom: 4 }} key={thisDevice}>
<li key={thisDevice} style={{ paddingBottom: 4 }}>
/dev/{thisDevice} - {label}
</li>
);
Expand All @@ -76,17 +77,12 @@ export const ConfigRow = React.memo((props: Props) => {
const InterfaceList = (
<StyledUl>
{interfaces.map((interfaceEntry, idx) => {
// The order of the config.interfaces array as returned by the API is significant.
// Index 0 is eth0, index 1 is eth1, index 2 is eth2.
const interfaceName = `eth${idx}`;

return (
<li
style={{ paddingBottom: 4 }}
<InterfaceListItem
idx={idx}
interfaceEntry={interfaceEntry}
key={interfaceEntry.label ?? 'public' + idx}
>
{interfaceName}{getInterfaceLabel(interfaceEntry)}
</li>
/>
);
})}
</StyledUl>
Expand Down Expand Up @@ -132,15 +128,3 @@ const StyledTableCell = styled(TableCell, { label: 'StyledTableCell' })({
},
padding: '0 !important',
});

export const getInterfaceLabel = (configInterface: Interface): string => {
if (configInterface.purpose === 'public') {
return 'Public Internet';
}

const interfaceLabel = configInterface.label;
const ipamAddress = configInterface.ipam_address;
const hasIPAM = Boolean(ipamAddress);

return `VLAN: ${interfaceLabel} ${hasIPAM ? `(${ipamAddress})` : ''}`;
};
Loading

0 comments on commit e777479

Please sign in to comment.