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

Merged
merged 13 commits into from
Dec 17, 2024
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))
hana-akamai marked this conversation as resolved.
Show resolved Hide resolved
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
---

Surface Node Pool Tags ([#11368](https://github.com/linode/manager/pull/11368))
hana-akamai marked this conversation as resolved.
Show resolved Hide resolved
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
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,11 +13,13 @@ 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 { TagCell } from 'src/components/TagCell/TagCell';
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';
Expand All @@ -27,21 +30,25 @@ 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 +57,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 +157,44 @@ 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>
<Box
sx={(theme) => ({
background: theme.bg.bgPaper,
})}
alignItems="center"
display="flex"
justifyContent="space-between"
px={2}
>
<Box display="flex" width="100%">
{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>
)}
</Box>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mobile.mov

We'll want to consider mobile view here, and might need some UX input. (Should we still have the ability to see tags in this view?) Currently, the tag cell will overlap with the pool id/encryption box once we get to ~500px in the viewport, and it gets worse at smaller screens (iPhone SE). Adjusting the box's width of 100% to less helps, but the tooltip still gets hidden. Maybe displaying tags under the pool id and encryption status is most readable?

Screenshot 2024-12-06 at 9 24 37β€―AM

Screenshot 2024-12-06 at 9 26 07β€―AM

Looks like there is also an existing issue where the Unlock icon has a min width of 16px, but the Lock icon does not, leading it to shrink and virtually disappear (and leave some odd spacing) behind at the smaller screen sizes. We could fix that here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mjac0bs Good catches, should be addressed now. I moved the tags to a separate row on mobile and removed the minWidth of 16px

Copy link
Contributor

@mjac0bs mjac0bs Dec 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks better! I think we should actually do this for tablet screen sizes too. There's an edge case with really long tags where, between tablet and mobile breakpoints, we still get some overlapping. Switching to displaying the tags below at the md breakpoint is also consistent with the cluster's tags. (Which seem to have their own buggy display issue... I'll create a follow up ticket to fix cluster tags. Update: M3-9009)

No issues after using md breakpoints instead of sm:

Screen.Recording.2024-12-11.at.12.59.02.PM.mov

<TagCell
sx={{
width: '100%',
}}
tags={tags}
updateTags={updateTags}
view="inline"
/>
</Box>
<PaginationFooter
count={count}
eventCategory="Node Table"
Expand Down
Loading