Skip to content

change: [UIE-8743] - Replaced Button component in DBAAS with Akamai CDS button Web Component #12148

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

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Changed
---

Replace the button component under DBAAS with Akamai CDS button web component ([#12148](https://github.com/linode/manager/pull/12148))
Original file line number Diff line number Diff line change
Expand Up @@ -385,18 +385,21 @@
ui.tabList.findTabByTitle('Resize').click();

// Confirm that "Resize Database Cluster" button is disabled
ui.button
.findByTitle('Resize Database Cluster')
ui.cdsButton
.findButtonByTitle('Resize Database Cluster')
.should('be.visible')
.should('be.disabled');

// Navigate to "Settings" tab
ui.tabList.findTabByTitle('Settings').click();

// Confirm that "Manage Access" button is disabled
cy.get('[data-testid="button-access-control"]')
.should('be.visible')
.should('be.disabled');
cy.get('[data-testid="button-access-control"]').within(() => {

Check warning on line 397 in packages/manager/cypress/e2e/core/account/restricted-user-details-pages.spec.ts

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Refactor this code to not nest functions more than 4 levels deep. Raw Output: {"ruleId":"sonarjs/no-nested-functions","severity":1,"message":"Refactor this code to not nest functions more than 4 levels deep.","line":397,"column":69,"nodeType":null,"endLine":397,"endColumn":71}
ui.cdsButton
.findButtonByTitle('Manage Access')
.should('be.visible')
.should('be.disabled');
});

// Confirm that "Remove" button is disabled
ui.button
Expand All @@ -405,20 +408,20 @@
.should('be.disabled');

// Confirm that "Reset Root Password" button is disabled
ui.button
.findByTitle('Reset Root Password')
ui.cdsButton
.findButtonByTitle('Reset Root Password')
.should('be.visible')
.should('be.disabled');

// Confirm that "Delete Cluster" button is disabled
ui.button
.findByTitle('Delete Cluster')
ui.cdsButton
.findButtonByTitle('Delete Cluster')
.should('be.visible')
.should('be.disabled');

// Confirm that "Save Changes" button is disabled
ui.button
.findByTitle('Save Changes')
ui.cdsButton
.findButtonByTitle('Save Changes')
.should('be.visible')
.should('be.disabled');
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,11 @@
cy.findAllByTestId('currentSummary').should('be.visible');

// Create database, confirm redirect, and that new instance is listed.
cy.findByText('Create Database Cluster').should('be.visible').click();
ui.cdsButton
.findButtonByTitle('Create Database Cluster')

Check warning on line 198 in packages/manager/cypress/e2e/core/databases/create-database.spec.ts

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Define a constant instead of duplicating this literal 3 times. Raw Output: {"ruleId":"sonarjs/no-duplicate-string","severity":1,"message":"Define a constant instead of duplicating this literal 3 times.","line":198,"column":30,"nodeType":"Literal","endLine":198,"endColumn":55}
.then((btn) => {
btn[0].click(); // Native DOM click
});
Comment on lines +199 to +201
Copy link
Contributor

Choose a reason for hiding this comment

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

@stayal712 and @tvijay-akamai, do you have an explanation for why this workaround is necessary?

In my testing, I'm able to implement a simple (non-CDS) button in a shadow root, set up an event handler, and trigger it by clicking via cy.click(). Why doesn't this work with the CDS button? Why aren't we running into any of these same issues with the text field component?

I'm willing to resort to hacky workarounds like this if they're necessary, but we should at least understand why. This is a lot of work to click on a button, and the fact that this is only an issue with the CDS button makes it sound like a bug. I want to rule that out, and I especially want to avoid implementing more hacks like this as we introduce more CDS components.

Copy link
Contributor

Choose a reason for hiding this comment

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

I am going to try to offer a potential fix, without confidence it'll work. Bear in mind, my knowledge of web components is limited, however I'd still like to explore this before moving forward with the workaround proposed, which seem to be pointing at a potential limitation of the component.

https://git.source.akamai.com/projects/FEE/repos/cds-web-components/browse/packages/cds-web-components/src/components/button/button.element.ts

Custom elements sometimes don’t work well with React’s synthetic event system. onClick in React won’t fire unless the component dispatches a standard DOM event from the host. This may be the issue we've been noticing with Cypress.

Currently, this listens to @click internally on the native button, but we don't re-emit the event from the custom element. Which could perhaps be done this way:

private _handleClick(e: MouseEvent) {
  if (this.type === 'submit') {
    const form = this.closest('form');
    if (form) {
      e.preventDefault();
      form.requestSubmit();
    }
  }

  // Re-emit the click event from the custom element host
  this.dispatchEvent(new MouseEvent('click', {
    bubbles: true,
    composed: true,
    cancelable: true,
  }));
}

https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent

Again, not sure this will fix it, but before approving I'd like to explore this because it would also have important implications for other Web Components making their way to Cloud Manager. It could even be expanded further from what I could read, but not sure how helpful that would be.

this.dispatchEvent(new MouseEvent('click', {
    bubbles: true,
    composed: true,
    cancelable: true,
    detail: e.detail,
    screenX: e.screenX,
    screenY: e.screenY,
    clientX: e.clientX,
    clientY: e.clientY,
    ctrlKey: e.ctrlKey,
    shiftKey: e.shiftKey,
    altKey: e.altKey,
    metaKey: e.metaKey,
    button: e.button,
    relatedTarget: e.relatedTarget,
  }));

Copy link

@spadhi-akamai spadhi-akamai May 22, 2025

Choose a reason for hiding this comment

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

@abailly-akamai
Thank you for your suggestion. Tarun has started looking into your suggestion and will also explore alternative solutions with QA team to find the best way out to access shadow dom elements in cypress, in the next release. Meanwhile, request you to move forward with this PR for the current release.

Copy link
Contributor

Choose a reason for hiding this comment

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

Approved. It would be greatly appreciated if the next PR for a web component takes in consideration all the problems raised in this one, and the compatibility with our codebase not to be an afterthought. Merging this as-is gives us no guaranty there will be a follow up. The branch cut is in a week by the way.

cy.wait('@createDatabase');

// TODO Update assertions upon completion of M3-7030.
Expand Down Expand Up @@ -327,11 +331,10 @@
// table present for restricted user but its inputs will be disabled
cy.get('table[aria-label="List of Linode Plans"]').should('exist');
// Assert that Create Database button is visible and disabled
ui.button
.findByTitle('Create Database Cluster')
ui.cdsButton
.findButtonByTitle('Create Database Cluster')
.should('be.visible')
.and('be.disabled')
.trigger('mouseover');
.should('be.disabled');

// Info message is visible
cy.findByText(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@
cy.wait(['@getAccount', '@getDatabase', '@getDatabaseTypes']);

// Click "Delete Cluster" button.
ui.button
.findByAttribute('data-qa-settings-button', 'Delete Cluster')
ui.cdsButton
.findButtonByTitle('Delete Cluster')

Check warning on line 57 in packages/manager/cypress/e2e/core/databases/delete-database.spec.ts

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Define a constant instead of duplicating this literal 4 times. Raw Output: {"ruleId":"sonarjs/no-duplicate-string","severity":1,"message":"Define a constant instead of duplicating this literal 4 times.","line":57,"column":32,"nodeType":"Literal","endLine":57,"endColumn":48}
.should('be.visible')
.click();

Expand Down Expand Up @@ -116,8 +116,8 @@
cy.wait(['@getAccount', '@getDatabase', '@getDatabaseTypes']);

// Click "Delete Cluster" button.
ui.button
.findByAttribute('data-qa-settings-button', 'Delete Cluster')
ui.cdsButton
.findButtonByTitle('Delete Cluster')
.should('be.visible')
.click();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
*/

const resizeDatabase = (initialLabel: string) => {
ui.button
.findByTitle('Resize Database Cluster')
ui.cdsButton
.findButtonByTitle('Resize Database Cluster')

Check warning on line 36 in packages/manager/cypress/e2e/core/databases/resize-database.spec.ts

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Define a constant instead of duplicating this literal 3 times. Raw Output: {"ruleId":"sonarjs/no-duplicate-string","severity":1,"message":"Define a constant instead of duplicating this literal 3 times.","line":36,"column":24,"nodeType":"Literal","endLine":36,"endColumn":49}
.should('be.visible')
.should('be.enabled')
.click();
Expand Down Expand Up @@ -98,8 +98,8 @@
cy.get('[data-reach-tab-list]').within(() => {
cy.findByText('Resize').should('be.visible').click();
});
ui.button
.findByTitle('Resize Database Cluster')
ui.cdsButton
.findButtonByTitle('Resize Database Cluster')
.should('be.visible')
.should('be.disabled');

Expand Down Expand Up @@ -246,8 +246,9 @@
cy.get('[data-reach-tab-list]').within(() => {
cy.findByText('Resize').should('be.visible').click();
});
ui.button
.findByTitle('Resize Database Cluster')

ui.cdsButton
.findButtonByTitle('Resize Database Cluster')
.should('be.visible')
.should('be.disabled');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@
* @param existingIps - The number of existing IPs. Optional, default is `0`.
*/
const manageAccessControl = (allowedIps: string[], existingIps: number = 0) => {
cy.findByTestId('button-access-control').click();
cy.get('[data-testid="button-access-control"]').within(() => {
ui.cdsButton.findButtonByTitle('Manage Access').click();

Check warning on line 97 in packages/manager/cypress/e2e/core/databases/update-database.spec.ts

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Define a constant instead of duplicating this literal 3 times. Raw Output: {"ruleId":"sonarjs/no-duplicate-string","severity":1,"message":"Define a constant instead of duplicating this literal 3 times.","line":97,"column":36,"nodeType":"Literal","endLine":97,"endColumn":51}
});

ui.drawer
.findByTitle('Manage Access')
Expand Down Expand Up @@ -126,8 +128,8 @@
* made on the result of the root password reset attempt.
*/
const resetRootPassword = () => {
ui.button
.findByAttribute('data-qa-settings-button', 'Reset Root Password')
ui.cdsButton
.findButtonByTitle('Reset Root Password')

Check warning on line 132 in packages/manager/cypress/e2e/core/databases/update-database.spec.ts

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Define a constant instead of duplicating this literal 4 times. Raw Output: {"ruleId":"sonarjs/no-duplicate-string","severity":1,"message":"Define a constant instead of duplicating this literal 4 times.","line":132,"column":24,"nodeType":"Literal","endLine":132,"endColumn":45}
.should('be.visible')
.click();

Expand Down Expand Up @@ -159,7 +161,7 @@
cy.findByText('Maintenance');
cy.findByText('Version');
cy.findByText(`${dbEngine} v${version}`);
ui.button.findByTitle('Upgrade Version').should('be.visible');
ui.cdsButton.findButtonByTitle('Upgrade Version').should('be.visible');
});
};

Expand All @@ -174,11 +176,16 @@
*/
const modifyMaintenanceWindow = (label: string, windowValue: string) => {
cy.findByText('Set a Weekly Maintenance Window');
cy.findByTitle('Save Changes').should('be.visible').should('be.disabled');
ui.cdsButton
.findButtonByTitle('Save Changes')
.should('be.visible')
.should('be.disabled');

ui.autocomplete.findByLabel(label).should('be.visible').type(windowValue);
cy.contains(windowValue).should('be.visible').click();
ui.button.findByTitle('Save Changes').should('be.visible').click();
ui.cdsButton.findButtonByTitle('Save Changes').then((btn) => {
btn[0].click(); // Native DOM click
});
};

describe('Update database clusters', () => {
Expand Down Expand Up @@ -233,18 +240,14 @@

cy.findByText('Connection Details');
// "Show" button should be enabled to reveal password when DB is active.
ui.button
.findByTitle('Show')
.should('be.visible')
.should('be.enabled')
.click();
ui.cdsButton.findButtonByTitle('Show').should('be.enabled').click();

cy.wait('@getCredentials');
cy.findByText(`${initialPassword}`);

// "Hide" button should be enabled to hide password when password is revealed.
ui.button
.findByTitle('Hide')
ui.cdsButton
.findButtonByTitle('Hide')
.should('be.visible')
.should('be.enabled')
.click();
Expand Down Expand Up @@ -368,8 +371,8 @@
cy.findByText(hostnameRegex).should('be.visible');

// DBaaS passwords cannot be revealed until database/cluster has provisioned.
ui.button
.findByTitle('Show')
ui.cdsButton
.findButtonByTitle('Show')
.should('be.visible')
.should('be.disabled');

Expand Down
17 changes: 17 additions & 0 deletions packages/manager/cypress/support/ui/buttons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,20 @@ export const buttonGroup = {
.closest('button');
},
};

export const cdsButton = {
/**
* Finds a cds button within shadow DOM by its title and returns the Cypress chainable.
*
* @param cdsButtonTitle - Title of cds button to find
*
* @returns Cypress chainable.
*/
findButtonByTitle: (cdsButtonTitle: string): Cypress.Chainable => {
return cy
.findByText(cdsButtonTitle)
.closest('cds-button')
.shadow()
.find('button');
},
};
2 changes: 1 addition & 1 deletion packages/manager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"@tanstack/react-query-devtools": "5.51.24",
"@tanstack/react-router": "^1.111.11",
"@xterm/xterm": "^5.5.0",
"akamai-cds-react-components": "0.0.1-alpha.6",
"akamai-cds-react-components": "0.0.1-alpha.9",
"algoliasearch": "^4.14.3",
"axios": "~1.8.3",
"braintree-web": "^3.92.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Box, Button, TextField, Typography } from '@linode/ui';
import { Box, TextField, Typography } from '@linode/ui';
import { Grid, styled } from '@mui/material';
import { Button } from 'akamai-cds-react-components';

import { PlansPanel } from 'src/features/components/PlansPanel/PlansPanel';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
import { accountFactory, databaseTypeFactory } from 'src/factories';
import { makeResourcePage } from 'src/mocks/serverHandlers';
import { http, HttpResponse, server } from 'src/mocks/testServer';
import { mockMatchMedia, renderWithTheme } from 'src/utilities/testHelpers';
import {
getShadowRootElement,
mockMatchMedia,
renderWithTheme,
} from 'src/utilities/testHelpers';

import DatabaseCreate from './DatabaseCreate';
const loadingTestId = 'circle-progress';
Expand Down Expand Up @@ -122,15 +126,18 @@
it('should have the "Create Database Cluster" button disabled for restricted users', async () => {
queryMocks.useProfile.mockReturnValue({ data: { restricted: true } });

const { findByText, getByTestId } = renderWithTheme(<DatabaseCreate />);
const { getByTestId } = renderWithTheme(<DatabaseCreate />);

expect(getByTestId(loadingTestId)).toBeInTheDocument();

await waitForElementToBeRemoved(getByTestId(loadingTestId));
const createClusterButtonSpan = await findByText('Create Database Cluster');
const createClusterButton = createClusterButtonSpan.closest('button');

expect(createClusterButton).toBeInTheDocument();
const buttonHost = getByTestId('create-database-cluster');

Check warning on line 135 in packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Avoid destructuring queries from `render` result, use `screen.getByTestId` instead Raw Output: {"ruleId":"testing-library/prefer-screen-queries","severity":1,"message":"Avoid destructuring queries from `render` result, use `screen.getByTestId` instead","line":135,"column":24,"nodeType":"Identifier","messageId":"preferScreenQueries","endLine":135,"endColumn":35}
const createClusterButton = buttonHost
? await getShadowRootElement(buttonHost, 'button')
: null;

expect(buttonHost).toBeInTheDocument();
expect(createClusterButton).toBeDisabled();
});

Expand Down Expand Up @@ -168,15 +175,18 @@
it('should have the "Create Database Cluster" button enabled for users with full access', async () => {
queryMocks.useProfile.mockReturnValue({ data: { restricted: false } });

const { findByText, getByTestId } = renderWithTheme(<DatabaseCreate />);
const { getByTestId } = renderWithTheme(<DatabaseCreate />);

expect(getByTestId(loadingTestId)).toBeInTheDocument();

await waitForElementToBeRemoved(getByTestId(loadingTestId));
const createClusterButtonSpan = await findByText('Create Database Cluster');
const createClusterButton = createClusterButtonSpan.closest('button');

expect(createClusterButton).toBeInTheDocument();
const buttonHost = getByTestId('create-database-cluster');

Check warning on line 184 in packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Avoid destructuring queries from `render` result, use `screen.getByTestId` instead Raw Output: {"ruleId":"testing-library/prefer-screen-queries","severity":1,"message":"Avoid destructuring queries from `render` result, use `screen.getByTestId` instead","line":184,"column":24,"nodeType":"Identifier","messageId":"preferScreenQueries","endLine":184,"endColumn":35}
const createClusterButton = buttonHost
? await getShadowRootElement(buttonHost, 'button')
: null;

expect(buttonHost).toBeInTheDocument();
expect(createClusterButton).toBeEnabled();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,10 +330,11 @@ const DatabaseCreate = () => {
provision.
</StyledTypography>
<StyledCreateBtn
buttonType="primary"
data-testid="create-database-cluster"
disabled={isRestricted}
loading={isSubmitting}
processing={isSubmitting}
type="submit"
variant="primary"
>
Create Database Cluster
</StyledCreateBtn>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
import * as React from 'react';

import { databaseFactory } from 'src/factories';
import { mockMatchMedia, renderWithTheme } from 'src/utilities/testHelpers';
import {
getShadowRootElement,
mockMatchMedia,
renderWithTheme,
} from 'src/utilities/testHelpers';

import AccessControls from './AccessControls';

Expand Down Expand Up @@ -36,12 +40,13 @@
['enable', false],
])(
'should %s "Manage Access" button when disabled is %s',
(_, isDisabled) => {
async (_, isDisabled) => {
const database = databaseFactory.build();
const { getByRole } = renderWithTheme(
const { getByTestId } = renderWithTheme(
<AccessControls database={database} disabled={isDisabled} />
);
const button = getByRole('button', { name: 'Manage Access' });
const buttonHost = getByTestId('button-access-control');

Check warning on line 48 in packages/manager/src/features/Databases/DatabaseDetail/AccessControls.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Avoid destructuring queries from `render` result, use `screen.getByTestId` instead Raw Output: {"ruleId":"testing-library/prefer-screen-queries","severity":1,"message":"Avoid destructuring queries from `render` result, use `screen.getByTestId` instead","line":48,"column":26,"nodeType":"Identifier","messageId":"preferScreenQueries","endLine":48,"endColumn":37}
const button = await getShadowRootElement(buttonHost, 'button');

if (isDisabled) {
expect(button).toBeDisabled();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ActionsPanel, Button, Notice, Typography } from '@linode/ui';
import { ActionsPanel, Notice, Typography } from '@linode/ui';
import { Button } from 'akamai-cds-react-components';
import * as React from 'react';
import { makeStyles } from 'tss-react/mui';

Expand Down Expand Up @@ -171,11 +172,11 @@ export const AccessControls = (props: Props) => {
<div className={classes.sectionText}>{description ?? null}</div>
</div>
<Button
buttonType="outlined"
className={classes.addAccessControlBtn}
data-testid="button-access-control"
disabled={disabled}
onClick={() => setAddAccessControlDrawerOpen(true)}
variant="secondary"
>
Manage Access
</Button>
Expand Down
Loading
Loading