Skip to content

Commit 9a29bdc

Browse files
feat: [M3-9890] - Support Akamai Employee Banners on the Linode Create flow when using Linode Interfaces (#12143)
* inital support * clean up * add to changelog * account for vlan linode interfaces * feedback @hkhalil-akamai * feedback @mjac0bs --------- Co-authored-by: Banks Nussman <[email protected]>
1 parent bb8ad10 commit 9a29bdc

File tree

8 files changed

+166
-47
lines changed

8 files changed

+166
-47
lines changed

packages/manager/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
101101
- Add non-dismissible option support to Dismissible Banner ([#12115](https://github.com/linode/manager/pull/12115))
102102
- Add mocks and update `PlansPanel` to support `mtc-tt-2025` plans in selected regions (#12050)
103103
- IAM RBAC: Implement method to merge user-selected roles into existing roles ([#12125](https://github.com/linode/manager/pull/12125))
104+
- Support Akamai Employee Banners on the Linode Create flow when using Linode Interfaces ([#12143](https://github.com/linode/manager/pull/12143))
104105
- Add navigation to Linode Network tab after successfully upgrading interfaces ([#12146](https://github.com/linode/manager/pull/12146))
105106
- Fix "Error retrieving Firewalls" message in Subnet Linode Row after upgrading interfaces ([#12146](https://github.com/linode/manager/pull/12146))
106107

packages/manager/src/features/Linodes/LinodeCreate/Actions.tsx

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Box, Button } from '@linode/ui';
22
import { scrollErrorIntoView } from '@linode/utilities';
33
import React, { useState } from 'react';
4-
import { useFormContext } from 'react-hook-form';
4+
import { useFormContext, useWatch } from 'react-hook-form';
55

66
import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck';
77
import { sendApiAwarenessClickEvent } from 'src/utilities/analytics/customEventAnalytics';
@@ -22,18 +22,41 @@ export const Actions = () => {
2222

2323
const { isLinodeInterfacesEnabled } = useIsLinodeInterfacesEnabled();
2424

25-
const {
26-
formState,
27-
getValues,
28-
trigger,
29-
} = useFormContext<LinodeCreateFormValues>();
25+
const { formState, getValues, trigger, control } =
26+
useFormContext<LinodeCreateFormValues>();
3027

3128
const isLinodeCreateRestricted = useRestrictedGlobalGrantCheck({
3229
globalGrantType: 'add_linodes',
3330
});
3431

32+
const [
33+
legacyFirewallId,
34+
firstLinodeInterfaceFirewallId,
35+
firstLinodeInterfaceType,
36+
interfaceGeneration,
37+
] = useWatch({
38+
name: [
39+
'firewall_id',
40+
'linodeInterfaces.0.firewall_id',
41+
'linodeInterfaces.0.purpose',
42+
'interface_generation',
43+
],
44+
control,
45+
});
46+
47+
const firewallId =
48+
interfaceGeneration === 'linode'
49+
? firstLinodeInterfaceFirewallId
50+
: legacyFirewallId;
51+
52+
const userNeedsToTakeActionAboutInternalFirewallPolicy =
53+
interfaceGeneration === 'linode' && firstLinodeInterfaceType === 'vlan'
54+
? false
55+
: 'firewallOverride' in formState.errors && !firewallId;
56+
3557
const disableSubmitButton =
36-
isLinodeCreateRestricted || 'firewallOverride' in formState.errors;
58+
isLinodeCreateRestricted ||
59+
userNeedsToTakeActionAboutInternalFirewallPolicy;
3760

3861
const onOpenAPIAwareness = async () => {
3962
sendApiAwarenessClickEvent('Button', 'View Code Snippets');

packages/manager/src/features/Linodes/LinodeCreate/Firewall.tsx

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Box, Paper, Stack, Typography } from '@linode/ui';
22
import React, { useState } from 'react';
3-
import { useController, useFormContext } from 'react-hook-form';
3+
import { useController } from 'react-hook-form';
44

55
import { AkamaiBanner } from 'src/components/AkamaiBanner/AkamaiBanner';
66
import { GenerateFirewallDialog } from 'src/components/GenerateFirewallDialog/GenerateFirewallDialog';
@@ -16,12 +16,10 @@ import { sendLinodeCreateFormInputEvent } from 'src/utilities/analytics/formEven
1616

1717
import { useLinodeCreateQueryParams } from './utilities';
1818

19-
import type { LinodeCreateFormValues } from './utilities';
2019
import type { CreateLinodeRequest } from '@linode/api-v4';
2120
import type { LinodeCreateFormEventOptions } from 'src/utilities/analytics/types';
2221

2322
export const Firewall = () => {
24-
const { clearErrors } = useFormContext<LinodeCreateFormValues>();
2523
const { field, fieldState } = useController<
2624
CreateLinodeRequest,
2725
'firewall_id'
@@ -42,13 +40,6 @@ export const Firewall = () => {
4240
globalGrantType: 'add_linodes',
4341
});
4442

45-
const onChange = (firewallId: number | undefined) => {
46-
if (firewallId !== undefined) {
47-
clearErrors('firewallOverride');
48-
}
49-
field.onChange(firewallId);
50-
};
51-
5243
const firewallFormEventOptions: LinodeCreateFormEventOptions = {
5344
createType: params.type ?? 'OS',
5445
headerName: 'Firewall',
@@ -95,7 +86,7 @@ export const Firewall = () => {
9586
<Stack spacing={1.5}>
9687
<FirewallSelect
9788
onChange={(e, firewall) => {
98-
onChange(firewall?.id);
89+
field.onChange(firewall?.id);
9990
if (!firewall?.id) {
10091
sendLinodeCreateFormInputEvent({
10192
...firewallFormEventOptions,
@@ -141,11 +132,13 @@ export const Firewall = () => {
141132
onFirewallCreated={(firewall) => field.onChange(firewall.id)}
142133
open={isDrawerOpen}
143134
/>
144-
<GenerateFirewallDialog
145-
onClose={() => setIsGenerateDialogOpen(false)}
146-
onFirewallGenerated={(firewall) => onChange(firewall.id)}
147-
open={isGenerateDialogOpen}
148-
/>
135+
{secureVMNoticesEnabled && (
136+
<GenerateFirewallDialog
137+
onClose={() => setIsGenerateDialogOpen(false)}
138+
onFirewallGenerated={(firewall) => field.onChange(firewall.id)}
139+
open={isGenerateDialogOpen}
140+
/>
141+
)}
149142
</Paper>
150143
);
151144
};

packages/manager/src/features/Linodes/LinodeCreate/FirewallAuthorization.tsx

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Checkbox, FormControlLabel } from '@linode/ui';
2-
import { isNotNullOrUndefined } from '@linode/utilities';
32
import React from 'react';
4-
import { useController, useFormContext } from 'react-hook-form';
3+
import { useController, useFormContext, useWatch } from 'react-hook-form';
54

65
import { AkamaiBanner } from 'src/components/AkamaiBanner/AkamaiBanner';
76
import { useFlags } from 'src/hooks/useFlags';
@@ -10,33 +9,57 @@ import type { LinodeCreateFormValues } from './utilities';
109

1110
export const FirewallAuthorization = () => {
1211
const flags = useFlags();
13-
const { control, watch } = useFormContext<LinodeCreateFormValues>();
12+
13+
const { control } = useFormContext<LinodeCreateFormValues>();
14+
1415
const { field, fieldState } = useController({
1516
control,
1617
name: 'firewallOverride',
1718
});
1819

19-
const watchFirewall = watch('firewall_id');
20+
const [
21+
legacyFirewallId,
22+
firstLinodeInterfaceFirewallId,
23+
firstLinodeInterfaceType,
24+
interfaceGeneration,
25+
] = useWatch({
26+
name: [
27+
'firewall_id',
28+
'linodeInterfaces.0.firewall_id',
29+
'linodeInterfaces.0.purpose',
30+
'interface_generation',
31+
],
32+
control,
33+
});
34+
35+
// Special case ❗️
36+
// VLAN interfaces do not support Firewalls, so we hide this notice
37+
// if that's what the user selects.
38+
if (firstLinodeInterfaceType === 'vlan' && interfaceGeneration === 'linode') {
39+
return null;
40+
}
41+
42+
const firewallId =
43+
interfaceGeneration === 'linode'
44+
? firstLinodeInterfaceFirewallId
45+
: legacyFirewallId;
2046

21-
if (
22-
isNotNullOrUndefined(watchFirewall) ||
23-
!(fieldState.isDirty || fieldState.error)
24-
) {
25-
return;
47+
if (firewallId || !(fieldState.isDirty || fieldState.error)) {
48+
return null;
2649
}
2750

2851
return (
2952
<AkamaiBanner
3053
action={
3154
<FormControlLabel
32-
label={
33-
flags.secureVmCopy?.firewallAuthorizationLabel ??
34-
'I am authorized to create a Linode without a firewall'
35-
}
3655
checked={field.value ?? false}
3756
className="error-for-scroll"
3857
control={<Checkbox />}
3958
disableTypography
59+
label={
60+
flags.secureVmCopy?.firewallAuthorizationLabel ??
61+
'I am authorized to create a Linode without a firewall'
62+
}
4063
onChange={field.onChange}
4164
sx={{ fontSize: 14 }}
4265
/>

packages/manager/src/features/Linodes/LinodeCreate/Networking/Firewall.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@ import { Box, Stack } from '@linode/ui';
22
import React, { useState } from 'react';
33
import { useController } from 'react-hook-form';
44

5+
import { AkamaiBanner } from 'src/components/AkamaiBanner/AkamaiBanner';
6+
import { GenerateFirewallDialog } from 'src/components/GenerateFirewallDialog/GenerateFirewallDialog';
57
import { LinkButton } from 'src/components/LinkButton';
68
import { FirewallSelect } from 'src/features/Firewalls/components/FirewallSelect';
79
import { CreateFirewallDrawer } from 'src/features/Firewalls/FirewallLanding/CreateFirewallDrawer';
10+
import { useFlags } from 'src/hooks/useFlags';
811
import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck';
12+
import { useSecureVMNoticesEnabled } from 'src/hooks/useSecureVMNoticesEnabled';
913

1014
import type { LinodeCreateFormValues } from '../utilities';
1115

@@ -17,6 +21,13 @@ export const Firewall = () => {
1721
name: 'firewall_id',
1822
});
1923

24+
const { secureVMNoticesEnabled } = useSecureVMNoticesEnabled();
25+
const flags = useFlags();
26+
27+
const [
28+
isGenerateAkamaiEmployeeFirewallDialogOpen,
29+
setIsGenerateAkamaiEmployeeFirewallDialogOpen,
30+
] = useState(false);
2031
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
2132

2233
const isLinodeCreateRestricted = useRestrictedGlobalGrantCheck({
@@ -26,6 +37,24 @@ export const Firewall = () => {
2637
return (
2738
<Stack spacing={2}>
2839
<Stack spacing={1.5}>
40+
{secureVMNoticesEnabled && (
41+
<AkamaiBanner
42+
action={
43+
<LinkButton
44+
onClick={() =>
45+
setIsGenerateAkamaiEmployeeFirewallDialogOpen(true)
46+
}
47+
>
48+
{flags.secureVmCopy?.generateActionText ??
49+
'Generate Compliant Firewall'}
50+
</LinkButton>
51+
}
52+
text={
53+
flags.secureVmCopy?.linodeCreate?.text ??
54+
'All accounts must apply an compliant firewall to all their Linodes.'
55+
}
56+
/>
57+
)}
2958
<FirewallSelect
3059
disabled={isLinodeCreateRestricted}
3160
errorText={fieldState.error?.message}
@@ -49,6 +78,13 @@ export const Firewall = () => {
4978
onFirewallCreated={(firewall) => field.onChange(firewall.id)}
5079
open={isDrawerOpen}
5180
/>
81+
{secureVMNoticesEnabled && (
82+
<GenerateFirewallDialog
83+
onClose={() => setIsGenerateAkamaiEmployeeFirewallDialogOpen(false)}
84+
onFirewallGenerated={(firewall) => field.onChange(firewall.id)}
85+
open={isGenerateAkamaiEmployeeFirewallDialogOpen}
86+
/>
87+
)}
5288
</Stack>
5389
);
5490
};

packages/manager/src/features/Linodes/LinodeCreate/Networking/InterfaceFirewall.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@ import { Box, Stack } from '@linode/ui';
22
import React, { useState } from 'react';
33
import { useController, useFormContext, useWatch } from 'react-hook-form';
44

5+
import { AkamaiBanner } from 'src/components/AkamaiBanner/AkamaiBanner';
6+
import { GenerateFirewallDialog } from 'src/components/GenerateFirewallDialog/GenerateFirewallDialog';
57
import { LinkButton } from 'src/components/LinkButton';
68
import { FirewallSelect } from 'src/features/Firewalls/components/FirewallSelect';
79
import { CreateFirewallDrawer } from 'src/features/Firewalls/FirewallLanding/CreateFirewallDrawer';
10+
import { useFlags } from 'src/hooks/useFlags';
811
import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck';
12+
import { useSecureVMNoticesEnabled } from 'src/hooks/useSecureVMNoticesEnabled';
913

1014
import type { LinodeCreateFormValues } from '../utilities';
1115

@@ -14,6 +18,14 @@ interface Props {
1418
}
1519

1620
export const InterfaceFirewall = ({ index }: Props) => {
21+
const { secureVMNoticesEnabled } = useSecureVMNoticesEnabled();
22+
const flags = useFlags();
23+
24+
const [
25+
isGenerateAkamaiEmployeeFirewallDialogOpen,
26+
setIsGenerateAkamaiEmployeeFirewallDialogOpen,
27+
] = useState(false);
28+
1729
const { control } = useFormContext<LinodeCreateFormValues>();
1830

1931
const interfaceType = useWatch({
@@ -35,6 +47,24 @@ export const InterfaceFirewall = ({ index }: Props) => {
3547
return (
3648
<Stack spacing={2}>
3749
<Stack spacing={1.5}>
50+
{secureVMNoticesEnabled && (
51+
<AkamaiBanner
52+
action={
53+
<LinkButton
54+
onClick={() =>
55+
setIsGenerateAkamaiEmployeeFirewallDialogOpen(true)
56+
}
57+
>
58+
{flags.secureVmCopy?.generateActionText ??
59+
'Generate Compliant Firewall'}
60+
</LinkButton>
61+
}
62+
text={
63+
flags.secureVmCopy?.linodeCreate?.text ??
64+
'All accounts must apply an compliant firewall to all their Linodes.'
65+
}
66+
/>
67+
)}
3868
<FirewallSelect
3969
disabled={isLinodeCreateRestricted}
4070
errorText={fieldState.error?.message}
@@ -59,6 +89,13 @@ export const InterfaceFirewall = ({ index }: Props) => {
5989
onFirewallCreated={(firewall) => field.onChange(firewall.id)}
6090
open={isDrawerOpen}
6191
/>
92+
{secureVMNoticesEnabled && (
93+
<GenerateFirewallDialog
94+
onClose={() => setIsGenerateAkamaiEmployeeFirewallDialogOpen(false)}
95+
onFirewallGenerated={(firewall) => field.onChange(firewall.id)}
96+
open={isGenerateAkamaiEmployeeFirewallDialogOpen}
97+
/>
98+
)}
6299
</Stack>
63100
);
64101
};

packages/manager/src/features/Linodes/LinodeCreate/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ export const LinodeCreate = () => {
265265
<EUAgreement />
266266
<Summary />
267267
<SMTP />
268-
<FirewallAuthorization />
268+
{secureVMNoticesEnabled && <FirewallAuthorization />}
269269
<Actions />
270270
</Stack>
271271
</form>

packages/manager/src/features/Linodes/LinodeCreate/resolvers.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { yupResolver } from '@hookform/resolvers/yup';
22
import { accountQueries, regionQueries } from '@linode/queries';
3-
import { isNullOrUndefined } from '@linode/utilities';
43
import type { FieldErrors, Resolver } from 'react-hook-form';
54

65
import { getRegionCountryGroup, isEURegion } from 'src/utilities/formatRegion';
@@ -88,15 +87,22 @@ export const getLinodeCreateResolver = (
8887
}
8988
}
9089

91-
const secureVMViolation =
92-
context?.secureVMNoticesEnabled &&
93-
!values.firewallOverride &&
94-
isNullOrUndefined(values.firewall_id);
95-
96-
if (secureVMViolation) {
97-
(errors as FieldErrors<LinodeCreateFormValues>)['firewallOverride'] = {
98-
type: 'validate',
99-
};
90+
// If we're dealing with an employee account and they did not bypass
91+
// the firewall banner....
92+
if (context?.secureVMNoticesEnabled && !values.firewallOverride) {
93+
// Get the selected Firewall ID depending on what Interface Generation is selected
94+
const firewallId =
95+
values.interface_generation === 'linode'
96+
? values.linodeInterfaces[0].firewall_id
97+
: values.firewall_id;
98+
99+
if (!firewallId) {
100+
(errors as FieldErrors<LinodeCreateFormValues>)['firewallOverride'] = {
101+
// This message does not get surfaced, see FirewallAuthorization.tsx
102+
message: 'You must select a Firewall or bypass the Firewall policy.',
103+
type: 'validate',
104+
};
105+
}
100106
}
101107

102108
if (errors) {

0 commit comments

Comments
 (0)