From dc891f342c6e64a95dcc63a735a7b371a489463e Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Wed, 11 Sep 2024 15:26:58 -0400 Subject: [PATCH 01/62] Fix misaligned banner text --- packages/manager/src/features/Domains/DomainBanner.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/manager/src/features/Domains/DomainBanner.tsx b/packages/manager/src/features/Domains/DomainBanner.tsx index 911be5f9f45..42442193e73 100644 --- a/packages/manager/src/features/Domains/DomainBanner.tsx +++ b/packages/manager/src/features/Domains/DomainBanner.tsx @@ -30,9 +30,9 @@ export const DomainBanner = React.memo((props: DomainBannerProps) => { variant="warning" > <> - + Your DNS zones are not being served. - + Your domains will not be served by Linode’s nameservers unless you have at least one active Linode on your account.{` `} @@ -43,12 +43,6 @@ export const DomainBanner = React.memo((props: DomainBannerProps) => { ); }); -const StyledTypography = styled(Typography, { label: 'StyledTypography' })( - ({ theme }) => ({ - marginBottom: theme.spacing(), - }) -); - const StyledDismissibleBanner = styled(DismissibleBanner, { label: 'StyledDismissableBanner', })(({ theme }) => ({ From 4896dcddb0fa5664ceef2c6d5a4c985befbaad37 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Wed, 11 Sep 2024 16:05:39 -0400 Subject: [PATCH 02/62] Added changeset: Misaligned DNS banner text --- packages/manager/.changeset/pr-10924-fixed-1726085139068.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-10924-fixed-1726085139068.md diff --git a/packages/manager/.changeset/pr-10924-fixed-1726085139068.md b/packages/manager/.changeset/pr-10924-fixed-1726085139068.md new file mode 100644 index 00000000000..9d3eb10136a --- /dev/null +++ b/packages/manager/.changeset/pr-10924-fixed-1726085139068.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Misaligned DNS banner text ([#10924](https://github.com/linode/manager/pull/10924)) From fc524ff99a08c545b1122ea4c0255aa2e66bead0 Mon Sep 17 00:00:00 2001 From: Hussain Khalil Date: Thu, 12 Sep 2024 13:44:52 -0400 Subject: [PATCH 03/62] Use to ensure spacing between elements --- packages/manager/src/features/Domains/DomainBanner.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/manager/src/features/Domains/DomainBanner.tsx b/packages/manager/src/features/Domains/DomainBanner.tsx index 42442193e73..44160e35d1b 100644 --- a/packages/manager/src/features/Domains/DomainBanner.tsx +++ b/packages/manager/src/features/Domains/DomainBanner.tsx @@ -4,6 +4,7 @@ import * as React from 'react'; import { DismissibleBanner } from 'src/components/DismissibleBanner/DismissibleBanner'; import { Link } from 'src/components/Link'; +import { Stack } from 'src/components/Stack'; import { Typography } from 'src/components/Typography'; interface DomainBannerProps { @@ -29,7 +30,7 @@ export const DomainBanner = React.memo((props: DomainBannerProps) => { preferenceKey={KEY} variant="warning" > - <> + Your DNS zones are not being served. @@ -38,7 +39,7 @@ export const DomainBanner = React.memo((props: DomainBannerProps) => { you have at least one active Linode on your account.{` `} You can create one here. - + ); }); @@ -47,6 +48,6 @@ const StyledDismissibleBanner = styled(DismissibleBanner, { label: 'StyledDismissableBanner', })(({ theme }) => ({ '& h3:first-of-type': { - marginBottom: theme.spacing(1), + margin: theme.spacing(1), }, })); From 67b686beaefb46c2a4488036dfb888c6e9e830d9 Mon Sep 17 00:00:00 2001 From: Azure-akamai Date: Fri, 13 Sep 2024 09:58:17 -0400 Subject: [PATCH 04/62] test: [M3-8316,M3-8317] - Tag cypress tests by adding the "method:e2e" and "purpose:dcTesting" (#10915) * tag cypress tests * Added changeset: Tag cypress tests by adding the method:e2e and purpose:dcTesting * Update cy.tag --- .../pr-10915-tests-1725992312603.md | 5 +++ .../e2e/core/account/account-logout.spec.ts | 2 +- .../e2e/core/account/service-transfer.spec.ts | 2 + .../core/domains/smoke-clone-domain.spec.ts | 3 ++ .../smoke-create-domain-records.spec.ts | 3 ++ .../core/domains/smoke-create-domain.spec.ts | 1 + .../core/domains/smoke-delete-domain.spec.ts | 3 ++ .../core/firewalls/create-firewall.spec.ts | 3 ++ .../core/firewalls/delete-firewall.spec.ts | 3 ++ .../migrate-linode-with-firewall.spec.ts | 1 + .../core/firewalls/update-firewall.spec.ts | 3 ++ .../core/general/account-activation.spec.ts | 3 ++ .../e2e/core/general/smoke-deep-link.spec.ts | 4 +- .../e2e/core/images/create-image.spec.ts | 1 + .../e2e/core/images/search-images.spec.ts | 3 ++ .../e2e/core/kubernetes/lke-create.spec.ts | 1 + .../e2e/core/linodes/backup-linode.spec.ts | 2 + .../e2e/core/linodes/clone-linode.spec.ts | 1 + .../e2e/core/linodes/linode-config.spec.ts | 3 ++ .../e2e/core/linodes/linode-storage.spec.ts | 39 ++++++---------- .../e2e/core/linodes/rebuild-linode.spec.ts | 3 ++ .../e2e/core/linodes/rescue-linode.spec.ts | 1 + .../e2e/core/linodes/resize-linode.spec.ts | 1 + .../core/linodes/smoke-delete-linode.spec.ts | 3 ++ .../core/linodes/switch-linode-state.spec.ts | 1 + .../core/linodes/update-linode-labels.spec.ts | 1 + .../smoke-create-nodebal.spec.ts | 3 ++ .../core/objectStorage/access-key.e2e.spec.ts | 3 ++ .../objectStorage/object-storage.e2e.spec.ts | 3 ++ .../core/oneClickApps/one-click-apps.spec.ts | 1 + .../stackscripts/create-stackscripts.spec.ts | 3 ++ .../smoke-community-stackscripts.spec.ts | 2 + .../e2e/core/volumes/attach-volume.spec.ts | 3 ++ .../e2e/core/volumes/clone-volume.spec.ts | 3 ++ .../e2e/core/volumes/create-volume.spec.ts | 4 +- .../e2e/core/volumes/delete-volume.spec.ts | 3 ++ .../e2e/core/volumes/resize-volume.spec.ts | 3 ++ .../e2e/core/volumes/search-volumes.spec.ts | 4 +- .../e2e/core/volumes/update-volume.spec.ts | 3 ++ .../cypress/support/intercepts/linodes.ts | 45 +++++++++++++++++++ 40 files changed, 150 insertions(+), 29 deletions(-) create mode 100644 packages/manager/.changeset/pr-10915-tests-1725992312603.md diff --git a/packages/manager/.changeset/pr-10915-tests-1725992312603.md b/packages/manager/.changeset/pr-10915-tests-1725992312603.md new file mode 100644 index 00000000000..6d7703fd558 --- /dev/null +++ b/packages/manager/.changeset/pr-10915-tests-1725992312603.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Tag cypress tests by adding the "method:e2e" and "purpose:dcTesting" ([#10915](https://github.com/linode/manager/pull/10915)) diff --git a/packages/manager/cypress/e2e/core/account/account-logout.spec.ts b/packages/manager/cypress/e2e/core/account/account-logout.spec.ts index bb72c9795b4..550c4b39c98 100644 --- a/packages/manager/cypress/e2e/core/account/account-logout.spec.ts +++ b/packages/manager/cypress/e2e/core/account/account-logout.spec.ts @@ -4,7 +4,7 @@ import { ui } from 'support/ui'; describe('Logout Test', () => { beforeEach(() => { - cy.tag('purpose:syntheticTesting'); + cy.tag('purpose:syntheticTesting', 'method:e2e'); }); /* diff --git a/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts b/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts index 71428f889ea..51c3059d59f 100644 --- a/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts +++ b/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts @@ -121,6 +121,7 @@ describe('Account service transfers', () => { * - Confirms user can navigate to service transfer page via user menu. */ it('can navigate to service transfers landing page', () => { + cy.tag('method:e2e'); cy.visitWithLogin('/'); cy.findByLabelText('Profile & Account').should('be.visible').click(); @@ -244,6 +245,7 @@ describe('Account service transfers', () => { * - Confirms that users can cancel a service transfer */ it('can initiate and cancel a service transfer', () => { + cy.tag('method:e2e'); // Create a Linode to transfer. const setupLinode = async (): Promise => { const payload = createLinodeRequestFactory.build({ diff --git a/packages/manager/cypress/e2e/core/domains/smoke-clone-domain.spec.ts b/packages/manager/cypress/e2e/core/domains/smoke-clone-domain.spec.ts index d45a2bf886d..4e9b28fb0b9 100644 --- a/packages/manager/cypress/e2e/core/domains/smoke-clone-domain.spec.ts +++ b/packages/manager/cypress/e2e/core/domains/smoke-clone-domain.spec.ts @@ -14,6 +14,9 @@ describe('Clone a Domain', () => { before(() => { cleanUp('domains'); }); + beforeEach(() => { + cy.tag('method:e2e'); + }); /* * - Clicks "Clone" action menu item for domain but cancels operation. diff --git a/packages/manager/cypress/e2e/core/domains/smoke-create-domain-records.spec.ts b/packages/manager/cypress/e2e/core/domains/smoke-create-domain-records.spec.ts index 3b1cb74e422..950719ce399 100644 --- a/packages/manager/cypress/e2e/core/domains/smoke-create-domain-records.spec.ts +++ b/packages/manager/cypress/e2e/core/domains/smoke-create-domain-records.spec.ts @@ -4,6 +4,9 @@ import { interceptCreateDomainRecord } from 'support/intercepts/domains'; import { createDomainRecords } from 'support/constants/domains'; authenticate(); +beforeEach(() => { + cy.tag('method:e2e'); +}); describe('Creates Domains records with Form', () => { it('Adds domain records to a newly created Domain', () => { diff --git a/packages/manager/cypress/e2e/core/domains/smoke-create-domain.spec.ts b/packages/manager/cypress/e2e/core/domains/smoke-create-domain.spec.ts index 04d206a0bc8..e3d8d513de1 100644 --- a/packages/manager/cypress/e2e/core/domains/smoke-create-domain.spec.ts +++ b/packages/manager/cypress/e2e/core/domains/smoke-create-domain.spec.ts @@ -16,6 +16,7 @@ describe('Create a Domain', () => { }); it('Creates first Domain', () => { + cy.tag('method:e2e'); // Mock Domains to modify incoming response. const mockDomains = new Array(2).fill(null).map( (_item: null, index: number): Domain => { diff --git a/packages/manager/cypress/e2e/core/domains/smoke-delete-domain.spec.ts b/packages/manager/cypress/e2e/core/domains/smoke-delete-domain.spec.ts index 80d9b632aa2..4e9223cca14 100644 --- a/packages/manager/cypress/e2e/core/domains/smoke-delete-domain.spec.ts +++ b/packages/manager/cypress/e2e/core/domains/smoke-delete-domain.spec.ts @@ -7,6 +7,9 @@ import { createDomain } from '@linode/api-v4/lib/domains'; import { ui } from 'support/ui'; authenticate(); +beforeEach(() => { + cy.tag('method:e2e'); +}); describe('Delete a Domain', () => { /* * - Clicks "Delete" action menu item for domain but cancels operation. diff --git a/packages/manager/cypress/e2e/core/firewalls/create-firewall.spec.ts b/packages/manager/cypress/e2e/core/firewalls/create-firewall.spec.ts index 4da8d8c2dab..3007167215a 100644 --- a/packages/manager/cypress/e2e/core/firewalls/create-firewall.spec.ts +++ b/packages/manager/cypress/e2e/core/firewalls/create-firewall.spec.ts @@ -12,6 +12,9 @@ describe('create firewall', () => { before(() => { cleanUp(['lke-clusters', 'linodes', 'firewalls']); }); + beforeEach(() => { + cy.tag('method:e2e'); + }); /* * - Creates a firewall that is not assigned to a Linode. diff --git a/packages/manager/cypress/e2e/core/firewalls/delete-firewall.spec.ts b/packages/manager/cypress/e2e/core/firewalls/delete-firewall.spec.ts index 2cbedb29e5f..7be2db7cd11 100644 --- a/packages/manager/cypress/e2e/core/firewalls/delete-firewall.spec.ts +++ b/packages/manager/cypress/e2e/core/firewalls/delete-firewall.spec.ts @@ -11,6 +11,9 @@ describe('delete firewall', () => { before(() => { cleanUp('firewalls'); }); + beforeEach(() => { + cy.tag('method:e2e'); + }); /* * - Clicks "Delete" action menu item for firewall but cancels operation. diff --git a/packages/manager/cypress/e2e/core/firewalls/migrate-linode-with-firewall.spec.ts b/packages/manager/cypress/e2e/core/firewalls/migrate-linode-with-firewall.spec.ts index f8feabaf6db..2074a0785db 100644 --- a/packages/manager/cypress/e2e/core/firewalls/migrate-linode-with-firewall.spec.ts +++ b/packages/manager/cypress/e2e/core/firewalls/migrate-linode-with-firewall.spec.ts @@ -144,6 +144,7 @@ describe('Migrate Linode With Firewall', () => { * - Uses real API data to create a Firewall, attach a Linode to it, then migrate the Linode. */ it('migrates linode with firewall - real data', () => { + cy.tag('method:e2e', 'purpose:dcTesting'); const [migrationRegionStart, migrationRegionEnd] = chooseRegions(2); const firewallLabel = randomLabel(); const linodePayload = createLinodeRequestFactory.build({ diff --git a/packages/manager/cypress/e2e/core/firewalls/update-firewall.spec.ts b/packages/manager/cypress/e2e/core/firewalls/update-firewall.spec.ts index d67ac52fd3a..7269f369631 100644 --- a/packages/manager/cypress/e2e/core/firewalls/update-firewall.spec.ts +++ b/packages/manager/cypress/e2e/core/firewalls/update-firewall.spec.ts @@ -170,6 +170,9 @@ describe('update firewall', () => { before(() => { cleanUp('firewalls'); }); + beforeEach(() => { + cy.tag('method:e2e'); + }); /* * - Confirms that a linode can be added and removed from a firewall. diff --git a/packages/manager/cypress/e2e/core/general/account-activation.spec.ts b/packages/manager/cypress/e2e/core/general/account-activation.spec.ts index c6ab1764182..1c5273e999b 100644 --- a/packages/manager/cypress/e2e/core/general/account-activation.spec.ts +++ b/packages/manager/cypress/e2e/core/general/account-activation.spec.ts @@ -1,5 +1,8 @@ import { apiMatcher } from 'support/util/intercepts'; +beforeEach(() => { + cy.tag('method:e2e'); +}); describe('account activation', () => { /** * The API will return 403 with the body below for most endpoint except `/v4/profile`. diff --git a/packages/manager/cypress/e2e/core/general/smoke-deep-link.spec.ts b/packages/manager/cypress/e2e/core/general/smoke-deep-link.spec.ts index bc9f2af2951..e11f5707ae6 100644 --- a/packages/manager/cypress/e2e/core/general/smoke-deep-link.spec.ts +++ b/packages/manager/cypress/e2e/core/general/smoke-deep-link.spec.ts @@ -1,7 +1,9 @@ import { pages } from 'support/ui/constants'; - import type { Page } from 'support/ui/constants'; +beforeEach(() => { + cy.tag('method:e2e'); +}); describe('smoke - deep links', () => { beforeEach(() => { cy.visitWithLogin('/null'); diff --git a/packages/manager/cypress/e2e/core/images/create-image.spec.ts b/packages/manager/cypress/e2e/core/images/create-image.spec.ts index b118a4cad41..8ea89934bab 100644 --- a/packages/manager/cypress/e2e/core/images/create-image.spec.ts +++ b/packages/manager/cypress/e2e/core/images/create-image.spec.ts @@ -49,6 +49,7 @@ describe('create image (e2e)', () => { }); it('create image from a linode', () => { + cy.tag('method:e2e'); const label = randomLabel(); const description = randomPhrase(); diff --git a/packages/manager/cypress/e2e/core/images/search-images.spec.ts b/packages/manager/cypress/e2e/core/images/search-images.spec.ts index 9620a2312e7..8da716b8d33 100644 --- a/packages/manager/cypress/e2e/core/images/search-images.spec.ts +++ b/packages/manager/cypress/e2e/core/images/search-images.spec.ts @@ -13,6 +13,9 @@ describe('Search Images', () => { before(() => { cleanUp(['linodes', 'images']); }); + beforeEach(() => { + cy.tag('method:e2e'); + }); /* * - Confirm that images are API searchable and filtered in the UI. diff --git a/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts b/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts index bff073133a9..f354607d4a0 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts @@ -81,6 +81,7 @@ describe('LKE Cluster Creation', () => { * - Confirms that correct information is shown on the LKE cluster summary page */ it('can create an LKE cluster', () => { + cy.tag('method:e2e', 'purpose:dcTesting'); const clusterLabel = randomLabel(); const clusterRegion = chooseRegion(); const clusterVersion = '1.27'; diff --git a/packages/manager/cypress/e2e/core/linodes/backup-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/backup-linode.spec.ts index f973a37651d..54eaf02e5b6 100644 --- a/packages/manager/cypress/e2e/core/linodes/backup-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/backup-linode.spec.ts @@ -41,6 +41,7 @@ describe('linode backups', () => { * - Confirms that Linode details page updates to reflect that backups are enabled. */ it('can enable backups', () => { + cy.tag('method:e2e'); // Skip or optionally fail if test account has Managed enabled. // This is necessary because Managed accounts have backups enabled implicitly. expectManagedDisabled(); @@ -107,6 +108,7 @@ describe('linode backups', () => { * - Confirms that backups page content updates to reflect new snapshot. */ it('can capture a manual snapshot', () => { + cy.tag('method:e2e'); // Create a Linode that is not booted and which has backups enabled. const createLinodeRequest = createLinodeRequestFactory.build({ label: randomLabel(), diff --git a/packages/manager/cypress/e2e/core/linodes/clone-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/clone-linode.spec.ts index c64e43abba6..55a1ce53a76 100644 --- a/packages/manager/cypress/e2e/core/linodes/clone-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/clone-linode.spec.ts @@ -47,6 +47,7 @@ describe('clone linode', () => { * - Confirms that Linode can be cloned successfully. */ it('can clone a Linode from Linode details page', () => { + cy.tag('method:e2e', 'purpose:dcTesting'); const linodeRegion = chooseRegion({ capabilities: ['Vlans'] }); const linodePayload = createLinodeRequestFactory.build({ label: randomLabel(), diff --git a/packages/manager/cypress/e2e/core/linodes/linode-config.spec.ts b/packages/manager/cypress/e2e/core/linodes/linode-config.spec.ts index 2e73ca80de0..68ec28b9701 100644 --- a/packages/manager/cypress/e2e/core/linodes/linode-config.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/linode-config.spec.ts @@ -91,6 +91,9 @@ describe('Linode Config management', () => { } ); }); + beforeEach(() => { + cy.tag('method:e2e'); + }); /* * - Tests Linode config creation end-to-end using real API requests. diff --git a/packages/manager/cypress/e2e/core/linodes/linode-storage.spec.ts b/packages/manager/cypress/e2e/core/linodes/linode-storage.spec.ts index 0e256bbbb41..9403d258e72 100644 --- a/packages/manager/cypress/e2e/core/linodes/linode-storage.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/linode-storage.spec.ts @@ -5,7 +5,11 @@ import { createTestLinode } from 'support/util/linodes'; import { containsVisible, fbtClick, fbtVisible } from 'support/helpers'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; -import { apiMatcher } from 'support/util/intercepts'; +import { + interceptDeleteDisks, + interceptAddDisks, + interceptResizeDisks, +} from 'support/intercepts/linodes'; // 3 minutes. const LINODE_PROVISION_TIMEOUT = 180_000; @@ -97,6 +101,9 @@ const addDisk = (diskName: string) => { }; authenticate(); +beforeEach(() => { + cy.tag('method:e2e'); +}); describe('linode storage tab', () => { before(() => { cleanUp(['linodes', 'lke-clusters']); @@ -105,10 +112,7 @@ describe('linode storage tab', () => { it('try to delete in use disk', () => { const diskName = 'Debian 11 Disk'; cy.defer(() => createTestLinode({ booted: true })).then((linode) => { - cy.intercept( - 'DELETE', - apiMatcher(`linode/instances/${linode.id}/disks/*`) - ).as('deleteDisk'); + interceptDeleteDisks(linode.id).as('deleteDisk'); cy.visitWithLogin(`linodes/${linode.id}/storage`); containsVisible('RUNNING'); fbtVisible(diskName); @@ -128,14 +132,8 @@ describe('linode storage tab', () => { it('delete disk', () => { const diskName = 'cy-test-disk'; cy.defer(() => createTestLinode({ image: null })).then((linode) => { - cy.intercept( - 'DELETE', - apiMatcher(`linode/instances/${linode.id}/disks/*`) - ).as('deleteDisk'); - cy.intercept( - 'POST', - apiMatcher(`linode/instances/${linode.id}/disks`) - ).as('addDisk'); + interceptDeleteDisks(linode.id).as('deleteDisk'); + interceptAddDisks(linode.id).as('addDisk'); cy.visitWithLogin(`/linodes/${linode.id}/storage`); addDisk(diskName); fbtVisible(diskName); @@ -160,10 +158,7 @@ describe('linode storage tab', () => { it('add a disk', () => { const diskName = 'cy-test-disk'; cy.defer(() => createTestLinode({ image: null })).then((linode: Linode) => { - cy.intercept( - 'POST', - apiMatcher(`/linode/instances/${linode.id}/disks`) - ).as('addDisk'); + interceptAddDisks(linode.id).as('addDisk'); cy.visitWithLogin(`/linodes/${linode.id}/storage`); addDisk(diskName); fbtVisible(diskName); @@ -174,14 +169,8 @@ describe('linode storage tab', () => { it('resize disk', () => { const diskName = 'Debian 10 Disk'; cy.defer(() => createTestLinode({ image: null })).then((linode: Linode) => { - cy.intercept( - 'POST', - apiMatcher(`linode/instances/${linode.id}/disks`) - ).as('addDisk'); - cy.intercept( - 'POST', - apiMatcher(`linode/instances/${linode.id}/disks/*/resize`) - ).as('resizeDisk'); + interceptAddDisks(linode.id).as('addDisk'); + interceptResizeDisks(linode.id).as('resizeDisk'); cy.visitWithLogin(`/linodes/${linode.id}/storage`); addDisk(diskName); fbtVisible(diskName); diff --git a/packages/manager/cypress/e2e/core/linodes/rebuild-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/rebuild-linode.spec.ts index 2c2c98ce89b..dd0314af1b0 100644 --- a/packages/manager/cypress/e2e/core/linodes/rebuild-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/rebuild-linode.spec.ts @@ -110,6 +110,7 @@ describe('rebuild linode', () => { * - Confirms that password complexity */ it('rebuilds a linode from Image', () => { + cy.tag('method:e2e'); const weakPassword = 'abc123'; const fairPassword = 'Akamai123'; @@ -164,6 +165,7 @@ describe('rebuild linode', () => { * - Confirms that a Linode can be rebuilt using a Community StackScript. */ it('rebuilds a linode from Community StackScript', () => { + cy.tag('method:e2e'); const stackScriptId = '443929'; const stackScriptName = 'OpenLiteSpeed-WordPress'; const image = 'AlmaLinux 9'; @@ -226,6 +228,7 @@ describe('rebuild linode', () => { * - Confirms that a Linode can be rebuilt using an Account StackScript. */ it('rebuilds a linode from Account StackScript', () => { + cy.tag('method:e2e'); const image = 'Alpine'; const region = 'us-east'; diff --git a/packages/manager/cypress/e2e/core/linodes/rescue-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/rescue-linode.spec.ts index 533eac2535f..07e1c262ff0 100644 --- a/packages/manager/cypress/e2e/core/linodes/rescue-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/rescue-linode.spec.ts @@ -38,6 +38,7 @@ describe('Rescue Linodes', () => { * - Confirms that toast appears confirming successful reboot into rescue mode. */ it('Can reboot a Linode into rescue mode', () => { + cy.tag('method:e2e'); const linodePayload = createLinodeRequestFactory.build({ label: randomLabel(), region: chooseRegion().id, diff --git a/packages/manager/cypress/e2e/core/linodes/resize-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/resize-linode.spec.ts index 02e2cfc7e17..d3ab5c2bd09 100644 --- a/packages/manager/cypress/e2e/core/linodes/resize-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/resize-linode.spec.ts @@ -9,6 +9,7 @@ authenticate(); describe('resize linode', () => { beforeEach(() => { cleanUp(['linodes']); + cy.tag('method:e2e'); }); it('resizes a linode by increasing size: warm migration', () => { diff --git a/packages/manager/cypress/e2e/core/linodes/smoke-delete-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/smoke-delete-linode.spec.ts index 13ad0efaf4b..5520041418f 100644 --- a/packages/manager/cypress/e2e/core/linodes/smoke-delete-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/smoke-delete-linode.spec.ts @@ -67,6 +67,9 @@ describe('delete linode', () => { before(() => { cleanUp(['linodes', 'lke-clusters']); }); + beforeEach(() => { + cy.tag('method:e2e'); + }); it('deletes linode from linode details page', () => { const linodeCreatePayload = createLinodeRequestFactory.build({ diff --git a/packages/manager/cypress/e2e/core/linodes/switch-linode-state.spec.ts b/packages/manager/cypress/e2e/core/linodes/switch-linode-state.spec.ts index 7375e3e1e27..c0e03a30ea6 100644 --- a/packages/manager/cypress/e2e/core/linodes/switch-linode-state.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/switch-linode-state.spec.ts @@ -8,6 +8,7 @@ authenticate(); describe('switch linode state', () => { beforeEach(() => { cleanUp(['linodes']); + cy.tag('method:e2e'); }); /* diff --git a/packages/manager/cypress/e2e/core/linodes/update-linode-labels.spec.ts b/packages/manager/cypress/e2e/core/linodes/update-linode-labels.spec.ts index 4e87aa948a2..08c14da2a33 100644 --- a/packages/manager/cypress/e2e/core/linodes/update-linode-labels.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/update-linode-labels.spec.ts @@ -8,6 +8,7 @@ authenticate(); describe('update linode label', () => { beforeEach(() => { cleanUp(['linodes']); + cy.tag('method:e2e'); }); it('updates a linode label from details page', () => { diff --git a/packages/manager/cypress/e2e/core/nodebalancers/smoke-create-nodebal.spec.ts b/packages/manager/cypress/e2e/core/nodebalancers/smoke-create-nodebal.spec.ts index 3155ecc1d53..63c1e8080cf 100644 --- a/packages/manager/cypress/e2e/core/nodebalancers/smoke-create-nodebal.spec.ts +++ b/packages/manager/cypress/e2e/core/nodebalancers/smoke-create-nodebal.spec.ts @@ -78,6 +78,9 @@ const createNodeBalancerWithUI = ( }; authenticate(); +beforeEach(() => { + cy.tag('method:e2e', 'purpose:dcTesting'); +}); describe('create NodeBalancer', () => { before(() => { cleanUp(['tags', 'node-balancers', 'linodes']); diff --git a/packages/manager/cypress/e2e/core/objectStorage/access-key.e2e.spec.ts b/packages/manager/cypress/e2e/core/objectStorage/access-key.e2e.spec.ts index 85688840e98..17fda30faa9 100644 --- a/packages/manager/cypress/e2e/core/objectStorage/access-key.e2e.spec.ts +++ b/packages/manager/cypress/e2e/core/objectStorage/access-key.e2e.spec.ts @@ -21,6 +21,9 @@ describe('object storage access key end-to-end tests', () => { before(() => { cleanUp(['obj-buckets', 'obj-access-keys']); }); + beforeEach(() => { + cy.tag('method:e2e'); + }); /* * - Creates an access key with unlimited access diff --git a/packages/manager/cypress/e2e/core/objectStorage/object-storage.e2e.spec.ts b/packages/manager/cypress/e2e/core/objectStorage/object-storage.e2e.spec.ts index 1c17e3e7596..8599f4a1621 100644 --- a/packages/manager/cypress/e2e/core/objectStorage/object-storage.e2e.spec.ts +++ b/packages/manager/cypress/e2e/core/objectStorage/object-storage.e2e.spec.ts @@ -155,6 +155,9 @@ const assertStatusForUrlAtAlias = ( }; authenticate(); +beforeEach(() => { + cy.tag('method:e2e'); +}); describe('object storage end-to-end tests', () => { before(() => { cleanUp('obj-buckets'); diff --git a/packages/manager/cypress/e2e/core/oneClickApps/one-click-apps.spec.ts b/packages/manager/cypress/e2e/core/oneClickApps/one-click-apps.spec.ts index f453f6b7153..a6961e86709 100644 --- a/packages/manager/cypress/e2e/core/oneClickApps/one-click-apps.spec.ts +++ b/packages/manager/cypress/e2e/core/oneClickApps/one-click-apps.spec.ts @@ -23,6 +23,7 @@ import { mockGetAllImages } from 'support/intercepts/images'; describe('OneClick Apps (OCA)', () => { it('Lists all the OneClick Apps', () => { + cy.tag('method:e2e'); interceptGetStackScripts().as('getStackScripts'); cy.visitWithLogin(`/linodes/create?type=One-Click`); diff --git a/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts b/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts index 7619780800d..c2fa9e65557 100644 --- a/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts +++ b/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts @@ -172,6 +172,9 @@ describe('Create stackscripts', () => { before(() => { cleanUp(['linodes', 'images', 'stackscripts']); }); + beforeEach(() => { + cy.tag('method:e2e', 'purpose:dcTesting'); + }); /* * - Creates a StackScript with user-defined fields. diff --git a/packages/manager/cypress/e2e/core/stackscripts/smoke-community-stackscripts.spec.ts b/packages/manager/cypress/e2e/core/stackscripts/smoke-community-stackscripts.spec.ts index 43ae5fa6dc0..26453578d7b 100644 --- a/packages/manager/cypress/e2e/core/stackscripts/smoke-community-stackscripts.spec.ts +++ b/packages/manager/cypress/e2e/core/stackscripts/smoke-community-stackscripts.spec.ts @@ -190,6 +190,7 @@ describe('Community Stackscripts integration tests', () => { * - Confirms that pagination works as expected. */ it('pagination works with infinite scrolling', () => { + cy.tag('method:e2e'); interceptGetStackScripts().as('getStackScripts'); // Fetch all public Images to later use while filtering StackScripts. @@ -263,6 +264,7 @@ describe('Community Stackscripts integration tests', () => { * - Confirms that search can filter the expected results. */ it('search function filters results correctly', () => { + cy.tag('method:e2e'); const stackScript = mockStackScripts[0]; interceptGetStackScripts().as('getStackScripts'); diff --git a/packages/manager/cypress/e2e/core/volumes/attach-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/attach-volume.spec.ts index 76c2ec63a0f..41c53259a98 100644 --- a/packages/manager/cypress/e2e/core/volumes/attach-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/attach-volume.spec.ts @@ -50,6 +50,9 @@ describe('volume attach and detach flows', () => { before(() => { cleanUp(['volumes', 'linodes']); }); + beforeEach(() => { + cy.tag('method:e2e'); + }); /* * - Clicks "Attach" action menu item for volume, selects Linode with common region, and submits form. diff --git a/packages/manager/cypress/e2e/core/volumes/clone-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/clone-volume.spec.ts index f589c9b979b..e65a582243b 100644 --- a/packages/manager/cypress/e2e/core/volumes/clone-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/clone-volume.spec.ts @@ -35,6 +35,9 @@ describe('volume clone flow', () => { before(() => { cleanUp('volumes'); }); + beforeEach(() => { + cy.tag('method:e2e'); + }); /* * - Clicks "Clone" action menu item for volume, enters new label, and submits form. diff --git a/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts index ff4d3deb0dd..eea86e8d44e 100644 --- a/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts @@ -53,7 +53,7 @@ describe('volume create flow', () => { * - Confirms that volume is listed correctly on volumes landing page. */ it('creates an unattached volume', () => { - cy.tag('purpose:syntheticTesting'); + cy.tag('purpose:syntheticTesting', 'method:e2e', 'purpose:dcTesting'); const region = chooseRegion(); const volume = { @@ -98,6 +98,7 @@ describe('volume create flow', () => { * - Confirms that volume is listed correctly on Linode 'Storage' details page. */ it('creates an attached volume', () => { + cy.tag('method:e2e', 'purpose:dcTesting'); const region = chooseRegion(); const linodeRequest = createLinodeRequestFactory.build({ @@ -352,6 +353,7 @@ describe('volume create flow', () => { * - Confirms that volume is listed correctly on Volumes landing page. */ it('creates a volume from an existing Linode', () => { + cy.tag('method:e2e'); const linodeRequest = createLinodeRequestFactory.build({ label: randomLabel(), root_pass: randomString(16), diff --git a/packages/manager/cypress/e2e/core/volumes/delete-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/delete-volume.spec.ts index 7897d0c7f2c..6828618fb70 100644 --- a/packages/manager/cypress/e2e/core/volumes/delete-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/delete-volume.spec.ts @@ -20,6 +20,9 @@ describe('volume delete flow', () => { before(() => { cleanUp('volumes'); }); + beforeEach(() => { + cy.tag('method:e2e'); + }); /* * - Clicks "Delete" action menu item for volume but cancels operation. diff --git a/packages/manager/cypress/e2e/core/volumes/resize-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/resize-volume.spec.ts index f1ef8a5ddb7..74b9955ea76 100644 --- a/packages/manager/cypress/e2e/core/volumes/resize-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/resize-volume.spec.ts @@ -35,6 +35,9 @@ describe('volume resize flow', () => { before(() => { cleanUp('volumes'); }); + beforeEach(() => { + cy.tag('method:e2e'); + }); /* * - Clicks "Resize" action menu item for volume, enters new size, and submits form. diff --git a/packages/manager/cypress/e2e/core/volumes/search-volumes.spec.ts b/packages/manager/cypress/e2e/core/volumes/search-volumes.spec.ts index e6fc05b38b0..4b95b1a407f 100644 --- a/packages/manager/cypress/e2e/core/volumes/search-volumes.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/search-volumes.spec.ts @@ -1,7 +1,6 @@ import { createVolume } from '@linode/api-v4/lib/volumes'; import { Volume } from '@linode/api-v4'; import { ui } from 'support/ui'; - import { authenticate } from 'support/api/authentication'; import { randomLabel } from 'support/util/random'; import { cleanUp } from 'support/util/cleanup'; @@ -11,6 +10,9 @@ describe('Search Volumes', () => { before(() => { cleanUp(['volumes']); }); + beforeEach(() => { + cy.tag('method:e2e'); + }); /* * - Confirm that volumes are API searchable and filtered in the UI. diff --git a/packages/manager/cypress/e2e/core/volumes/update-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/update-volume.spec.ts index e5a3d9f5de0..70f4840bbad 100644 --- a/packages/manager/cypress/e2e/core/volumes/update-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/update-volume.spec.ts @@ -11,6 +11,9 @@ describe('volume update flow', () => { before(() => { cleanUp(['tags', 'volumes']); }); + beforeEach(() => { + cy.tag('method:e2e'); + }); /* * - Confirms that volume label and tags can be changed from the Volumes landing page. diff --git a/packages/manager/cypress/support/intercepts/linodes.ts b/packages/manager/cypress/support/intercepts/linodes.ts index 00b2e4c3f1b..2a665ac49bc 100644 --- a/packages/manager/cypress/support/intercepts/linodes.ts +++ b/packages/manager/cypress/support/intercepts/linodes.ts @@ -295,6 +295,51 @@ export const mockGetLinodeDisks = ( ); }; +/** + * Intercepts DELETE request to delete a Linode's Disks + * + * @param linodeId - ID of Linode for intercepted request. + * + * @returns Cypress chainable. + */ +export const interceptDeleteDisks = ( + linodeId: number +): Cypress.Chainable => { + return cy.intercept( + 'DELETE', + apiMatcher(`linode/instances/${linodeId}/disks/*`) + ); +}; + +/** + * Intercepts POST request to add a Linode's Disks + * + * @param linodeId - ID of Linode for intercepted request. + * + * @returns Cypress chainable. + */ +export const interceptAddDisks = ( + linodeId: number +): Cypress.Chainable => { + return cy.intercept('POST', apiMatcher(`linode/instances/${linodeId}/disks`)); +}; + +/** + * Intercepts POST request to resize a Linode's Disks + * + * @param linodeId - ID of Linode for intercepted request. + * + * @returns Cypress chainable. + */ +export const interceptResizeDisks = ( + linodeId: number +): Cypress.Chainable => { + return cy.intercept( + 'POST', + apiMatcher(`linode/instances/${linodeId}/disks/*/resize`) + ); +}; + /** * Intercepts DELETE request to delete linode and mocks response. * From a85f327837534c1a28ad73465ae22ad17fff839e Mon Sep 17 00:00:00 2001 From: Dajahi Wiley <114682940+dwiley-akamai@users.noreply.github.com> Date: Fri, 13 Sep 2024 10:09:32 -0400 Subject: [PATCH 05/62] =?UTF-8?q?upcoming:=20[M3-8576]=20=E2=80=93=20Fix?= =?UTF-8?q?=20"Create=20Volume"=20button=20state=20when=20"Encrypt=20Volum?= =?UTF-8?q?e"=20checkbox=20is=20checked=20(#10929)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...r-10929-upcoming-features-1726156754302.md | 5 ++++ .../e2e/core/volumes/create-volume.spec.ts | 27 ++++++++++++++++--- .../src/features/Volumes/VolumeCreate.tsx | 5 ++-- 3 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 packages/manager/.changeset/pr-10929-upcoming-features-1726156754302.md diff --git a/packages/manager/.changeset/pr-10929-upcoming-features-1726156754302.md b/packages/manager/.changeset/pr-10929-upcoming-features-1726156754302.md new file mode 100644 index 00000000000..70e24c98b29 --- /dev/null +++ b/packages/manager/.changeset/pr-10929-upcoming-features-1726156754302.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Fix 'Create Volume' button state on Volume Create page when 'Encrypt Volume' checkbox is checked ([#10929](https://github.com/linode/manager/pull/10929)) diff --git a/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts index eea86e8d44e..ceac6fc7cf1 100644 --- a/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts @@ -220,8 +220,12 @@ describe('volume create flow', () => { cy.get('[data-qa-checked]').should('be.visible').click(); // }); - // Ensure warning notice is displayed + // Ensure warning notice is displayed and "Create Volume" button is disabled cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('be.visible'); + ui.button + .findByTitle('Create Volume') + .should('be.visible') + .should('be.disabled'); } ); }); @@ -274,15 +278,19 @@ describe('volume create flow', () => { cy.get('[data-qa-checked]').should('be.visible').click(); // }); - // Ensure warning notice is not displayed + // Ensure warning notice is not displayed and "Create Volume" button is enabled cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('not.exist'); + ui.button + .findByTitle('Create Volume') + .should('be.visible') + .should('be.enabled'); }); /* * - Checks for Block Storage Encryption client library update notice in the Create/Attach Volume drawer from the 'Storage' details page of an existing Linode. */ - it('displays a warning notice re: rebooting for client library updates under the appropriate conditions', () => { + it('displays a warning notice re: rebooting for client library updates under the appropriate conditions in Create/Attach Volume drawer', () => { // Conditions: Block Storage encryption feature flag is on; user has Block Storage Encryption capability; Linode does not support Block Storage Encryption and the user is trying to attach an encrypted volume // Mock feature flag -- @TODO BSE: Remove feature flag once BSE is fully rolled out @@ -321,18 +329,25 @@ describe('volume create flow', () => { // Click "Add Volume" button cy.findByText('Add Volume').click(); + // Check "Encrypt Volume" checkbox cy.get('[data-qa-drawer="true"]').within(() => { cy.get('[data-qa-checked]').should('be.visible').click(); }); + // Ensure client library update notice is displayed and the "Create Volume" button is disabled cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('be.visible'); + ui.button.findByTitle('Create Volume').should('be.disabled'); // Ensure notice is cleared when switching views in drawer cy.get('[data-qa-radio="Attach Existing Volume"]').click(); cy.wait(['@getVolumes']); cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('not.exist'); + ui.button + .findByTitle('Attach Volume') + .should('be.visible') + .should('be.enabled'); - // Ensure notice is displayed in "Attach Existing Volume" view when an encrypted volume is selected + // Ensure notice is displayed in "Attach Existing Volume" view when an encrypted volume is selected, & that the "Attach Volume" button is disabled cy.findByPlaceholderText('Select a Volume') .should('be.visible') .click() @@ -343,6 +358,10 @@ describe('volume create flow', () => { .click(); cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('be.visible'); + ui.button + .findByTitle('Attach Volume') + .should('be.visible') + .should('be.disabled'); } ); }); diff --git a/packages/manager/src/features/Volumes/VolumeCreate.tsx b/packages/manager/src/features/Volumes/VolumeCreate.tsx index aadec8974d9..a9c9ec96df3 100644 --- a/packages/manager/src/features/Volumes/VolumeCreate.tsx +++ b/packages/manager/src/features/Volumes/VolumeCreate.tsx @@ -297,7 +297,7 @@ export const VolumeCreate = () => { const shouldDisplayClientLibraryCopy = isBlockStorageEncryptionFeatureEnabled && - values.linode_id !== null && + linode_id !== null && !linodeSupportsBlockStorageEncryption; return ( @@ -502,7 +502,8 @@ export const VolumeCreate = () => { */} ); @@ -823,6 +794,30 @@ class UserPermissions extends React.Component { setAllPerm: 'null', userType: null, }; + + componentDidMount() { + this.getUserGrants(); + this.getUserType(); + } + + componentDidUpdate(prevProps: CombinedProps) { + if (prevProps.currentUsername !== this.props.currentUsername) { + this.getUserGrants(); + this.getUserType(); + } + } + + render() { + const { loading } = this.state; + const { currentUsername } = this.props; + + return ( +
+ + {loading ? : this.renderBody()} +
+ ); + } } export default withQueryClient(withFeatureFlags(UserPermissions)); diff --git a/packages/manager/src/features/Users/UserPermissionsEntitySection.tsx b/packages/manager/src/features/Users/UserPermissionsEntitySection.tsx index 78d66dc3c3e..7876e6cdec7 100644 --- a/packages/manager/src/features/Users/UserPermissionsEntitySection.tsx +++ b/packages/manager/src/features/Users/UserPermissionsEntitySection.tsx @@ -6,33 +6,37 @@ * I'll create a tech debt ticket in jira to keep track of this issue. */ +import { useTheme } from '@mui/material/styles'; import React from 'react'; -import { Grant, GrantLevel, GrantType } from '@linode/api-v4/lib/account'; + import { Box } from 'src/components/Box'; -import { Theme } from '@mui/material/styles'; -import { TableHead } from 'src/components/TableHead'; -import { TableRow } from 'src/components/TableRow'; -import { TableCell } from 'src/components/TableCell'; +import { FormControlLabel } from 'src/components/FormControlLabel'; +import { createDisplayPage } from 'src/components/Paginate'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; import { Radio } from 'src/components/Radio/Radio'; import { TableBody } from 'src/components/TableBody'; -import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; -import { usePagination } from 'src/hooks/usePagination'; -import { createDisplayPage } from 'src/components/Paginate'; +import { TableCell } from 'src/components/TableCell'; +import { TableHead } from 'src/components/TableHead'; +import { TableRow } from 'src/components/TableRow'; import { Typography } from 'src/components/Typography'; -import { useTheme } from '@mui/material/styles'; -import { StyledGrantsTable } from './UserPermissionsEntitySection.styles'; import { grantTypeMap } from 'src/features/Account/constants'; +import { usePagination } from 'src/hooks/usePagination'; + +import { StyledGrantsTable } from './UserPermissionsEntitySection.styles'; + +import type { Grant, GrantLevel, GrantType } from '@linode/api-v4/lib/account'; +import type { Theme } from '@mui/material/styles'; interface Props { entity: GrantType; + entitySetAllTo: (entity: GrantType, value: GrantLevel) => () => void; grants: Grant[] | undefined; setGrantTo: (entity: string, idx: number, value: GrantLevel) => () => void; - entitySetAllTo: (entity: GrantType, value: GrantLevel) => () => void; showHeading?: boolean; } export const UserPermissionsEntitySection = React.memo( - ({ entity, grants, setGrantTo, entitySetAllTo, showHeading }: Props) => { + ({ entity, entitySetAllTo, grants, setGrantTo, showHeading }: Props) => { const theme: Theme = useTheme(); const pagination = usePagination(1); @@ -54,61 +58,79 @@ export const UserPermissionsEntitySection = React.memo( }; return ( - + {showHeading && ( {grantTypeMap[entity]} )} - + ({ + 'span.MuiFormControlLabel-label': { + fontFamily: theme.font.bold, + }, + })} + > Label - {/* eslint-disable-next-line */} - + + } + label="None" + /> - {/* eslint-disable-next-line */} - + + } + label="Read Only" + /> - {/* eslint-disable-next-line */} - + + } + label="Read-Write" + /> @@ -117,7 +139,7 @@ export const UserPermissionsEntitySection = React.memo( // Index must be corrected to account for pagination const idx = (pagination.page - 1) * pagination.pageSize + _idx; return ( - + {grant.label} - + - + - + @@ -167,11 +198,11 @@ export const UserPermissionsEntitySection = React.memo( ); From f0d9765ce2c30a2e64ef8ea32704a482d479e565 Mon Sep 17 00:00:00 2001 From: mpolotsk-akamai <157619599+mpolotsk-akamai@users.noreply.github.com> Date: Fri, 13 Sep 2024 19:31:57 +0200 Subject: [PATCH 11/62] feat: [UIE-8002] - DBaaS 2.0 Landing Page (#10823) * feat: [UIE-8054] - DBaaS enhancements 1 * UIE-8002 feat(DBaaS): New/Existing Customer Landing Page * UIE-8002 feat(DBaaS): Landing Page with feature flag * Added changeset: Add Landing Page for V2 * Added changeset: Add Landing Page and update Empty-State Landing page for DBaaS V2 * feat: [UIE-8002] - Review fix: refactoring and variable renaming * feat: [UIE-8002] - Review fix: change capability for region select --------- Co-authored-by: Conal Ryan --- packages/api-v4/src/account/types.ts | 2 +- packages/api-v4/src/databases/types.ts | 1 + packages/api-v4/src/regions/types.ts | 2 +- ...r-10823-upcoming-features-1724867496417.md | 5 + packages/manager/src/__data__/regionsData.ts | 5 - .../src/assets/icons/db-logo-white.svg | 23 ++ .../ResourcesLinksTypes.ts | 1 + .../ResourcesSection.tsx | 3 +- .../components/PrimaryNav/PrimaryNav.test.tsx | 6 +- packages/manager/src/factories/account.ts | 1 + packages/manager/src/factories/databases.ts | 6 +- .../DatabaseCreate/DatabaseCreate.test.tsx | 4 +- .../DatabaseCreate/DatabaseCreate.tsx | 13 +- .../DatabaseResize/DatabaseResize.test.tsx | 2 +- .../DatabaseDetail/DatabaseStatusDisplay.tsx | 27 ++- .../DatabaseLanding/DatabaseLanding.test.tsx | 146 ++++++++++- .../DatabaseLanding/DatabaseLanding.tsx | 226 ++++++++++-------- ...a.ts => DatabaseLandingEmptyStateData.tsx} | 4 + .../DatabaseLanding/DatabaseLandingTable.tsx | 140 +++++++++++ .../DatabaseLanding/DatabaseLogo.tsx | 53 ++-- .../Databases/DatabaseLanding/DatabaseRow.tsx | 26 +- .../src/features/Databases/utilities.test.ts | 4 +- .../src/features/Databases/utilities.ts | 2 +- .../DatabaseClusterInfoBanner.tsx | 46 ++++ packages/manager/src/mocks/serverHandlers.ts | 2 +- 25 files changed, 573 insertions(+), 177 deletions(-) create mode 100644 packages/manager/.changeset/pr-10823-upcoming-features-1724867496417.md create mode 100644 packages/manager/src/assets/icons/db-logo-white.svg rename packages/manager/src/features/Databases/DatabaseLanding/{DatabaseLandingEmptyStateData.ts => DatabaseLandingEmptyStateData.tsx} (91%) create mode 100644 packages/manager/src/features/Databases/DatabaseLanding/DatabaseLandingTable.tsx create mode 100644 packages/manager/src/features/GlobalNotifications/DatabaseClusterInfoBanner.tsx diff --git a/packages/api-v4/src/account/types.ts b/packages/api-v4/src/account/types.ts index c11000ff945..6cec39f8f4d 100644 --- a/packages/api-v4/src/account/types.ts +++ b/packages/api-v4/src/account/types.ts @@ -72,7 +72,7 @@ export type AccountCapability = | 'LKE HA Control Planes' | 'Machine Images' | 'Managed Databases' - | 'Managed Databases V2' + | 'Managed Databases Beta' | 'NodeBalancers' | 'Object Storage Access Key Regions' | 'Object Storage Endpoint Types' diff --git a/packages/api-v4/src/databases/types.ts b/packages/api-v4/src/databases/types.ts index 933f1978c86..5d9650c6eab 100644 --- a/packages/api-v4/src/databases/types.ts +++ b/packages/api-v4/src/databases/types.ts @@ -151,6 +151,7 @@ export interface BaseDatabase { * A key/value object where the key is an IP address and the value is a member type. */ members: Record; + platform?: string; } export interface MySQLDatabase extends BaseDatabase { diff --git a/packages/api-v4/src/regions/types.ts b/packages/api-v4/src/regions/types.ts index e95c7dc3f13..c3db5ec700e 100644 --- a/packages/api-v4/src/regions/types.ts +++ b/packages/api-v4/src/regions/types.ts @@ -13,7 +13,7 @@ export type Capabilities = | 'Kubernetes' | 'Linodes' | 'Managed Databases' - | 'Managed Databases V2' + | 'Managed Databases Beta' | 'Metadata' | 'NodeBalancers' | 'Object Storage' diff --git a/packages/manager/.changeset/pr-10823-upcoming-features-1724867496417.md b/packages/manager/.changeset/pr-10823-upcoming-features-1724867496417.md new file mode 100644 index 00000000000..be33c99dea2 --- /dev/null +++ b/packages/manager/.changeset/pr-10823-upcoming-features-1724867496417.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add Landing Page and update Empty-State Landing page for DBaaS V2 ([#10823](https://github.com/linode/manager/pull/10823)) diff --git a/packages/manager/src/__data__/regionsData.ts b/packages/manager/src/__data__/regionsData.ts index 836fbcafea7..0a3ab6eaf2e 100644 --- a/packages/manager/src/__data__/regionsData.ts +++ b/packages/manager/src/__data__/regionsData.ts @@ -98,7 +98,6 @@ export const regions: Region[] = [ 'Vlans', 'VPCs', 'Managed Databases', - 'Managed Databases V2', 'Metadata', 'Premium Plans', 'Placement Group', @@ -130,7 +129,6 @@ export const regions: Region[] = [ 'Vlans', 'VPCs', 'Managed Databases', - 'Managed Databases V2', 'Metadata', 'Premium Plans', ], @@ -486,7 +484,6 @@ export const regions: Region[] = [ 'VPCs', 'Block Storage Migrations', 'Managed Databases', - 'Managed Databases V2', 'Placement Group', ], country: 'us', @@ -514,7 +511,6 @@ export const regions: Region[] = [ 'Cloud Firewall', 'Block Storage Migrations', 'Managed Databases', - 'Managed Databases V2', 'Placement Group', ], country: 'us', @@ -546,7 +542,6 @@ export const regions: Region[] = [ 'VPCs', 'Block Storage Migrations', 'Managed Databases', - 'Managed Databases V2', 'Placement Group', ], country: 'us', diff --git a/packages/manager/src/assets/icons/db-logo-white.svg b/packages/manager/src/assets/icons/db-logo-white.svg new file mode 100644 index 00000000000..0cf1495fd94 --- /dev/null +++ b/packages/manager/src/assets/icons/db-logo-white.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/manager/src/components/EmptyLandingPageResources/ResourcesLinksTypes.ts b/packages/manager/src/components/EmptyLandingPageResources/ResourcesLinksTypes.ts index 942ff3d8a31..c94d2a6f6a1 100644 --- a/packages/manager/src/components/EmptyLandingPageResources/ResourcesLinksTypes.ts +++ b/packages/manager/src/components/EmptyLandingPageResources/ResourcesLinksTypes.ts @@ -11,6 +11,7 @@ export interface linkAnalyticsEvent { export interface ResourcesHeaders { description: string; + logo?: React.ReactNode; subtitle: string; title: string; } diff --git a/packages/manager/src/components/EmptyLandingPageResources/ResourcesSection.tsx b/packages/manager/src/components/EmptyLandingPageResources/ResourcesSection.tsx index 33386afde6b..6e57fa458d9 100644 --- a/packages/manager/src/components/EmptyLandingPageResources/ResourcesSection.tsx +++ b/packages/manager/src/components/EmptyLandingPageResources/ResourcesSection.tsx @@ -111,7 +111,7 @@ export const ResourcesSection = (props: ResourcesSectionProps) => { wide = false, youtubeLinkData, } = props; - const { description, subtitle, title } = headers; + const { description, logo, subtitle, title } = headers; return ( { subtitle={subtitle} title={title} > + {logo} {description} ); diff --git a/packages/manager/src/components/PrimaryNav/PrimaryNav.test.tsx b/packages/manager/src/components/PrimaryNav/PrimaryNav.test.tsx index 565b4c33451..80714ff7e42 100644 --- a/packages/manager/src/components/PrimaryNav/PrimaryNav.test.tsx +++ b/packages/manager/src/components/PrimaryNav/PrimaryNav.test.tsx @@ -89,7 +89,7 @@ describe('PrimaryNav', () => { it('should show Databases menu item if the user has the account capability V2 Beta', async () => { const account = accountFactory.build({ - capabilities: ['Managed Databases V2'], + capabilities: ['Managed Databases Beta'], }); server.use( @@ -121,7 +121,7 @@ describe('PrimaryNav', () => { it('should show Databases menu item if the user has the account capability V2', async () => { const account = accountFactory.build({ - capabilities: ['Managed Databases V2'], + capabilities: ['Managed Databases Beta'], }); server.use( @@ -152,7 +152,7 @@ describe('PrimaryNav', () => { it('should show Databases menu item if the user has the account capability V2', async () => { const account = accountFactory.build({ - capabilities: ['Managed Databases V2'], + capabilities: ['Managed Databases Beta'], }); server.use( diff --git a/packages/manager/src/factories/account.ts b/packages/manager/src/factories/account.ts index eff241f37d5..c056cf9ac56 100644 --- a/packages/manager/src/factories/account.ts +++ b/packages/manager/src/factories/account.ts @@ -46,6 +46,7 @@ export const accountFactory = Factory.Sync.makeFactory({ 'LKE HA Control Planes', 'Machine Images', 'Managed Databases', + 'Managed Databases Beta', 'NodeBalancers', 'Object Storage Access Key Regions', 'Object Storage Endpoint Types', diff --git a/packages/manager/src/factories/databases.ts b/packages/manager/src/factories/databases.ts index 657a262d804..14fe03d22f0 100644 --- a/packages/manager/src/factories/databases.ts +++ b/packages/manager/src/factories/databases.ts @@ -163,7 +163,7 @@ export const databaseTypeFactory = Factory.Sync.makeFactory({ }, ], }, - id: Factory.each((i) => `g6-standard-${i}`), + id: Factory.each((i) => possibleTypes[i % possibleTypes.length]), label: Factory.each((i) => `Linode ${i} GB`), memory: Factory.each((i) => i * 2048), vcpus: Factory.each((i) => i * 2), @@ -190,7 +190,9 @@ export const databaseInstanceFactory = Factory.Sync.makeFactory (adb10(i) ? 'adb10' : 'adb20')), + platform: Factory.each((i) => + adb10(i) ? 'rdbms-legacy' : 'rdbms-default' + ), region: Factory.each((i) => possibleRegions[i % possibleRegions.length]), status: Factory.each((i) => possibleStatuses[i % possibleStatuses.length]), type: Factory.each((i) => possibleTypes[i % possibleTypes.length]), diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.test.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.test.tsx index 0faa8c693c9..40b0bf1c6ce 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.test.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.test.tsx @@ -39,7 +39,7 @@ describe('Database Create', () => { const standardTypes = [ databaseTypeFactory.build({ class: 'nanode', - id: 'g6-standard-0', + id: 'g6-nanode-1', label: `Nanode 1 GB`, memory: 1024, }), @@ -117,7 +117,7 @@ describe('Database Create', () => { server.use( http.get('*/account', () => { const account = accountFactory.build({ - capabilities: ['Managed Databases V2'], + capabilities: ['Managed Databases Beta'], }); return HttpResponse.json(account); }) diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx index 20fadc053c9..716a4d46a36 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx @@ -64,9 +64,6 @@ import type { Item } from 'src/components/EnhancedSelect/Select'; import type { PlanSelectionType } from 'src/features/components/PlansPanel/types'; import type { ExtendedIP } from 'src/utilities/ipUtils'; -const V1 = 'Managed Databases'; -const V2 = `Managed Databases V2`; - const useStyles = makeStyles()((theme: Theme) => ({ btnCtn: { alignItems: 'center', @@ -342,7 +339,7 @@ const DatabaseCreate = () => { return dbtypes.map((type) => { const { label } = type; const formattedLabel = formatStorageUnits(label); - const singleNodePricing = type.engines[selectedEngine].find( + const singleNodePricing = type.engines[selectedEngine]?.find( (cluster) => cluster.quantity === 1 ); const price = singleNodePricing?.price ?? { @@ -448,13 +445,13 @@ const DatabaseCreate = () => { const engineType = values.engine.split('/')[0] as Engine; setNodePricing({ - double: type.engines[engineType].find( + double: type.engines[engineType]?.find( (cluster: DatabaseClusterSizeObject) => cluster.quantity === 2 )?.price, - multi: type.engines[engineType].find( + multi: type.engines[engineType]?.find( (cluster: DatabaseClusterSizeObject) => cluster.quantity === 3 )?.price, - single: type.engines[engineType].find( + single: type.engines[engineType]?.find( (cluster: DatabaseClusterSizeObject) => cluster.quantity === 1 )?.price, }); @@ -544,7 +541,7 @@ const DatabaseCreate = () => { setFieldValue('region', region.id)} diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.test.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.test.tsx index a2e8aef8909..46841b41615 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.test.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseResize/DatabaseResize.test.tsx @@ -65,7 +65,7 @@ describe('database resize', () => { }); describe('On rendering of page', () => { - const examplePlanType = 'g6-standard-60'; + const examplePlanType = 'g6-dedicated-50'; const dedicatedTypes = databaseTypeFactory.buildList(7, { class: 'dedicated', }); diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseStatusDisplay.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseStatusDisplay.tsx index f35942adede..5ee8c9f33f7 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseStatusDisplay.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseStatusDisplay.tsx @@ -1,50 +1,51 @@ import React from 'react'; + +import { StatusIcon } from 'src/components/StatusIcon/StatusIcon'; import { Typography } from 'src/components/Typography'; import { capitalize } from 'src/utilities/capitalize'; -import { Event } from '@linode/api-v4'; -import { + +import type { Event } from '@linode/api-v4'; +import type { Database, DatabaseInstance, DatabaseStatus, } from '@linode/api-v4/lib/databases/types'; -import { Status, StatusIcon } from 'src/components/StatusIcon/StatusIcon'; +import type { Status } from 'src/components/StatusIcon/StatusIcon'; export const databaseStatusMap: Record = { active: 'active', degraded: 'inactive', failed: 'error', provisioning: 'other', + resizing: 'other', restoring: 'other', resuming: 'other', suspended: 'error', suspending: 'other', - resizing: 'other', }; interface Props { - events: Event[] | undefined; database: Database | DatabaseInstance; + events: Event[] | undefined; } export const DatabaseStatusDisplay = (props: Props) => { - const { events, database } = props; + const { database, events } = props; const recentEvent = events?.find( (event: Event) => event.entity?.id === database.id && event.entity?.type === 'database' ); let progress: number | undefined; - if (recentEvent?.action === 'database_resize') { - progress = recentEvent?.percent_complete ?? 0; - } - let displayedStatus; + if ( - recentEvent?.status === 'started' || - recentEvent?.status === 'scheduled' + recentEvent?.action === 'database_resize' && + (recentEvent?.status === 'started' || recentEvent?.status === 'scheduled') ) { + progress = recentEvent?.percent_complete ?? 0; displayedStatus = ( <> - + {`Resizing ${progress ? `(${progress}%)` : '(0%)'}`} diff --git a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLanding.test.tsx b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLanding.test.tsx index d23e132ec4b..fa5710561da 100644 --- a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLanding.test.tsx +++ b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLanding.test.tsx @@ -1,10 +1,14 @@ +import { screen, within } from '@testing-library/react'; +import { fireEvent } from '@testing-library/react'; import { waitForElementToBeRemoved } from '@testing-library/react'; import { DateTime } from 'luxon'; import * as React from 'react'; -import { databaseInstanceFactory } from 'src/factories'; +import { accountFactory, databaseInstanceFactory } from 'src/factories'; +import DatabaseLanding from 'src/features/Databases/DatabaseLanding/DatabaseLanding'; +import DatabaseRow from 'src/features/Databases/DatabaseLanding/DatabaseRow'; import { makeResourcePage } from 'src/mocks/serverHandlers'; -import { http, HttpResponse, server } from 'src/mocks/testServer'; +import { HttpResponse, http, server } from 'src/mocks/testServer'; import { capitalize } from 'src/utilities/capitalize'; import { formatDate } from 'src/utilities/formatDate'; import { @@ -13,9 +17,6 @@ import { wrapWithTableBody, } from 'src/utilities/testHelpers'; -import DatabaseLanding from './DatabaseLanding'; -import DatabaseRow from './DatabaseRow'; - const queryMocks = vi.hoisted(() => ({ useProfile: vi.fn().mockReturnValue({ data: { restricted: false } }), })); @@ -93,13 +94,22 @@ describe('Database Table', () => { }); it('should render database landing with empty state', async () => { + const mockAccount = accountFactory.build({ + capabilities: ['Managed Databases Beta'], + }); + server.use( + http.get('*/account', () => { + return HttpResponse.json(mockAccount); + }) + ); server.use( http.get('*/databases/instances', () => { return HttpResponse.json(makeResourcePage([])); }) ); - - const { getByTestId, getByText } = renderWithTheme(); + const { getByTestId, getByText } = renderWithTheme(, { + flags: { dbaasV2: { beta: true, enabled: true } }, + }); await waitForElementToBeRemoved(getByTestId(loadingTestId)); @@ -109,6 +119,128 @@ describe('Database Table', () => { ) ).toBeInTheDocument(); }); + + it('should render tabs with legacy and new databases ', async () => { + server.use( + http.get('*/databases/instances', () => { + const databases = databaseInstanceFactory.buildList(5, { + status: 'active', + }); + + return HttpResponse.json(makeResourcePage(databases)); + }) + ); + + const { getByTestId } = renderWithTheme(, { + flags: { dbaasV2: { beta: true, enabled: true } }, + }); + + // Loading state should render + expect(getByTestId(loadingTestId)).toBeInTheDocument(); + + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + + const newDatabasesTab = screen.getByText('New Database Clusters'); + const legacyDatabasesTab = screen.getByText('Legacy Database Clusters'); + + expect(newDatabasesTab).toBeInTheDocument(); + expect(legacyDatabasesTab).toBeInTheDocument(); + }); + + it('should render logo in new databases tab ', async () => { + server.use( + http.get('*/databases/instances', () => { + const databases = databaseInstanceFactory.buildList(5, { + status: 'active', + }); + return HttpResponse.json(makeResourcePage(databases)); + }) + ); + + const { getByTestId } = renderWithTheme(, { + flags: { dbaasV2: { beta: true, enabled: true } }, + }); + + // Loading state should render + expect(getByTestId(loadingTestId)).toBeInTheDocument(); + + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + + const newDatabaseTab = screen.getByText('New Database Clusters'); + fireEvent.click(newDatabaseTab); + + expect(screen.getByText('Powered by')).toBeInTheDocument(); + }); + + it('should render a single legacy database table without logo ', async () => { + server.use( + http.get('*/databases/instances', () => { + const databases = databaseInstanceFactory.buildList(5, { + status: 'active', + }); + return HttpResponse.json(makeResourcePage(databases)); + }) + ); + + const { getByTestId } = renderWithTheme(, { + flags: { dbaasV2: { beta: false, enabled: false } }, + }); + + expect(getByTestId(loadingTestId)).toBeInTheDocument(); + + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + + const tables = screen.getAllByRole('table'); + expect(tables).toHaveLength(1); + + const table = tables[0]; + + const headers = within(table).getAllByRole('columnheader'); + expect( + headers.some((header) => header.textContent === 'Configuration') + ).toBe(true); + expect(headers.some((header) => header.textContent === 'Nodes')).toBe( + false + ); + + expect(screen.queryByText('Legacy Database Clusters')).toBeNull(); + expect(screen.queryByText('New Database Clusters')).toBeNull(); + expect(screen.queryByText('Powered by')).toBeNull(); + }); +}); + +describe('Database Landing', () => { + it('should have the "Create Database Cluster" button disabled for restricted users', async () => { + queryMocks.useProfile.mockReturnValue({ data: { restricted: true } }); + + const { container, getByTestId } = renderWithTheme(); + + expect(getByTestId(loadingTestId)).toBeInTheDocument(); + + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + + const createClusterButton = container.querySelector('button'); + + expect(createClusterButton).toBeInTheDocument(); + expect(createClusterButton).toHaveTextContent('Create Database Cluster'); + expect(createClusterButton).toBeDisabled(); + }); + + it('should have the "Create Database Cluster" button enabled for users with full access', async () => { + queryMocks.useProfile.mockReturnValue({ data: { restricted: false } }); + + const { container, getByTestId } = renderWithTheme(); + + expect(getByTestId(loadingTestId)).toBeInTheDocument(); + + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + + const createClusterButton = container.querySelector('button'); + + expect(createClusterButton).toBeInTheDocument(); + expect(createClusterButton).toHaveTextContent('Create Database Cluster'); + expect(createClusterButton).not.toBeDisabled(); + }); }); describe('Database Landing', () => { diff --git a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLanding.tsx b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLanding.tsx index 99363528efe..d5e2845e668 100644 --- a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLanding.tsx +++ b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLanding.tsx @@ -1,62 +1,110 @@ +import { Box } from '@mui/material'; import * as React from 'react'; import { useHistory } from 'react-router-dom'; import { CircleProgress } from 'src/components/CircleProgress'; import { ErrorState } from 'src/components/ErrorState/ErrorState'; -import { Hidden } from 'src/components/Hidden'; import { LandingHeader } from 'src/components/LandingHeader'; -import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; -import { Table } from 'src/components/Table'; -import { TableBody } from 'src/components/TableBody'; -import { TableCell } from 'src/components/TableCell'; -import { TableHead } from 'src/components/TableHead'; -import { TableRow } from 'src/components/TableRow'; -import { TableSortCell } from 'src/components/TableSortCell'; +import { SafeTabPanel } from 'src/components/Tabs/SafeTabPanel'; +import { Tab } from 'src/components/Tabs/Tab'; +import { TabList } from 'src/components/Tabs/TabList'; +import { TabPanels } from 'src/components/Tabs/TabPanels'; +import { Tabs } from 'src/components/Tabs/Tabs'; +import { getRestrictedResourceText } from 'src/features/Account/utils'; +import DatabaseLandingTable from 'src/features/Databases/DatabaseLanding/DatabaseLandingTable'; +import { useIsDatabasesEnabled } from 'src/features/Databases/utilities'; +import { DatabaseClusterInfoBanner } from 'src/features/GlobalNotifications/DatabaseClusterInfoBanner'; import { useOrder } from 'src/hooks/useOrder'; import { usePagination } from 'src/hooks/usePagination'; import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; -import { useDatabasesQuery } from 'src/queries/databases/databases'; -import { useInProgressEvents } from 'src/queries/events/events'; +import { + useDatabaseTypesQuery, + useDatabasesQuery, +} from 'src/queries/databases/databases'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { DatabaseEmptyState } from './DatabaseEmptyState'; -import { DatabaseRow } from './DatabaseRow'; - -import type { DatabaseInstance } from '@linode/api-v4/lib/databases'; -import { getRestrictedResourceText } from 'src/features/Account/utils'; const preferenceKey = 'databases'; const DatabaseLanding = () => { const history = useHistory(); - const pagination = usePagination(1, preferenceKey); + const newDatabasesPagination = usePagination(1, preferenceKey, 'new'); + const legacyDatabasesPagination = usePagination(1, preferenceKey, 'legacy'); const isRestricted = useRestrictedGlobalGrantCheck({ globalGrantType: 'add_databases', }); - const { data: events } = useInProgressEvents(); + const { isLoading: isTypeLoading } = useDatabaseTypesQuery(); + const { isDatabasesV2Enabled } = useIsDatabasesEnabled(); + + const { + handleOrderChange: newDatabaseHandleOrderChange, + order: newDatabaseOrder, + orderBy: newDatabaseOrderBy, + } = useOrder( + { + order: 'desc', + orderBy: 'label', + }, + `new-${preferenceKey}-order` + ); + + const newDatabasesFilter: Record = { + ['+order']: newDatabaseOrder, + ['+order_by']: newDatabaseOrderBy, + }; + + if (isDatabasesV2Enabled) { + newDatabasesFilter['platform'] = 'rdbms-default'; + } + + const { + data: newDatabases, + error: newDatabasesError, + isLoading: newDatabasesIsLoading, + } = useDatabasesQuery( + { + page: newDatabasesPagination.page, + page_size: newDatabasesPagination.pageSize, + }, + newDatabasesFilter + ); - const { handleOrderChange, order, orderBy } = useOrder( + const { + handleOrderChange: legacyDatabaseHandleOrderChange, + order: legacyDatabaseOrder, + orderBy: legacyDatabaseOrderBy, + } = useOrder( { order: 'desc', orderBy: 'label', }, - `${preferenceKey}-order` + `legacy-${preferenceKey}-order` ); - const filter = { - ['+order']: order, - ['+order_by']: orderBy, + const legacyDatabasesFilter: Record = { + ['+order']: legacyDatabaseOrder, + ['+order_by']: legacyDatabaseOrderBy, }; - const { data, error, isLoading } = useDatabasesQuery( + if (isDatabasesV2Enabled) { + legacyDatabasesFilter['platform'] = 'rdbms-legacy'; + } + + const { + data: legacyDatabases, + error: legacyDatabasesError, + isLoading: legacyDatabasesIsLoading, + } = useDatabasesQuery( { - page: pagination.page, - page_size: pagination.pageSize, + page: legacyDatabasesPagination.page, + page_size: legacyDatabasesPagination.pageSize, }, - filter + legacyDatabasesFilter ); + const error = newDatabasesError || legacyDatabasesError; if (error) { return ( { ); } - if (isLoading) { + if (newDatabasesIsLoading || legacyDatabasesIsLoading || isTypeLoading) { return ; } - if (data?.results === 0) { + const showTabs = isDatabasesV2Enabled && legacyDatabases?.data.length !== 0; + + const showEmpty = + newDatabases?.data.length === 0 && legacyDatabases?.data.length === 0; + + if (showEmpty) { return ; } @@ -91,77 +144,54 @@ const DatabaseLanding = () => { onButtonClick={() => history.push('/databases/create')} title="Database Clusters" /> - - - - - Cluster Label - - - Status - - - - Configuration - - - - Engine - - - {/* TODO add back TableSortCell once API is updated to support sort by Region */} - Region - - - - Created - - - - - - {data?.data.map((database: DatabaseInstance) => ( - - ))} - -
- + {showTabs && } + + {showTabs ? ( + + + Legacy Database Clusters + New Database Clusters + + + + + + + + + + + ) : ( + + )} + ); }; diff --git a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLandingEmptyStateData.ts b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLandingEmptyStateData.tsx similarity index 91% rename from packages/manager/src/features/Databases/DatabaseLanding/DatabaseLandingEmptyStateData.ts rename to packages/manager/src/features/Databases/DatabaseLanding/DatabaseLandingEmptyStateData.tsx index 22334152b16..648b0380c1b 100644 --- a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLandingEmptyStateData.ts +++ b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLandingEmptyStateData.tsx @@ -1,3 +1,6 @@ +import React from 'react'; + +import DatabaseLogo from 'src/features/Databases/DatabaseLanding/DatabaseLogo'; import { docsLink, guidesMoreLinkText, @@ -14,6 +17,7 @@ import type { export const headers: ResourcesHeaders = { description: "Deploy popular database engines such as MySQL and PostgreSQL using Linode's performant, reliable, and fully managed database solution.", + logo: , subtitle: 'Fully managed cloud database clusters', title: 'Databases', }; diff --git a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLandingTable.tsx b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLandingTable.tsx new file mode 100644 index 00000000000..9f4ea829697 --- /dev/null +++ b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLandingTable.tsx @@ -0,0 +1,140 @@ +import { TableCell } from '@mui/material'; +import React from 'react'; + +import { Hidden } from 'src/components/Hidden'; +import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; +import { Table } from 'src/components/Table'; +import { TableBody } from 'src/components/TableBody'; +import { TableHead } from 'src/components/TableHead'; +import { TableRow } from 'src/components/TableRow/TableRow'; +import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty'; +import { TableSortCell } from 'src/components/TableSortCell'; +import DatabaseLogo from 'src/features/Databases/DatabaseLanding/DatabaseLogo'; +import DatabaseRow from 'src/features/Databases/DatabaseLanding/DatabaseRow'; +import { usePagination } from 'src/hooks/usePagination'; +import { useInProgressEvents } from 'src/queries/events/events'; + +import type { DatabaseInstance } from '@linode/api-v4/lib/databases'; +import type { Order } from 'src/hooks/useOrder'; + +const preferenceKey = 'databases'; + +interface Props { + data: DatabaseInstance[] | undefined; + handleOrderChange: (newOrderBy: string, newOrder: Order) => void; + isNewDatabase?: boolean; + order: 'asc' | 'desc'; + orderBy: string; +} +const DatabaseLandingTable = ({ + data, + handleOrderChange, + isNewDatabase, + order, + orderBy, +}: Props) => { + const { data: events } = useInProgressEvents(); + + const dbPlatformType = isNewDatabase ? 'new' : 'legacy'; + const pagination = usePagination(1, preferenceKey, dbPlatformType); + + return ( + <> + + + + + Cluster Label + + + Status + + {isNewDatabase && ( + /* TODO add back TableSortCell once API is updated to support sort by Plan */ + Plan + )} + + + {isNewDatabase ? 'Nodes' : 'Configuration'} + + + + Engine + + + + Region + + + + + Created + + + + + + {data?.map((database: DatabaseInstance) => ( + + ))} + {data?.length === 0 && ( + + )} + +
+ + {isNewDatabase && } + + ); +}; + +export default DatabaseLandingTable; diff --git a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLogo.tsx b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLogo.tsx index bce0caa0a2b..71c7dd23b1e 100644 --- a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLogo.tsx +++ b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLogo.tsx @@ -1,43 +1,52 @@ +import { useTheme } from '@mui/material/styles'; import * as React from 'react'; -import { makeStyles } from 'tss-react/mui'; +import LogoWhite from 'src/assets/icons/db-logo-white.svg'; import Logo from 'src/assets/icons/db-logo.svg'; import { BetaChip } from 'src/components/BetaChip/BetaChip'; import { Box } from 'src/components/Box'; import { Typography } from 'src/components/Typography'; -import type { Theme } from '@mui/material/styles'; +import type { SxProps } from '@mui/material/styles'; interface Props { - style?: React.CSSProperties; + sx?: SxProps; } -const useStyles = makeStyles()((theme: Theme) => ({ - betaChip: { - backgroundColor: '#727272', - color: theme.color.white, - }, - logo: { - color: '#32363C', - display: 'flex', - marginTop: '8px', - }, -})); - -export const DatabaseLogo = ({ style }: Props) => { - const { classes } = useStyles(); +export const DatabaseLogo = ({ sx }: Props) => { + const theme = useTheme(); return ( - - - Powered by - + + + Powered by   + {theme.palette.mode === 'light' ? : } + ); }; + +export default DatabaseLogo; diff --git a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseRow.tsx b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseRow.tsx index 5646cb54753..f471fa9e7b0 100644 --- a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseRow.tsx +++ b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseRow.tsx @@ -1,9 +1,3 @@ -import { Event } from '@linode/api-v4'; -import { - Database, - DatabaseInstance, - Engine, -} from '@linode/api-v4/lib/databases/types'; import * as React from 'react'; import { Link } from 'react-router-dom'; @@ -11,12 +5,21 @@ import { Chip } from 'src/components/Chip'; import { Hidden } from 'src/components/Hidden'; import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; +import { DatabaseStatusDisplay } from 'src/features/Databases/DatabaseDetail/DatabaseStatusDisplay'; +import { useDatabaseTypesQuery } from 'src/queries/databases/databases'; import { useProfile } from 'src/queries/profile/profile'; import { useRegionsQuery } from 'src/queries/regions/regions'; import { isWithinDays, parseAPIDate } from 'src/utilities/date'; import { formatDate } from 'src/utilities/formatDate'; +import { formatStorageUnits } from 'src/utilities/formatStorageUnits'; -import { DatabaseStatusDisplay } from '../DatabaseDetail/DatabaseStatusDisplay'; +import type { Event } from '@linode/api-v4'; +import type { + Database, + DatabaseInstance, + DatabaseType, + Engine, +} from '@linode/api-v4/lib/databases/types'; export const databaseEngineMap: Record = { mongodb: 'MongoDB', @@ -28,9 +31,10 @@ export const databaseEngineMap: Record = { interface Props { database: Database | DatabaseInstance; events?: Event[]; + isNewDatabase?: boolean; } -export const DatabaseRow = ({ database, events }: Props) => { +export const DatabaseRow = ({ database, events, isNewDatabase }: Props) => { const { cluster_size, created, @@ -38,12 +42,15 @@ export const DatabaseRow = ({ database, events }: Props) => { id, label, region, + type, version, } = database; const { data: regions } = useRegionsQuery(); const { data: profile } = useProfile(); - + const { data: types } = useDatabaseTypesQuery(); + const plan = types?.find((t: DatabaseType) => t.id === type); + const formattedPlan = plan && formatStorageUnits(plan.label); const actualRegion = regions?.find((r) => r.id === region); const configuration = @@ -69,6 +76,7 @@ export const DatabaseRow = ({ database, events }: Props) => { + {isNewDatabase && {formattedPlan}} {configuration} diff --git a/packages/manager/src/features/Databases/utilities.test.ts b/packages/manager/src/features/Databases/utilities.test.ts index 51c367ac709..11447244793 100644 --- a/packages/manager/src/features/Databases/utilities.test.ts +++ b/packages/manager/src/features/Databases/utilities.test.ts @@ -34,9 +34,9 @@ describe('useIsDatabasesEnabled', () => { }); }); - it('should return true for an unrestricted user with the account capability V2', async () => { + it('should return true for an unrestricted user with the account capability Beta', async () => { const account = accountFactory.build({ - capabilities: ['Managed Databases V2'], + capabilities: ['Managed Databases Beta'], }); server.use( diff --git a/packages/manager/src/features/Databases/utilities.ts b/packages/manager/src/features/Databases/utilities.ts index 8e1e203235c..c044026a989 100644 --- a/packages/manager/src/features/Databases/utilities.ts +++ b/packages/manager/src/features/Databases/utilities.ts @@ -33,7 +33,7 @@ export const useIsDatabasesEnabled = () => { ); const isDatabasesV2Enabled = - account.capabilities.includes('Managed Databases V2') && + account.capabilities.includes('Managed Databases Beta') && flags.dbaasV2?.enabled; return { diff --git a/packages/manager/src/features/GlobalNotifications/DatabaseClusterInfoBanner.tsx b/packages/manager/src/features/GlobalNotifications/DatabaseClusterInfoBanner.tsx new file mode 100644 index 00000000000..256a2b8dee6 --- /dev/null +++ b/packages/manager/src/features/GlobalNotifications/DatabaseClusterInfoBanner.tsx @@ -0,0 +1,46 @@ +import { Box } from '@mui/material'; +import { styled } from '@mui/material/styles'; +import * as React from 'react'; + +import { Notice } from 'src/components/Notice/Notice'; +import { Typography } from 'src/components/Typography'; + +import { useIsDatabasesEnabled } from '../Databases/utilities'; + +const StyledNotice = styled(Notice, { label: 'StyledNotice' })(({ theme }) => ({ + background: theme.bg.bgPaper, + marginTop: '15px', +})); + +export const DatabaseClusterInfoBanner = () => { + const { isDatabasesV2Enabled } = useIsDatabasesEnabled(); + + if (!isDatabasesV2Enabled) { + return null; + } + + return ( + + + + Important Database Cluster Beta Information + +
    +
  • + + As a Beta customer you can only create Aiven Database clusters. + +
  • +
  • + + You won’t be charged for Aiven database clusters created during + duration of the Beta phase. If you decide to keep the new clusters + later on, you’ll be charged according to the new payment. You can + always remove unwanted clusters. + +
  • +
+
+
+ ); +}; diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index 06edff43466..062b1c9796b 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -206,7 +206,7 @@ const databases = [ const standardTypes = [ databaseTypeFactory.build({ class: 'nanode', - id: 'g6-standard-0', + id: 'g6-nanode-1', label: `Nanode 1 GB`, memory: 1024, }), From 0ac8742d2e4c0b6dc333198f3fa0a6c870c2dbbe Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Mon, 16 Sep 2024 09:14:46 -0400 Subject: [PATCH 12/62] fix: [M3-8572] - Scrollbar showing briefly on Splash Screen (#10922) * add missing `srSpeak` utility * Added changeset: Scrollbar showing briefly on Splash Screen --------- Co-authored-by: Banks Nussman --- .../.changeset/pr-10922-fixed-1726078413547.md | 5 +++++ packages/manager/src/components/SplashScreen.tsx | 1 - packages/manager/src/index.css | 13 +++++++++++++ packages/manager/src/utilities/accessibility.ts | 6 ++++-- 4 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 packages/manager/.changeset/pr-10922-fixed-1726078413547.md diff --git a/packages/manager/.changeset/pr-10922-fixed-1726078413547.md b/packages/manager/.changeset/pr-10922-fixed-1726078413547.md new file mode 100644 index 00000000000..c05ef12b040 --- /dev/null +++ b/packages/manager/.changeset/pr-10922-fixed-1726078413547.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Scrollbar showing briefly on Splash Screen ([#10922](https://github.com/linode/manager/pull/10922)) diff --git a/packages/manager/src/components/SplashScreen.tsx b/packages/manager/src/components/SplashScreen.tsx index af0b993c47d..5b5836a5d3c 100644 --- a/packages/manager/src/components/SplashScreen.tsx +++ b/packages/manager/src/components/SplashScreen.tsx @@ -7,7 +7,6 @@ import { Box } from './Box'; export const SplashScreen = () => { React.useEffect(() => { - // @TODO: The utilility cases a scrollbar to show in the browser, fix it. srSpeak('Loading Linode Cloud Manager', 'polite'); }, []); diff --git a/packages/manager/src/index.css b/packages/manager/src/index.css index e4be4eae756..96ad3e87a85 100644 --- a/packages/manager/src/index.css +++ b/packages/manager/src/index.css @@ -346,3 +346,16 @@ See: https://blog.chromium.org/2020/09/giving-users-and-developers-more.html :focus:not(:focus-visible) { outline: none; } + +/* Used by `srSpeak` in `accessibility.ts` */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} \ No newline at end of file diff --git a/packages/manager/src/utilities/accessibility.ts b/packages/manager/src/utilities/accessibility.ts index a1c229a5a2c..1d7aa78c282 100644 --- a/packages/manager/src/utilities/accessibility.ts +++ b/packages/manager/src/utilities/accessibility.ts @@ -1,5 +1,7 @@ -// Function to send aria-live messages -// Fo instance, when page is loading +/** + * Function to send aria-live messages + * For instance, when page is loading + */ export const srSpeak = (text: string, priority: 'assertive' | 'polite') => { const el = document.createElement('div'); const id = 'speak-' + Math.random().toString(36).substr(2, 9); From 3ce07216f5159c0747939a21b2963ebb11a6ea76 Mon Sep 17 00:00:00 2001 From: Purvesh Makode Date: Tue, 17 Sep 2024 19:29:27 +0530 Subject: [PATCH 13/62] test: [M3-8544, M3-8545] - Add unit tests for Dialog and DeletionDialog components (#10917) * Add unit tests for dialog and deletiondialog components * Added changeset: Add unit tests for Dialog and DeletionDialog components * Clean up... * Fix typo --- .../pr-10917-tests-1726061618551.md | 5 + .../DeletionDialog/DeletionDialog.test.tsx | 148 ++++++++++++++++++ .../DeletionDialog/DeletionDialog.tsx | 4 +- .../src/components/Dialog/Dialog.test.tsx | 69 ++++++++ 4 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 packages/manager/.changeset/pr-10917-tests-1726061618551.md create mode 100644 packages/manager/src/components/DeletionDialog/DeletionDialog.test.tsx create mode 100644 packages/manager/src/components/Dialog/Dialog.test.tsx diff --git a/packages/manager/.changeset/pr-10917-tests-1726061618551.md b/packages/manager/.changeset/pr-10917-tests-1726061618551.md new file mode 100644 index 00000000000..40ad85e0440 --- /dev/null +++ b/packages/manager/.changeset/pr-10917-tests-1726061618551.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Add unit tests for Dialog and DeletionDialog components ([#10917](https://github.com/linode/manager/pull/10917)) diff --git a/packages/manager/src/components/DeletionDialog/DeletionDialog.test.tsx b/packages/manager/src/components/DeletionDialog/DeletionDialog.test.tsx new file mode 100644 index 00000000000..c1385003852 --- /dev/null +++ b/packages/manager/src/components/DeletionDialog/DeletionDialog.test.tsx @@ -0,0 +1,148 @@ +import { fireEvent } from '@testing-library/react'; +import * as React from 'react'; + +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { DeletionDialog } from './DeletionDialog'; + +import type { DeletionDialogProps } from './DeletionDialog'; + +describe('DeletionDialog', () => { + const defaultArgs: DeletionDialogProps = { + entity: 'Linode', + label: 'my-linode-0', + loading: false, + onClose: vi.fn(), + onDelete: vi.fn(), + open: false, + }; + + it.each([ + ['not render', false], + ['render', true], + ])( + 'should %s a DeletionDialog with the correct title, close button, and action buttons when open is %s', + (_, isOpen) => { + const { queryByRole, queryByTestId, queryByText } = renderWithTheme( + + ); + const title = queryByText( + `Delete ${defaultArgs.entity} ${defaultArgs.label}?` + ); + const dialog = queryByTestId('drawer'); + const closeButton = queryByRole('button', { name: 'Close' }); + const cancelButton = queryByTestId('cancel'); + const deleteButton = queryByTestId('confirm'); + + if (isOpen) { + expect(title).toBeInTheDocument(); + expect(dialog).toBeInTheDocument(); + expect(closeButton).toBeInTheDocument(); + expect(cancelButton).toBeInTheDocument(); + expect(deleteButton).toBeInTheDocument(); + expect(deleteButton).toHaveTextContent(`Delete ${defaultArgs.entity}`); + } else { + expect(title).not.toBeInTheDocument(); + expect(dialog).not.toBeInTheDocument(); + expect(closeButton).not.toBeInTheDocument(); + expect(cancelButton).not.toBeInTheDocument(); + expect(deleteButton).not.toBeInTheDocument(); + } + } + ); + + it('should call onClose when the DeletionDialog close button or Action cancel button is clicked', () => { + const { getByRole, getByTestId } = renderWithTheme( + + ); + + // For close icon button + const closeButton = getByRole('button', { name: 'Close' }); + expect(closeButton).not.toBeDisabled(); + fireEvent.click(closeButton); + + expect(defaultArgs.onClose).toHaveBeenCalled(); + + // For action cancel button + const cancelButton = getByTestId('cancel'); + expect(cancelButton).not.toBeDisabled(); + fireEvent.click(cancelButton); + + expect(defaultArgs.onClose).toHaveBeenCalled(); + }); + + it('should call onDelete when the DeletionDialog delete button is clicked', () => { + const { getByTestId } = renderWithTheme( + + ); + + const deleteButton = getByTestId('confirm'); + expect(deleteButton).not.toBeDisabled(); + fireEvent.click(deleteButton); + + expect(defaultArgs.onDelete).toHaveBeenCalled(); + }); + + it('should render a DeletionDialog with an error message if provided', () => { + const { getByText } = renderWithTheme( + + ); + + expect(getByText('Error that will be shown in the dialog.')).toBeVisible(); + }); + + it('should disable delete button and show loading icon if loading is true', () => { + const { getByTestId } = renderWithTheme( + + ); + + const deleteButton = getByTestId('confirm'); + expect(deleteButton).toBeInTheDocument(); + expect(deleteButton).toBeDisabled(); + + const loadingSvgIcon = deleteButton.querySelector( + '[data-test-id="ReloadIcon"]' + ); + + expect(loadingSvgIcon).toBeInTheDocument(); + }); + + it('should display the correct warning text in the DeletionDialog', () => { + const { getByTestId } = renderWithTheme( + + ); + + const dialog = getByTestId('drawer'); + const warningText = `Warning: Deleting this ${defaultArgs.entity} is permanent and can\u2019t be undone.`; + + expect(dialog).toHaveTextContent(warningText); + }); + + it.each([ + ['not render', false], + ['render', true], + ])( + 'should %s input field with label when typeToConfirm is %s', + (_, typeToConfirm) => { + const { queryByTestId } = renderWithTheme( + + ); + + if (typeToConfirm) { + expect(queryByTestId('inputLabelWrapper')).toBeInTheDocument(); + expect(queryByTestId('textfield-input')).toBeInTheDocument(); + } else { + expect(queryByTestId('inputLabelWrapper')).not.toBeInTheDocument(); + expect(queryByTestId('textfield-input')).not.toBeInTheDocument(); + } + } + ); +}); diff --git a/packages/manager/src/components/DeletionDialog/DeletionDialog.tsx b/packages/manager/src/components/DeletionDialog/DeletionDialog.tsx index b33df97103d..bce1c53ad8d 100644 --- a/packages/manager/src/components/DeletionDialog/DeletionDialog.tsx +++ b/packages/manager/src/components/DeletionDialog/DeletionDialog.tsx @@ -10,9 +10,9 @@ import { titlecase } from 'src/features/Linodes/presentation'; import { usePreferences } from 'src/queries/profile/preferences'; import { capitalize } from 'src/utilities/capitalize'; -import { DialogProps } from '../Dialog/Dialog'; +import type { DialogProps } from '../Dialog/Dialog'; -interface DeletionDialogProps extends Omit { +export interface DeletionDialogProps extends Omit { entity: string; error?: string; label: string; diff --git a/packages/manager/src/components/Dialog/Dialog.test.tsx b/packages/manager/src/components/Dialog/Dialog.test.tsx new file mode 100644 index 00000000000..1e20c5c22ef --- /dev/null +++ b/packages/manager/src/components/Dialog/Dialog.test.tsx @@ -0,0 +1,69 @@ +import { fireEvent } from '@testing-library/react'; +import * as React from 'react'; + +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { Dialog } from './Dialog'; + +import type { DialogProps } from './Dialog'; + +describe('Dialog', () => { + const defaultArgs: DialogProps = { + onClose: vi.fn(), + open: false, + title: 'This is a Dialog', + }; + + it.each([ + ['not render', false], + ['render', true], + ])('should %s a Dialog with title when open is %s', (_, isOpen) => { + const { queryByTestId, queryByText } = renderWithTheme( + + ); + + const title = queryByText('This is a Dialog'); + const dialog = queryByTestId('drawer'); + + if (isOpen) { + expect(title).toBeInTheDocument(); + expect(dialog).toBeInTheDocument(); + } else { + expect(title).not.toBeInTheDocument(); + expect(dialog).not.toBeInTheDocument(); + } + }); + + it('should render a Dialog with children if provided', () => { + const { getByText } = renderWithTheme( + +

Child items can go here!

+
+ ); + + expect(getByText('Child items can go here!')).toBeInTheDocument(); + }); + + it('should call onClose when the Dialog close button is clicked', () => { + const { getByRole } = renderWithTheme( + + ); + + const closeButton = getByRole('button', { name: 'Close' }); + fireEvent.click(closeButton); + + expect(defaultArgs.onClose).toHaveBeenCalled(); + }); + + it('should render a Dialog with an error message if provided', () => { + const { getByText } = renderWithTheme( + + ); + + expect(getByText('Error that will be shown in the dialog.')).toBeVisible(); + }); +}); From 3758ec1864321da401624cb80e11300f7b4606a9 Mon Sep 17 00:00:00 2001 From: Hussain Khalil <122488130+hkhalil-akamai@users.noreply.github.com> Date: Tue, 17 Sep 2024 10:15:29 -0400 Subject: [PATCH 14/62] fix: [M3-7149] - Fix textfield tooltip icon focus area (#10938) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description 📝 Fixes the focus area for tooltips to be a circle rather than an oval as before. ## How to test 🧪 Verify textfield tooltips' focus areas render as a circle and otherwise work as before. --- .../.changeset/pr-10938-fixed-1726262803311.md | 5 +++++ packages/manager/src/components/TextField.tsx | 15 ++++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) create mode 100644 packages/manager/.changeset/pr-10938-fixed-1726262803311.md diff --git a/packages/manager/.changeset/pr-10938-fixed-1726262803311.md b/packages/manager/.changeset/pr-10938-fixed-1726262803311.md new file mode 100644 index 00000000000..407b55e45ec --- /dev/null +++ b/packages/manager/.changeset/pr-10938-fixed-1726262803311.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Textarea tooltip icon focus area ([#10938](https://github.com/linode/manager/pull/10938)) diff --git a/packages/manager/src/components/TextField.tsx b/packages/manager/src/components/TextField.tsx index a0c1a9240fa..824ec03748b 100644 --- a/packages/manager/src/components/TextField.tsx +++ b/packages/manager/src/components/TextField.tsx @@ -1,9 +1,6 @@ import KeyboardArrowDown from '@mui/icons-material/KeyboardArrowDown'; -import { Theme, useTheme } from '@mui/material/styles'; -import { - default as _TextField, - StandardTextFieldProps, -} from '@mui/material/TextField'; +import { useTheme } from '@mui/material/styles'; +import { default as _TextField } from '@mui/material/TextField'; import { clamp } from 'ramda'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; @@ -13,11 +10,13 @@ import { CircleProgress } from 'src/components/CircleProgress'; import { FormHelperText } from 'src/components/FormHelperText'; import { InputAdornment } from 'src/components/InputAdornment'; import { InputLabel } from 'src/components/InputLabel'; -import { TooltipProps } from 'src/components/Tooltip'; import { TooltipIcon } from 'src/components/TooltipIcon'; import { convertToKebabCase } from 'src/utilities/convertToKebobCase'; +import type { Theme } from '@mui/material/styles'; +import type { StandardTextFieldProps } from '@mui/material/TextField'; import type { BoxProps } from 'src/components/Box'; +import type { TooltipProps } from 'src/components/Tooltip'; const useStyles = makeStyles()((theme: Theme) => ({ absolute: { @@ -480,8 +479,10 @@ export const TextField = (props: TextFieldProps) => { {tooltipText && ( Date: Tue, 17 Sep 2024 20:44:33 +0530 Subject: [PATCH 15/62] upcoming: [DI-20709] - CSS updates for widget level filters for ACLP (#10903) * upcoming: [DI-20709] - CSS updates for widget level filters * upcoming: [DI-20709] - ES lint fixes * upcoming: [DI-20709] - Added comments * upcoming: [DI-20709] - Added changeset * upcoming: [DI-20709] - Color updates for zoomer component with use them hook * upcoming: [DI-20585] - CSS changes * upcoming: [DI-20585] - Removed unused values and imports * upcoming: [DI-20585] - Removed unused values and imports * upcoming: [DI-20585] - Use common utils * upcoming: [DI-20585] - Comment updates * upcoming: [DI-20585] - Remove any * upcoming: [DI-20585] - Removing height and adjusting width. Removed divider in widget * upcoming: [DI-20585] - Removed placeholder on selection and UT updates * upcoming: [DI-20585] - UT updates * upcoming: [DI-20585] - Using form control for width of filters * upcoming: [DI-20585] - Alignment fix * upcoming: [DI-20585] - PR comments * upcoming: [DI-20585] - PR comments * upcoming: [DI-20585] - Updated code syntax for handling small size screens * upcoming: [DI-20585] - CamelCase for properties * upcoming: [DI-20585] - Style to sx --------- Co-authored-by: vmangalr --- ...r-10903-upcoming-features-1725857694589.md | 5 ++ .../Dashboard/CloudPulseDashboard.tsx | 4 +- .../CloudPulseDashboardWithFilters.test.tsx | 4 +- .../CloudPulse/Utils/CloudPulseWidgetUtils.ts | 18 +++++++ .../CloudPulse/Widget/CloudPulseWidget.tsx | 14 ++---- .../CloudPulseAggregateFunction.tsx | 4 +- .../CloudPulseIntervalSelect.test.tsx | 6 +-- .../components/CloudPulseIntervalSelect.tsx | 50 ++++++++++++------- .../CloudPulse/Widget/components/Zoomer.tsx | 15 +++++- .../shared/CloudPulseCustomSelect.test.tsx | 18 ++++--- .../shared/CloudPulseCustomSelect.tsx | 8 ++- .../shared/CloudPulseResourcesSelect.tsx | 4 +- 12 files changed, 100 insertions(+), 50 deletions(-) create mode 100644 packages/manager/.changeset/pr-10903-upcoming-features-1725857694589.md diff --git a/packages/manager/.changeset/pr-10903-upcoming-features-1725857694589.md b/packages/manager/.changeset/pr-10903-upcoming-features-1725857694589.md new file mode 100644 index 00000000000..66017bb7ed9 --- /dev/null +++ b/packages/manager/.changeset/pr-10903-upcoming-features-1725857694589.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Update CSS for widget level filters and widget heading title for ACLP ([#10903](https://github.com/linode/manager/pull/10903)) diff --git a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboard.tsx b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboard.tsx index 3d010993877..a019a829a70 100644 --- a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboard.tsx +++ b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboard.tsx @@ -16,7 +16,7 @@ import { getUserPreferenceObject } from '../Utils/UserPreference'; import { createObjectCopy } from '../Utils/utils'; import { CloudPulseWidget } from '../Widget/CloudPulseWidget'; import { - all_interval_options, + allIntervalOptions, getInSeconds, getIntervalIndex, } from '../Widget/components/CloudPulseIntervalSelect'; @@ -124,7 +124,7 @@ export const CloudPulseDashboard = (props: DashboardProperties) => { const getTimeGranularity = (scrapeInterval: string) => { const scrapeIntervalValue = getInSeconds(scrapeInterval); const index = getIntervalIndex(scrapeIntervalValue); - return index < 0 ? all_interval_options[0] : all_interval_options[index]; + return index < 0 ? allIntervalOptions[0] : allIntervalOptions[index]; }; const { diff --git a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardWithFilters.test.tsx b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardWithFilters.test.tsx index 5cdd820736c..b2382979830 100644 --- a/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardWithFilters.test.tsx +++ b/packages/manager/src/features/CloudPulse/Dashboard/CloudPulseDashboardWithFilters.test.tsx @@ -13,7 +13,6 @@ const queryMocks = vi.hoisted(() => ({ const selectTimeDurationPlaceholder = 'Select Time Duration'; const circleProgress = 'circle-progress'; const mandatoryFiltersError = 'Mandatory Filters not Selected'; -const customNodeTypePlaceholder = 'Select Node Type'; vi.mock('src/queries/cloudpulse/dashboards', async () => { const actual = await vi.importActual('src/queries/cloudpulse/dashboards'); @@ -98,8 +97,7 @@ describe('CloudPulseDashboardWithFilters component tests', () => { expect(screen.getByTestId('CloseIcon')).toBeDefined(); - const inputBox = screen.getByPlaceholderText(customNodeTypePlaceholder); - fireEvent.change(inputBox, { target: { value: '' } }); // clear the value + fireEvent.click(screen.getByTitle('Clear')); // clear the value expect(screen.getByText(mandatoryFiltersError)).toBeDefined(); }); diff --git a/packages/manager/src/features/CloudPulse/Utils/CloudPulseWidgetUtils.ts b/packages/manager/src/features/CloudPulse/Utils/CloudPulseWidgetUtils.ts index 40d5d2d480b..29eabd3a7a9 100644 --- a/packages/manager/src/features/CloudPulse/Utils/CloudPulseWidgetUtils.ts +++ b/packages/manager/src/features/CloudPulse/Utils/CloudPulseWidgetUtils.ts @@ -1,3 +1,6 @@ +import { styled } from '@mui/material'; + +import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; import { isToday } from 'src/utilities/isToday'; import { getMetrics } from 'src/utilities/statMetrics'; @@ -331,3 +334,18 @@ export const isDataEmpty = (data: DataSet[]): boolean => { thisSeries.data.every((thisPoint) => thisPoint[1] === null) ); }; + +/** + * Returns an autocomplete with updated styles according to UX, this will be used at widget level + */ +export const StyledWidgetAutocomplete = styled(Autocomplete, { + label: 'StyledAutocomplete', +})(({ theme }) => ({ + '&& .MuiFormControl-root': { + minWidth: '90px', + [theme.breakpoints.down('sm')]: { + width: '100%', // 100% width for xs and small screens + }, + width: '90px', + }, +})); diff --git a/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx b/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx index 06847a0573e..feff3f8e0cd 100644 --- a/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx +++ b/packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx @@ -2,7 +2,6 @@ import { Box, Grid, Paper, Stack, Typography } from '@mui/material'; import { DateTime } from 'luxon'; import React from 'react'; -import { Divider } from 'src/components/Divider'; import { useFlags } from 'src/hooks/useFlags'; import { useCloudPulseMetricsQuery } from 'src/queries/cloudpulse/metrics'; import { useProfile } from 'src/queries/profile/profile'; @@ -298,11 +297,7 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => { justifyContent={{ sm: 'space-between' }} padding={1} > - + {convertStringToCamelCasesWithSpaces(widget.label)}{' '} {!isLoading && `(${currentUnit}${unit.endsWith('ps') ? '/s' : ''})`} @@ -310,14 +305,14 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => { {availableMetrics?.scrape_interval && ( )} {Boolean( @@ -339,7 +334,6 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => {
- { return option.label == value.label; }} diff --git a/packages/manager/src/features/CloudPulse/Widget/components/CloudPulseIntervalSelect.test.tsx b/packages/manager/src/features/CloudPulse/Widget/components/CloudPulseIntervalSelect.test.tsx index 96023a33da8..f2ae44b21ac 100644 --- a/packages/manager/src/features/CloudPulse/Widget/components/CloudPulseIntervalSelect.test.tsx +++ b/packages/manager/src/features/CloudPulse/Widget/components/CloudPulseIntervalSelect.test.tsx @@ -7,7 +7,7 @@ import { CloudPulseIntervalSelect } from './CloudPulseIntervalSelect'; import type { TimeGranularity } from '@linode/api-v4'; describe('Interval select component', () => { - const intervalSelectionChange = (_selectedInterval: TimeGranularity) => {}; + const intervalSelectionChange = (_selectedInterval: TimeGranularity) => { }; it('should check for the selected value in interval select dropdown', () => { const scrape_interval = '30s'; @@ -15,9 +15,9 @@ describe('Interval select component', () => { const { getByRole } = renderWithTheme( ); diff --git a/packages/manager/src/features/CloudPulse/Widget/components/CloudPulseIntervalSelect.tsx b/packages/manager/src/features/CloudPulse/Widget/components/CloudPulseIntervalSelect.tsx index fe0bfed6a9d..7370937f1e3 100644 --- a/packages/manager/src/features/CloudPulse/Widget/components/CloudPulseIntervalSelect.tsx +++ b/packages/manager/src/features/CloudPulse/Widget/components/CloudPulseIntervalSelect.tsx @@ -1,14 +1,20 @@ import React from 'react'; -import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; +import { StyledWidgetAutocomplete } from '../../Utils/CloudPulseWidgetUtils'; import type { TimeGranularity } from '@linode/api-v4'; +interface IntervalOptions { + label: string; + unit: string; + value: number; +} + export interface IntervalSelectProperties { /** * Default time granularity to be selected */ - default_interval?: TimeGranularity | undefined; + defaultInterval?: TimeGranularity | undefined; /** * Function to be triggered on aggregate function changed from dropdown @@ -18,7 +24,7 @@ export interface IntervalSelectProperties { /** * scrape intervalto filter out minimum time granularity */ - scrape_interval: string; + scrapeInterval: string; } export const getInSeconds = (interval: string) => { @@ -39,7 +45,7 @@ export const getInSeconds = (interval: string) => { }; // Intervals must be in ascending order here -export const all_interval_options = [ +export const allIntervalOptions: IntervalOptions[] = [ { label: '1 min', unit: 'min', @@ -62,14 +68,14 @@ export const all_interval_options = [ }, ]; -const autoIntervalOption = { +const autoIntervalOption: IntervalOptions = { label: 'Auto', unit: 'Auto', value: -1, }; export const getIntervalIndex = (scrapeIntervalValue: number) => { - return all_interval_options.findIndex( + return allIntervalOptions.findIndex( (interval) => scrapeIntervalValue <= getInSeconds(String(interval.value) + interval.unit.slice(0, 1)) @@ -78,26 +84,26 @@ export const getIntervalIndex = (scrapeIntervalValue: number) => { export const CloudPulseIntervalSelect = React.memo( (props: IntervalSelectProperties) => { - const scrapeIntervalValue = getInSeconds(props.scrape_interval); + const scrapeIntervalValue = getInSeconds(props.scrapeInterval); const firstIntervalIndex = getIntervalIndex(scrapeIntervalValue); // all intervals displayed if srape interval > highest available interval. Error handling done by api - const available_interval_options = + const availableIntervalOptions = firstIntervalIndex < 0 - ? all_interval_options.slice() - : all_interval_options.slice( + ? allIntervalOptions.slice() + : allIntervalOptions.slice( firstIntervalIndex, - all_interval_options.length + allIntervalOptions.length ); let default_interval = - props.default_interval?.unit === 'Auto' + props.defaultInterval?.unit === 'Auto' ? autoIntervalOption - : available_interval_options.find( + : availableIntervalOptions.find( (obj) => - obj.value === props.default_interval?.value && - obj.unit === props.default_interval?.unit + obj.value === props.defaultInterval?.value && + obj.unit === props.defaultInterval?.unit ); if (!default_interval) { @@ -109,11 +115,17 @@ export const CloudPulseIntervalSelect = React.memo( } return ( - { + { return option?.value === value?.value && option?.unit === value?.unit; }} - onChange={(_: any, selectedInterval: any) => { + onChange={( + _: React.SyntheticEvent, + selectedInterval: IntervalOptions + ) => { props.onIntervalChange({ unit: selectedInterval?.unit, value: selectedInterval?.value, @@ -127,7 +139,7 @@ export const CloudPulseIntervalSelect = React.memo( fullWidth={false} label="Select an Interval" noMarginTop={true} - options={[autoIntervalOption, ...available_interval_options]} + options={[autoIntervalOption, ...availableIntervalOptions]} sx={{ width: { xs: '100%' } }} /> ); diff --git a/packages/manager/src/features/CloudPulse/Widget/components/Zoomer.tsx b/packages/manager/src/features/CloudPulse/Widget/components/Zoomer.tsx index 6ed093ed513..7b439274d2f 100644 --- a/packages/manager/src/features/CloudPulse/Widget/components/Zoomer.tsx +++ b/packages/manager/src/features/CloudPulse/Widget/components/Zoomer.tsx @@ -1,5 +1,6 @@ import ZoomInMap from '@mui/icons-material/ZoomInMap'; import ZoomOutMap from '@mui/icons-material/ZoomOutMap'; +import { useTheme } from '@mui/material/styles'; import * as React from 'react'; export interface ZoomIconProperties { @@ -9,6 +10,8 @@ export interface ZoomIconProperties { } export const ZoomIcon = React.memo((props: ZoomIconProperties) => { + const theme = useTheme(); + const handleClick = (needZoomIn: boolean) => { props.handleZoomToggle(needZoomIn); }; @@ -17,18 +20,26 @@ export const ZoomIcon = React.memo((props: ZoomIconProperties) => { if (props.zoomIn) { return ( handleClick(false)} - style={{ color: 'grey', fontSize: 'x-large' }} /> ); } return ( handleClick(true)} - style={{ color: 'grey', fontSize: 'x-large' }} /> ); }; diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.test.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.test.tsx index d4a401825ca..78ae7c77efe 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.test.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.test.tsx @@ -61,8 +61,7 @@ describe('CloudPulseCustomSelect component tests', () => { type={CloudPulseSelectTypes.static} /> ); - - expect(screen.getByPlaceholderText(testFilter)).toBeDefined(); + expect(screen.queryByPlaceholderText(testFilter)).toBeNull(); const keyDown = screen.getByTestId(keyboardArrowDownIcon); fireEvent.click(keyDown); fireEvent.click(screen.getByText('Test1')); @@ -82,8 +81,7 @@ describe('CloudPulseCustomSelect component tests', () => { type={CloudPulseSelectTypes.static} /> ); - - expect(screen.getByPlaceholderText(testFilter)).toBeDefined(); + expect(screen.queryByPlaceholderText(testFilter)).toBeNull(); const keyDown = screen.getByTestId(keyboardArrowDownIcon); fireEvent.click(keyDown); expect(screen.getAllByText('Test1').length).toEqual(2); // here it should be 2 @@ -111,13 +109,17 @@ describe('CloudPulseCustomSelect component tests', () => { type={CloudPulseSelectTypes.dynamic} /> ); - expect(screen.getByPlaceholderText(testFilter)).toBeDefined(); + expect(screen.queryByPlaceholderText(testFilter)).toBeNull(); const keyDown = screen.getByTestId(keyboardArrowDownIcon); fireEvent.click(keyDown); fireEvent.click(screen.getByText('Test1')); const textField = screen.getByTestId('textfield-input'); expect(textField.getAttribute('value')).toEqual('Test1'); expect(selectionChnage).toHaveBeenCalledTimes(1); + + // if we click on clear icon , placeholder should appear for single select + fireEvent.click(screen.getByTitle('Clear')); + expect(screen.getByPlaceholderText(testFilter)).toBeDefined(); }); it('should render a component successfully with required props dynamic multi select', () => { @@ -133,7 +135,7 @@ describe('CloudPulseCustomSelect component tests', () => { type={CloudPulseSelectTypes.dynamic} /> ); - expect(screen.getByPlaceholderText(testFilter)).toBeDefined(); + expect(screen.queryByPlaceholderText(testFilter)).toBeNull(); const keyDown = screen.getByTestId(keyboardArrowDownIcon); fireEvent.click(keyDown); expect(screen.getAllByText('Test1').length).toEqual(2); // here it should be 2 @@ -148,5 +150,9 @@ describe('CloudPulseCustomSelect component tests', () => { expect(screen.getAllByText('Test1').length).toEqual(1); expect(screen.getAllByText('Test2').length).toEqual(1); expect(selectionChnage).toHaveBeenCalledTimes(2); // check if selection change is called twice as we selected two options + + // if we click on clear icon , placeholder should appear + fireEvent.click(screen.getByTitle('Clear')); + expect(screen.getByPlaceholderText(testFilter)).toBeDefined(); }); }); diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx index 098d3a301e7..6209063e228 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseCustomSelect.tsx @@ -175,7 +175,6 @@ export const CloudPulseCustomSelect = React.memo( }; let staticErrorText = ''; - // check for input prop errors if ( (CloudPulseSelectTypes.static === type && @@ -205,6 +204,12 @@ export const CloudPulseCustomSelect = React.memo( ? options ?? [] : queriedResources ?? [] } + placeholder={ + selectedResource && + (!Array.isArray(selectedResource) || selectedResource.length) + ? '' + : placeholder || 'Select a Value' + } textFieldProps={{ hideLabel: true, }} @@ -214,7 +219,6 @@ export const CloudPulseCustomSelect = React.memo( label="Select a Value" multiple={isMultiSelect} onChange={handleChange} - placeholder={placeholder ?? 'Select a Value'} value={selectedResource ?? (isMultiSelect ? [] : null)} /> ); diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseResourcesSelect.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseResourcesSelect.tsx index fa0595d6c81..c7ec0850ac6 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseResourcesSelect.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseResourcesSelect.tsx @@ -90,6 +90,9 @@ export const CloudPulseResourcesSelect = React.memo( setSelectedResources(resourceSelections); handleResourcesSelection(resourceSelections); }} + placeholder={ + selectedResources?.length ? '' : placeholder || 'Select Resources' + } textFieldProps={{ InputProps: { sx: { @@ -111,7 +114,6 @@ export const CloudPulseResourcesSelect = React.memo( limitTags={2} multiple options={getResourcesList()} - placeholder={placeholder ? placeholder : 'Select Resources'} value={selectedResources} /> ); From 63ae19a74915cb83b5b8359c55bf72a3253c8758 Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Tue, 17 Sep 2024 11:49:52 -0400 Subject: [PATCH 16/62] fix: [M3-8589] - Flickering on the user profile page when updating the currently signed in user's username (#10947) * update the profile cache manually rather than doing an invalidation * Added changeset: Flickering on the user profile page when updating the currently signed in user's username --------- Co-authored-by: Banks Nussman --- .../.changeset/pr-10947-fixed-1726507951785.md | 5 +++++ packages/manager/src/queries/account/users.ts | 18 ++++++++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 packages/manager/.changeset/pr-10947-fixed-1726507951785.md diff --git a/packages/manager/.changeset/pr-10947-fixed-1726507951785.md b/packages/manager/.changeset/pr-10947-fixed-1726507951785.md new file mode 100644 index 00000000000..8f9cc90912a --- /dev/null +++ b/packages/manager/.changeset/pr-10947-fixed-1726507951785.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Flickering on the user profile page when updating the currently signed in user's username ([#10947](https://github.com/linode/manager/pull/10947)) diff --git a/packages/manager/src/queries/account/users.ts b/packages/manager/src/queries/account/users.ts index ac819e4bde3..ae720f52cb7 100644 --- a/packages/manager/src/queries/account/users.ts +++ b/packages/manager/src/queries/account/users.ts @@ -15,6 +15,7 @@ import type { Filter, Grants, Params, + Profile, ResourcePage, User, } from '@linode/api-v4'; @@ -65,11 +66,20 @@ export const useUpdateUserMutation = (username: string) => { accountQueries.users._ctx.user(user.username).queryKey, user ); - // If the currently logged in user updates their user + + // If the currently logged in user updates their user, we need to update the profile + // query to reflect the latest data. if (username === profile?.username) { - queryClient.invalidateQueries({ - queryKey: profileQueries.profile().queryKey, - }); + queryClient.setQueryData( + profileQueries.profile().queryKey, + (oldProfile) => { + if (!oldProfile) { + return; + } + + return { ...oldProfile, ...user }; + } + ); } }, }); From 83654e1b7ffc320ffa54b621cb7e05d51b220b1e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Sep 2024 12:04:55 -0400 Subject: [PATCH 17/62] chore(deps): bump dompurify from 3.1.0 to 3.1.3 (#10949) Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.1.0 to 3.1.3. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](https://github.com/cure53/DOMPurify/compare/3.1.0...3.1.3) --- updated-dependencies: - dependency-name: dompurify dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- packages/manager/package.json | 2 +- yarn.lock | 60 ++++++++++++++++++++++------------- 2 files changed, 39 insertions(+), 23 deletions(-) diff --git a/packages/manager/package.json b/packages/manager/package.json index 143749c053c..cafb84d1524 100644 --- a/packages/manager/package.json +++ b/packages/manager/package.json @@ -35,7 +35,7 @@ "chart.js": "~2.9.4", "copy-to-clipboard": "^3.0.8", "country-region-data": "^3.0.0", - "dompurify": "^3.1.0", + "dompurify": "^3.1.3", "flag-icons": "^6.6.5", "font-logos": "^0.18.0", "formik": "~2.1.3", diff --git a/yarn.lock b/yarn.lock index 6cdf3057e09..a0b7c36fa09 100644 --- a/yarn.lock +++ b/yarn.lock @@ -191,12 +191,12 @@ "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" -"@babel/generator@^7.25.0": - version "7.25.0" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.25.0.tgz#f858ddfa984350bc3d3b7f125073c9af6988f18e" - integrity sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw== +"@babel/generator@^7.25.6": + version "7.25.6" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.25.6.tgz#0df1ad8cb32fe4d2b01d8bf437f153d19342a87c" + integrity sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw== dependencies: - "@babel/types" "^7.25.0" + "@babel/types" "^7.25.6" "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.25" jsesc "^2.5.1" @@ -307,7 +307,7 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.9.tgz#7b903b6149b0f8fa7ad564af646c4c38a77fc44b" integrity sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA== -"@babel/parser@^7.25.0", "@babel/parser@^7.25.3": +"@babel/parser@^7.25.0": version "7.25.3" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.25.3.tgz#91fb126768d944966263f0657ab222a642b82065" integrity sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw== @@ -321,6 +321,13 @@ dependencies: "@babel/types" "^7.25.4" +"@babel/parser@^7.25.6": + version "7.25.6" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.25.6.tgz#85660c5ef388cbbf6e3d2a694ee97a38f18afe2f" + integrity sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q== + dependencies: + "@babel/types" "^7.25.6" + "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.6", "@babel/runtime@^7.15.4", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.9", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.23.9" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.9.tgz#47791a15e4603bb5f905bc0753801cf21d6345f7" @@ -347,15 +354,15 @@ "@babel/types" "^7.25.0" "@babel/traverse@^7.18.9", "@babel/traverse@^7.23.3", "@babel/traverse@^7.23.9", "@babel/traverse@^7.7.0": - version "7.25.3" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.3.tgz#f1b901951c83eda2f3e29450ce92743783373490" - integrity sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ== + version "7.25.6" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.6.tgz#04fad980e444f182ecf1520504941940a90fea41" + integrity sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ== dependencies: "@babel/code-frame" "^7.24.7" - "@babel/generator" "^7.25.0" - "@babel/parser" "^7.25.3" + "@babel/generator" "^7.25.6" + "@babel/parser" "^7.25.6" "@babel/template" "^7.25.0" - "@babel/types" "^7.25.2" + "@babel/types" "^7.25.6" debug "^4.3.1" globals "^11.1.0" @@ -386,6 +393,15 @@ "@babel/helper-validator-identifier" "^7.24.7" to-fast-properties "^2.0.0" +"@babel/types@^7.25.6": + version "7.25.6" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.25.6.tgz#893942ddb858f32ae7a004ec9d3a76b3463ef8e6" + integrity sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw== + dependencies: + "@babel/helper-string-parser" "^7.24.8" + "@babel/helper-validator-identifier" "^7.24.7" + to-fast-properties "^2.0.0" + "@base2/pretty-print-object@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@base2/pretty-print-object/-/pretty-print-object-1.0.1.tgz#371ba8be66d556812dc7fb169ebc3c08378f69d4" @@ -5160,10 +5176,10 @@ dompurify@^2.2.0: resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.4.7.tgz#277adeb40a2c84be2d42a8bcd45f582bfa4d0cfc" integrity sha512-kxxKlPEDa6Nc5WJi+qRgPbOAbgTpSULL+vI3NUXsZMlkJxTqYI9wg5ZTay2sFrdZRWHPWNi+EdAhcJf81WtoMQ== -dompurify@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.1.0.tgz#8c6b9fe986969a33aa4686bd829cbe8e14dd9445" - integrity sha512-yoU4rhgPKCo+p5UrWWWNKiIq+ToGqmVVhk0PmMYBK4kRsR3/qhemNFL8f6CFmBd4gMwm3F4T7HBoydP5uY07fA== +dompurify@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.1.3.tgz#cfe3ce4232c216d923832f68f2aa18b2fb9bd223" + integrity sha512-5sOWYSNPaxz6o2MUPvtyxTTqR4D3L77pr5rUQoWgD5ROQtVIZQgJkXbo1DLlK3vj11YGw5+LnF4SYti4gZmwng== dot-case@^3.0.4: version "3.0.4" @@ -11498,9 +11514,9 @@ typescript@^5.5.4: integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== ua-parser-js@^0.7.30, ua-parser-js@^0.7.33: - version "0.7.38" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.38.tgz#f497d8a4dc1fec6e854e5caa4b2f9913422ef054" - integrity sha512-fYmIy7fKTSFAhG3fuPlubeGaMoAd6r0rSnfEsO5nEY55i26KSLt9EH7PLQiiqPUhNqYIJvSkTy1oArIcXAbPbA== + version "0.7.39" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.39.tgz#c71efb46ebeabc461c4612d22d54f88880fabe7e" + integrity sha512-IZ6acm6RhQHNibSt7+c09hhvsKy9WUr4DVbeq9U8o71qxyYtJpQeDxQnMrVqnIFMLcQjHO0I9wgfO2vIahht4w== uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" @@ -12111,9 +12127,9 @@ yallist@^3.0.2: integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== yaml@^1.10.0, yaml@^1.7.2, yaml@^2.2.2, yaml@^2.3.0, yaml@~2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.5.0.tgz#c6165a721cf8000e91c36490a41d7be25176cf5d" - integrity sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw== + version "2.5.1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.5.1.tgz#c9772aacf62cb7494a95b0c4f1fb065b563db130" + integrity sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q== yargs-parser@^11.1.1, yargs-parser@^21.1.1: version "21.1.1" From 451d63a0f1d0c2e5189993369a2d2418343940e8 Mon Sep 17 00:00:00 2001 From: Mariah Jacobs <114685994+mjac0bs@users.noreply.github.com> Date: Tue, 17 Sep 2024 09:18:23 -0700 Subject: [PATCH 18/62] feat: [M3-8570] - Finish sunsetting Gravatar (#10930) * Clean up, clean up * Remove missed clean up on Profile Details page * Add changeset * Use 'Akamai' instead of 'Linode' username for sys events * Update unit tests --- .../pr-10930-removed-1726159393499.md | 5 ++ .../AccessPanel/UserSSHKeyPanel.tsx | 43 ++------- .../src/components/Avatar/Avatar.test.tsx | 2 +- .../manager/src/components/Avatar/Avatar.tsx | 2 +- .../src/components/GravatarByEmail.tsx | 36 -------- .../src/components/GravatarByUsername.tsx | 36 -------- .../src/components/GravatarOrAvatar.tsx | 33 ------- .../src/features/Events/EventRow.styles.ts | 14 --- .../manager/src/features/Events/EventRow.tsx | 25 ++---- .../src/features/Events/utils.test.tsx | 6 +- .../manager/src/features/Events/utils.tsx | 2 +- .../GlobalNotifications.tsx | 2 - .../GravatarSunsetBanner.tsx | 27 ------ .../Events/NotificationCenterEvent.tsx | 22 ++--- .../NotificationCenter.styles.ts | 12 --- .../DisplaySettings/DisplaySettings.tsx | 88 +++---------------- .../Support/ExpandableTicketPanel.tsx | 38 +++----- .../features/TopMenu/UserMenu/UserMenu.tsx | 13 +-- .../manager/src/features/Users/UserRow.tsx | 19 ++-- packages/manager/src/hooks/useGravatar.ts | 45 ---------- packages/manager/src/utilities/gravatar.ts | 29 ------ 21 files changed, 62 insertions(+), 437 deletions(-) create mode 100644 packages/manager/.changeset/pr-10930-removed-1726159393499.md delete mode 100644 packages/manager/src/components/GravatarByEmail.tsx delete mode 100644 packages/manager/src/components/GravatarByUsername.tsx delete mode 100644 packages/manager/src/components/GravatarOrAvatar.tsx delete mode 100644 packages/manager/src/features/Events/EventRow.styles.ts delete mode 100644 packages/manager/src/features/GlobalNotifications/GravatarSunsetBanner.tsx delete mode 100644 packages/manager/src/hooks/useGravatar.ts delete mode 100644 packages/manager/src/utilities/gravatar.ts diff --git a/packages/manager/.changeset/pr-10930-removed-1726159393499.md b/packages/manager/.changeset/pr-10930-removed-1726159393499.md new file mode 100644 index 00000000000..d6e275ef800 --- /dev/null +++ b/packages/manager/.changeset/pr-10930-removed-1726159393499.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Removed +--- + +Support for Gravatar as user profile avatars ([#10930](https://github.com/linode/manager/pull/10930)) diff --git a/packages/manager/src/components/AccessPanel/UserSSHKeyPanel.tsx b/packages/manager/src/components/AccessPanel/UserSSHKeyPanel.tsx index fa9ed19f713..e9d6e75da35 100644 --- a/packages/manager/src/components/AccessPanel/UserSSHKeyPanel.tsx +++ b/packages/manager/src/components/AccessPanel/UserSSHKeyPanel.tsx @@ -19,8 +19,6 @@ import { useProfile, useSSHKeysQuery } from 'src/queries/profile/profile'; import { truncateAndJoinList } from 'src/utilities/stringUtils'; import { Avatar } from '../Avatar/Avatar'; -import { GravatarByEmail } from '../GravatarByEmail'; -import { GravatarOrAvatar } from '../GravatarOrAvatar'; import { PaginationFooter } from '../PaginationFooter/PaginationFooter'; import { TableRowLoading } from '../TableRowLoading/TableRowLoading'; @@ -37,12 +35,6 @@ const useStyles = makeStyles()((theme: Theme) => ({ cellUser: { width: '30%', }, - gravatar: { - borderRadius: '50%', - height: 24, - marginRight: theme.spacing(1), - width: 24, - }, title: { marginBottom: theme.spacing(2), }, @@ -150,15 +142,7 @@ const UserSSHKeyPanel = (props: Props) => {
- - } - avatar={} - /> + {profile.username}
@@ -187,25 +171,16 @@ const UserSSHKeyPanel = (props: Props) => {
- - } - gravatar={ - + + {user.username}
diff --git a/packages/manager/src/components/Avatar/Avatar.test.tsx b/packages/manager/src/components/Avatar/Avatar.test.tsx index e1c553d2c15..65e4ab1baa0 100644 --- a/packages/manager/src/components/Avatar/Avatar.test.tsx +++ b/packages/manager/src/components/Avatar/Avatar.test.tsx @@ -61,7 +61,7 @@ describe('Avatar', () => { }); it('should render an svg instead of first letter for system users', async () => { - const systemUsernames = ['Linode', 'lke-service-account-123']; + const systemUsernames = ['Akamai', 'lke-service-account-123']; systemUsernames.forEach((username, i) => { const { getAllByRole, queryByTestId } = renderWithTheme( diff --git a/packages/manager/src/components/Avatar/Avatar.tsx b/packages/manager/src/components/Avatar/Avatar.tsx index 968ec8b5834..e50bf8bf4fa 100644 --- a/packages/manager/src/components/Avatar/Avatar.tsx +++ b/packages/manager/src/components/Avatar/Avatar.tsx @@ -55,7 +55,7 @@ export const Avatar = (props: AvatarProps) => { const _username = username ?? profile?.username ?? ''; const isAkamai = - _username === 'Linode' || _username.startsWith('lke-service-account'); + _username === 'Akamai' || _username.startsWith('lke-service-account'); const savedAvatarColor = isAkamai || !preferences?.avatarColor diff --git a/packages/manager/src/components/GravatarByEmail.tsx b/packages/manager/src/components/GravatarByEmail.tsx deleted file mode 100644 index 4ff64876d93..00000000000 --- a/packages/manager/src/components/GravatarByEmail.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import Avatar from '@mui/material/Avatar'; -import * as React from 'react'; - -import UserIcon from 'src/assets/icons/account.svg'; -import { getGravatarUrl } from 'src/utilities/gravatar'; - -export const DEFAULT_AVATAR_SIZE = 28; - -export interface GravatarByEmailProps { - className?: string; - email: string; - height?: number; - width?: number; -} - -export const GravatarByEmail = (props: GravatarByEmailProps) => { - const { - className, - email, - height = DEFAULT_AVATAR_SIZE, - width = DEFAULT_AVATAR_SIZE, - } = props; - - const url = getGravatarUrl(email); - - return ( - - - - ); -}; diff --git a/packages/manager/src/components/GravatarByUsername.tsx b/packages/manager/src/components/GravatarByUsername.tsx deleted file mode 100644 index 3adb2262aa3..00000000000 --- a/packages/manager/src/components/GravatarByUsername.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import Avatar from '@mui/material/Avatar'; -import * as React from 'react'; - -import UserIcon from 'src/assets/icons/account.svg'; -import { useAccountUser } from 'src/queries/account/users'; -import { getGravatarUrl } from 'src/utilities/gravatar'; - -import { Box } from './Box'; -import { DEFAULT_AVATAR_SIZE } from './GravatarByEmail'; - -export interface GravatarByUsernameProps { - className?: string; - username: null | string; -} - -export const GravatarByUsername = (props: GravatarByUsernameProps) => { - const { className, username } = props; - const { data: user, isLoading } = useAccountUser(username ?? ''); - const url = user?.email ? getGravatarUrl(user.email) : undefined; - - // Render placeholder instead of flashing default user icon briefly - if (isLoading) { - return ; - } - - return ( - - - - ); -}; diff --git a/packages/manager/src/components/GravatarOrAvatar.tsx b/packages/manager/src/components/GravatarOrAvatar.tsx deleted file mode 100644 index 6a9ab209775..00000000000 --- a/packages/manager/src/components/GravatarOrAvatar.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; - -import { useGravatar } from 'src/hooks/useGravatar'; -import { useProfile } from 'src/queries/profile/profile'; - -import { DEFAULT_AVATAR_SIZE } from './Avatar/Avatar'; -import { Box } from './Box'; - -interface Props { - avatar: JSX.Element; - gravatar: JSX.Element; - height?: number; - width?: number; -} - -export const GravatarOrAvatar = (props: Props) => { - const { - avatar, - gravatar, - height = DEFAULT_AVATAR_SIZE, - width = DEFAULT_AVATAR_SIZE, - } = props; - const { data: profile } = useProfile(); - const { hasGravatar, isLoadingGravatar } = useGravatar(profile?.email); - - return isLoadingGravatar ? ( - - ) : hasGravatar ? ( - gravatar - ) : ( - avatar - ); -}; diff --git a/packages/manager/src/features/Events/EventRow.styles.ts b/packages/manager/src/features/Events/EventRow.styles.ts deleted file mode 100644 index cf2743a97ed..00000000000 --- a/packages/manager/src/features/Events/EventRow.styles.ts +++ /dev/null @@ -1,14 +0,0 @@ -// @TODO: delete file once Gravatar is sunset -import { styled } from '@mui/material/styles'; - -import { fadeIn } from 'src/styles/keyframes'; - -import { GravatarByUsername } from '../../components/GravatarByUsername'; - -export const StyledGravatar = styled(GravatarByUsername, { - label: 'StyledGravatar', -})(({ theme }) => ({ - animation: `${fadeIn} .2s ease-in-out forwards`, - height: theme.spacing(3), - width: theme.spacing(3), -})); diff --git a/packages/manager/src/features/Events/EventRow.tsx b/packages/manager/src/features/Events/EventRow.tsx index 40acfd1a063..e28aead3b4c 100644 --- a/packages/manager/src/features/Events/EventRow.tsx +++ b/packages/manager/src/features/Events/EventRow.tsx @@ -5,14 +5,12 @@ import { Avatar } from 'src/components/Avatar/Avatar'; import { BarPercent } from 'src/components/BarPercent'; import { Box } from 'src/components/Box'; import { DateTimeDisplay } from 'src/components/DateTimeDisplay'; -import { GravatarOrAvatar } from 'src/components/GravatarOrAvatar'; import { Hidden } from 'src/components/Hidden'; import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; import { useProfile } from 'src/queries/profile/profile'; import { getEventTimestamp } from 'src/utilities/eventUtils'; -import { StyledGravatar } from './EventRow.styles'; import { formatProgressEvent, getEventMessage, @@ -60,25 +58,14 @@ export const EventRow = (props: EventRowProps) => { - - } - gravatar={ - + {username} diff --git a/packages/manager/src/features/Events/utils.test.tsx b/packages/manager/src/features/Events/utils.test.tsx index 7cb5c5bddc8..792fb81e0be 100644 --- a/packages/manager/src/features/Events/utils.test.tsx +++ b/packages/manager/src/features/Events/utils.test.tsx @@ -116,7 +116,7 @@ describe('getEventUsername', () => { username: 'test-user', }); - expect(getEventUsername(mockEvent)).toBe('Linode'); + expect(getEventUsername(mockEvent)).toBe('Akamai'); }); it('returns "Linode" if the username does not exist', () => { @@ -125,7 +125,7 @@ describe('getEventUsername', () => { username: null, }); - expect(getEventUsername(mockEvent)).toBe('Linode'); + expect(getEventUsername(mockEvent)).toBe('Akamai'); }); it('returns "Linode" if the username does not exist and action is in ACTIONS_WITHOUT_USERNAMES', () => { @@ -140,7 +140,7 @@ describe('getEventUsername', () => { username: null, }); - expect(getEventUsername(mockEvent)).toBe('Linode'); + expect(getEventUsername(mockEvent)).toBe('Akamai'); }); }); diff --git a/packages/manager/src/features/Events/utils.tsx b/packages/manager/src/features/Events/utils.tsx index d9546fe31db..74349332247 100644 --- a/packages/manager/src/features/Events/utils.tsx +++ b/packages/manager/src/features/Events/utils.tsx @@ -55,7 +55,7 @@ export const getEventUsername = (event: Event) => { return event.username; } - return 'Linode'; + return 'Akamai'; }; /** diff --git a/packages/manager/src/features/GlobalNotifications/GlobalNotifications.tsx b/packages/manager/src/features/GlobalNotifications/GlobalNotifications.tsx index 8654d0858ad..2d600740b1b 100644 --- a/packages/manager/src/features/GlobalNotifications/GlobalNotifications.tsx +++ b/packages/manager/src/features/GlobalNotifications/GlobalNotifications.tsx @@ -15,7 +15,6 @@ import { APIMaintenanceBanner } from './APIMaintenanceBanner'; import { ComplianceBanner } from './ComplianceBanner'; import { ComplianceUpdateModal } from './ComplianceUpdateModal'; import { EmailBounceNotificationSection } from './EmailBounce'; -import { GravatarSunsetBanner } from './GravatarSunsetBanner'; import { RegionStatusBanner } from './RegionStatusBanner'; import { TaxCollectionBanner } from './TaxCollectionBanner'; import { DesignUpdateBanner } from './TokensUpdateBanner'; @@ -87,7 +86,6 @@ export const GlobalNotifications = () => { Object.keys(flags.taxCollectionBanner).length > 0 ? ( ) : null} - ); }; diff --git a/packages/manager/src/features/GlobalNotifications/GravatarSunsetBanner.tsx b/packages/manager/src/features/GlobalNotifications/GravatarSunsetBanner.tsx deleted file mode 100644 index efbc1d2ef71..00000000000 --- a/packages/manager/src/features/GlobalNotifications/GravatarSunsetBanner.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; - -import { DismissibleBanner } from 'src/components/DismissibleBanner/DismissibleBanner'; -import { Typography } from 'src/components/Typography'; -import { useGravatar } from 'src/hooks/useGravatar'; - -interface Props { - email: string; -} - -export const GravatarSunsetBanner = (props: Props) => { - const { email } = props; - const GRAVATAR_DEPRECATION_DATE = 'September 30th, 2024'; - - const { hasGravatar, isLoadingGravatar } = useGravatar(email); - - if (isLoadingGravatar || !hasGravatar) { - return; - } - return ( - - - {`Support for using Gravatar as your profile photo will be deprecated on ${GRAVATAR_DEPRECATION_DATE}. Your profile photo will automatically be changed to your username initial.`} - - - ); -}; diff --git a/packages/manager/src/features/NotificationCenter/Events/NotificationCenterEvent.tsx b/packages/manager/src/features/NotificationCenter/Events/NotificationCenterEvent.tsx index fd0a62cdb70..9940a8527d7 100644 --- a/packages/manager/src/features/NotificationCenter/Events/NotificationCenterEvent.tsx +++ b/packages/manager/src/features/NotificationCenter/Events/NotificationCenterEvent.tsx @@ -3,7 +3,6 @@ import * as React from 'react'; import { BarPercent } from 'src/components/BarPercent'; import { Box } from 'src/components/Box'; -import { GravatarOrAvatar } from 'src/components/GravatarOrAvatar'; import { Typography } from 'src/components/Typography'; import { formatProgressEvent, @@ -14,7 +13,6 @@ import { useProfile } from 'src/queries/profile/profile'; import { NotificationEventAvatar, - NotificationEventGravatar, NotificationEventStyledBox, notificationEventStyles, } from '../NotificationCenter.styles'; @@ -55,21 +53,15 @@ export const NotificationCenterEvent = React.memo( data-qa-event-seen={event.seen} data-testid={event.action} > - + } - height={32} - width={32} + username={username} /> + {message} {showProgress && ( diff --git a/packages/manager/src/features/NotificationCenter/NotificationCenter.styles.ts b/packages/manager/src/features/NotificationCenter/NotificationCenter.styles.ts index 620151a4769..6820ce92683 100644 --- a/packages/manager/src/features/NotificationCenter/NotificationCenter.styles.ts +++ b/packages/manager/src/features/NotificationCenter/NotificationCenter.styles.ts @@ -4,10 +4,8 @@ import { makeStyles } from 'tss-react/mui'; import { Avatar } from 'src/components/Avatar/Avatar'; import { Box } from 'src/components/Box'; -import { GravatarByUsername } from 'src/components/GravatarByUsername'; import { Link } from 'src/components/Link'; import { Typography } from 'src/components/Typography'; -import { fadeIn } from 'src/styles/keyframes'; import { omittedProps } from 'src/utilities/omittedProps'; import type { NotificationCenterNotificationMessageProps } from './types'; @@ -120,16 +118,6 @@ export const NotificationEventStyledBox = styled(Box, { width: '100%', })); -export const NotificationEventGravatar = styled(GravatarByUsername, { - label: 'StyledGravatarByUsername', -})(() => ({ - animation: `${fadeIn} .2s ease-in-out forwards`, - height: 32, - marginTop: 2, - minWidth: 32, - width: 32, -})); - export const NotificationEventAvatar = styled(Avatar, { label: 'StyledAvatar', })(() => ({ diff --git a/packages/manager/src/features/Profile/DisplaySettings/DisplaySettings.tsx b/packages/manager/src/features/Profile/DisplaySettings/DisplaySettings.tsx index 6c003f183c2..f4305fd948c 100644 --- a/packages/manager/src/features/Profile/DisplaySettings/DisplaySettings.tsx +++ b/packages/manager/src/features/Profile/DisplaySettings/DisplaySettings.tsx @@ -9,14 +9,10 @@ import { Avatar } from 'src/components/Avatar/Avatar'; import { Box } from 'src/components/Box'; import { Button } from 'src/components/Button/Button'; import { Divider } from 'src/components/Divider'; -import { GravatarOrAvatar } from 'src/components/GravatarOrAvatar'; -import { Link } from 'src/components/Link'; import { Paper } from 'src/components/Paper'; import { SingleTextFieldForm } from 'src/components/SingleTextFieldForm/SingleTextFieldForm'; -import { TooltipIcon } from 'src/components/TooltipIcon'; import { Typography } from 'src/components/Typography'; import { RESTRICTED_FIELD_TOOLTIP } from 'src/features/Account/constants'; -import { useGravatar } from 'src/hooks/useGravatar'; import { useNotificationsQuery } from 'src/queries/account/notifications'; import { useMutateProfile, useProfile } from 'src/queries/profile/profile'; @@ -24,7 +20,6 @@ import { AvatarColorPickerDialog } from './AvatarColorPickerDialog'; import { TimezoneForm } from './TimezoneForm'; import type { ApplicationState } from 'src/store'; -import { GravatarByEmail } from 'src/components/GravatarByEmail'; export const DisplaySettings = () => { const theme = useTheme(); @@ -39,8 +34,6 @@ export const DisplaySettings = () => { const isProxyUser = profile?.user_type === 'proxy'; - const { hasGravatar } = useGravatar(profile?.email); - const [ isColorPickerDialogOpen, setAvatarColorPickerDialogOpen, @@ -70,15 +63,6 @@ export const DisplaySettings = () => { return updateProfile({ email: newEmail }); }; - const tooltipIconText = ( - <> - Go to gravatar.com and register - an account using the same email address as your Cloud Manager account. - Upload your desired profile image to your Gravatar account and it will be - automatically linked. - - ); - const tooltipForDisabledUsernameField = profile?.restricted ? 'Restricted users cannot update their username. Please contact an account administrator.' : isProxyUser @@ -101,50 +85,22 @@ export const DisplaySettings = () => { }} display="flex" > - - } - avatar={} - height={88} - width={88} - /> +
- {hasGravatar ? 'Profile photo' : 'Avatar'} - {hasGravatar && ( - - )} + Avatar - {hasGravatar - ? 'Create, upload, and manage your globally recognized avatar from a single place with Gravatar.' - : 'Your avatar is automatically generated using the first character of your username.'} + Your avatar is automatically generated using the first character + of your username. - {hasGravatar ? ( - - Manage photo - - ) : ( - - )} + +
@@ -194,20 +150,6 @@ export const DisplaySettings = () => { ); }; -const StyledAddImageLink = styled(Link, { - label: 'StyledAddImageLink', -})(({ theme }) => ({ - '& svg': { - height: '1rem', - left: 6, - position: 'relative', - top: 3, - width: '1rem', - }, - fontFamily: theme.font.bold, - fontSize: '1rem', -})); - const StyledProfileCopy = styled(Typography, { label: 'StyledProfileCopy', })(({ theme }) => ({ @@ -215,11 +157,3 @@ const StyledProfileCopy = styled(Typography, { marginTop: 4, maxWidth: 360, })); - -const StyledTooltipIcon = styled(TooltipIcon, { - label: 'StyledTooltip', -})(() => ({ - '& .MuiTooltip-tooltip': { - minWidth: 350, - }, -})); diff --git a/packages/manager/src/features/Support/ExpandableTicketPanel.tsx b/packages/manager/src/features/Support/ExpandableTicketPanel.tsx index 80d5ab5cd5a..5c6efcea93d 100644 --- a/packages/manager/src/features/Support/ExpandableTicketPanel.tsx +++ b/packages/manager/src/features/Support/ExpandableTicketPanel.tsx @@ -1,13 +1,10 @@ -import Avatar from '@mui/material/Avatar'; import { useTheme } from '@mui/material/styles'; import Grid from '@mui/material/Unstable_Grid2'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; -import UserIcon from 'src/assets/icons/account.svg'; -import { Avatar as NewAvatar } from 'src/components/Avatar/Avatar'; +import { Avatar } from 'src/components/Avatar/Avatar'; import { DateTimeDisplay } from 'src/components/DateTimeDisplay'; -import { GravatarOrAvatar } from 'src/components/GravatarOrAvatar'; import { Typography } from 'src/components/Typography'; import { useProfile } from 'src/queries/profile/profile'; @@ -143,30 +140,17 @@ export const ExpandableTicketPanel = React.memo((props: Props) => { } }, [parentTicket, reply, ticket, ticketUpdated]); - const renderAvatar = (id: string) => { + const renderAvatar = () => { return (
- - } - gravatar={ - - - +
); @@ -182,12 +166,12 @@ export const ExpandableTicketPanel = React.memo((props: Props) => { return ( - {renderAvatar(data.gravatar_id)} + {renderAvatar()} - {data.friendly_name} + {data.friendly_name === 'Linode' ? 'Akamai' : data.friendly_name} {data.from_linode && !OFFICIAL_USERNAMES.includes(data.username) ? (