Skip to content
Merged
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
24 changes: 1 addition & 23 deletions packages/manager/src/components/ShowMore/ShowMore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const ShowMore = <T extends {}>(props: ShowMoreProps<T>) => {

return (
<React.Fragment>
<StyledChip
<Chip
{...chipProps}
sx={
anchorEl
Expand Down Expand Up @@ -62,28 +62,6 @@ export const ShowMore = <T extends {}>(props: ShowMoreProps<T>) => {
);
};

const StyledChip = styled(Chip)(({ theme }) => ({

@bnussman-akamai bnussman-akamai Apr 16, 2025

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

From what I could tell, this component is essentially visually broken right now, so I just reversed back to using a plain "Chip".

I checked the Akamai design system and didn't see anything resembling this component so I don't really know what we want it to look like. In my opinion, a plain chip is a safe bet for now, but open to feedback here.

'& .MuiChip-label': {
paddingLeft: 6,
paddingRight: 6,
},
'&:focus': {
backgroundColor: theme.bg.lightBlue1,
outline: `1px dotted ${theme.tokens.color.Neutrals[50]}`,
},
'&:hover': {
backgroundColor: theme.palette.primary.main,
color: theme.tokens.color.Neutrals.White,
},
backgroundColor: theme.bg.lightBlue1,
font: theme.font.bold,
lineHeight: 1,
marginLeft: theme.spacing(0.5),
paddingLeft: 2,
paddingRight: 2,
position: 'relative',
}));

const StyledPopover = styled(Popover)(({ theme }) => ({
'& .MuiPopover-paper': {
'&::-webkit-scrollbar': {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import React, { useState } from 'react';
import { Controller, useFormContext, useWatch } from 'react-hook-form';

import { LinkButton } from 'src/components/LinkButton';
import { VPCPublicIPLabel } from 'src/features/VPCs/components/VPCPublicIPLabel';
import {
REGION_CAVEAT_HELPER_TEXT,
VPC_AUTO_ASSIGN_IPV4_TOOLTIP,
Expand All @@ -24,7 +25,6 @@ import { VPCAvailability } from './VPCAvailability';
import { VPCRanges } from './VPCRanges';

import type { LinodeCreateFormValues } from '../utilities';
import { VPCPublicIPLabel } from 'src/features/VPCs/components/VPCPublicIPLabel';

interface Props {
index: number;
Expand Down Expand Up @@ -80,6 +80,7 @@ export const VPC = ({ index }: Props) => {
label="VPC"
loading={isLoading}
noMarginTop
noOptionsText="There are no VPCs in the selected region."
onBlur={field.onBlur}
onChange={(e, vpc) => {
field.onChange(vpc?.id ?? null);
Expand Down
14 changes: 5 additions & 9 deletions packages/manager/src/features/Linodes/LinodeCreate/VPC/VPC.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useAllVPCsQuery, useRegionsQuery } from '@linode/queries';
import { useAllVPCsQuery, useRegionQuery } from '@linode/queries';
import {
Autocomplete,
Box,
Expand All @@ -12,7 +12,6 @@ import {
TooltipIcon,
Typography,
} from '@linode/ui';
import { doesRegionSupportFeature } from '@linode/utilities';
import React, { useState } from 'react';
import { Controller, useFormContext, useWatch } from 'react-hook-form';

Expand All @@ -39,8 +38,6 @@ export const VPC = () => {
const { control, formState, setValue } =
useFormContext<CreateLinodeRequest>();

const { data: regions } = useRegionsQuery();

const [regionId, selectedVPCId, selectedSubnetId, linodeVPCIPAddress] =
useWatch({
control,
Expand All @@ -52,11 +49,9 @@ export const VPC = () => {
],
});

const regionSupportsVPCs = doesRegionSupportFeature(
regionId,
regions ?? [],
'VPCs'
);
const { data: region } = useRegionQuery(regionId);

const regionSupportsVPCs = region?.capabilities.includes('VPCs') ?? false;

const {
data: vpcs,
Expand Down Expand Up @@ -117,6 +112,7 @@ export const VPC = () => {
label="Assign VPC"
loading={isLoading}
noMarginTop
noOptionsText="There are no VPCs in the selected region."
Comment thread
bnussman-akamai marked this conversation as resolved.
onBlur={field.onBlur}
onChange={(e, vpc) => {
field.onChange(vpc?.id ?? null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { TableRow } from 'src/components/TableRow';
import { TableSortCell } from 'src/components/TableSortCell';
import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted';
import { useVPCInterface } from 'src/hooks/useVPCInterface';
import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes';

import { AddIPDrawer } from './AddIPDrawer';
import { DeleteIPDialog } from './DeleteIPDialog';
Expand Down Expand Up @@ -63,6 +64,7 @@ export const LinodeIPAddresses = (props: LinodeIPAddressesProps) => {
const { data: ips, error, isLoading } = useLinodeIPsQuery(linodeID);
const { data: linode } = useLinodeQuery(linodeID);
const { data: regions } = useRegionsQuery();
const { isLinodeInterfacesEnabled } = useIsLinodeInterfacesEnabled();

const linodeIsInDistributedRegion = getIsDistributedRegion(
regions ?? [],
Expand Down Expand Up @@ -161,11 +163,15 @@ export const LinodeIPAddresses = (props: LinodeIPAddressesProps) => {
{isSmallScreen ? (
<ActionMenu
actionsList={[
{
disabled: isLinodesGrantReadOnly,
onClick: () => setIsAddDrawerOpen(true),
title: 'Add an IP Address',
},
...(!isLinodeInterfacesEnabled
? [
{
disabled: isLinodesGrantReadOnly,
onClick: () => setIsAddDrawerOpen(true),
title: 'Add an IP Address',
},
]
: []),
{
disabled: isLinodesGrantReadOnly,
onClick: () => setIsTransferDialogOpen(true),
Expand Down Expand Up @@ -195,13 +201,15 @@ export const LinodeIPAddresses = (props: LinodeIPAddressesProps) => {
>
IP Sharing
</Button>
<Button
buttonType="primary"
disabled={isLinodesGrantReadOnly}
onClick={() => setIsAddDrawerOpen(true)}
>
Add an IP Address
</Button>
{!isLinodeInterfacesEnabled && (
<Button
buttonType="primary"
disabled={isLinodesGrantReadOnly}
onClick={() => setIsAddDrawerOpen(true)}
>
Add an IP Address
</Button>
)}
</Stack>
)}
</Paper>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const VPCDetails = ({ regionId }: Props) => {
label="VPC"
loading={isLoading}
noMarginTop
noOptionsText="You have no VPCs in this Linode's region."
onBlur={field.onBlur}
onChange={(e, vpc) => {
field.onChange(vpc?.id ?? null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import React from 'react';
import { Controller, useFormContext } from 'react-hook-form';

import { ErrorMessage } from 'src/components/ErrorMessage';
import { VPCPublicIPLabel } from 'src/features/VPCs/components/VPCPublicIPLabel';

import type {
Expand All @@ -31,7 +32,11 @@ export const VPCIPv4Address = (props: Props) => {

return (
<Stack spacing={1}>
{error && <Notice text={error} variant="error" />}
{error && (
<Notice variant="error">
<ErrorMessage message={error} />
</Notice>
)}
<Stack spacing={1.5}>
<Controller
control={control}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Notice, Stack, Typography } from '@linode/ui';
import React from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';

import { ErrorMessage } from 'src/components/ErrorMessage';

import { VPCIPv4Address } from './VPCIPv4Address';

import type {
Expand Down Expand Up @@ -35,7 +37,9 @@ export const VPCIPv4Addresses = (props: Props) => {
<Stack spacing={1}>
<Typography variant="h3">IPv4 Addresses</Typography>
{errors.vpc?.ipv4?.addresses?.message && (
<Notice text={errors.vpc?.ipv4?.addresses?.message} variant="error" />
<Notice variant="error">
<ErrorMessage message={errors.vpc?.ipv4?.addresses?.message} />
</Notice>
)}
<Stack spacing={2}>
{fields.map((field, index) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { VPCRangesDescription } from 'src/features/VPCs/components/VPCRangesDesc
import type { ModifyLinodeInterfacePayload } from '@linode/api-v4';

export const VPCIPv4Ranges = () => {
const { control } = useFormContext<ModifyLinodeInterfacePayload>();
const { control, setFocus } = useFormContext<ModifyLinodeInterfacePayload>();
const { fields, remove, append } = useFieldArray({
control,
name: 'vpc.ipv4.ranges',
Expand All @@ -40,14 +40,24 @@ export const VPCIPv4Ranges = () => {
containerProps={{ flexGrow: 1 }}
errorText={fieldState.error?.message}
hideLabel
inputRef={field.ref}
label={`VPC IPv4 Range ${index}`}
onChange={field.onChange}
value={field.value}
/>
)}
/>
<IconButton
onClick={() => remove(index)}
onClick={() => {
remove(index);

const previousRangeIndex = index - 1;

// If there is a previous range, focus it when the current one is removed
if (previousRangeIndex >= 0) {
setFocus(`vpc.ipv4.ranges.${previousRangeIndex}.range`);
}
}}
sx={{ p: 1 }}
title="Remove"
>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Stack, Typography } from '@linode/ui';
import React from 'react';

import { MaskableText } from 'src/components/MaskableText/MaskableText';
import { ShowMore } from 'src/components/ShowMore/ShowMore';

import { getLinodeInterfaceIPs } from './LinodeInterfaceIPs.utils';

import type { LinodeInterface } from '@linode/api-v4';

interface Props {
linodeInterface: LinodeInterface;
}

export const LinodeInterfaceIPs = ({ linodeInterface }: Props) => {
const [primary, ...ips] = getLinodeInterfaceIPs(linodeInterface);

if (!primary && ips.length === 0) {
return <Typography>None</Typography>;
}

return (
<Stack direction="row" spacing={1.5}>
<MaskableText isToggleable text={primary} />
{ips.length > 0 && (
<ShowMore
ariaItemType="IP Address"
items={ips}
render={(ips) => (
<Stack>
{ips.map((ip) => (
<MaskableText isToggleable key={ip} text={ip} />
))}
</Stack>
)}
/>
)}
</Stack>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import {
linodeInterfaceFactoryPublic,
linodeInterfaceFactoryVlan,
linodeInterfaceFactoryVPC,
} from '@linode/utilities';

import { getLinodeInterfaceIPs } from './LinodeInterfaceIPs.utils';

describe('getLinodeInterfaceIPs', () => {
it('should return VPC IPs with the primary IP first', () => {
const linodeInterface = linodeInterfaceFactoryVPC.build({
vpc: {
ipv4: {
addresses: [
{ address: '10.0.0.1' },
{
address: '10.0.0.2',
primary: true,
nat_1_1_address: '255.255.255.255',
},
{ address: '10.0.0.3' },
],
ranges: [{ range: '10.0.0.5/32' }],
},
},
});

const ips = getLinodeInterfaceIPs(linodeInterface);

expect(ips).toStrictEqual([
'10.0.0.2',
'255.255.255.255',
'10.0.0.1',
'10.0.0.3',
'10.0.0.5/32',
]);
});

it('should return Public Interface IPs with the primary IP first', () => {
const linodeInterface = linodeInterfaceFactoryPublic.build({
public: {
ipv4: {
addresses: [
{ address: '10.0.0.1' },
{
address: '10.0.0.2',
primary: true,
},
{ address: '10.0.0.3' },
],
},
ipv6: {
ranges: [{ range: '192.168.1.0/24' }],
shared: [],
slaac: [{ address: '2600:3c11::f03c:93ff:fe3a:130f', prefix: '64' }],
},
},
});

const ips = getLinodeInterfaceIPs(linodeInterface);

expect(ips).toStrictEqual([
'10.0.0.2',
'10.0.0.1',
'10.0.0.3',
'192.168.1.0/24',
'2600:3c11::f03c:93ff:fe3a:130f',
]);
});

it('should return an empty array for VLAN without a IPAM Address', () => {
const linodeInterface = linodeInterfaceFactoryVlan.build({
vlan: {
ipam_address: '',
vlan_label: 'vlan-1',
},
});

const ips = getLinodeInterfaceIPs(linodeInterface);

expect(ips).toStrictEqual([]);
});

it('should return an empty with the IPAM Addresss for VLAN with an IPAM Address', () => {
const linodeInterface = linodeInterfaceFactoryVlan.build({
vlan: {
ipam_address: '192.168.21.34',
vlan_label: 'vlan-1',
},
});

const ips = getLinodeInterfaceIPs(linodeInterface);

expect(ips).toStrictEqual(['192.168.21.34']);
});
});
Loading