Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: [M3-8855] - Surface Node Pool Tags #11368

Open
wants to merge 12 commits into
base: develop
Choose a base branch
from
5 changes: 5 additions & 0 deletions packages/api-v4/.changeset/pr-11368-added-1733420616390.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/api-v4": Added
---

Tags to `KubeNodePoolResponse` and `CreateNodePoolData` ([#11368](https://github.com/linode/manager/pull/11368))
2 changes: 2 additions & 0 deletions packages/api-v4/src/kubernetes/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface KubeNodePoolResponse {
count: number;
id: number;
nodes: PoolNodeResponse[];
tags: string[];
type: string;
autoscaler: AutoscaleSettings;
disk_encryption?: EncryptionStatus; // @TODO LDE: remove optionality once LDE is fully rolled out
Expand All @@ -42,6 +43,7 @@ export interface CreateNodePoolData {
export interface UpdateNodePoolData {
autoscaler: AutoscaleSettings;
count: number;
tags: string[];
}

export interface AutoscaleSettings {
Expand Down
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-11368-added-1733415278919.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Added
---

Node Pool Tags to LKE Cluster details page ([#11368](https://github.com/linode/manager/pull/11368))
66 changes: 66 additions & 0 deletions packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -860,6 +860,72 @@ describe('LKE cluster updates', () => {
});
});

it('can add and delete node pool tags', () => {
const mockCluster = kubernetesClusterFactory.build({
k8s_version: latestKubernetesVersion,
});

const mockNodePoolNoTags = nodePoolFactory.build({
id: 1,
type: 'g6-dedicated-4',
});

const mockNodePoolWithTags = {
...mockNodePoolNoTags,
tags: ['test-tag'],
};

mockGetCluster(mockCluster).as('getCluster');
mockGetClusterPools(mockCluster.id, [mockNodePoolNoTags]).as(
'getNodePoolsNoTags'
);
mockGetKubernetesVersions().as('getVersions');
mockUpdateNodePool(mockCluster.id, mockNodePoolWithTags).as('addTag');
mockGetDashboardUrl(mockCluster.id);
mockGetApiEndpoints(mockCluster.id);

cy.visitWithLogin(`/kubernetes/clusters/${mockCluster.id}`);
cy.wait(['@getCluster', '@getNodePoolsNoTags', '@getVersions']);

cy.get(`[data-qa-node-pool-id="${mockNodePoolNoTags.id}"]`).within(() => {
ui.button.findByTitle('Add a tag').should('be.visible').click();

cy.findByLabelText('Create or Select a Tag')
.should('be.visible')
.type(`${mockNodePoolWithTags.tags[0]}`);

ui.autocompletePopper
.findByTitle(`Create "${mockNodePoolWithTags.tags[0]}"`)
.scrollIntoView()
.should('be.visible')
.click();
});

mockGetClusterPools(mockCluster.id, [mockNodePoolWithTags]).as(
'getNodePoolsWithTags'
);

cy.wait(['@addTag', '@getNodePoolsWithTags']);

mockUpdateNodePool(mockCluster.id, mockNodePoolNoTags).as('deleteTag');
mockGetClusterPools(mockCluster.id, [mockNodePoolNoTags]).as(
'getNodePoolsNoTags'
);

// Delete the newly added node pool tag.
cy.get(`[data-qa-tag="${mockNodePoolWithTags.tags[0]}"]`)
.should('be.visible')
.within(() => {
cy.get('[data-qa-delete-tag="true"]').should('be.visible').click();
});

cy.wait(['@deleteTag', '@getNodePoolsNoTags']);

cy.get(`[data-qa-tag="${mockNodePoolWithTags.tags[0]}"]`).should(
'not.exist'
);
});

describe('LKE cluster updates for DC-specific prices', () => {
/*
* - Confirms node pool resize UI flow using mocked API responses.
Expand Down
1 change: 1 addition & 0 deletions packages/manager/src/factories/kubernetesCluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const nodePoolFactory = Factory.Sync.makeFactory<KubeNodePoolResponse>({
disk_encryption: 'enabled',
id: Factory.each((id) => id),
nodes: kubeLinodeFactory.buildList(3),
tags: [],
type: 'g6-standard-1',
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type { EncryptionStatus } from '@linode/api-v4/lib/linodes/types';

interface Props {
autoscaler: AutoscaleSettings;
clusterId: number;
encryptionStatus: EncryptionStatus | undefined;
handleClickResize: (poolId: number) => void;
isOnlyNodePool: boolean;
Expand All @@ -30,12 +31,14 @@ interface Props {
openRecycleAllNodesDialog: (poolId: number) => void;
openRecycleNodeDialog: (nodeID: string, linodeLabel: string) => void;
poolId: number;
tags: string[];
typeLabel: string;
}

export const NodePool = (props: Props) => {
const {
autoscaler,
clusterId,
encryptionStatus,
handleClickResize,
isOnlyNodePool,
Expand All @@ -45,6 +48,7 @@ export const NodePool = (props: Props) => {
openRecycleAllNodesDialog,
openRecycleNodeDialog,
poolId,
tags,
typeLabel,
} = props;

Expand Down Expand Up @@ -134,10 +138,12 @@ export const NodePool = (props: Props) => {
</Hidden>
</Paper>
<NodeTable
clusterId={clusterId}
encryptionStatus={encryptionStatus}
nodes={nodes}
openRecycleNodeDialog={openRecycleNodeDialog}
poolId={poolId}
tags={tags}
typeLabel={typeLabel}
/>
</Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export const NodePoolsDisplay = (props: Props) => {
{poolsError && <ErrorState errorText={poolsError[0].reason} />}
<Stack spacing={2}>
{_pools?.map((thisPool) => {
const { disk_encryption, id, nodes } = thisPool;
const { disk_encryption, id, nodes, tags } = thisPool;

const thisPoolType = types?.find(
(thisType) => thisType.id === thisPool.type
Expand All @@ -131,12 +131,14 @@ export const NodePoolsDisplay = (props: Props) => {
setIsRecycleNodeOpen(true);
}}
autoscaler={thisPool.autoscaler}
clusterId={clusterID}
encryptionStatus={disk_encryption}
handleClickResize={handleOpenResizeDrawer}
isOnlyNodePool={pools?.length === 1}
key={id}
nodes={nodes ?? []}
poolId={thisPool.id}
tags={tags}
typeLabel={typeLabel}
/>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Typography } from '@linode/ui';
import { Box, Typography } from '@linode/ui';
import { styled } from '@mui/material/styles';

import VerticalDivider from 'src/assets/icons/divider-vertical.svg';
import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip';
import { TagCell } from 'src/components/TagCell/TagCell';

export const StyledCopyTooltip = styled(CopyTooltip, {
label: 'CopyTooltip',
Expand All @@ -27,3 +28,33 @@ export const StyledTypography = styled(Typography, {
})(({ theme }) => ({
margin: `0 0 0 ${theme.spacing()}`,
}));

export const StyledPoolInfoBox = styled(Box, {
label: 'StyledPoolInfoBox',
})(({ theme }) => ({
display: 'flex',
[theme.breakpoints.down('sm')]: {
padding: `${theme.spacing()} 0`,
},
width: '100%',
}));

export const StyledTagCell = styled(TagCell, {
label: 'StyledTagCell',
})(() => ({
width: '100%',
}));

export const StyledTableFooter = styled(Box, {
label: 'StyledTableFooter',
})(({ theme }) => ({
alignItems: 'center',
background: theme.bg.bgPaper,
display: 'flex',
justifyContent: 'space-between',
padding: `0 ${theme.spacing(2)}`,
[theme.breakpoints.down('sm')]: {
display: 'block',
flexDirection: 'column',
},
}));
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,21 @@ import { kubeLinodeFactory } from 'src/factories/kubernetesCluster';
import { linodeFactory } from 'src/factories/linodes';
import { renderWithTheme } from 'src/utilities/testHelpers';

import { NodeTable, Props, encryptionStatusTestId } from './NodeTable';
import { NodeTable, encryptionStatusTestId } from './NodeTable';

import type { Props } from './NodeTable';

const mockLinodes = linodeFactory.buildList(3);

const mockKubeNodes = kubeLinodeFactory.buildList(3);

const props: Props = {
clusterId: 1,
encryptionStatus: 'enabled',
nodes: mockKubeNodes,
openRecycleNodeDialog: vi.fn(),
poolId: 1,
tags: [],
typeLabel: 'Linode 2G',
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Box, TooltipIcon, Typography } from '@linode/ui';
import { enqueueSnackbar } from 'notistack';
import * as React from 'react';

import Lock from 'src/assets/icons/lock.svg';
Expand All @@ -12,36 +13,47 @@ import { Table } from 'src/components/Table';
import { TableBody } from 'src/components/TableBody';
import { TableCell } from 'src/components/TableCell';
import { TableContentWrapper } from 'src/components/TableContentWrapper/TableContentWrapper';
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 { useUpdateNodePoolMutation } from 'src/queries/kubernetes';
import { useAllLinodesQuery } from 'src/queries/linodes/linodes';
import { getAPIErrorOrDefault } from 'src/utilities/errorUtils';

import { NodeRow as _NodeRow } from './NodeRow';
import { StyledTypography, StyledVerticalDivider } from './NodeTable.styles';
import {
StyledPoolInfoBox,
StyledTableFooter,
StyledTagCell,
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';
import type { LinodeWithMaintenance } from 'src/utilities/linodes';

export interface Props {
clusterId: number;
encryptionStatus: EncryptionStatus | undefined;
nodes: PoolNodeResponse[];
openRecycleNodeDialog: (nodeID: string, linodeLabel: string) => void;
poolId: number;
tags: string[];
typeLabel: string;
}

export const encryptionStatusTestId = 'encryption-status-fragment';

export const NodeTable = React.memo((props: Props) => {
const {
clusterId,
encryptionStatus,
nodes,
openRecycleNodeDialog,
poolId,
tags,
typeLabel,
} = props;

Expand All @@ -50,6 +62,25 @@ export const NodeTable = React.memo((props: Props) => {
isDiskEncryptionFeatureEnabled,
} = useIsDiskEncryptionFeatureEnabled();

const { mutateAsync: updateNodePool } = useUpdateNodePoolMutation(
clusterId,
poolId
);

const updateTags = React.useCallback(
(tags: string[]) => {
return updateNodePool({ tags }).catch((e) =>
enqueueSnackbar(
getAPIErrorOrDefault(e, 'Error updating tags')[0].reason,
{
variant: 'error',
}
)
);
},
[updateNodePool]
);

const rowData = nodes.map((thisNode) => nodeToRow(thisNode, linodes ?? []));

return (
Expand Down Expand Up @@ -131,33 +162,33 @@ export const NodeTable = React.memo((props: Props) => {
})}
</TableContentWrapper>
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={4}>
{isDiskEncryptionFeatureEnabled &&
encryptionStatus !== undefined ? (
<Box
alignItems="center"
data-testid={encryptionStatusTestId}
display="flex"
flexDirection="row"
>
<Typography>Pool ID {poolId}</Typography>
<StyledVerticalDivider />
<EncryptedStatus
tooltipText={
DISK_ENCRYPTION_NODE_POOL_GUIDANCE_COPY
}
encryptionStatus={encryptionStatus}
/>
</Box>
) : (
<Typography>Pool ID {poolId}</Typography>
)}
</TableCell>
</TableRow>
</TableFooter>
</Table>
<StyledTableFooter>
<StyledPoolInfoBox>
{isDiskEncryptionFeatureEnabled &&
encryptionStatus !== undefined ? (
<Box
alignItems="center"
data-testid={encryptionStatusTestId}
display="flex"
>
<Typography>Pool ID {poolId}</Typography>
<StyledVerticalDivider />
<EncryptedStatus
encryptionStatus={encryptionStatus}
tooltipText={DISK_ENCRYPTION_NODE_POOL_GUIDANCE_COPY}
/>
</Box>
) : (
<Typography>Pool ID {poolId}</Typography>
)}
</StyledPoolInfoBox>
<StyledTagCell
tags={tags}
updateTags={updateTags}
view="inline"
/>
</StyledTableFooter>
<PaginationFooter
count={count}
eventCategory="Node Table"
Expand Down Expand Up @@ -209,7 +240,7 @@ export const EncryptedStatus = ({
</>
) : encryptionStatus === 'disabled' ? (
<>
<Unlock style={{ minWidth: 16 }} />
<Unlock />
<StyledTypography sx={{ whiteSpace: 'nowrap' }}>
Not Encrypted
</StyledTypography>
Expand Down
Loading