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-9197] - Collapsible Node Pool tables & filterable status #11589

Merged
Merged
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-11589-added-1738345796763.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Added
---

Collapsible Node Pool tables & filterable status ([#11589](https://github.com/linode/manager/pull/11589))
166 changes: 166 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 @@ -1610,6 +1610,172 @@ describe('LKE cluster updates', () => {
});
});

it('does not collapse the accordion when an action button is clicked in the accordion header', () => {
const mockCluster = kubernetesClusterFactory.build({
k8s_version: latestKubernetesVersion,
});
mockGetCluster(mockCluster).as('getCluster');
mockGetClusterPools(mockCluster.id, mockNodePools).as('getNodePools');

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

cy.get(`[data-qa-node-pool-id="${mockNodePools[0].id}"]`).within(() => {
// Accordion should be expanded by default
cy.get(`[data-qa-panel-summary]`).should(
'have.attr',
'aria-expanded',
'true'
);

// Click on an action button
cy.get('[data-testid="node-pool-actions"]')
.should('be.visible')
.within(() => {
ui.button
.findByTitle('Autoscale Pool')
.should('be.visible')
.should('be.enabled')
.click();
});
});

// Exit dialog
ui.dialog
.findByTitle('Autoscale Pool')
.should('be.visible')
.within(() => {
ui.button
.findByTitle('Cancel')
.should('be.visible')
.should('be.enabled')
.click();
});

cy.get(`[data-qa-node-pool-id="${mockNodePools[0].id}"]`).within(() => {
// Check that the accordion is still expanded
cy.get(`[data-qa-panel-summary]`).should(
'have.attr',
'aria-expanded',
'true'
);

// Accordion should close on non-action button clicks
cy.get('[data-qa-panel-subheading]').click();
cy.get(`[data-qa-panel-summary]`).should(
'have.attr',
'aria-expanded',
'false'
);
});
});

it('filters the node tables based on selected status filter', () => {
const mockCluster = kubernetesClusterFactory.build({
k8s_version: latestKubernetesVersion,
});
const mockNodePools = [
nodePoolFactory.build({
count: 4,
nodes: [
...kubeLinodeFactory.buildList(3),
kubeLinodeFactory.build({ status: 'not_ready' }),
],
}),
nodePoolFactory.build({
nodes: kubeLinodeFactory.buildList(2),
}),
];
const mockLinodes: Linode[] = [
linodeFactory.build({
id: mockNodePools[0].nodes[0].instance_id ?? undefined,
}),
linodeFactory.build({
id: mockNodePools[0].nodes[1].instance_id ?? undefined,
}),
linodeFactory.build({
id: mockNodePools[0].nodes[2].instance_id ?? undefined,
status: 'offline',
}),
linodeFactory.build({
id: mockNodePools[0].nodes[3].instance_id ?? undefined,
status: 'provisioning',
}),
linodeFactory.build({
id: mockNodePools[1].nodes[0].instance_id ?? undefined,
}),
linodeFactory.build({
id: mockNodePools[1].nodes[1].instance_id ?? undefined,
status: 'offline',
}),
];
mockGetCluster(mockCluster).as('getCluster');
mockGetClusterPools(mockCluster.id, mockNodePools).as('getNodePools');
mockGetLinodes(mockLinodes).as('getLinodes');

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

// Filter is initially set to Show All nodes
cy.get(`[data-qa-node-pool-id="${mockNodePools[0].id}"]`).within(() => {
cy.get('[data-qa-node-row]').should('have.length', 4);
});
cy.get(`[data-qa-node-pool-id="${mockNodePools[1].id}"]`).within(() => {
cy.get('[data-qa-node-row]').should('have.length', 2);
});

// Filter by Running status
ui.autocomplete.findByLabel('Status').click();
ui.autocompletePopper.findByTitle('Running').should('be.visible').click();

// Only Running nodes should be displayed
cy.get(`[data-qa-node-pool-id="${mockNodePools[0].id}"]`).within(() => {
cy.get('[data-qa-node-row]').should('have.length', 2);
});
cy.get(`[data-qa-node-pool-id="${mockNodePools[1].id}"]`).within(() => {
cy.get('[data-qa-node-row]').should('have.length', 1);
});

// Filter by Offline status
ui.autocomplete.findByLabel('Status').click();
ui.autocompletePopper.findByTitle('Offline').should('be.visible').click();

// Only Offline nodes should be displayed
cy.get(`[data-qa-node-pool-id="${mockNodePools[0].id}"]`).within(() => {
cy.get('[data-qa-node-row]').should('have.length', 1);
});
cy.get(`[data-qa-node-pool-id="${mockNodePools[1].id}"]`).within(() => {
cy.get('[data-qa-node-row]').should('have.length', 1);
});

// Filter by Provisioning status
ui.autocomplete.findByLabel('Status').click();
ui.autocompletePopper
.findByTitle('Provisioning')
.should('be.visible')
.click();

// Only Provisioning nodes should be displayed
cy.get(`[data-qa-node-pool-id="${mockNodePools[0].id}"]`).within(() => {
cy.get('[data-qa-node-row]').should('have.length', 1);
});
cy.get(`[data-qa-node-pool-id="${mockNodePools[1].id}"]`).within(() => {
cy.get('[data-qa-node-row]').should('have.length', 0);
});

// Filter by Show All status
ui.autocomplete.findByLabel('Status').click();
ui.autocompletePopper.findByTitle('Show All').should('be.visible').click();

// All nodes are displayed
cy.get(`[data-qa-node-pool-id="${mockNodePools[0].id}"]`).within(() => {
cy.get('[data-qa-node-row]').should('have.length', 4);
});
cy.get(`[data-qa-node-pool-id="${mockNodePools[1].id}"]`).within(() => {
cy.get('[data-qa-node-row]').should('have.length', 2);
});
});

describe('LKE cluster updates for DC-specific prices', () => {
/*
* - Confirms node pool resize UI flow using mocked API responses.
Expand Down
19 changes: 15 additions & 4 deletions packages/manager/src/components/ActionMenu/ActionMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ export interface ActionMenuProps {
* A function that is called when the Menu is opened. Useful for analytics.
*/
onOpen?: () => void;
/**
* If true, stop event propagation when handling clicks
* Ex: If the action menu is in an accordion, we don't want the click also opening/closing the accordion
*/
stopClickPropagation?: boolean;
}

/**
Expand All @@ -35,7 +40,7 @@ export interface ActionMenuProps {
* No more than 8 items should be displayed within an action menu.
*/
export const ActionMenu = React.memo((props: ActionMenuProps) => {
const { actionsList, ariaLabel, onOpen } = props;
const { actionsList, ariaLabel, onOpen, stopClickPropagation } = props;

const menuId = convertToKebabCase(ariaLabel);
const buttonId = `${convertToKebabCase(ariaLabel)}-button`;
Expand All @@ -44,13 +49,19 @@ export const ActionMenu = React.memo((props: ActionMenuProps) => {
const open = Boolean(anchorEl);

const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
if (stopClickPropagation) {
event.stopPropagation();
}
setAnchorEl(event.currentTarget);
if (onOpen) {
onOpen();
}
};

const handleClose = () => {
const handleClose = (event: React.MouseEvent<HTMLLIElement>) => {
if (stopClickPropagation) {
event.stopPropagation();
}
setAnchorEl(null);
};

Expand Down Expand Up @@ -131,9 +142,9 @@ export const ActionMenu = React.memo((props: ActionMenuProps) => {
>
{actionsList.map((a, idx) => (
<MenuItem
onClick={() => {
onClick={(e) => {
if (!a.disabled) {
handleClose();
handleClose(e);
a.onClick();
}
}}
Expand Down
Loading