From 1c5e92617d7f77a78454284a7dcb9ec160f05a1d Mon Sep 17 00:00:00 2001 From: Dajahi Wiley <114682940+dwiley-akamai@users.noreply.github.com> Date: Thu, 23 May 2024 10:03:22 -0400 Subject: [PATCH] =?UTF-8?q?upcoming:=20[M3-8032]=20=E2=80=93=20Add=20Encry?= =?UTF-8?q?pted/Not=20Encrypted=20status=20to=20Node=20Pool=20table=20(#10?= =?UTF-8?q?480)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/api-v4/src/images/images.ts | 4 +- packages/api-v4/src/kubernetes/types.ts | 2 +- ...r-10480-upcoming-features-1716321944627.md | 5 ++ .../src/assets/icons/divider-vertical.svg | 3 + packages/manager/src/assets/icons/lock.svg | 3 + packages/manager/src/assets/icons/unlock.svg | 4 ++ .../components/DiskEncryption/constants.tsx | 3 + .../NodePoolsDisplay/NodePool.tsx | 17 +++-- .../NodePoolsDisplay/NodePoolsDisplay.tsx | 11 +-- .../NodePoolsDisplay/NodeTable.styles.ts | 15 +++- .../NodePoolsDisplay/NodeTable.test.tsx | 50 ++++++++++++- .../NodePoolsDisplay/NodeTable.tsx | 71 +++++++++++++++++-- packages/manager/src/mocks/serverHandlers.ts | 9 ++- 13 files changed, 174 insertions(+), 23 deletions(-) create mode 100644 packages/manager/.changeset/pr-10480-upcoming-features-1716321944627.md create mode 100644 packages/manager/src/assets/icons/divider-vertical.svg create mode 100644 packages/manager/src/assets/icons/lock.svg create mode 100644 packages/manager/src/assets/icons/unlock.svg diff --git a/packages/api-v4/src/images/images.ts b/packages/api-v4/src/images/images.ts index 9c9984bdbd1..720d75bcdda 100644 --- a/packages/api-v4/src/images/images.ts +++ b/packages/api-v4/src/images/images.ts @@ -11,8 +11,8 @@ import Request, { setURL, setXFilter, } from '../request'; -import { Filter, Params, ResourcePage as Page } from '../types'; -import { +import type { Filter, Params, ResourcePage as Page } from '../types'; +import type { CreateImagePayload, Image, ImageUploadPayload, diff --git a/packages/api-v4/src/kubernetes/types.ts b/packages/api-v4/src/kubernetes/types.ts index c8d25118e35..8e2d176572c 100644 --- a/packages/api-v4/src/kubernetes/types.ts +++ b/packages/api-v4/src/kubernetes/types.ts @@ -1,4 +1,4 @@ -import type { EncryptionStatus } from 'src/linodes'; +import type { EncryptionStatus } from '../linodes'; export interface KubernetesCluster { created: string; diff --git a/packages/manager/.changeset/pr-10480-upcoming-features-1716321944627.md b/packages/manager/.changeset/pr-10480-upcoming-features-1716321944627.md new file mode 100644 index 00000000000..ccf6bb170dd --- /dev/null +++ b/packages/manager/.changeset/pr-10480-upcoming-features-1716321944627.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add Encrypted/Not Encrypted status to LKE Node Pool table ([#10480](https://github.com/linode/manager/pull/10480)) diff --git a/packages/manager/src/assets/icons/divider-vertical.svg b/packages/manager/src/assets/icons/divider-vertical.svg new file mode 100644 index 00000000000..79add159022 --- /dev/null +++ b/packages/manager/src/assets/icons/divider-vertical.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/manager/src/assets/icons/lock.svg b/packages/manager/src/assets/icons/lock.svg new file mode 100644 index 00000000000..ca135909b4f --- /dev/null +++ b/packages/manager/src/assets/icons/lock.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/manager/src/assets/icons/unlock.svg b/packages/manager/src/assets/icons/unlock.svg new file mode 100644 index 00000000000..ce413046282 --- /dev/null +++ b/packages/manager/src/assets/icons/unlock.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/manager/src/components/DiskEncryption/constants.tsx b/packages/manager/src/components/DiskEncryption/constants.tsx index dbdce42a9c7..5d0ffe10ec8 100644 --- a/packages/manager/src/components/DiskEncryption/constants.tsx +++ b/packages/manager/src/components/DiskEncryption/constants.tsx @@ -19,3 +19,6 @@ export const DISK_ENCRYPTION_UNAVAILABLE_IN_REGION_COPY = export const DISK_ENCRYPTION_BACKUPS_CAVEAT_COPY = 'Virtual Machine Backups are not encrypted.'; + +export const DISK_ENCRYPTION_NODE_POOL_GUIDANCE_COPY = + 'To enable disk encryption, delete the node pool and create a new node pool. New node pools are always encrypted.'; diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodePool.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodePool.tsx index ed4ff6d7878..02e61f7aec2 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodePool.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodePool.tsx @@ -1,7 +1,3 @@ -import { - AutoscaleSettings, - PoolNodeResponse, -} from '@linode/api-v4/lib/kubernetes'; import { Theme } from '@mui/material/styles'; import Grid from '@mui/material/Unstable_Grid2'; import * as React from 'react'; @@ -13,8 +9,15 @@ import { Typography } from 'src/components/Typography'; import { NodeTable } from './NodeTable'; +import type { + AutoscaleSettings, + PoolNodeResponse, +} from '@linode/api-v4/lib/kubernetes'; +import type { EncryptionStatus } from '@linode/api-v4/lib/linodes/types'; + interface Props { autoscaler: AutoscaleSettings; + encryptionStatus: EncryptionStatus | undefined; handleClickResize: (poolId: number) => void; isOnlyNodePool: boolean; nodes: PoolNodeResponse[]; @@ -40,9 +43,10 @@ const useStyles = makeStyles()((theme: Theme) => ({ }, })); -const NodePool: React.FC = (props) => { +export const NodePool = (props: Props) => { const { autoscaler, + encryptionStatus, handleClickResize, isOnlyNodePool, nodes, @@ -126,6 +130,7 @@ const NodePool: React.FC = (props) => { xs={12} > = (props) => { ); }; - -export default NodePool; diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodePoolsDisplay.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodePoolsDisplay.tsx index e4f5d34f542..97cb7c652e8 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodePoolsDisplay.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodePoolsDisplay.tsx @@ -1,14 +1,14 @@ -import Grid from '@mui/material/Unstable_Grid2'; import { Theme } from '@mui/material/styles'; -import { makeStyles } from 'tss-react/mui'; +import Grid from '@mui/material/Unstable_Grid2'; import React, { useState } from 'react'; import { Waypoint } from 'react-waypoint'; +import { makeStyles } from 'tss-react/mui'; import { Button } from 'src/components/Button/Button'; import { CircleProgress } from 'src/components/CircleProgress'; import { ErrorState } from 'src/components/ErrorState/ErrorState'; -import { Typography } from 'src/components/Typography'; import { Paper } from 'src/components/Paper'; +import { Typography } from 'src/components/Typography'; import { useAllKubernetesNodePoolQuery } from 'src/queries/kubernetes'; import { useSpecificTypes } from 'src/queries/types'; import { extendTypesQueryResult } from 'src/utilities/extendType'; @@ -18,7 +18,7 @@ import { RecycleNodePoolDialog } from '../RecycleNodePoolDialog'; import { AddNodePoolDrawer } from './AddNodePoolDrawer'; import { AutoscalePoolDialog } from './AutoscalePoolDialog'; import { DeleteNodePoolDialog } from './DeleteNodePoolDialog'; -import NodePool from './NodePool'; +import { NodePool } from './NodePool'; import { RecycleNodeDialog } from './RecycleNodeDialog'; import { ResizeNodePoolDrawer } from './ResizeNodePoolDrawer'; @@ -152,7 +152,7 @@ export const NodePoolsDisplay = (props: Props) => { {_pools?.map((thisPool) => { - const { id, nodes } = thisPool; + const { disk_encryption, id, nodes } = thisPool; const thisPoolType = types?.find( (thisType) => thisType.id === thisPool.type @@ -181,6 +181,7 @@ export const NodePoolsDisplay = (props: Props) => { setIsRecycleNodeOpen(true); }} autoscaler={thisPool.autoscaler} + encryptionStatus={disk_encryption} handleClickResize={handleOpenResizeDrawer} isOnlyNodePool={pools?.length === 1} nodes={nodes ?? []} diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.styles.ts b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.styles.ts index 482ab5b66fc..f272e64c72c 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.styles.ts +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.styles.ts @@ -1,8 +1,10 @@ import { styled } from '@mui/material/styles'; +import VerticalDivider from 'src/assets/icons/divider-vertical.svg'; import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; import { Table } from 'src/components/Table'; import { TableRow } from 'src/components/TableRow'; +import { Typography } from 'src/components/Typography'; export const StyledTableRow = styled(TableRow, { label: 'TableRow', @@ -19,7 +21,6 @@ export const StyledTableRow = styled(TableRow, { opacity: 1, }, marginLeft: 4, - top: 1, })); export const StyledTable = styled(Table, { @@ -40,3 +41,15 @@ export const StyledCopyTooltip = styled(CopyTooltip, { marginLeft: 4, top: 1, })); + +export const StyledVerticalDivider = styled(VerticalDivider, { + label: 'StyledVerticalDivider', +})(({ theme }) => ({ + margin: `0 ${theme.spacing(2)}`, +})); + +export const StyledTypography = styled(Typography, { + label: 'StyledTypography', +})(({ theme }) => ({ + margin: `0 0 0 ${theme.spacing()}`, +})); diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.test.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.test.tsx index 4298d833767..49216784a3f 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.test.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.test.tsx @@ -4,13 +4,14 @@ import { kubeLinodeFactory } from 'src/factories/kubernetesCluster'; import { linodeFactory } from 'src/factories/linodes'; import { renderWithTheme } from 'src/utilities/testHelpers'; -import { NodeTable, Props } from './NodeTable'; +import { NodeTable, Props, encryptionStatusTestId } from './NodeTable'; const mockLinodes = linodeFactory.buildList(3); const mockKubeNodes = kubeLinodeFactory.buildList(3); const props: Props = { + encryptionStatus: 'enabled', nodes: mockKubeNodes, openRecycleNodeDialog: vi.fn(), poolId: 1, @@ -20,6 +21,29 @@ const props: Props = { beforeAll(() => linodeFactory.resetSequenceNumber()); describe('NodeTable', () => { + const mocks = vi.hoisted(() => { + return { + useIsDiskEncryptionFeatureEnabled: vi.fn(), + }; + }); + + vi.mock('src/components/DiskEncryption/utils.ts', async () => { + const actual = await vi.importActual( + 'src/components/DiskEncryption/utils.ts' + ); + return { + ...actual, + __esModule: true, + useIsDiskEncryptionFeatureEnabled: mocks.useIsDiskEncryptionFeatureEnabled.mockImplementation( + () => { + return { + isDiskEncryptionFeatureEnabled: false, // indicates the feature flag is off or account capability is absent + }; + } + ), + }; + }); + it('includes label, status, and IP columns', () => { const { findByText } = renderWithTheme(); mockLinodes.forEach(async (thisLinode) => { @@ -28,8 +52,32 @@ describe('NodeTable', () => { await findByText('Ready'); }); }); + it('includes the Pool ID', () => { const { getByText } = renderWithTheme(); getByText('Pool ID 1'); }); + + it('does not display the encryption status of the pool if the account lacks the capability or the feature flag is off', () => { + // situation where isDiskEncryptionFeatureEnabled === false + const { queryByTestId } = renderWithTheme(); + const encryptionStatusFragment = queryByTestId(encryptionStatusTestId); + + expect(encryptionStatusFragment).not.toBeInTheDocument(); + }); + + it('displays the encryption status of the pool if the feature flag is on and the account has the capability', () => { + mocks.useIsDiskEncryptionFeatureEnabled.mockImplementationOnce(() => { + return { + isDiskEncryptionFeatureEnabled: true, + }; + }); + + const { queryByTestId } = renderWithTheme(); + const encryptionStatusFragment = queryByTestId(encryptionStatusTestId); + + expect(encryptionStatusFragment).toBeInTheDocument(); + + mocks.useIsDiskEncryptionFeatureEnabled.mockRestore(); + }); }); diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.tsx index abb227b83c6..be95c8ab1df 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.tsx @@ -1,6 +1,10 @@ -import { PoolNodeResponse } from '@linode/api-v4/lib/kubernetes'; import * as React from 'react'; +import Lock from 'src/assets/icons/lock.svg'; +import Unlock from 'src/assets/icons/unlock.svg'; +import { Box } from 'src/components/Box'; +import { DISK_ENCRYPTION_NODE_POOL_GUIDANCE_COPY } from 'src/components/DiskEncryption/constants'; +import { useIsDiskEncryptionFeatureEnabled } from 'src/components/DiskEncryption/utils'; import OrderBy from 'src/components/OrderBy'; import Paginate from 'src/components/Paginate'; import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; @@ -11,26 +15,45 @@ import { TableFooter } from 'src/components/TableFooter'; import { TableHead } from 'src/components/TableHead'; import { TableRow } from 'src/components/TableRow'; import { TableSortCell } from 'src/components/TableSortCell'; +import { TooltipIcon } from 'src/components/TooltipIcon'; import { Typography } from 'src/components/Typography'; import { useAllLinodesQuery } from 'src/queries/linodes/linodes'; import { LinodeWithMaintenance } from 'src/utilities/linodes'; import { NodeRow as _NodeRow } from './NodeRow'; -import { StyledTable } from './NodeTable.styles'; +import { + StyledTable, + StyledTypography, + StyledVerticalDivider, +} from './NodeTable.styles'; import type { NodeRow } from './NodeRow'; +import type { PoolNodeResponse } from '@linode/api-v4/lib/kubernetes'; +import type { EncryptionStatus } from '@linode/api-v4/lib/linodes/types'; export interface Props { + encryptionStatus: EncryptionStatus | undefined; nodes: PoolNodeResponse[]; openRecycleNodeDialog: (nodeID: string, linodeLabel: string) => void; poolId: number; typeLabel: string; } +export const encryptionStatusTestId = 'encryption-status-fragment'; + export const NodeTable = React.memo((props: Props) => { - const { nodes, openRecycleNodeDialog, poolId, typeLabel } = props; + const { + encryptionStatus, + nodes, + openRecycleNodeDialog, + poolId, + typeLabel, + } = props; const { data: linodes, error, isLoading } = useAllLinodesQuery(); + const { + isDiskEncryptionFeatureEnabled, + } = useIsDiskEncryptionFeatureEnabled(); const rowData = nodes.map((thisNode) => nodeToRow(thisNode, linodes ?? [])); @@ -116,7 +139,26 @@ export const NodeTable = React.memo((props: Props) => { - Pool ID {poolId} + {isDiskEncryptionFeatureEnabled && + encryptionStatus !== undefined ? ( + + Pool ID {poolId} + + + + ) : ( + Pool ID {poolId} + )} @@ -157,3 +199,24 @@ export const nodeToRow = ( nodeStatus: node.status, }; }; + +export const EncryptedStatus = ({ + encryptionStatus, + tooltipText, +}: { + encryptionStatus: EncryptionStatus; + tooltipText: string | undefined; +}) => { + return encryptionStatus === 'enabled' ? ( + <> + + Encrypted + + ) : encryptionStatus === 'disabled' ? ( + <> + + Not Encrypted + {tooltipText ? : null} + + ) : null; +}; diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index c0dceb9e76e..e1c89236f27 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -849,9 +849,14 @@ export const handlers = [ return HttpResponse.json(cluster); }), http.get('*/lke/clusters/:clusterId/pools', async () => { - const pools = nodePoolFactory.buildList(10); + const encryptedPools = nodePoolFactory.buildList(5); + const unencryptedPools = nodePoolFactory.buildList(5, { + disk_encryption: 'disabled', + }); nodePoolFactory.resetSequenceNumber(); - return HttpResponse.json(makeResourcePage(pools)); + return HttpResponse.json( + makeResourcePage([...encryptedPools, ...unencryptedPools]) + ); }), http.get('*/lke/clusters/*/api-endpoints', async () => { const endpoints = kubeEndpointFactory.buildList(2);