Skip to content

Commit

Permalink
feat: [M3-6731] – Add VPC and Firewall sections in Linode Create flow (
Browse files Browse the repository at this point in the history
  • Loading branch information
dwiley-akamai authored Sep 28, 2023
1 parent df005bc commit d642c62
Show file tree
Hide file tree
Showing 20 changed files with 904 additions and 46 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/api-v4": Upcoming Features
---

VPCs added to region Capabilities type ([#9635](https://github.com/linode/manager/pull/9635))
12 changes: 6 additions & 6 deletions packages/api-v4/src/account/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,16 @@ export interface Account {
export type BillingSource = 'linode' | 'akamai';

export type AccountCapability =
| 'Linodes'
| 'NodeBalancers'
| 'Block Storage'
| 'Object Storage'
| 'Kubernetes'
| 'Cloud Firewall'
| 'Vlans'
| 'Machine Images'
| 'Kubernetes'
| 'Linodes'
| 'LKE HA Control Planes'
| 'Machine Images'
| 'Managed Databases'
| 'NodeBalancers'
| 'Object Storage'
| 'Vlans'
| 'VPCs';

export interface AccountSettings {
Expand Down
15 changes: 8 additions & 7 deletions packages/api-v4/src/regions/types.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
export type Capabilities =
| 'Bare Metal'
| 'Block Storage'
| 'Cloud Firewall'
| 'GPU Linodes'
| 'Kubernetes'
| 'Linodes'
| 'Metadata'
| 'NodeBalancers'
| 'Block Storage'
| 'Object Storage'
| 'Kubernetes'
| 'GPU Linodes'
| 'Cloud Firewall'
| 'Premium Plans'
| 'Vlans'
| 'Bare Metal'
| 'Metadata'
| 'Premium Plans';
| 'VPCs';

export interface DNSResolvers {
ipv4: string; // Comma-separated IP addresses
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

VPC and Firewall assignment within Linode Create flow ([#9635](https://github.com/linode/manager/pull/9635))
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { waitFor } from '@testing-library/react';
import * as React from 'react';
import { QueryClient } from 'react-query';

import { mockMatchMedia, renderWithTheme } from 'src/utilities/testHelpers';

import { SelectFirewallPanel } from './SelectFirewallPanel';

const queryClient = new QueryClient();

beforeAll(() => mockMatchMedia());
afterEach(() => {
queryClient.clear();
});

const testId = 'select-firewall-panel';

describe('SelectFirewallPanel', () => {
it('should render', async () => {
const wrapper = renderWithTheme(
<SelectFirewallPanel
handleFirewallChange={jest.fn()}
helperText={<span>Testing</span>}
/>,
{
queryClient,
}
);

await waitFor(() => {
expect(wrapper.getByTestId(testId)).toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import Stack from '@mui/material/Stack';
import * as React from 'react';

import Select from 'src/components/EnhancedSelect';
import { Paper } from 'src/components/Paper';
import { Typography } from 'src/components/Typography';
import { APP_ROOT } from 'src/constants';
import { useFirewallsQuery } from 'src/queries/firewalls';

import { StyledCreateLink } from '../../features/Linodes/LinodesCreate/LinodeCreate.styles';

interface Props {
handleFirewallChange: (firewallID: number) => void;
helperText: JSX.Element;
}

export const SelectFirewallPanel = (props: Props) => {
const { handleFirewallChange, helperText } = props;

const { data: firewallsData, error, isLoading } = useFirewallsQuery();

const firewalls = firewallsData?.data ?? [];
const firewallsDropdownOptions = firewalls.map((firewall) => ({
label: firewall.label,
value: firewall.id,
}));

firewallsDropdownOptions.unshift({
label: 'None',
value: -1,
});

return (
<Paper
data-testid="select-firewall-panel"
sx={(theme) => ({ marginTop: theme.spacing(3) })}
>
<Typography
sx={(theme) => ({ marginBottom: theme.spacing(2) })}
variant="h2"
>
Firewall
</Typography>
<Stack>
{helperText}
<Select
defaultValue={firewallsDropdownOptions[0]}
errorText={error?.[0].reason}
isClearable={false}
isLoading={isLoading}
label="Assign Firewall"
noOptionsMessage={() => 'Create a Firewall to assign to this Linode.'}
onChange={(selection) => handleFirewallChange(selection.value)}
options={firewallsDropdownOptions}
placeholder={''}
/>
<StyledCreateLink to={`${APP_ROOT}/firewalls/create`}>
Create Firewall
</StyledCreateLink>
</Stack>
</Paper>
);
};
23 changes: 23 additions & 0 deletions packages/manager/src/containers/account.container.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Account } from '@linode/api-v4/lib';
import { APIError } from '@linode/api-v4/lib/types';
import * as React from 'react';
import { UseQueryResult } from 'react-query';

import { useAccount } from 'src/queries/account';

export interface WithAccountProps {
account: UseQueryResult<Account, APIError[]>;
}

export const withAccount = <Props>(
Component: React.ComponentType<Props & WithAccountProps>
) => {
return (props: Props) => {
const account = useAccount();

return React.createElement(Component, {
...props,
account,
});
};
};
1 change: 1 addition & 0 deletions packages/manager/src/featureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export interface Flags {
dcSpecificPricing: boolean;
ipv6Sharing: boolean;
kubernetesDashboardAvailability: boolean;
linodeCreateWithFirewall: boolean;
mainContentBanner: MainContentBanner;
metadata: boolean;
oneClickApps: OneClickApp;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { waitFor } from '@testing-library/react';
import React from 'react';

import { linodeTypeFactory } from 'src/factories/linodes';
Expand Down Expand Up @@ -166,6 +167,10 @@ const props: AddonsPanelProps = {
vlanLabel: 'abc',
};

const privateIPContextualCopyTestId = 'private-ip-contextual-copy';
const vlanAccordionTestId = 'vlan-accordion';
const attachVLANTestId = 'attach-vlan';

describe('AddonsPanel', () => {
it('should render AddonsPanel', () => {
renderWithTheme(<AddonsPanel {...props} />);
Expand Down Expand Up @@ -245,4 +250,56 @@ describe('AddonsPanel', () => {

expect(getByText(/\$3.57/)).toBeInTheDocument();
});

it('should display the VLANAccordion component instead of the AttachVLAN component when VPCs are viewable in the flow', async () => {
const _props: AddonsPanelProps = { ...props, createType: 'fromImage' };

const wrapper = renderWithTheme(<AddonsPanel {..._props} />, {
flags: { vpc: true },
});

await waitFor(() => {
expect(wrapper.getByTestId(vlanAccordionTestId)).toBeInTheDocument();
expect(wrapper.queryByTestId(attachVLANTestId)).not.toBeInTheDocument();
});
});

it('should display the AttachVLAN component instead of the VLANAccordion component when VPCs are not viewable in the flow', async () => {
const _props: AddonsPanelProps = { ...props, createType: 'fromImage' };

const wrapper = renderWithTheme(<AddonsPanel {..._props} />, {
flags: { vpc: false },
});

await waitFor(() => {
expect(wrapper.getByTestId(attachVLANTestId)).toBeInTheDocument();
expect(
wrapper.queryByTestId(vlanAccordionTestId)
).not.toBeInTheDocument();
});
});

it('should have contextual copy for the Private IP add-on when VPC is enabled', async () => {
const wrapper = renderWithTheme(<AddonsPanel {...props} />, {
flags: { vpc: true },
});

await waitFor(() => {
expect(
wrapper.getByTestId(privateIPContextualCopyTestId)
).toBeInTheDocument();
});
});

it('should not have contextual copy for the Private IP add-on if VPC is not enabled', async () => {
const wrapper = renderWithTheme(<AddonsPanel {...props} />, {
flags: { vpc: false },
});

await waitFor(() => {
expect(
wrapper.queryByTestId(privateIPContextualCopyTestId)
).not.toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ import { TooltipIcon } from 'src/components/TooltipIcon';
import { Typography } from 'src/components/Typography';
import { UserDataAccordionProps } from 'src/features/Linodes/LinodesCreate/UserDataAccordion/UserDataAccordion';
import { useFlags } from 'src/hooks/useFlags';
import { useAccount } from 'src/queries/account';
import { useImageQuery } from 'src/queries/images';
import { CreateTypes } from 'src/store/linodeCreate/linodeCreate.actions';
import { isFeatureEnabled } from 'src/utilities/accountCapabilities';
import { privateIPRegex } from 'src/utilities/ipUtils';

import { AttachVLAN } from './AttachVLAN';
Expand Down Expand Up @@ -72,6 +74,7 @@ export const AddonsPanel = React.memo((props: AddonsPanelProps) => {

const theme = useTheme();
const flags = useFlags();
const { data: account } = useAccount();

const { data: image } = useImageQuery(
selectedImageID ?? '',
Expand Down Expand Up @@ -124,6 +127,12 @@ export const AddonsPanel = React.memo((props: AddonsPanelProps) => {
// The backups warning is shown when the user checks to enable backups, but they are using a custom image that may not be compatible.
const showBackupsWarning = checkBackupsWarning();

const showVPCs = isFeatureEnabled(
'VPCs',
Boolean(flags.vpc),
account?.capabilities ?? []
);

// Check whether the source Linode has been allocated a private IP to select/unselect the 'Private IP' checkbox.
React.useEffect(() => {
if (selectedLinodeID) {
Expand All @@ -144,8 +153,8 @@ export const AddonsPanel = React.memo((props: AddonsPanelProps) => {

return (
<>
{!flags.vpc &&
showVlans && ( // @TODO Delete this conditional and AttachVLAN component once VPC is released
{!showVPCs &&
showVlans && ( // @TODO VPC: Delete this conditional and AttachVLAN component once VPC is fully rolled out
<AttachVLAN
handleVLANChange={handleVLANChange}
helperText={vlanDisabledReason}
Expand All @@ -157,7 +166,7 @@ export const AddonsPanel = React.memo((props: AddonsPanelProps) => {
vlanLabel={vlanLabel}
/>
)}
{flags.vpc && showVlans && (
{showVPCs && showVlans && (
<VLANAccordion
handleVLANChange={handleVLANChange}
helperText={vlanDisabledReason}
Expand Down Expand Up @@ -236,6 +245,15 @@ export const AddonsPanel = React.memo((props: AddonsPanelProps) => {
}
label="Private IP"
/>
{showVPCs && (
<StyledTypography
data-testid="private-ip-contextual-copy"
variant="body1"
>
Use this for a backend node to a NodeBalancer. Use VPC instead for
private communication between your Linodes.
</StyledTypography>
)}
</Paper>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,11 @@ export const AttachVLAN = React.memo((props: Props) => {
)}.`;

return (
<Paper sx={{ marginTop: theme.spacing(3) }} data-qa-add-ons>
<Paper
data-qa-add-ons
data-testid="attach-vlan"
sx={{ marginTop: theme.spacing(3) }}
>
<Typography
sx={{
'& button': {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { styled } from '@mui/material/styles';
import { isPropValid } from 'src/utilities/isPropValid';

import { Box } from 'src/components/Box';
import { Button } from 'src/components/Button/Button';
import { Link } from 'src/components/Link';
import { Paper } from 'src/components/Paper';
import type { LinodeCreateProps } from './LinodeCreate';
import { TabPanels } from 'src/components/ReachTabPanels';
import { isPropValid } from 'src/utilities/isPropValid';

import type { LinodeCreateProps } from './LinodeCreate';

type StyledLinodeCreateProps = Pick<LinodeCreateProps, 'showAgreement'>;

Expand Down Expand Up @@ -33,7 +36,7 @@ export const StyledForm = styled('form', { label: 'StyledForm' })({
export const StyledMessageDiv = styled('div', {
label: 'StyledMessageDiv',
shouldForwardProp: (prop) => isPropValid(['showAgreement'], prop),
})<StyledLinodeCreateProps>(({ theme, showAgreement }) => ({
})<StyledLinodeCreateProps>(({ showAgreement, theme }) => ({
display: 'flex',
flexDirection: 'column' as const,
flexGrow: 1,
Expand Down Expand Up @@ -69,3 +72,13 @@ export const StyledTabPanel = styled(TabPanels, { label: 'StyledTabPanel' })(
},
})
);

// Currently used in VPC and Firewall panels
export const StyledCreateLink = styled(Link, {
label: 'StyledCreateLink',
})(({ theme }) => ({
fontSize: '14px',
marginBottom: theme.spacing(2),
marginTop: theme.spacing(1.5),
width: '100px',
}));
Loading

0 comments on commit d642c62

Please sign in to comment.