Skip to content

Commit

Permalink
upcoming: [M3-8074] – Add "Disk Encryption" section to Linode Create …
Browse files Browse the repository at this point in the history
…flow (#10462)
  • Loading branch information
dwiley-akamai authored May 21, 2024
1 parent b64491f commit 2579ad8
Show file tree
Hide file tree
Showing 18 changed files with 324 additions and 24 deletions.
5 changes: 5 additions & 0 deletions packages/api-v4/.changeset/pr-10462-changed-1715896838291.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/api-v4": Changed
---

Add Disk Encryption to AccountCapability type and region Capabilities type ([#10462](https://github.com/linode/manager/pull/10462))
1 change: 1 addition & 0 deletions packages/api-v4/src/account/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export type AccountCapability =
| 'Akamai Cloud Load Balancer'
| 'Block Storage'
| 'Cloud Firewall'
| 'Disk Encryption'
| 'Kubernetes'
| 'Linodes'
| 'LKE HA Control Planes'
Expand Down
1 change: 1 addition & 0 deletions packages/api-v4/src/regions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type Capabilities =
| 'Block Storage'
| 'Block Storage Migrations'
| 'Cloud Firewall'
| 'Disk Encryption'
| 'GPU Linodes'
| 'Kubernetes'
| 'Linodes'
Expand Down
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-10462-tests-1716303824484.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Tests
---

Add Cypress test coverage for Disk Encryption in Linode Create flow ([#10462](https://github.com/linode/manager/pull/10462))
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Add Disk Encryption section to Linode Create flow ([#10462](https://github.com/linode/manager/pull/10462))
76 changes: 76 additions & 0 deletions packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
VLANFactory,
LinodeConfigInterfaceFactory,
LinodeConfigInterfaceFactoryWithVPC,
accountFactory,
} from '@src/factories';
import { authenticate } from 'support/api/authentication';
import { cleanUp } from 'support/util/cleanup';
Expand All @@ -44,8 +45,13 @@ import {
mockGetFeatureFlagClientstream,
} from 'support/intercepts/feature-flags';
import { makeFeatureFlagData } from 'support/util/feature-flags';
import {
checkboxTestId,
headerTestId,
} from 'src/components/DiskEncryption/DiskEncryption';

import type { Config, VLAN, Disk, Region } from '@linode/api-v4';
import { mockGetAccount } from 'support/intercepts/account';

const mockRegions: Region[] = [
regionFactory.build({
Expand Down Expand Up @@ -511,4 +517,74 @@ describe('create linode', () => {
containsVisible(`eth2 – VPC: ${mockVPC.label}`);
});
});

it('should not have a "Disk Encryption" section visible if the feature flag is off and user does not have capability', () => {
// Mock feature flag -- @TODO LDE: Remove feature flag once LDE is fully rolled out
mockAppendFeatureFlags({
linodeDiskEncryption: makeFeatureFlagData(false),
}).as('getFeatureFlags');
mockGetFeatureFlagClientstream().as('getClientStream');

// Mock account response
const mockAccount = accountFactory.build({
capabilities: ['Linodes'],
});

mockGetAccount(mockAccount).as('getAccount');

// intercept request
cy.visitWithLogin('/linodes/create');
cy.wait(['@getFeatureFlags', '@getClientStream', '@getAccount']);

// Check if section is visible
cy.get(`[data-testid=${headerTestId}]`).should('not.exist');
});

it('should have a "Disk Encryption" section visible if feature flag is on and user has the capability', () => {
// Mock feature flag -- @TODO LDE: Remove feature flag once LDE is fully rolled out
mockAppendFeatureFlags({
linodeDiskEncryption: makeFeatureFlagData(true),
}).as('getFeatureFlags');
mockGetFeatureFlagClientstream().as('getClientStream');

// Mock account response
const mockAccount = accountFactory.build({
capabilities: ['Linodes', 'Disk Encryption'],
});

const mockRegion = regionFactory.build({
capabilities: ['Linodes', 'Disk Encryption'],
});

const mockRegionWithoutDiskEncryption = regionFactory.build({
capabilities: ['Linodes'],
});

const mockRegions = [mockRegion, mockRegionWithoutDiskEncryption];

mockGetAccount(mockAccount).as('getAccount');
mockGetRegions(mockRegions);

// intercept request
cy.visitWithLogin('/linodes/create');
cy.wait(['@getFeatureFlags', '@getClientStream', '@getAccount']);

// Check if section is visible
cy.get(`[data-testid="${headerTestId}"]`).should('exist');

// "Encrypt Disk" checkbox should be disabled if a region that does not support LDE is selected
ui.regionSelect.find().click();
ui.select
.findItemByText(
`${mockRegionWithoutDiskEncryption.label} (${mockRegionWithoutDiskEncryption.id})`
)
.click();

cy.get(`[data-testid="${checkboxTestId}"]`).should('be.disabled');

ui.regionSelect.find().click();
ui.select.findItemByText(`${mockRegion.label} (${mockRegion.id})`).click();

cy.get(`[data-testid="${checkboxTestId}"]`).should('be.enabled');
});
});
61 changes: 61 additions & 0 deletions packages/manager/src/components/AccessPanel/AccessPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,17 @@ import { Theme } from '@mui/material/styles';
import * as React from 'react';
import { makeStyles } from 'tss-react/mui';

import {
DISK_ENCRYPTION_GENERAL_DESCRIPTION,
DISK_ENCRYPTION_UNAVAILABLE_IN_REGION_COPY,
} from 'src/components/DiskEncryption/constants';
import { DiskEncryption } from 'src/components/DiskEncryption/DiskEncryption';
import { useIsDiskEncryptionFeatureEnabled } from 'src/components/DiskEncryption/utils';
import { Paper } from 'src/components/Paper';
import { SuspenseLoader } from 'src/components/SuspenseLoader';
import { Typography } from 'src/components/Typography';
import { useRegionsQuery } from 'src/queries/regions/regions';
import { doesRegionSupportFeature } from 'src/utilities/doesRegionSupportFeature';

import { Divider } from '../Divider';
import UserSSHKeyPanel from './UserSSHKeyPanel';
Expand Down Expand Up @@ -31,6 +40,8 @@ interface Props {
className?: string;
disabled?: boolean;
disabledReason?: JSX.Element | string;
diskEncryptionEnabled?: boolean;
displayDiskEncryption?: boolean;
error?: string;
handleChange: (value: string) => void;
heading?: string;
Expand All @@ -41,8 +52,10 @@ interface Props {
passwordHelperText?: string;
placeholder?: string;
required?: boolean;
selectedRegion?: string;
setAuthorizedUsers?: (usernames: string[]) => void;
small?: boolean;
toggleDiskEncryptionEnabled?: () => void;
tooltipInteractive?: boolean;
}

Expand All @@ -52,6 +65,8 @@ export const AccessPanel = (props: Props) => {
className,
disabled,
disabledReason,
diskEncryptionEnabled,
displayDiskEncryption,
error,
handleChange: _handleChange,
hideStrengthLabel,
Expand All @@ -61,15 +76,52 @@ export const AccessPanel = (props: Props) => {
passwordHelperText,
placeholder,
required,
selectedRegion,
setAuthorizedUsers,
toggleDiskEncryptionEnabled,
tooltipInteractive,
} = props;

const { classes, cx } = useStyles();

const {
isDiskEncryptionFeatureEnabled,
} = useIsDiskEncryptionFeatureEnabled();

const regions = useRegionsQuery().data ?? [];

const regionSupportsDiskEncryption = doesRegionSupportFeature(
selectedRegion ?? '',
regions,
'Disk Encryption'
);

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) =>
_handleChange(e.target.value);

/**
* Display the "Disk Encryption" section if:
* 1) the feature is enabled
* 2) "displayDiskEncryption" is explicitly passed -- <AccessPanel />
* gets used in several places, but we don't want to display Disk Encryption in all
* 3) toggleDiskEncryptionEnabled is defined
*/
const diskEncryptionJSX =
isDiskEncryptionFeatureEnabled &&
displayDiskEncryption &&
toggleDiskEncryptionEnabled !== undefined ? (
<>
<Divider spacingBottom={20} spacingTop={24} />
<DiskEncryption
descriptionCopy={DISK_ENCRYPTION_GENERAL_DESCRIPTION}
disabled={!regionSupportsDiskEncryption}
disabledReason={DISK_ENCRYPTION_UNAVAILABLE_IN_REGION_COPY}
isEncryptDiskChecked={diskEncryptionEnabled ?? false}
toggleDiskEncryptionEnabled={toggleDiskEncryptionEnabled}
/>
</>
) : null;

return (
<Paper
className={cx(
Expand All @@ -80,6 +132,14 @@ export const AccessPanel = (props: Props) => {
className
)}
>
{isDiskEncryptionFeatureEnabled && (
<Typography
sx={(theme) => ({ paddingBottom: theme.spacing(2) })}
variant="h2"
>
Security
</Typography>
)}
<React.Suspense fallback={<SuspenseLoader />}>
<PasswordInput
autoComplete="off"
Expand Down Expand Up @@ -110,6 +170,7 @@ export const AccessPanel = (props: Props) => {
/>
</>
) : null}
{diskEncryptionJSX}
</Paper>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import {
describe('DiskEncryption', () => {
it('should render a header', () => {
const { getByTestId } = renderWithTheme(
<DiskEncryption descriptionCopy="Description for unit test" />
<DiskEncryption
descriptionCopy="Description for unit test"
isEncryptDiskChecked={true}
toggleDiskEncryptionEnabled={vi.fn()}
/>
);

const heading = getByTestId(headerTestId);
Expand All @@ -23,7 +27,11 @@ describe('DiskEncryption', () => {

it('should render a description', () => {
const { getByTestId } = renderWithTheme(
<DiskEncryption descriptionCopy="Description for unit test" />
<DiskEncryption
descriptionCopy="Description for unit test"
isEncryptDiskChecked={true}
toggleDiskEncryptionEnabled={vi.fn()}
/>
);

const description = getByTestId(descriptionTestId);
Expand All @@ -33,7 +41,11 @@ describe('DiskEncryption', () => {

it('should render a checkbox', () => {
const { getByTestId } = renderWithTheme(
<DiskEncryption descriptionCopy="Description for unit test" />
<DiskEncryption
descriptionCopy="Description for unit test"
isEncryptDiskChecked={true}
toggleDiskEncryptionEnabled={vi.fn()}
/>
);

const checkbox = getByTestId(checkboxTestId);
Expand Down
18 changes: 11 additions & 7 deletions packages/manager/src/components/DiskEncryption/DiskEncryption.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,22 @@ export interface DiskEncryptionProps {
descriptionCopy: JSX.Element | string;
disabled?: boolean;
disabledReason?: string;
// encryptionStatus
// toggleEncryption
isEncryptDiskChecked: boolean;
toggleDiskEncryptionEnabled: () => void;
}

export const headerTestId = 'disk-encryption-header';
export const descriptionTestId = 'disk-encryption-description';
export const checkboxTestId = 'encrypt-disk-checkbox';

export const DiskEncryption = (props: DiskEncryptionProps) => {
const { descriptionCopy, disabled, disabledReason } = props;

const [checked, setChecked] = React.useState<boolean>(false); // @TODO LDE: temporary placeholder until toggleEncryption logic is in place
const {
descriptionCopy,
disabled,
disabledReason,
isEncryptDiskChecked,
toggleDiskEncryptionEnabled,
} = props;

return (
<>
Expand All @@ -41,10 +45,10 @@ export const DiskEncryption = (props: DiskEncryptionProps) => {
flexDirection="row"
>
<Checkbox
checked={checked} // @TODO LDE: in Create flows, this will be defaulted to be checked. Otherwise, we will rely on the current encryption status for the initial value
checked={disabled ? false : isEncryptDiskChecked} // in Create flows, this will be defaulted to be checked. Otherwise, we will rely on the current encryption status for the initial value
data-testid={checkboxTestId}
disabled={disabled}
onChange={() => setChecked(!checked)} // @TODO LDE: toggleEncryption will be used here
onChange={toggleDiskEncryptionEnabled}
text="Encrypt Disk"
toolTipText={disabled ? disabledReason : ''}
/>
Expand Down
3 changes: 3 additions & 0 deletions packages/manager/src/components/DiskEncryption/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ export const DISK_ENCRYPTION_DESCRIPTION_NODE_POOL_REBUILD_CAVEAT =

export const DISK_ENCRYPTION_UNAVAILABLE_IN_REGION_COPY =
'Disk encryption is not available in the selected region.';

export const DISK_ENCRYPTION_BACKUPS_CAVEAT_COPY =
'Virtual Machine Backups are not encrypted.';
31 changes: 31 additions & 0 deletions packages/manager/src/components/DiskEncryption/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useFlags } from 'src/hooks/useFlags';
import { useAccount } from 'src/queries/account/account';

/**
* Hook to determine if the Disk Encryption feature should be visible to the user.
* Based on the user's account capability and the feature flag.
*
* @returns { boolean } - Whether the Disk Encryption feature is enabled for the current user.
*/
export const useIsDiskEncryptionFeatureEnabled = (): {
isDiskEncryptionFeatureEnabled: boolean;
} => {
const { data: account, error } = useAccount();
const flags = useFlags();

if (error || !flags) {
return { isDiskEncryptionFeatureEnabled: false };
}

const hasAccountCapability = account?.capabilities?.includes(
'Disk Encryption'
);

const isFeatureFlagEnabled = flags.linodeDiskEncryption;

const isDiskEncryptionFeatureEnabled = Boolean(
hasAccountCapability && isFeatureFlagEnabled
);

return { isDiskEncryptionFeatureEnabled };
};
11 changes: 6 additions & 5 deletions packages/manager/src/factories/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,18 @@ export const accountFactory = Factory.Sync.makeFactory<Account>({
balance_uninvoiced: 0.0,
billing_source: 'linode',
capabilities: [
'Linodes',
'NodeBalancers',
'Block Storage',
'Object Storage',
'Kubernetes',
'Cloud Firewall',
'Vlans',
'Disk Encryption',
'Kubernetes',
'Linodes',
'LKE HA Control Planes',
'Machine Images',
'Managed Databases',
'NodeBalancers',
'Object Storage',
'Placement Group',
'Vlans',
],
city: 'Colorado',
company: Factory.each((i) => `company-${i}`),
Expand Down
Loading

0 comments on commit 2579ad8

Please sign in to comment.