From 33e29836fc1a874774419ba4ae83e098ba2b0d8a Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Fri, 21 Jun 2024 12:34:28 -0400 Subject: [PATCH 01/37] test: Update Object Storage tests to mock account `capabilities` as needed (#10602) * mock account capabilities as needed * Added changeset: Update Object Storage tests to mock account capabilities as needed for multi cluster --------- Co-authored-by: Banks Nussman --- .../pr-10602-tests-1718985699307.md | 5 ++ .../core/objectStorage/access-key.e2e.spec.ts | 4 ++ .../objectStorage/access-keys.smoke.spec.ts | 10 ++- .../enable-object-storage.spec.ts | 7 +- .../objectStorage/object-storage.e2e.spec.ts | 8 ++- .../object-storage.smoke.spec.ts | 71 ++++++++++++++++++- 6 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 packages/manager/.changeset/pr-10602-tests-1718985699307.md diff --git a/packages/manager/.changeset/pr-10602-tests-1718985699307.md b/packages/manager/.changeset/pr-10602-tests-1718985699307.md new file mode 100644 index 00000000000..82ccb0de864 --- /dev/null +++ b/packages/manager/.changeset/pr-10602-tests-1718985699307.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Update Object Storage tests to mock account capabilities as needed for multi cluster ([#10602](https://github.com/linode/manager/pull/10602)) 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 9d8ef3a7757..3806fb0ebb0 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 @@ -17,6 +17,8 @@ import { makeFeatureFlagData } from 'support/util/feature-flags'; import { randomLabel } from 'support/util/random'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; +import { mockGetAccount } from 'support/intercepts/account'; +import { accountFactory } from 'src/factories'; authenticate(); describe('object storage access key end-to-end tests', () => { @@ -37,6 +39,7 @@ describe('object storage access key end-to-end tests', () => { interceptGetAccessKeys().as('getKeys'); interceptCreateAccessKey().as('createKey'); + mockGetAccount(accountFactory.build({ capabilities: [] })); mockAppendFeatureFlags({ objMultiCluster: makeFeatureFlagData(false), }); @@ -131,6 +134,7 @@ describe('object storage access key end-to-end tests', () => { ).then(() => { const keyLabel = randomLabel(); + mockGetAccount(accountFactory.build({ capabilities: [] })); mockAppendFeatureFlags({ objMultiCluster: makeFeatureFlagData(false), }); diff --git a/packages/manager/cypress/e2e/core/objectStorage/access-keys.smoke.spec.ts b/packages/manager/cypress/e2e/core/objectStorage/access-keys.smoke.spec.ts index 84d67db2cb4..f3972f56cbc 100644 --- a/packages/manager/cypress/e2e/core/objectStorage/access-keys.smoke.spec.ts +++ b/packages/manager/cypress/e2e/core/objectStorage/access-keys.smoke.spec.ts @@ -25,10 +25,11 @@ import { randomString, } from 'support/util/random'; import { ui } from 'support/ui'; -import { regionFactory } from 'src/factories'; +import { accountFactory, regionFactory } from 'src/factories'; import { mockGetRegions } from 'support/intercepts/regions'; import { buildArray } from 'support/util/arrays'; import { Scope } from '@linode/api-v4'; +import { mockGetAccount } from 'support/intercepts/account'; describe('object storage access keys smoke tests', () => { /* @@ -44,6 +45,7 @@ describe('object storage access keys smoke tests', () => { secret_key: randomString(39), }); + mockGetAccount(accountFactory.build({ capabilities: [] })); mockAppendFeatureFlags({ objMultiCluster: makeFeatureFlagData(false), }); @@ -115,6 +117,7 @@ describe('object storage access keys smoke tests', () => { secret_key: randomString(39), }); + mockGetAccount(accountFactory.build({ capabilities: [] })); mockAppendFeatureFlags({ objMultiCluster: makeFeatureFlagData(false), }); @@ -164,6 +167,11 @@ describe('object storage access keys smoke tests', () => { const mockRegions = [...mockRegionsObj, ...mockRegionsNoObj]; beforeEach(() => { + mockGetAccount( + accountFactory.build({ + capabilities: ['Object Storage Access Key Regions'], + }) + ); mockAppendFeatureFlags({ objMultiCluster: makeFeatureFlagData(true), }); diff --git a/packages/manager/cypress/e2e/core/objectStorage/enable-object-storage.spec.ts b/packages/manager/cypress/e2e/core/objectStorage/enable-object-storage.spec.ts index 76459bd7b5b..08e52e042a4 100644 --- a/packages/manager/cypress/e2e/core/objectStorage/enable-object-storage.spec.ts +++ b/packages/manager/cypress/e2e/core/objectStorage/enable-object-storage.spec.ts @@ -14,8 +14,12 @@ import { profileFactory, regionFactory, objectStorageKeyFactory, + accountFactory, } from '@src/factories'; -import { mockGetAccountSettings } from 'support/intercepts/account'; +import { + mockGetAccount, + mockGetAccountSettings, +} from 'support/intercepts/account'; import { mockCancelObjectStorage, mockCreateAccessKey, @@ -56,6 +60,7 @@ describe('Object Storage enrollment', () => { * - Confirms that consistent pricing information is shown for all regions in the enable modal. */ it('can enroll in Object Storage', () => { + mockGetAccount(accountFactory.build({ capabilities: [] })); mockAppendFeatureFlags({ objMultiCluster: makeFeatureFlagData(false), }); 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 87cdaf3371d..be2705f89e3 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 @@ -4,9 +4,12 @@ import 'cypress-file-upload'; import { createBucket } from '@linode/api-v4/lib/object-storage'; -import { objectStorageBucketFactory } from 'src/factories'; +import { accountFactory, objectStorageBucketFactory } from 'src/factories'; import { authenticate } from 'support/api/authentication'; -import { interceptGetNetworkUtilization } from 'support/intercepts/account'; +import { + interceptGetNetworkUtilization, + mockGetAccount, +} from 'support/intercepts/account'; import { interceptCreateBucket, interceptDeleteBucket, @@ -132,6 +135,7 @@ describe('object storage end-to-end tests', () => { interceptDeleteBucket(bucketLabel, bucketCluster).as('deleteBucket'); interceptGetNetworkUtilization().as('getNetworkUtilization'); + mockGetAccount(accountFactory.build({ capabilities: [] })); mockAppendFeatureFlags({ objMultiCluster: makeFeatureFlagData(false), }).as('getFeatureFlags'); diff --git a/packages/manager/cypress/e2e/core/objectStorage/object-storage.smoke.spec.ts b/packages/manager/cypress/e2e/core/objectStorage/object-storage.smoke.spec.ts index 261c4a10491..505ba19b880 100644 --- a/packages/manager/cypress/e2e/core/objectStorage/object-storage.smoke.spec.ts +++ b/packages/manager/cypress/e2e/core/objectStorage/object-storage.smoke.spec.ts @@ -23,7 +23,8 @@ import { import { makeFeatureFlagData } from 'support/util/feature-flags'; import { randomLabel, randomString } from 'support/util/random'; import { ui } from 'support/ui'; -import { regionFactory } from 'src/factories'; +import { accountFactory, regionFactory } from 'src/factories'; +import { mockGetAccount } from 'support/intercepts/account'; describe('object storage smoke tests', () => { /* @@ -56,6 +57,11 @@ describe('object storage smoke tests', () => { objects: 0, }); + mockGetAccount( + accountFactory.build({ + capabilities: ['Object Storage Access Key Regions'], + }) + ); mockAppendFeatureFlags({ objMultiCluster: makeFeatureFlagData(true), }).as('getFeatureFlags'); @@ -160,6 +166,7 @@ describe('object storage smoke tests', () => { hostname: bucketHostname, }); + mockGetAccount(accountFactory.build({ capabilities: [] })); mockAppendFeatureFlags({ objMultiCluster: makeFeatureFlagData(false), }).as('getFeatureFlags'); @@ -286,7 +293,7 @@ describe('object storage smoke tests', () => { * - Mocks existing buckets. * - Deletes mocked bucket, confirms that landing page reflects deletion. */ - it('can delete object storage bucket - smoke', () => { + it('can delete object storage bucket - smoke - Multi Cluster Disabled', () => { const bucketLabel = randomLabel(); const bucketCluster = 'us-southeast-1'; const bucketMock = objectStorageBucketFactory.build({ @@ -296,6 +303,12 @@ describe('object storage smoke tests', () => { objects: 0, }); + mockGetAccount(accountFactory.build({ capabilities: [] })); + mockAppendFeatureFlags({ + objMultiCluster: makeFeatureFlagData(false), + }); + mockGetFeatureFlagClientstream(); + mockGetBuckets([bucketMock]).as('getBuckets'); mockDeleteBucket(bucketLabel, bucketCluster).as('deleteBucket'); @@ -324,4 +337,58 @@ describe('object storage smoke tests', () => { cy.wait('@deleteBucket'); cy.findByText('S3-compatible storage solution').should('be.visible'); }); + + /* + * - Tests core object storage bucket deletion flow using mocked API responses. + * - Mocks existing buckets. + * - Deletes mocked bucket, confirms that landing page reflects deletion. + */ + it('can delete object storage bucket - smoke - Multi Cluster Enabled', () => { + const bucketLabel = randomLabel(); + const bucketCluster = 'us-southeast-1'; + const bucketMock = objectStorageBucketFactory.build({ + label: bucketLabel, + cluster: bucketCluster, + hostname: `${bucketLabel}.${bucketCluster}.linodeobjects.com`, + objects: 0, + }); + + mockGetAccount( + accountFactory.build({ + capabilities: ['Object Storage Access Key Regions'], + }) + ); + mockAppendFeatureFlags({ + objMultiCluster: makeFeatureFlagData(true), + }); + mockGetFeatureFlagClientstream(); + + mockGetBuckets([bucketMock]).as('getBuckets'); + mockDeleteBucket(bucketLabel, bucketMock.region!).as('deleteBucket'); + + cy.visitWithLogin('/object-storage'); + cy.wait('@getBuckets'); + + cy.findByText(bucketLabel) + .should('be.visible') + .closest('tr') + .within(() => { + cy.findByText('Delete').should('be.visible').click(); + }); + + ui.dialog + .findByTitle(`Delete Bucket ${bucketLabel}`) + .should('be.visible') + .within(() => { + cy.findByLabelText('Bucket Name').click().type(bucketLabel); + ui.buttonGroup + .findButtonByTitle('Delete') + .should('be.enabled') + .should('be.visible') + .click(); + }); + + cy.wait('@deleteBucket'); + cy.findByText('S3-compatible storage solution').should('be.visible'); + }); }); From 9c9a2b6f11b0204c1fc88f38f7f399b7cf88d1f9 Mon Sep 17 00:00:00 2001 From: Jaalah Ramos <125309814+jaalah-akamai@users.noreply.github.com> Date: Mon, 24 Jun 2024 11:44:54 -0400 Subject: [PATCH 02/37] fix: [M3-8264] - Conditional invocation of useEventInfiniteQuery hook (#10584) Co-authored-by: Jaalah Ramos --- .../pr-10584-fixed-1718376872460.md | 5 +++ .../useEventNotifications.tsx | 40 ++++++++++--------- 2 files changed, 26 insertions(+), 19 deletions(-) create mode 100644 packages/manager/.changeset/pr-10584-fixed-1718376872460.md diff --git a/packages/manager/.changeset/pr-10584-fixed-1718376872460.md b/packages/manager/.changeset/pr-10584-fixed-1718376872460.md new file mode 100644 index 00000000000..586c1a96686 --- /dev/null +++ b/packages/manager/.changeset/pr-10584-fixed-1718376872460.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Potential runtime issue with conditional hook ([#10584](https://github.com/linode/manager/pull/10584)) diff --git a/packages/manager/src/features/NotificationCenter/NotificationData/useEventNotifications.tsx b/packages/manager/src/features/NotificationCenter/NotificationData/useEventNotifications.tsx index 9da07d66e95..1fba29828e2 100644 --- a/packages/manager/src/features/NotificationCenter/NotificationData/useEventNotifications.tsx +++ b/packages/manager/src/features/NotificationCenter/NotificationData/useEventNotifications.tsx @@ -23,40 +23,42 @@ const defaultUnwantedEvents: EventAction[] = [ 'volume_update', ]; -export const useEventNotifications = (givenEvents?: Event[]) => { - const events = removeBlocklistedEvents( - givenEvents ?? useEventsInfiniteQuery().events - ); +export const useEventNotifications = (): NotificationItem[] => { + const { events: fetchedEvents } = useEventsInfiniteQuery(); + const relevantEvents = removeBlocklistedEvents(fetchedEvents); const { isTaxIdEnabled } = useIsTaxIdEnabled(); const notificationContext = React.useContext(_notificationContext); // TODO: TaxId - This entire function can be removed when we cleanup tax id feature flags - const unwantedEvents = React.useMemo(() => { - const events = [...defaultUnwantedEvents]; + const unwantedEventTypes = React.useMemo(() => { + const eventTypes = [...defaultUnwantedEvents]; if (!isTaxIdEnabled) { - events.push('tax_id_invalid'); + eventTypes.push('tax_id_invalid'); } - return events; + return eventTypes; }, [isTaxIdEnabled]); - const _events = events.filter( - (thisEvent) => !unwantedEvents.includes(thisEvent.action) + const filteredEvents = relevantEvents.filter( + (event) => !unwantedEventTypes.includes(event.action) ); - const [inProgress, completed] = partition(isInProgressEvent, _events); + const [inProgressEvents, completedEvents] = partition( + isInProgressEvent, + filteredEvents + ); - const allEvents = [ - ...inProgress.map((thisEvent) => - formatProgressEventForDisplay(thisEvent, notificationContext.closeMenu) + const allNotificationItems = [ + ...inProgressEvents.map((event) => + formatProgressEventForDisplay(event, notificationContext.closeMenu) ), - ...completed.map((thisEvent) => - formatEventForDisplay(thisEvent, notificationContext.closeMenu) + ...completedEvents.map((event) => + formatEventForDisplay(event, notificationContext.closeMenu) ), ]; - return allEvents.filter((thisAction) => - Boolean(thisAction.body) - ) as NotificationItem[]; + return allNotificationItems.filter((notification) => + Boolean(notification.body) + ); }; const formatEventForDisplay = ( From 2d6f1d0adced7be2ba026126566e7c56c948fe4e Mon Sep 17 00:00:00 2001 From: Azure-akamai Date: Mon, 24 Jun 2024 13:19:52 -0400 Subject: [PATCH 03/37] test: [M3-8107] - Refactor Cypress Longview test to use mock API data/events (#10579) * update tests for check longview install with mock data * Update tests * Added changeset: Refactor Cypress Longview test to use mock API data/events * update according to the review * update after reviews --- .../pr-10579-tests-1718297804893.md | 5 + .../e2e/core/longview/longview.spec.ts | 256 +++++++++++------- .../cypress/support/intercepts/longview.ts | 36 ++- .../manager/src/factories/longviewDisks.ts | 125 ++++++++- .../manager/src/factories/longviewResponse.ts | 50 +++- 5 files changed, 353 insertions(+), 119 deletions(-) create mode 100644 packages/manager/.changeset/pr-10579-tests-1718297804893.md diff --git a/packages/manager/.changeset/pr-10579-tests-1718297804893.md b/packages/manager/.changeset/pr-10579-tests-1718297804893.md new file mode 100644 index 00000000000..ecd89ebaf74 --- /dev/null +++ b/packages/manager/.changeset/pr-10579-tests-1718297804893.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Refactor Cypress Longview test to use mock API data/events ([#10579](https://github.com/linode/manager/pull/10579)) diff --git a/packages/manager/cypress/e2e/core/longview/longview.spec.ts b/packages/manager/cypress/e2e/core/longview/longview.spec.ts index 4ecfed6ca39..320f57da41e 100644 --- a/packages/manager/cypress/e2e/core/longview/longview.spec.ts +++ b/packages/manager/cypress/e2e/core/longview/longview.spec.ts @@ -1,28 +1,26 @@ -import type { Linode, LongviewClient } from '@linode/api-v4'; -import { createLongviewClient } from '@linode/api-v4'; -import { longviewResponseFactory, longviewClientFactory } from 'src/factories'; -import { LongviewResponse } from 'src/features/Longview/request.types'; +import type { LongviewClient } from '@linode/api-v4'; +import { DateTime } from 'luxon'; +import { + longviewResponseFactory, + longviewClientFactory, + longviewAppsFactory, + longviewLatestStatsFactory, + longviewPackageFactory, +} from 'src/factories'; import { authenticate } from 'support/api/authentication'; import { - longviewInstallTimeout, longviewStatusTimeout, longviewEmptyStateMessage, longviewAddClientButtonText, } from 'support/constants/longview'; import { interceptFetchLongviewStatus, - interceptGetLongviewClients, mockGetLongviewClients, mockFetchLongviewStatus, mockCreateLongviewClient, } from 'support/intercepts/longview'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; -import { createTestLinode } from 'support/util/linodes'; -import { randomLabel, randomString } from 'support/util/random'; - -// Timeout if Linode creation and boot takes longer than 1 and a half minutes. -const linodeCreateTimeout = 90000; /** * Returns the command used to install Longview which is shown in Cloud's UI. @@ -35,31 +33,6 @@ const getInstallCommand = (installCode: string): string => { return `curl -s https://lv.linode.com/${installCode} | sudo bash`; }; -/** - * Installs Longview on a Linode. - * - * @param linodeIp - IP of Linode on which to install Longview. - * @param linodePass - Root password of Linode on which to install Longview. - * @param installCommand - Longview installation command. - * - * @returns Cypress chainable. - */ -const installLongview = ( - linodeIp: string, - linodePass: string, - installCommand: string -) => { - return cy.exec('./cypress/support/scripts/longview/install-longview.sh', { - failOnNonZeroExit: true, - timeout: longviewInstallTimeout, - env: { - LINODEIP: linodeIp, - LINODEPASSWORD: linodePass, - CURLCOMMAND: installCommand, - }, - }); -}; - /** * Waits for Cloud Manager to fetch Longview data and receive updates. * @@ -100,6 +73,58 @@ const waitForLongviewData = ( ); }; +/* + * Mocks that represent the state of Longview while waiting for client to be installed. + */ +const longviewLastUpdatedWaiting = longviewResponseFactory.build({ + ACTION: 'lastUpdated', + DATA: { updated: 0 }, + NOTIFICATIONS: [], + VERSION: 0.4, +}); + +const longviewGetValuesWaiting = longviewResponseFactory.build({ + ACTION: 'getValues', + DATA: {}, + NOTIFICATIONS: [], + VERSION: 0.4, +}); + +const longviewGetLatestValueWaiting = longviewResponseFactory.build({ + ACTION: 'getLatestValue', + DATA: {}, + NOTIFICATIONS: [], + VERSION: 0.4, +}); + +/* + * Mocks that represent the state of Longview once client is installed and data is received. + */ +const longviewLastUpdatedInstalled = longviewResponseFactory.build({ + ACTION: 'lastUpdated', + DATA: { + updated: DateTime.now().plus({ minutes: 1 }).toSeconds(), + }, + NOTIFICATIONS: [], + VERSION: 0.4, +}); + +const longviewGetValuesInstalled = longviewResponseFactory.build({ + ACTION: 'getValues', + DATA: { + Packages: longviewPackageFactory.buildList(5), + }, + NOTIFICATIONS: [], + VERSION: 0.4, +}); + +const longviewGetLatestValueInstalled = longviewResponseFactory.build({ + ACTION: 'getLatestValue', + DATA: longviewLatestStatsFactory.build(), + NOTIFICATIONS: [], + VERSION: 0.4, +}); + authenticate(); describe('longview', () => { before(() => { @@ -107,78 +132,76 @@ describe('longview', () => { }); /* - * - Tests Longview installation end-to-end using real API data. - * - Creates a Linode, connects to it via SSH, and installs Longview using the given cURL command. + * - Tests Longview installation end-to-end using mock API data. * - Confirms that Cloud Manager UI updates to reflect Longview installation and data. */ - // TODO Unskip for M3-8107. - it.skip('can install Longview client on a Linode', () => { - const linodePassword = randomString(32, { - symbols: false, - lowercase: true, - uppercase: true, - numbers: true, - spaces: false, + + it('can install Longview client on a Linode', () => { + const client: LongviewClient = longviewClientFactory.build({ + api_key: '01AE82DD-6F99-44F6-95781512B64FFBC3', + apps: longviewAppsFactory.build(), + created: new Date().toISOString(), + id: 338283, + install_code: '748632FC-E92B-491F-A29D44019039017C', + label: 'longview-client-longview338283', + updated: new Date().toISOString(), }); - const createLinodeAndClient = async () => { - return Promise.all([ - createTestLinode({ - root_pass: linodePassword, - type: 'g6-standard-1', - booted: true, - }), - createLongviewClient(randomLabel()), - ]); - }; - - // Create Linode and Longview Client before loading Longview landing page. - cy.defer(createLinodeAndClient, { - label: 'Creating Linode and Longview Client...', - timeout: linodeCreateTimeout, - }).then(([linode, client]: [Linode, LongviewClient]) => { - const linodeIp = linode.ipv4[0]; - const installCommand = getInstallCommand(client.install_code); - - interceptGetLongviewClients().as('getLongviewClients'); - interceptFetchLongviewStatus().as('fetchLongviewStatus'); - cy.visitWithLogin('/longview'); - cy.wait('@getLongviewClients'); - - // Find the table row for the new Longview client, assert expected information - // is displayed inside of it. - cy.get(`[data-qa-longview-client="${client.id}"]`) - .should('be.visible') - .within(() => { - cy.findByText(client.label).should('be.visible'); - cy.findByText(client.api_key).should('be.visible'); - cy.contains(installCommand).should('be.visible'); - cy.findByText('Waiting for data...'); - }); - - // Install Longview on Linode by SSHing into machine and executing cURL command. - installLongview(linodeIp, linodePassword, installCommand); - - // Wait for Longview to begin serving data and confirm that Cloud Manager - // UI updates accordingly. - waitForLongviewData('fetchLongviewStatus', client.api_key); - - // Sometimes Cloud Manager UI does not updated automatically upon receiving - // Longivew status data. Performing a page reload mitigates this issue. - // TODO Remove call to `cy.reload()`. - cy.reload(); - cy.get(`[data-qa-longview-client="${client.id}"]`) - .should('be.visible') - .within(() => { - cy.findByText('Waiting for data...').should('not.exist'); - cy.findByText('CPU').should('be.visible'); - cy.findByText('RAM').should('be.visible'); - cy.findByText('Swap').should('be.visible'); - cy.findByText('Load').should('be.visible'); - cy.findByText('Network').should('be.visible'); - cy.findByText('Storage').should('be.visible'); - }); + mockGetLongviewClients([client]).as('getLongviewClients'); + mockFetchLongviewStatus(client, 'lastUpdated', longviewLastUpdatedWaiting); + mockFetchLongviewStatus(client, 'getValues', longviewGetValuesWaiting); + mockFetchLongviewStatus( + client, + 'getLatestValue', + longviewGetLatestValueWaiting + ).as('fetchLongview'); + + const installCommand = getInstallCommand(client.install_code); + + cy.visitWithLogin('/longview'); + cy.wait('@getLongviewClients'); + + // Confirm that Longview landing page lists a client that is still waiting for data... + cy.get(`[data-qa-longview-client="${client.id}"]`) + .should('be.visible') + .within(() => { + cy.findByText(client.label).should('be.visible'); + cy.findByText(client.api_key).should('be.visible'); + cy.contains(installCommand).should('be.visible'); + cy.findByText('Waiting for data...'); + }); + + // Update mocks after initial Longview fetch to simulate client installation and data retrieval. + // The next time Cloud makes a request to the fetch endpoint, data will start being returned. + // 3 fetches is necessary because the Longview landing page fires 3 requests to the Longview fetch endpoint for each client. + // See https://github.com/linode/manager/pull/10579#discussion_r1647945160 + cy.wait(['@fetchLongview', '@fetchLongview', '@fetchLongview']).then(() => { + mockFetchLongviewStatus( + client, + 'lastUpdated', + longviewLastUpdatedInstalled + ); + mockFetchLongviewStatus(client, 'getValues', longviewGetValuesInstalled); + mockFetchLongviewStatus( + client, + 'getLatestValue', + longviewGetLatestValueInstalled + ); }); + + // Confirms that UI updates to show that data has been retrieved. + cy.findByText(`${client.label}`).should('be.visible'); + cy.get(`[data-qa-longview-client="${client.id}"]`) + .should('be.visible') + .within(() => { + cy.findByText('Waiting for data...').should('not.exist'); + cy.findByText('CPU').should('be.visible'); + cy.findByText('RAM').should('be.visible'); + cy.findByText('Swap').should('be.visible'); + cy.findByText('Load').should('be.visible'); + cy.findByText('Network').should('be.visible'); + cy.findByText('Storage').should('be.visible'); + }); }); /* @@ -187,10 +210,15 @@ describe('longview', () => { */ it('displays empty state message when no clients are present and shows the new client when creating one', () => { const client: LongviewClient = longviewClientFactory.build(); - const status: LongviewResponse = longviewResponseFactory.build(); mockGetLongviewClients([]).as('getLongviewClients'); mockCreateLongviewClient(client).as('createLongviewClient'); - mockFetchLongviewStatus(status).as('fetchLongviewStatus'); + mockFetchLongviewStatus(client, 'lastUpdated', longviewLastUpdatedWaiting); + mockFetchLongviewStatus(client, 'getValues', longviewGetValuesWaiting); + mockFetchLongviewStatus( + client, + 'getLatestValue', + longviewGetLatestValueWaiting + ).as('fetchLongview'); cy.visitWithLogin('/longview'); cy.wait('@getLongviewClients'); @@ -206,6 +234,24 @@ describe('longview', () => { .click(); cy.wait('@createLongviewClient'); + // Update mocks after initial Longview fetch to simulate client installation and data retrieval. + // The next time Cloud makes a request to the fetch endpoint, data will start being returned. + // 3 fetches is necessary because the Longview landing page fires 3 requests to the Longview fetch endpoint for each client. + // See https://github.com/linode/manager/pull/10579#discussion_r1647945160 + cy.wait(['@fetchLongview', '@fetchLongview', '@fetchLongview']).then(() => { + mockFetchLongviewStatus( + client, + 'lastUpdated', + longviewLastUpdatedInstalled + ); + mockFetchLongviewStatus(client, 'getValues', longviewGetValuesInstalled); + mockFetchLongviewStatus( + client, + 'getLatestValue', + longviewGetLatestValueInstalled + ); + }); + // Confirms that UI updates to show the new client when creating one. cy.findByText(`${client.label}`).should('be.visible'); cy.get(`[data-qa-longview-client="${client.id}"]`) diff --git a/packages/manager/cypress/support/intercepts/longview.ts b/packages/manager/cypress/support/intercepts/longview.ts index d0cb2c742fc..2bc3eaf27a9 100644 --- a/packages/manager/cypress/support/intercepts/longview.ts +++ b/packages/manager/cypress/support/intercepts/longview.ts @@ -2,7 +2,10 @@ import { apiMatcher } from 'support/util/intercepts'; import { paginateResponse } from 'support/util/paginate'; import { makeResponse } from 'support/util/response'; import { LongviewClient } from '@linode/api-v4'; -import { LongviewResponse } from 'src/features/Longview/request.types'; +import type { + LongviewAction, + LongviewResponse, +} from 'src/features/Longview/request.types'; /** * Intercepts request to retrieve Longview status for a Longview client. @@ -16,15 +19,38 @@ export const interceptFetchLongviewStatus = (): Cypress.Chainable => { /** * Mocks request to retrieve Longview status for a Longview client. * + * @param client - Longview Client for which to intercept Longview fetch request. + * @param apiAction - Longview API action to intercept. + * @param mockStatus - + * * @returns Cypress chainable. */ export const mockFetchLongviewStatus = ( - status: LongviewResponse + client: LongviewClient, + apiAction: LongviewAction, + mockStatus: LongviewResponse ): Cypress.Chainable => { return cy.intercept( - 'POST', - 'https://longview.linode.com/fetch', - makeResponse(status) + { + url: 'https://longview.linode.com/fetch', + method: 'POST', + }, + async (req) => { + const payload = req.body; + const response = new Response(payload, { + headers: { + 'content-type': req.headers['content-type'] as string, + }, + }); + const formData = await response.formData(); + + if ( + formData.get('api_key') === client.api_key && + formData.get('api_action') === apiAction + ) { + req.reply(makeResponse([mockStatus])); + } + } ); }; diff --git a/packages/manager/src/factories/longviewDisks.ts b/packages/manager/src/factories/longviewDisks.ts index 35d77269cb6..5a5dc8ca1fc 100644 --- a/packages/manager/src/factories/longviewDisks.ts +++ b/packages/manager/src/factories/longviewDisks.ts @@ -1,11 +1,37 @@ import * as Factory from 'factory.ts'; -import { Disk, LongviewDisk } from 'src/features/Longview/request.types'; +import { + Disk, + LongviewDisk, + LongviewCPU, + CPU, + LongviewSystemInfo, + LongviewNetworkInterface, + InboundOutboundNetwork, + LongviewNetwork, + LongviewMemory, + LongviewLoad, + Uptime, +} from 'src/features/Longview/request.types'; const mockStats = [ - { x: 0, y: 1 }, - { x: 0, y: 2 }, - { x: 0, y: 3 }, + { x: 1717770900, y: 0 }, + { x: 1717770900, y: 20877.4637037037 }, + { x: 1717770900, y: 4.09420479302832 }, + { x: 1717770900, y: 83937959936 }, + { x: 1717770900, y: 5173267 }, + { x: 1717770900, y: 5210112 }, + { x: 1717770900, y: 82699642934.6133 }, + { x: 1717770900, y: 0.0372984749455338 }, + { x: 1717770900, y: 0.00723311546840959 }, + { x: 1717770900, y: 0.0918300653594771 }, + { x: 1717770900, y: 466.120718954248 }, + { x: 1717770900, y: 451.9651416122 }, + { x: 1717770900, y: 524284 }, + { x: 1717770900, y: 547242.706666667 }, + { x: 1717770900, y: 3466265.29333333 }, + { x: 1717770900, y: 57237.6133333333 }, + { x: 1717770900, y: 365385.893333333 }, ]; export const diskFactory = Factory.Sync.makeFactory({ @@ -14,8 +40,23 @@ export const diskFactory = Factory.Sync.makeFactory({ dm: 0, isswap: 0, mounted: 1, - reads: mockStats, - writes: mockStats, + reads: [mockStats[0]], + write_bytes: [mockStats[1]], + writes: [mockStats[2]], + fs: { + total: [mockStats[3]], + ifree: [mockStats[4]], + itotal: [mockStats[5]], + path: '/', + free: [mockStats[6]], + }, + read_bytes: [mockStats[0]], +}); + +export const cpuFactory = Factory.Sync.makeFactory({ + system: [mockStats[7]], + wait: [mockStats[8]], + user: [mockStats[9]], }); export const longviewDiskFactory = Factory.Sync.makeFactory({ @@ -24,3 +65,75 @@ export const longviewDiskFactory = Factory.Sync.makeFactory({ '/dev/sdb': diskFactory.build({ isswap: 1 }), }, }); + +export const longviewCPUFactory = Factory.Sync.makeFactory({ + CPU: { + cpu0: cpuFactory.build(), + cpu1: cpuFactory.build(), + }, +}); + +export const longviewSysInfoFactory = Factory.Sync.makeFactory( + { + SysInfo: { + arch: 'x86_64', + client: '1.1.5', + cpu: { + cores: 2, + type: 'AMD EPYC 7713 64-Core Processor', + }, + hostname: 'localhost', + kernel: 'Linux 5.10.0-28-amd64', + os: { + dist: 'Debian', + distversion: '11.9', + }, + type: 'kvm', + }, + } +); + +export const InboundOutboundNetworkFactory = Factory.Sync.makeFactory( + { + rx_bytes: [mockStats[10]], + tx_bytes: [mockStats[11]], + } +); + +export const LongviewNetworkInterfaceFactory = Factory.Sync.makeFactory( + { + eth0: InboundOutboundNetworkFactory.build(), + } +); + +export const longviewNetworkFactory = Factory.Sync.makeFactory( + { + Network: { + Interface: LongviewNetworkInterfaceFactory.build(), + mac_addr: 'f2:3c:94:e6:81:e2', + }, + } +); + +export const LongviewMemoryFactory = Factory.Sync.makeFactory({ + Memory: { + swap: { + free: [mockStats[12]], + used: [mockStats[0]], + }, + real: { + used: [mockStats[13]], + free: [mockStats[14]], + buffers: [mockStats[15]], + cache: [mockStats[16]], + }, + }, +}); + +export const LongviewLoadFactory = Factory.Sync.makeFactory({ + Load: [mockStats[0]], +}); + +export const UptimeFactory = Factory.Sync.makeFactory({ + uptime: 84516.53, +}); diff --git a/packages/manager/src/factories/longviewResponse.ts b/packages/manager/src/factories/longviewResponse.ts index 315fad71bff..fa992343ae9 100644 --- a/packages/manager/src/factories/longviewResponse.ts +++ b/packages/manager/src/factories/longviewResponse.ts @@ -1,14 +1,58 @@ import * as Factory from 'factory.ts'; import { LongviewResponse } from 'src/features/Longview/request.types'; +import { AllData, LongviewPackage } from 'src/features/Longview/request.types'; -import { longviewDiskFactory } from './longviewDisks'; +import { + longviewDiskFactory, + longviewCPUFactory, + longviewSysInfoFactory, + longviewNetworkFactory, + LongviewMemoryFactory, + LongviewLoadFactory, + UptimeFactory, +} from './longviewDisks'; + +const longviewResponseData = () => { + const diskData = longviewDiskFactory.build(); + const cpuData = longviewCPUFactory.build(); + const sysinfoData = longviewSysInfoFactory.build(); + const networkData = longviewNetworkFactory.build(); + const memoryData = LongviewMemoryFactory.build(); + const loadData = LongviewLoadFactory.build(); + const uptimeData = UptimeFactory.build(); + + return { + ...diskData, + ...cpuData, + ...sysinfoData, + ...networkData, + ...memoryData, + ...loadData, + ...uptimeData, + }; +}; export const longviewResponseFactory = Factory.Sync.makeFactory( { - ACTION: 'getValues', - DATA: longviewDiskFactory.build(), + ACTION: 'getLatestValue', + DATA: {}, NOTIFICATIONS: [], VERSION: 0.4, } ); + +export const longviewLatestStatsFactory = Factory.Sync.makeFactory< + Partial +>({ + ...longviewResponseData(), +}); + +export const longviewPackageFactory = Factory.Sync.makeFactory( + { + current: Factory.each((i) => `${i + 1}.${i + 2}.${i + 3}`), + held: 0, + name: Factory.each((i) => `mock-package-${i}`), + new: Factory.each((i) => `${i + 1}.${i + 2}.${i + 3}`), + } +); From fc680a6a41cdab74f5f0839381bacc596d8c805c Mon Sep 17 00:00:00 2001 From: Jaalah Ramos <125309814+jaalah-akamai@users.noreply.github.com> Date: Mon, 24 Jun 2024 13:31:22 -0400 Subject: [PATCH 04/37] feat: [M3-7579] - Design Tokens (CDS 2.0) (#10022) * Add design tokens * Updates to status colors * Saving... * Saving... * Version 14 * Update version to v1.0.0 * Added changeset: Added Design Tokens (CDS 2.0) * Update to latest version * Revert * Bump version * Updates to chip palette and update endpoint palette to use darker error color * Remove unnecessary background color for notice story * Update textfields, borders and outlines * Fix dark mode button * Revert input token changes * Revert border tokens and fix marketplace background * Fix darkmode * Dark mode tokens * More dark mode fixes * Update MuiFormHelperText dark mode color * Yarn lock * fix units * Fix disabled buttons * Saving * Saving... * Adjust billing paper * Work in progress * More style updates * Minor * Add feature flag * More updates * Modal close * Nearing the end * update snapshots * Snaclbar and placeholder update * Tefield updates dark theme * Update to menu items * react select placeholder * feedback @dwiley-akamai * feedback * Revert nav hover colors back to Linode green * last feedback bit --------- Co-authored-by: Jaalah Ramos Co-authored-by: Alban Bailly --- .../pr-10022-added-1703703204947.md | 5 + packages/manager/package.json | 1 + .../src/components/ActionMenu/ActionMenu.tsx | 31 +- .../src/components/BetaChip/BetaChip.test.tsx | 2 +- .../components/Breadcrumb/Crumbs.styles.tsx | 3 +- .../components/Button/StyledActionButton.ts | 6 +- .../src/components/Button/StyledLinkButton.ts | 17 +- .../src/components/Button/StyledTagButton.ts | 7 +- .../CollapsibleTable/CollapsibleTable.tsx | 40 +- .../ColorPalette/ColorPalette.test.tsx | 132 ----- .../components/ColorPalette/ColorPalette.tsx | 8 +- packages/manager/src/components/Divider.tsx | 7 - .../src/components/DocsLink/DocsLink.tsx | 5 +- .../EnhancedSelect/Select.styles.ts | 6 +- .../src/components/EnhancedSelect/Select.tsx | 24 +- .../EntityHeader/EntityHeader.stories.tsx | 24 +- .../HighlightedMarkdown.test.tsx.snap | 2 +- .../InlineMenuAction/InlineMenuAction.tsx | 2 +- .../src/components/Notice/Notice.stories.tsx | 2 - .../src/components/Notice/Notice.test.tsx | 2 +- .../PrimaryNav/PrimaryNav.styles.ts | 19 +- .../src/components/PrimaryNav/PrimaryNav.tsx | 100 ++-- .../src/components/PrimaryNav/SideMenu.tsx | 3 +- .../SelectionCard/CardBase.styles.ts | 20 +- .../src/components/Snackbar/Snackbar.tsx | 11 +- .../src/components/StatusIcon/StatusIcon.tsx | 6 +- .../src/components/Table/Table.styles.ts | 13 +- .../components/TableRow/TableRow.styles.ts | 11 +- .../TableSortCell/TableSortCell.tsx | 1 - .../manager/src/components/Tabs/Tab.test.tsx | 2 +- packages/manager/src/components/Tabs/Tab.tsx | 4 +- .../manager/src/components/Tabs/TabList.tsx | 4 +- .../Tabs/__snapshots__/TabList.test.tsx.snap | 2 +- .../manager/src/components/Tag/Tag.styles.ts | 22 +- .../src/components/TagCell/TagCell.tsx | 2 +- .../TextTooltip/TextTooltip.test.tsx | 2 +- .../components/TextTooltip/TextTooltip.tsx | 7 +- .../src/components/Tile/Tile.styles.ts | 6 +- .../manager/src/components/TooltipIcon.tsx | 12 +- .../BillingActivityPanel.tsx | 17 +- .../Billing/InvoiceDetail/InvoiceTable.tsx | 17 +- .../src/features/Help/Panels/PopularPosts.tsx | 4 +- .../src/features/Help/Panels/SearchPanel.tsx | 7 +- .../Linodes/LinodeEntityDetail.styles.ts | 4 +- .../Linodes/LinodeEntityDetailHeader.tsx | 21 +- .../Linodes/LinodesCreate/SelectAppPanel.tsx | 1 + .../TabbedContent/FromAppsContent.tsx | 2 +- .../EndpointHealth.test.tsx | 11 +- .../ServiceTargets/EndpointTable.tsx | 8 +- .../StackScriptForm/StackScriptForm.styles.ts | 2 +- .../TopMenu/AddNewMenu/AddNewMenu.tsx | 23 +- .../TopMenu/SearchBar/SearchBar.styles.ts | 24 +- .../manager/src/features/TopMenu/TopMenu.tsx | 9 +- .../src/features/TopMenu/TopMenuTooltip.tsx | 1 + .../VPCs/VPCDetail/VPCDetail.styles.ts | 6 +- .../src/features/VPCs/VPCDetail/VPCDetail.tsx | 6 +- .../PlansPanel/PlanContainer.styles.ts | 2 - .../manager/src/foundations/themes/dark.ts | 510 ++++++++++++------ .../manager/src/foundations/themes/index.ts | 2 + .../manager/src/foundations/themes/light.ts | 420 +++++++++------ packages/manager/src/index.css | 4 - yarn.lock | 402 +++++++++++++- 62 files changed, 1287 insertions(+), 789 deletions(-) create mode 100644 packages/manager/.changeset/pr-10022-added-1703703204947.md delete mode 100644 packages/manager/src/components/ColorPalette/ColorPalette.test.tsx diff --git a/packages/manager/.changeset/pr-10022-added-1703703204947.md b/packages/manager/.changeset/pr-10022-added-1703703204947.md new file mode 100644 index 00000000000..1a6769e2894 --- /dev/null +++ b/packages/manager/.changeset/pr-10022-added-1703703204947.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Added +--- + +Added Design Tokens (CDS 2.0) ([#10022](https://github.com/linode/manager/pull/10022)) diff --git a/packages/manager/package.json b/packages/manager/package.json index 7b0f2f562a6..5c1fdfb36b0 100644 --- a/packages/manager/package.json +++ b/packages/manager/package.json @@ -18,6 +18,7 @@ "@emotion/styled": "^11.11.0", "@hookform/resolvers": "2.9.11", "@linode/api-v4": "*", + "@linode/design-language-system": "^2.3.0", "@linode/validation": "*", "@lukemorales/query-key-factory": "^1.3.4", "@mui/icons-material": "^5.14.7", diff --git a/packages/manager/src/components/ActionMenu/ActionMenu.tsx b/packages/manager/src/components/ActionMenu/ActionMenu.tsx index 679c0b34df9..6f5faf359b1 100644 --- a/packages/manager/src/components/ActionMenu/ActionMenu.tsx +++ b/packages/manager/src/components/ActionMenu/ActionMenu.tsx @@ -1,4 +1,4 @@ -import { IconButton, ListItemText, useTheme } from '@mui/material'; +import { IconButton, ListItemText } from '@mui/material'; import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; import * as React from 'react'; @@ -37,7 +37,6 @@ export interface ActionMenuProps { */ export const ActionMenu = React.memo((props: ActionMenuProps) => { const { actionsList, ariaLabel, onOpen } = props; - const theme = useTheme(); const menuId = convertToKebabCase(ariaLabel); const buttonId = `${convertToKebabCase(ariaLabel)}-button`; @@ -70,16 +69,6 @@ export const ActionMenu = React.memo((props: ActionMenuProps) => { } const sxTooltipIcon = { - '& :hover': { - color: '#4d99f1', - }, - '&& .MuiSvgIcon-root': { - fill: theme.color.disabledText, - height: '20px', - width: '20px', - }, - - color: '#fff', padding: '0 0 0 8px', pointerEvents: 'all', // Allows the tooltip to be hovered on a disabled MenuItem }; @@ -89,12 +78,12 @@ export const ActionMenu = React.memo((props: ActionMenuProps) => { ({ ':hover': { - backgroundColor: theme.palette.primary.main, - color: '#fff', + backgroundColor: theme.color.buttonPrimaryHover, + color: theme.color.white, }, - backgroundColor: open ? theme.palette.primary.main : undefined, + backgroundColor: open ? theme.color.buttonPrimaryHover : undefined, borderRadius: 'unset', - color: open ? '#fff' : theme.textColors.linkActiveLight, + color: open ? theme.color.white : theme.textColors.linkActiveLight, height: '100%', minWidth: '40px', padding: '10px', @@ -122,7 +111,6 @@ export const ActionMenu = React.memo((props: ActionMenuProps) => { paper: { sx: (theme) => ({ backgroundColor: theme.palette.primary.main, - boxShadow: 'none', }), }, }} @@ -147,15 +135,6 @@ export const ActionMenu = React.memo((props: ActionMenuProps) => { a.onClick(); } }} - sx={{ - '&:hover': { - background: '#226dc3', - }, - background: '#3683dc', - borderBottom: '1px solid #5294e0', - color: '#fff', - padding: '10px 10px 10px 16px', - }} data-qa-action-menu-item={a.title} data-testid={a.title} disabled={a.disabled} diff --git a/packages/manager/src/components/BetaChip/BetaChip.test.tsx b/packages/manager/src/components/BetaChip/BetaChip.test.tsx index 47a86d03207..39d28178640 100644 --- a/packages/manager/src/components/BetaChip/BetaChip.test.tsx +++ b/packages/manager/src/components/BetaChip/BetaChip.test.tsx @@ -17,7 +17,7 @@ describe('BetaChip', () => { const { getByTestId } = renderWithTheme(); const betaChip = getByTestId('betaChip'); expect(betaChip).toBeInTheDocument(); - expect(betaChip).toHaveStyle('background-color: #3683dc'); + expect(betaChip).toHaveStyle('background-color: rgb(16, 138, 214)'); }); it('triggers an onClick callback', () => { diff --git a/packages/manager/src/components/Breadcrumb/Crumbs.styles.tsx b/packages/manager/src/components/Breadcrumb/Crumbs.styles.tsx index 8fe3480a3a9..848a0a164bc 100644 --- a/packages/manager/src/components/Breadcrumb/Crumbs.styles.tsx +++ b/packages/manager/src/components/Breadcrumb/Crumbs.styles.tsx @@ -4,11 +4,10 @@ import { Typography } from 'src/components/Typography'; export const StyledTypography = styled(Typography, { label: 'StyledTypography', -})(({ theme }) => ({ +})(({}) => ({ '&:hover': { textDecoration: 'underline', }, - color: theme.textColors.tableHeader, fontSize: '1.125rem', lineHeight: 'normal', textTransform: 'capitalize', diff --git a/packages/manager/src/components/Button/StyledActionButton.ts b/packages/manager/src/components/Button/StyledActionButton.ts index aa711d36626..008257f5a50 100644 --- a/packages/manager/src/components/Button/StyledActionButton.ts +++ b/packages/manager/src/components/Button/StyledActionButton.ts @@ -15,10 +15,12 @@ export const StyledActionButton = styled(Button, { })(({ theme, ...props }) => ({ ...(!props.disabled && { '&:hover': { - backgroundColor: theme.palette.primary.main, - color: theme.name === 'dark' ? theme.color.black : theme.color.white, + backgroundColor: theme.color.buttonPrimaryHover, + color: theme.color.white, }, }), + background: 'transparent', + color: theme.textColors.linkActiveLight, fontFamily: latoWeb.normal, fontSize: '14px', lineHeight: '16px', diff --git a/packages/manager/src/components/Button/StyledLinkButton.ts b/packages/manager/src/components/Button/StyledLinkButton.ts index 8c4cec0b4a8..1688a156f79 100644 --- a/packages/manager/src/components/Button/StyledLinkButton.ts +++ b/packages/manager/src/components/Button/StyledLinkButton.ts @@ -10,20 +10,5 @@ import { styled } from '@mui/material/styles'; export const StyledLinkButton = styled('button', { label: 'StyledLinkButton', })(({ theme }) => ({ - '&:disabled': { - color: theme.palette.text.disabled, - cursor: 'not-allowed', - }, - '&:hover:not(:disabled)': { - backgroundColor: 'transparent', - color: theme.palette.primary.main, - textDecoration: 'underline', - }, - background: 'none', - border: 'none', - color: theme.textColors.linkActiveLight, - cursor: 'pointer', - font: 'inherit', - minWidth: 0, - padding: 0, + ...theme.applyLinkStyles, })); diff --git a/packages/manager/src/components/Button/StyledTagButton.ts b/packages/manager/src/components/Button/StyledTagButton.ts index df83fcc4c88..d0dae58b7cd 100644 --- a/packages/manager/src/components/Button/StyledTagButton.ts +++ b/packages/manager/src/components/Button/StyledTagButton.ts @@ -24,11 +24,12 @@ export const StyledTagButton = styled(Button, { }), ...(!props.disabled && { '&:hover, &:focus': { - backgroundColor: theme.color.tagButton, + backgroundColor: theme.color.tagButtonBg, border: 'none', + color: theme.color.tagButtonText, }, - backgroundColor: theme.color.tagButton, - color: theme.textColors.linkActiveLight, + backgroundColor: theme.color.tagButtonBg, + color: theme.color.tagButtonText, }), })); diff --git a/packages/manager/src/components/CollapsibleTable/CollapsibleTable.tsx b/packages/manager/src/components/CollapsibleTable/CollapsibleTable.tsx index 0fe1a57cbae..bd844227670 100644 --- a/packages/manager/src/components/CollapsibleTable/CollapsibleTable.tsx +++ b/packages/manager/src/components/CollapsibleTable/CollapsibleTable.tsx @@ -1,5 +1,3 @@ -import Paper from '@mui/material/Paper'; -import TableContainer from '@mui/material/TableContainer'; import * as React from 'react'; import { Table } from 'src/components/Table'; @@ -25,25 +23,23 @@ export const CollapsibleTable = (props: Props) => { const { TableItems, TableRowEmpty, TableRowHead } = props; return ( - - - - {TableRowHead} - - - {TableItems.length === 0 && TableRowEmpty} - {TableItems.map((item) => { - return ( - - ); - })} - -
-
+ + + {TableRowHead} + + + {TableItems.length === 0 && TableRowEmpty} + {TableItems.map((item) => { + return ( + + ); + })} + +
); }; diff --git a/packages/manager/src/components/ColorPalette/ColorPalette.test.tsx b/packages/manager/src/components/ColorPalette/ColorPalette.test.tsx deleted file mode 100644 index a9f6024520e..00000000000 --- a/packages/manager/src/components/ColorPalette/ColorPalette.test.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import React from 'react'; - -import { renderWithTheme } from 'src/utilities/testHelpers'; - -import { ColorPalette } from './ColorPalette'; - -describe('Color Palette', () => { - it('renders the Color Palette', () => { - const { getAllByText, getByText } = renderWithTheme(); - - // primary colors - getByText('Primary Colors'); - getByText('theme.palette.primary.main'); - const mainHash = getAllByText('#3683dc'); - expect(mainHash).toHaveLength(2); - getByText('theme.palette.primary.light'); - getByText('#4d99f1'); - getByText('theme.palette.primary.dark'); - getByText('#2466b3'); - getByText('theme.palette.text.primary'); - const primaryHash = getAllByText('#606469'); - expect(primaryHash).toHaveLength(3); - getByText('theme.color.headline'); - const headlineHash = getAllByText('#32363c'); - expect(headlineHash).toHaveLength(2); - getByText('theme.palette.divider'); - const dividerHash = getAllByText('#f4f4f4'); - expect(dividerHash).toHaveLength(2); - const whiteColor = getAllByText('theme.color.white'); - expect(whiteColor).toHaveLength(2); - const whiteHash = getAllByText('#fff'); - expect(whiteHash).toHaveLength(3); - - // etc - getByText('Etc.'); - getByText('theme.color.red'); - getByText('#ca0813'); - getByText('theme.color.orange'); - getByText('#ffb31a'); - getByText('theme.color.yellow'); - getByText('#fecf2f'); - getByText('theme.color.green'); - getByText('#00b159'); - getByText('theme.color.teal'); - getByText('#17cf73'); - getByText('theme.color.border2'); - getByText('#c5c6c8'); - getByText('theme.color.border3'); - getByText('#eee'); - getByText('theme.color.grey1'); - getByText('#abadaf'); - getByText('theme.color.grey2'); - getByText('#e7e7e7'); - getByText('theme.color.grey3'); - getByText('#ccc'); - getByText('theme.color.grey4'); - getByText('#8C929D'); - getByText('theme.color.grey5'); - getByText('#f5f5f5'); - getByText('theme.color.grey6'); - const borderGreyHash = getAllByText('#e3e5e8'); - expect(borderGreyHash).toHaveLength(3); - getByText('theme.color.grey7'); - getByText('#e9eaef'); - getByText('theme.color.grey8'); - getByText('#dbdde1'); - getByText('theme.color.grey9'); - const borderGrey9Hash = getAllByText('#f4f5f6'); - expect(borderGrey9Hash).toHaveLength(3); - getByText('theme.color.black'); - getByText('#222'); - getByText('theme.color.offBlack'); - getByText('#444'); - getByText('theme.color.boxShadow'); - getByText('#ddd'); - getByText('theme.color.boxShadowDark'); - getByText('#aaa'); - getByText('theme.color.blueDTwhite'); - getByText('theme.color.tableHeaderText'); - getByText('rgba(0, 0, 0, 0.54)'); - getByText('theme.color.drawerBackdrop'); - getByText('rgba(255, 255, 255, 0.5)'); - getByText('theme.color.label'); - getByText('#555'); - getByText('theme.color.disabledText'); - getByText('#c9cacb'); - getByText('theme.color.tagButton'); - getByText('#f1f7fd'); - getByText('theme.color.tagIcon'); - getByText('#7daee8'); - - // background colors - getByText('Background Colors'); - getByText('theme.bg.app'); - getByText('theme.bg.main'); - getByText('theme.bg.offWhite'); - getByText('#fbfbfb'); - getByText('theme.bg.lightBlue1'); - getByText('#f0f7ff'); - getByText('theme.bg.lightBlue2'); - getByText('#e5f1ff'); - getByText('theme.bg.white'); - getByText('theme.bg.tableHeader'); - getByText('#f9fafa'); - getByText('theme.bg.primaryNavPaper'); - getByText('#3a3f46'); - getByText('theme.bg.mainContentBanner'); - getByText('#33373d'); - getByText('theme.bg.bgPaper'); - getByText('#ffffff'); - getByText('theme.bg.bgAccessRow'); - getByText('#fafafa'); - getByText('theme.bg.bgAccessRowTransparentGradient'); - getByText('rgb(255, 255, 255, .001)'); - - // typography colors - getByText('Typography Colors'); - getByText('theme.textColors.linkActiveLight'); - getByText('#2575d0'); - getByText('theme.textColors.headlineStatic'); - getByText('theme.textColors.tableHeader'); - getByText('#888f91'); - getByText('theme.textColors.tableStatic'); - getByText('theme.textColors.textAccessTable'); - - // border colors - getByText('Border Colors'); - getByText('theme.borderColors.borderTypography'); - getByText('theme.borderColors.borderTable'); - getByText('theme.borderColors.divider'); - }); -}); diff --git a/packages/manager/src/components/ColorPalette/ColorPalette.tsx b/packages/manager/src/components/ColorPalette/ColorPalette.tsx index a3bfaadb121..9404371eea9 100644 --- a/packages/manager/src/components/ColorPalette/ColorPalette.tsx +++ b/packages/manager/src/components/ColorPalette/ColorPalette.tsx @@ -1,12 +1,12 @@ -// eslint-disable-next-line no-restricted-imports import { useTheme } from '@mui/material'; import Grid from '@mui/material/Unstable_Grid2'; -import { Theme } from '@mui/material/styles'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; import { Typography } from 'src/components/Typography'; +import type { Theme } from '@mui/material/styles'; + interface Color { alias: string; color: string; @@ -45,7 +45,7 @@ const useStyles = makeStyles()((theme: Theme) => ({ /** * Add a new color to the palette, especially another tint of gray or blue, only after exhausting the option of using an existing color. * - * - Colors used in light mode are located in `foundations/light.ts + * - Colors used in light mode are located in `foundations/light.ts` * - Colors used in dark mode are located in `foundations/dark.ts` * * If a color does not exist in the current palette and is only used once, consider applying the color conditionally: @@ -102,7 +102,7 @@ export const ColorPalette = () => { { alias: 'theme.color.drawerBackdrop', color: theme.color.drawerBackdrop }, { alias: 'theme.color.label', color: theme.color.label }, { alias: 'theme.color.disabledText', color: theme.color.disabledText }, - { alias: 'theme.color.tagButton', color: theme.color.tagButton }, + { alias: 'theme.color.tagButton', color: theme.color.tagButtonBg }, { alias: 'theme.color.tagIcon', color: theme.color.tagIcon }, ]; diff --git a/packages/manager/src/components/Divider.tsx b/packages/manager/src/components/Divider.tsx index 6daa2d34fdb..cfd18a7fe5a 100644 --- a/packages/manager/src/components/Divider.tsx +++ b/packages/manager/src/components/Divider.tsx @@ -24,13 +24,6 @@ const StyledDivider = styled(_Divider, { 'dark', ]), })(({ theme, ...props }) => ({ - borderColor: props.dark - ? theme.color.border2 - : props.light - ? theme.name === 'light' - ? '#e3e5e8' - : '#2e3238' - : '', marginBottom: props.spacingBottom, marginTop: props.spacingTop, })); diff --git a/packages/manager/src/components/DocsLink/DocsLink.tsx b/packages/manager/src/components/DocsLink/DocsLink.tsx index fc13e6b3baf..aba71077a05 100644 --- a/packages/manager/src/components/DocsLink/DocsLink.tsx +++ b/packages/manager/src/components/DocsLink/DocsLink.tsx @@ -50,13 +50,10 @@ export const DocsLink = (props: DocsLinkProps) => { const StyledDocsLink = styled(Link, { label: 'StyledDocsLink', })(({ theme }) => ({ + ...theme.applyLinkStyles, '& svg': { marginRight: theme.spacing(), }, - '&:hover': { - color: theme.textColors.linkActiveLight, - textDecoration: 'underline', - }, alignItems: 'center', display: 'flex', fontFamily: theme.font.normal, diff --git a/packages/manager/src/components/EnhancedSelect/Select.styles.ts b/packages/manager/src/components/EnhancedSelect/Select.styles.ts index 9ffe05b76e6..597687971d1 100644 --- a/packages/manager/src/components/EnhancedSelect/Select.styles.ts +++ b/packages/manager/src/components/EnhancedSelect/Select.styles.ts @@ -1,6 +1,7 @@ -import { Theme } from '@mui/material/styles'; import { makeStyles } from 'tss-react/mui'; +import type { Theme } from '@mui/material/styles'; + // TODO jss-to-tss-react codemod: usages of this hook outside of this file will not be converted. export const useStyles = makeStyles()((theme: Theme) => ({ algoliaRoot: { @@ -225,6 +226,9 @@ export const useStyles = makeStyles()((theme: Theme) => ({ }, width: '100%', }, + '& .select-placeholder': { + color: theme.color.grey1, + }, '& [class*="MuiFormHelperText-error"]': { paddingBottom: theme.spacing(1), }, diff --git a/packages/manager/src/components/EnhancedSelect/Select.tsx b/packages/manager/src/components/EnhancedSelect/Select.tsx index 085210785f1..77f4ec721e5 100644 --- a/packages/manager/src/components/EnhancedSelect/Select.tsx +++ b/packages/manager/src/components/EnhancedSelect/Select.tsx @@ -1,18 +1,10 @@ -import { Theme, useTheme } from '@mui/material'; +import { useTheme } from '@mui/material'; import * as React from 'react'; -import ReactSelect, { - ActionMeta, - NamedProps as SelectProps, - ValueType, -} from 'react-select'; -import CreatableSelect, { - CreatableProps as CreatableSelectProps, -} from 'react-select/creatable'; +import ReactSelect from 'react-select'; +import CreatableSelect from 'react-select/creatable'; -import { TextFieldProps } from 'src/components/TextField'; import { convertToKebabCase } from 'src/utilities/convertToKebobCase'; -import { reactSelectStyles, useStyles } from './Select.styles'; import { DropdownIndicator } from './components/DropdownIndicator'; import Input from './components/Input'; import { LoadingIndicator } from './components/LoadingIndicator'; @@ -23,6 +15,16 @@ import NoOptionsMessage from './components/NoOptionsMessage'; import { Option } from './components/Option'; import Control from './components/SelectControl'; import { SelectPlaceholder as Placeholder } from './components/SelectPlaceholder'; +import { reactSelectStyles, useStyles } from './Select.styles'; + +import type { Theme } from '@mui/material'; +import type { + ActionMeta, + NamedProps as SelectProps, + ValueType, +} from 'react-select'; +import type { CreatableProps as CreatableSelectProps } from 'react-select/creatable'; +import type { TextFieldProps } from 'src/components/TextField'; export interface Item { data?: any; diff --git a/packages/manager/src/components/EntityHeader/EntityHeader.stories.tsx b/packages/manager/src/components/EntityHeader/EntityHeader.stories.tsx index 795daa1010f..e1b05f120ad 100644 --- a/packages/manager/src/components/EntityHeader/EntityHeader.stories.tsx +++ b/packages/manager/src/components/EntityHeader/EntityHeader.stories.tsx @@ -40,32 +40,14 @@ export const Default: Story = { variant: 'h2', }, render: (args) => { - const sxActionItem = { - '&:hover': { - backgroundColor: '#3683dc', - color: '#fff', - }, - color: '#2575d0', - fontFamily: '"LatoWeb", sans-serif', - fontSize: '0.875rem', - height: '34px', - minWidth: 'auto', - }; - return ( Chip / Progress Go Here - - - + + + should highlight text consistently 1`] = `

Some markdown diff --git a/packages/manager/src/components/InlineMenuAction/InlineMenuAction.tsx b/packages/manager/src/components/InlineMenuAction/InlineMenuAction.tsx index 94e0f7d417f..dba7a9ae442 100644 --- a/packages/manager/src/components/InlineMenuAction/InlineMenuAction.tsx +++ b/packages/manager/src/components/InlineMenuAction/InlineMenuAction.tsx @@ -52,7 +52,7 @@ export const InlineMenuAction = (props: InlineMenuActionProps) => { = { export default meta; const StyledWrapper = styled('div')(({ theme }) => ({ - backgroundColor: theme.color.grey2, - padding: theme.spacing(2), })); diff --git a/packages/manager/src/components/Notice/Notice.test.tsx b/packages/manager/src/components/Notice/Notice.test.tsx index cf12ad28ca2..e7d536cd907 100644 --- a/packages/manager/src/components/Notice/Notice.test.tsx +++ b/packages/manager/src/components/Notice/Notice.test.tsx @@ -58,7 +58,7 @@ describe('Notice Component', () => { it('applies variant prop', () => { const { container } = renderWithTheme(); - expect(container.firstChild).toHaveStyle('border-left: 5px solid #ca0813;'); + expect(container.firstChild).toHaveStyle('border-left: 5px solid #d63c42;'); }); it('displays icon for important notices', () => { diff --git a/packages/manager/src/components/PrimaryNav/PrimaryNav.styles.ts b/packages/manager/src/components/PrimaryNav/PrimaryNav.styles.ts index c62e5c2d996..03912e910e5 100644 --- a/packages/manager/src/components/PrimaryNav/PrimaryNav.styles.ts +++ b/packages/manager/src/components/PrimaryNav/PrimaryNav.styles.ts @@ -1,8 +1,9 @@ -import { Theme } from '@mui/material/styles'; import { makeStyles } from 'tss-react/mui'; import { SIDEBAR_WIDTH } from 'src/components/PrimaryNav/SideMenu'; +import type { Theme } from '@mui/material/styles'; + const useStyles = makeStyles()( (theme: Theme, _params, classes) => ({ active: { @@ -10,7 +11,7 @@ const useStyles = makeStyles()( opacity: 1, }, '& svg': { - color: theme.color.teal, + color: theme.palette.success.dark, }, backgroundImage: 'linear-gradient(98deg, #38584B 1%, #3A5049 166%)', textDecoration: 'none', @@ -22,12 +23,6 @@ const useStyles = makeStyles()( backgroundColor: 'rgba(0, 0, 0, 0.12)', color: '#222', }, - fadeContainer: { - display: 'flex', - flexDirection: 'column', - height: 'calc(100% - 90px)', - width: '100%', - }, linkItem: { '&.hiddenWhenCollapsed': { maxHeight: 36, @@ -70,8 +65,8 @@ const useStyles = makeStyles()( opacity: 1, }, '& svg': { - color: theme.color.teal, - fill: theme.color.teal, + color: theme.palette.success.dark, + fill: theme.palette.success.dark, }, [`& .${classes.linkItem}`]: { color: 'white', @@ -86,7 +81,6 @@ const useStyles = makeStyles()( minWidth: SIDEBAR_WIDTH, padding: '8px 13px', position: 'relative', - transition: theme.transitions.create(['background-color']), }, logo: { '& .akamai-logo-name': { @@ -96,7 +90,7 @@ const useStyles = makeStyles()( transition: 'width .1s linear', }, logoAkamaiCollapsed: { - background: theme.bg.primaryNavPaper, + background: theme.bg.appBar, width: 83, }, logoContainer: { @@ -111,6 +105,7 @@ const useStyles = makeStyles()( }, logoItemAkamai: { alignItems: 'center', + backgroundColor: theme.name === 'dark' ? theme.bg.appBar : undefined, display: 'flex', height: 50, paddingLeft: 13, diff --git a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx index 4c7ec032ab0..180c6c5a32b 100644 --- a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx +++ b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx @@ -1,6 +1,6 @@ import Grid from '@mui/material/Unstable_Grid2'; import * as React from 'react'; -import { Link, LinkProps, useLocation } from 'react-router-dom'; +import { Link, useLocation } from 'react-router-dom'; import Account from 'src/assets/icons/account.svg'; import CloudPulse from 'src/assets/icons/cloudpulse.svg'; @@ -43,6 +43,8 @@ import { isFeatureEnabled } from 'src/utilities/accountCapabilities'; import useStyles from './PrimaryNav.styles'; import { linkIsActive } from './utils'; +import type { LinkProps } from 'react-router-dom'; + type NavEntity = | 'Account' | 'Account' @@ -343,7 +345,7 @@ export const PrimaryNav = (props: PrimaryNavProps) => { spacing={0} wrap="nowrap" > - + { -
- {primaryLinkGroups.map((thisGroup, idx) => { - const filteredLinks = thisGroup.filter((thisLink) => !thisLink.hide); - if (filteredLinks.length === 0) { - return null; - } - return ( -
- - {filteredLinks.map((thisLink) => { - const props = { - closeMenu, - isCollapsed, - key: thisLink.display, - locationPathname: location.pathname, - locationSearch: location.search, - ...thisLink, - }; - - // PrefetchPrimaryLink and PrimaryLink are two separate components because invocation of - // hooks cannot be conditional. is a wrapper around - // that includes the usePrefetch hook. - return thisLink.prefetchRequestFn && - thisLink.prefetchRequestCondition !== undefined ? ( - - ) : ( - - ); + + {primaryLinkGroups.map((thisGroup, idx) => { + const filteredLinks = thisGroup.filter((thisLink) => !thisLink.hide); + if (filteredLinks.length === 0) { + return null; + } + return ( +
+ ({ + borderColor: + theme.name === 'light' + ? theme.borderColors.dividerDark + : 'rgba(0, 0, 0, 0.19)', })} -
- ); - })} -
+ className={classes.divider} + spacingBottom={11} + /> + {filteredLinks.map((thisLink) => { + const props = { + closeMenu, + isCollapsed, + key: thisLink.display, + locationPathname: location.pathname, + locationSearch: location.search, + ...thisLink, + }; + + // PrefetchPrimaryLink and PrimaryLink are two separate components because invocation of + // hooks cannot be conditional. is a wrapper around + // that includes the usePrefetch hook. + return thisLink.prefetchRequestFn && + thisLink.prefetchRequestCondition !== undefined ? ( + + ) : ( + + ); + })} +
+ ); + })}
); }; diff --git a/packages/manager/src/components/PrimaryNav/SideMenu.tsx b/packages/manager/src/components/PrimaryNav/SideMenu.tsx index e901df9883c..5be1241600f 100644 --- a/packages/manager/src/components/PrimaryNav/SideMenu.tsx +++ b/packages/manager/src/components/PrimaryNav/SideMenu.tsx @@ -66,7 +66,8 @@ const StyledDrawer = styled(Drawer, { shouldForwardProp: (prop) => prop !== 'collapse', })<{ collapse?: boolean }>(({ theme, ...props }) => ({ '& .MuiDrawer-paper': { - backgroundColor: theme.bg.primaryNavPaper, + backgroundColor: + theme.name === 'dark' ? theme.bg.appBar : theme.bg.primaryNavPaper, borderRight: 'none', boxShadow: 'none', height: '100%', diff --git a/packages/manager/src/components/SelectionCard/CardBase.styles.ts b/packages/manager/src/components/SelectionCard/CardBase.styles.ts index b07fe04d202..8c06a76beee 100644 --- a/packages/manager/src/components/SelectionCard/CardBase.styles.ts +++ b/packages/manager/src/components/SelectionCard/CardBase.styles.ts @@ -1,5 +1,5 @@ -import Grid from '@mui/material/Unstable_Grid2'; import { styled } from '@mui/material/styles'; +import Grid from '@mui/material/Unstable_Grid2'; import type { CardBaseProps } from './CardBase'; @@ -18,15 +18,25 @@ export const CardBaseGrid = styled(Grid, { width: 5, }, '&:hover': { - backgroundColor: props.checked ? theme.bg.lightBlue2 : theme.bg.main, + backgroundColor: props.checked + ? theme.name === 'dark' + ? `rgba(0, 49, 77, .2)` + : `rgba(1, 116, 188, .2)` + : theme.bg.interactionBgPrimary, borderColor: props.checked ? theme.palette.primary.main - : theme.color.border2, + : theme.borderColors.borderHover, }, alignItems: 'center', - backgroundColor: props.checked ? theme.bg.lightBlue2 : theme.bg.offWhite, + backgroundColor: props.checked + ? theme.name === 'dark' + ? `rgba(0, 49, 77, .2)` + : `rgba(1, 116, 188, .2)` + : theme.bg.interactionBgPrimary, border: `1px solid ${theme.bg.main}`, - borderColor: props.checked ? theme.palette.primary.main : undefined, + borderColor: props.checked + ? theme.palette.primary.main + : theme.borderColors.divider, height: '100%', margin: 0, minHeight: 60, diff --git a/packages/manager/src/components/Snackbar/Snackbar.tsx b/packages/manager/src/components/Snackbar/Snackbar.tsx index 6bebab056c2..3a02f8ad24d 100644 --- a/packages/manager/src/components/Snackbar/Snackbar.tsx +++ b/packages/manager/src/components/Snackbar/Snackbar.tsx @@ -1,25 +1,30 @@ import { styled } from '@mui/material/styles'; -import { SnackbarProvider, SnackbarProviderProps } from 'notistack'; import { MaterialDesignContent } from 'notistack'; +import { SnackbarProvider } from 'notistack'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; import { CloseSnackbar } from './CloseSnackbar'; import type { Theme } from '@mui/material/styles'; +import type { SnackbarProviderProps } from 'notistack'; const StyledMaterialDesignContent = styled(MaterialDesignContent)( ({ theme }: { theme: Theme }) => ({ '&.notistack-MuiContent-error': { + backgroundColor: theme.palette.error.light, borderLeft: `6px solid ${theme.palette.error.dark}`, }, '&.notistack-MuiContent-info': { + backgroundColor: theme.palette.info.light, borderLeft: `6px solid ${theme.palette.primary.main}`, }, '&.notistack-MuiContent-success': { - borderLeft: `6px solid ${theme.palette.success.main}`, // corrected to palette.success + backgroundColor: theme.palette.success.light, + borderLeft: `6px solid ${theme.palette.success.dark}`, }, '&.notistack-MuiContent-warning': { + backgroundColor: theme.palette.warning.light, borderLeft: `6px solid ${theme.palette.warning.dark}`, }, }) @@ -28,7 +33,7 @@ const StyledMaterialDesignContent = styled(MaterialDesignContent)( const useStyles = makeStyles()((theme: Theme) => ({ root: { '& div': { - backgroundColor: `${theme.bg.white} !important`, + backgroundColor: `transparent`, color: theme.palette.text.primary, fontSize: '0.875rem', }, diff --git a/packages/manager/src/components/StatusIcon/StatusIcon.tsx b/packages/manager/src/components/StatusIcon/StatusIcon.tsx index fab623fe350..54d84bfee4f 100644 --- a/packages/manager/src/components/StatusIcon/StatusIcon.tsx +++ b/packages/manager/src/components/StatusIcon/StatusIcon.tsx @@ -53,16 +53,16 @@ const StyledDiv = styled(Box, { transition: theme.transitions.create(['color']), width: '16px', ...(props.status === 'active' && { - backgroundColor: theme.color.teal, + backgroundColor: theme.palette.success.dark, }), ...(props.status === 'inactive' && { backgroundColor: theme.color.grey8, }), ...(props.status === 'error' && { - backgroundColor: theme.color.red, + backgroundColor: theme.palette.error.dark, }), ...(!['active', 'error', 'inactive'].includes(props.status) && { - backgroundColor: theme.color.orange, + backgroundColor: theme.palette.warning.dark, }), ...(props.pulse && { animation: 'pulse 1.5s ease-in-out infinite', diff --git a/packages/manager/src/components/Table/Table.styles.ts b/packages/manager/src/components/Table/Table.styles.ts index f6b70032f91..87318bf2aaf 100644 --- a/packages/manager/src/components/Table/Table.styles.ts +++ b/packages/manager/src/components/Table/Table.styles.ts @@ -26,11 +26,9 @@ export const StyledTableWrapper = styled('div', { borderRight: 'none', }, backgroundColor: theme.bg.tableHeader, - borderBottom: `2px solid ${theme.borderColors.borderTable}`, - borderLeft: `1px solid ${theme.borderColors.borderTable}`, + borderBottom: `1px solid ${theme.borderColors.borderTable}`, borderRight: `1px solid ${theme.borderColors.borderTable}`, - borderTop: `2px solid ${theme.borderColors.borderTable}`, - color: theme.textColors.tableHeader, + borderTop: `1px solid ${theme.borderColors.borderTable}`, fontFamily: theme.font.bold, padding: '10px 15px', }, @@ -43,11 +41,4 @@ export const StyledTableWrapper = styled('div', { border: 0, }, }), - ...(props.rowHoverState && { - '& tbody tr': { - '&:hover': { - backgroundColor: theme.bg.lightBlue1, - }, - }, - }), })); diff --git a/packages/manager/src/components/TableRow/TableRow.styles.ts b/packages/manager/src/components/TableRow/TableRow.styles.ts index 78fc55f09a4..745ff361b3c 100644 --- a/packages/manager/src/components/TableRow/TableRow.styles.ts +++ b/packages/manager/src/components/TableRow/TableRow.styles.ts @@ -9,9 +9,6 @@ export const StyledTableRow = styled(_TableRow, { label: 'StyledTableRow', shouldForwardProp: omittedProps(['forceIndex']), })(({ theme, ...props }) => ({ - backgroundColor: theme.bg.bgPaper, - borderLeft: `1px solid ${theme.borderColors.borderTable}`, - borderRight: `1px solid ${theme.borderColors.borderTable}`, [theme.breakpoints.up('md')]: { boxShadow: `inset 3px 0 0 transparent`, }, @@ -38,14 +35,14 @@ export const StyledTableRow = styled(_TableRow, { ...(props.selected && { '& td': { '&:first-of-type': { - borderLeft: `1px solid ${theme.palette.primary.light}`, + borderLeft: `1px solid ${theme.borderColors.borderTable}`, }, - borderBottomColor: theme.palette.primary.light, - borderTop: `1px solid ${theme.palette.primary.light}`, + borderBottomColor: theme.borderColors.borderTable, + borderTop: `1px solid ${theme.borderColors.borderTable}`, position: 'relative', [theme.breakpoints.down('lg')]: { '&:last-child': { - borderRight: `1px solid ${theme.palette.primary.light}`, + borderRight: `1px solid ${theme.borderColors.borderTable}`, }, }, }, diff --git a/packages/manager/src/components/TableSortCell/TableSortCell.tsx b/packages/manager/src/components/TableSortCell/TableSortCell.tsx index 8cd56dd0452..0928cc1787b 100644 --- a/packages/manager/src/components/TableSortCell/TableSortCell.tsx +++ b/packages/manager/src/components/TableSortCell/TableSortCell.tsx @@ -18,7 +18,6 @@ const useStyles = makeStyles()((theme: Theme) => ({ marginRight: 4, }, label: { - color: theme.textColors.tableHeader, fontSize: '.875rem', minHeight: 20, transition: 'none', diff --git a/packages/manager/src/components/Tabs/Tab.test.tsx b/packages/manager/src/components/Tabs/Tab.test.tsx index 38736410cdb..6463053b864 100644 --- a/packages/manager/src/components/Tabs/Tab.test.tsx +++ b/packages/manager/src/components/Tabs/Tab.test.tsx @@ -20,7 +20,7 @@ describe('Tab Component', () => { expect(tabElement).toHaveStyle(` display: inline-flex; - color: rgb(54, 131, 220); + color: rgb(0, 156, 222); `); }); diff --git a/packages/manager/src/components/Tabs/Tab.tsx b/packages/manager/src/components/Tabs/Tab.tsx index c940218ba07..ea65565187c 100644 --- a/packages/manager/src/components/Tabs/Tab.tsx +++ b/packages/manager/src/components/Tabs/Tab.tsx @@ -11,7 +11,7 @@ const useStyles = makeStyles()((theme: Theme) => ({ }, '&:hover': { backgroundColor: theme.color.grey7, - color: theme.palette.primary.main, + color: theme.textColors.linkHover, }, alignItems: 'center', borderBottom: '2px solid transparent', @@ -29,7 +29,7 @@ const useStyles = makeStyles()((theme: Theme) => ({ }, '&[data-reach-tab][data-selected]': { '&:hover': { - color: theme.palette.primary.main, + color: theme.textColors.linkHover, }, borderBottom: `3px solid ${theme.textColors.linkActiveLight}`, color: theme.textColors.headlineStatic, diff --git a/packages/manager/src/components/Tabs/TabList.tsx b/packages/manager/src/components/Tabs/TabList.tsx index 16a12a41296..0ae8e8a8714 100644 --- a/packages/manager/src/components/Tabs/TabList.tsx +++ b/packages/manager/src/components/Tabs/TabList.tsx @@ -23,9 +23,7 @@ export { TabList }; const StyledReachTabList = styled(ReachTabList)(({ theme }) => ({ '&[data-reach-tab-list]': { background: 'none !important', - boxShadow: `inset 0 -1px 0 ${ - theme.name === 'light' ? '#e3e5e8' : '#2e3238' - }`, + boxShadow: `inset 0 -1px 0 ${theme.borderColors.divider}`, marginBottom: theme.spacing(), [theme.breakpoints.down('lg')]: { overflowX: 'auto', diff --git a/packages/manager/src/components/Tabs/__snapshots__/TabList.test.tsx.snap b/packages/manager/src/components/Tabs/__snapshots__/TabList.test.tsx.snap index 947542f4e9b..7c5d66fe7d1 100644 --- a/packages/manager/src/components/Tabs/__snapshots__/TabList.test.tsx.snap +++ b/packages/manager/src/components/Tabs/__snapshots__/TabList.test.tsx.snap @@ -8,7 +8,7 @@ exports[`TabList component > renders TabList correctly 1`] = ` >
diff --git a/packages/manager/src/components/Tag/Tag.styles.ts b/packages/manager/src/components/Tag/Tag.styles.ts index a54f9b67755..74ab54e1dd9 100644 --- a/packages/manager/src/components/Tag/Tag.styles.ts +++ b/packages/manager/src/components/Tag/Tag.styles.ts @@ -16,7 +16,6 @@ export const StyledChip = styled(Chip, { borderTopRightRadius: props.onDelete && 0, }, borderRadius: 4, - color: theme.name === 'light' ? '#3a3f46' : '#fff', fontFamily: theme.font.normal, maxWidth: 350, padding: '7px 10px', @@ -32,18 +31,19 @@ export const StyledChip = styled(Chip, { ['& .StyledDeleteButton']: { color: theme.color.tagIcon, }, - backgroundColor: theme.color.tagButton, + backgroundColor: theme.color.tagButtonBg, }, // Overrides MUI chip default styles so these appear as separate elements. '&:hover': { ['& .StyledDeleteButton']: { color: theme.color.tagIcon, }, - backgroundColor: theme.color.tagButton, + backgroundColor: theme.color.tagButtonBg, }, fontSize: '0.875rem', height: 30, padding: 0, + transition: 'none', ...(props.colorVariant === 'blue' && { '& > span': { '&:hover, &:focus': { @@ -58,15 +58,16 @@ export const StyledChip = styled(Chip, { ...(props.colorVariant === 'lightBlue' && { '& > span': { '&:focus': { - backgroundColor: theme.color.tagButton, - color: theme.color.black, + backgroundColor: theme.color.tagButtonBg, + color: theme.color.white, }, '&:hover': { - backgroundColor: theme.palette.primary.main, - color: 'white', + backgroundColor: theme.color.tagButtonBgHover, + color: theme.color.tagButtonTextHover, }, }, - backgroundColor: theme.color.tagButton, + backgroundColor: theme.color.tagButtonBg, + color: theme.color.tagButtonText, }), })); @@ -85,10 +86,9 @@ export const StyledDeleteButton = styled(StyledLinkButton, { }, '&:hover': { '& svg': { - color: 'white', + color: theme.color.tagIconHover, }, - backgroundColor: theme.palette.primary.main, - color: 'white', + backgroundColor: theme.color.buttonPrimaryHover, }, borderBottomRightRadius: 3, borderLeft: `1px solid ${theme.name === 'light' ? '#fff' : '#2e3238'}`, diff --git a/packages/manager/src/components/TagCell/TagCell.tsx b/packages/manager/src/components/TagCell/TagCell.tsx index 9226281fee9..c1433e9bd9e 100644 --- a/packages/manager/src/components/TagCell/TagCell.tsx +++ b/packages/manager/src/components/TagCell/TagCell.tsx @@ -225,7 +225,7 @@ const StyledIconButton = styled(IconButton)(({ theme }) => ({ backgroundColor: theme.palette.primary.main, color: '#ffff', }, - backgroundColor: theme.color.tagButton, + backgroundColor: theme.color.tagButtonBg, borderRadius: 0, color: theme.color.tagIcon, height: 30, diff --git a/packages/manager/src/components/TextTooltip/TextTooltip.test.tsx b/packages/manager/src/components/TextTooltip/TextTooltip.test.tsx index 849a034e535..301c5787ca5 100644 --- a/packages/manager/src/components/TextTooltip/TextTooltip.test.tsx +++ b/packages/manager/src/components/TextTooltip/TextTooltip.test.tsx @@ -56,7 +56,7 @@ describe('TextTooltip', () => { const displayText = getByText(props.displayText); - expect(displayText).toHaveStyle('color: rgb(54, 131, 220)'); + expect(displayText).toHaveStyle('color: rgb(0, 156, 222)'); expect(displayText).toHaveStyle('font-size: 18px'); }); diff --git a/packages/manager/src/components/TextTooltip/TextTooltip.tsx b/packages/manager/src/components/TextTooltip/TextTooltip.tsx index 5ce4c7f1598..25e43179479 100644 --- a/packages/manager/src/components/TextTooltip/TextTooltip.tsx +++ b/packages/manager/src/components/TextTooltip/TextTooltip.tsx @@ -82,10 +82,13 @@ export const TextTooltip = (props: TextTooltipProps) => { const StyledRootTooltip = styled(Tooltip, { label: 'StyledRootTooltip', })(({ theme }) => ({ + '&:hover': { + color: theme.textColors.linkHover, + }, borderRadius: 4, - color: theme.palette.primary.main, + color: theme.textColors.linkActiveLight, cursor: 'pointer', position: 'relative', - textDecoration: `underline dotted ${theme.palette.primary.main}`, + textDecoration: `underline dotted ${theme.textColors.linkActiveLight}`, textUnderlineOffset: 4, })); diff --git a/packages/manager/src/components/Tile/Tile.styles.ts b/packages/manager/src/components/Tile/Tile.styles.ts index ddeb5994f3f..a1a26d525ea 100644 --- a/packages/manager/src/components/Tile/Tile.styles.ts +++ b/packages/manager/src/components/Tile/Tile.styles.ts @@ -15,8 +15,8 @@ export const useStyles = makeStyles()( }, card: { alignItems: 'center', - backgroundColor: theme.color.white, - border: `1px solid ${theme.color.grey2}`, + backgroundColor: theme.bg.bgPaper, + border: `1px solid ${theme.borderColors.divider}`, display: 'flex', flexDirection: 'column', height: '100%', @@ -51,7 +51,7 @@ export const useStyles = makeStyles()( icon: { '& .insidePath': { fill: 'none', - stroke: '#3683DC', + stroke: theme.palette.primary.main, strokeLinejoin: 'round', strokeWidth: 1.25, }, diff --git a/packages/manager/src/components/TooltipIcon.tsx b/packages/manager/src/components/TooltipIcon.tsx index 0977bf75fb9..fc982c1a1b6 100644 --- a/packages/manager/src/components/TooltipIcon.tsx +++ b/packages/manager/src/components/TooltipIcon.tsx @@ -110,16 +110,16 @@ export const TooltipIcon = (props: TooltipIconProps) => { const sxRootStyle = { '&&': { - fill: '#888f91', - stroke: '#888f91', + fill: theme.color.grey4, + stroke: theme.color.grey4, strokeWidth: 0, }, '&:hover': { - color: '#3683dc', - fill: '#3683dc', - stroke: '#3683dc', + color: theme.palette.primary.main, + fill: theme.palette.primary.main, + stroke: theme.palette.primary.main, }, - color: '#888f91', + color: theme.color.grey4, height: 20, width: 20, }; diff --git a/packages/manager/src/features/Billing/BillingPanels/BillingActivityPanel/BillingActivityPanel.tsx b/packages/manager/src/features/Billing/BillingPanels/BillingActivityPanel/BillingActivityPanel.tsx index c0891c7d83d..9a7235d8729 100644 --- a/packages/manager/src/features/Billing/BillingPanels/BillingActivityPanel/BillingActivityPanel.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/BillingActivityPanel/BillingActivityPanel.tsx @@ -4,7 +4,7 @@ import { Payment, getInvoiceItems, } from '@linode/api-v4/lib/account'; -import { Theme } from '@mui/material/styles'; +import { Theme, styled } from '@mui/material/styles'; import Grid from '@mui/material/Unstable_Grid2'; import { DateTime } from 'luxon'; import * as React from 'react'; @@ -335,9 +335,11 @@ export const BillingActivityPanel = (props: Props) => { }, [selectedTransactionType, combinedData]); return ( - +
-
+ {`${isAkamaiCustomer ? 'Usage' : 'Billing & Payment'} History`} @@ -397,7 +399,7 @@ export const BillingActivityPanel = (props: Props) => { />
-
+ { ); }; +const StyledBillingAndPaymentHistoryHeader = styled('div', { + name: 'BillingAndPaymentHistoryHeader', +})(({ theme }) => ({ + border: theme.name === 'dark' ? `1px solid ${theme.borderColors.divider}` : 0, + borderBottom: 0, +})); + // ============================================================================= // // ============================================================================= diff --git a/packages/manager/src/features/Billing/InvoiceDetail/InvoiceTable.tsx b/packages/manager/src/features/Billing/InvoiceDetail/InvoiceTable.tsx index cfe7c4a3cad..e633213429d 100644 --- a/packages/manager/src/features/Billing/InvoiceDetail/InvoiceTable.tsx +++ b/packages/manager/src/features/Billing/InvoiceDetail/InvoiceTable.tsx @@ -1,8 +1,6 @@ import { InvoiceItem } from '@linode/api-v4/lib/account'; import { APIError } from '@linode/api-v4/lib/types'; -import { Theme } from '@mui/material/styles'; import * as React from 'react'; -import { makeStyles } from 'tss-react/mui'; import { Currency } from 'src/components/Currency'; import { DateTimeDisplay } from 'src/components/DateTimeDisplay'; @@ -21,18 +19,6 @@ import { useRegionsQuery } from 'src/queries/regions/regions'; import { getInvoiceRegion } from '../PdfGenerator/utils'; -const useStyles = makeStyles()((theme: Theme) => ({ - table: { - '& thead th': { - '&:last-of-type': { - paddingRight: 15, - }, - borderBottom: `1px solid ${theme.borderColors.borderTable}`, - }, - border: `1px solid ${theme.borderColors.borderTable}`, - }, -})); - interface Props { errors?: APIError[]; items?: InvoiceItem[]; @@ -41,7 +27,6 @@ interface Props { } export const InvoiceTable = (props: Props) => { - const { classes } = useStyles(); const MIN_PAGE_SIZE = 25; const { @@ -157,7 +142,7 @@ export const InvoiceTable = (props: Props) => { }; return ( - +
Description diff --git a/packages/manager/src/features/Help/Panels/PopularPosts.tsx b/packages/manager/src/features/Help/Panels/PopularPosts.tsx index 96d2d47feac..ec5f14daa7e 100644 --- a/packages/manager/src/features/Help/Panels/PopularPosts.tsx +++ b/packages/manager/src/features/Help/Panels/PopularPosts.tsx @@ -1,5 +1,5 @@ -import Grid from '@mui/material/Unstable_Grid2'; import { Theme } from '@mui/material/styles'; +import Grid from '@mui/material/Unstable_Grid2'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; @@ -18,7 +18,7 @@ const useStyles = makeStyles()((theme: Theme) => ({ margin: `${theme.spacing(6)} 0`, }, withSeparator: { - borderLeft: `1px solid ${theme.palette.divider}`, + borderLeft: `1px solid ${theme.borderColors.divider}`, paddingLeft: theme.spacing(4), [theme.breakpoints.down('sm')]: { borderLeft: 'none', diff --git a/packages/manager/src/features/Help/Panels/SearchPanel.tsx b/packages/manager/src/features/Help/Panels/SearchPanel.tsx index 3a2150403b3..49cb8a97d1c 100644 --- a/packages/manager/src/features/Help/Panels/SearchPanel.tsx +++ b/packages/manager/src/features/Help/Panels/SearchPanel.tsx @@ -22,7 +22,10 @@ const StyledRootContainer = styled(Paper, { label: 'StyledRootContainer', })(({ theme }) => ({ alignItems: 'center', - backgroundColor: theme.color.green, + backgroundColor: + theme.name === 'dark' + ? theme.palette.primary.light + : theme.palette.primary.dark, display: 'flex', flexDirection: 'column', justifyContent: 'center', @@ -36,7 +39,7 @@ const StyledRootContainer = styled(Paper, { const StyledH1Header = styled(H1Header, { label: 'StyledH1Header', })(({ theme }) => ({ - color: theme.name === 'dark' ? theme.color.black : theme.color.white, + color: theme.color.white, marginBottom: theme.spacing(), position: 'relative', textAlign: 'center', diff --git a/packages/manager/src/features/Linodes/LinodeEntityDetail.styles.ts b/packages/manager/src/features/Linodes/LinodeEntityDetail.styles.ts index 47af5c1e3da..f59d4fb895c 100644 --- a/packages/manager/src/features/Linodes/LinodeEntityDetail.styles.ts +++ b/packages/manager/src/features/Linodes/LinodeEntityDetail.styles.ts @@ -122,7 +122,8 @@ export const StyledTable = styled(Table, { label: 'StyledTable' })( whiteSpace: 'nowrap', }, '& th': { - backgroundColor: theme.bg.app, + backgroundColor: + theme.name === 'light' ? theme.color.grey10 : theme.bg.app, borderBottom: `1px solid ${theme.bg.bgPaper}`, color: theme.textColors.textAccessTable, fontFamily: theme.font.bold, @@ -136,6 +137,7 @@ export const StyledTable = styled(Table, { label: 'StyledTable' })( '& tr': { height: 32, }, + border: 'none', tableLayout: 'fixed', }) ); diff --git a/packages/manager/src/features/Linodes/LinodeEntityDetailHeader.tsx b/packages/manager/src/features/Linodes/LinodeEntityDetailHeader.tsx index 398a5284d51..09ceb219071 100644 --- a/packages/manager/src/features/Linodes/LinodeEntityDetailHeader.tsx +++ b/packages/manager/src/features/Linodes/LinodeEntityDetailHeader.tsx @@ -134,10 +134,21 @@ export const LinodeEntityDetailHeader = ( formattedTransitionText !== formattedStatus; const sxActionItem = { + '&:focus': { + color: theme.color.white, + }, '&:hover': { - backgroundColor: theme.color.blue, - color: '#fff', + '&[aria-disabled="true"]': { + color: theme.color.disabledText, + }, + + color: theme.color.white, + }, + '&[aria-disabled="true"]': { + background: 'transparent', + color: theme.color.disabledText, }, + background: 'transparent', color: theme.textColors.linkActiveLight, fontFamily: theme.font.normal, fontSize: '0.875rem', @@ -197,14 +208,14 @@ export const LinodeEntityDetailHeader = ( onClick={() => handlers.onOpenPowerDialog(isRunning ? 'Power Off' : 'Power On') } - buttonType="secondary" + buttonType="primary" disabled={!(isRunning || isOffline) || isLinodesGrantReadOnly} sx={sxActionItem} > {isRunning ? 'Power Off' : 'Power On'}
+ + + {isFetching && } + {searchParseError && ( + + )} + setQuery('')} + size="small" + > + + + + ), + }} + tooltipText={ + type === 'Community' + ? 'Hint: try searching for a specific item by prepending your search term with "username:", "label:", or "description:"' + : undefined + } + hideLabel + label="Search" + onChange={debounce(400, (e) => setQuery(e.target.value))} + placeholder="Search StackScripts" + spellCheck={false} + value={query} + /> +
@@ -153,6 +203,7 @@ export const StackScriptSelectionList = ({ type }: Props) => { stackscript={stackscript} /> ))} + {data?.pages[0].results === 0 && } {error && } {isLoading && } {(isFetchingNextPage || hasNextPage) && ( diff --git a/packages/search/README.md b/packages/search/README.md new file mode 100644 index 00000000000..56980bfa80f --- /dev/null +++ b/packages/search/README.md @@ -0,0 +1,43 @@ +# Search + +Search is a parser written with [Peggy](https://peggyjs.org) that takes a human readable search query and transforms it into a [Linode API v4 filter](https://techdocs.akamai.com/linode-api/reference/filtering-and-sorting). + +The goal of this package is to provide a shared utility that enables a powerful, scalable, and consistent search experience throughout Akamai Connected Cloud Manager. + +## Example + +### Search Query +``` +label: my-volume and size >= 20 +``` +### Resulting `X-Filter` +```json +{ + "+and": [ + { + "label": { + "+contains": "my-volume" + } + }, + { + "size": { + "+gte": 20 + } + } + ] +} +``` + +## Supported Operations + +| Operation | Aliases | Example | Description | +|-----------|----------------|--------------------------------|-----------------------------------------------------------------| +| `and` | `&`, `&&` | `label: prod and size > 20` | Performs a boolean *and* on two expressions | +| `or` | `|`, `||` | `label: prod or size > 20` | Performs a boolean *or* on two expressions | +| `>` | None | `size > 20` | Greater than | +| `<` | None | `size < 20` | Less than | +| `>=` | None | `size >= 20` | Great than or equal to | +| `<=` | None | `size <= 20` | Less than or equal to | +| `!` | `-` | `!label = my-linode-1` | Not equal to (does not work as a *not* for boolean expressions) | +| `=` | None | `label = my-linode-1` | Equal to | +| `:` | `~` | `label: my-linode` | Contains | diff --git a/packages/search/package.json b/packages/search/package.json new file mode 100644 index 00000000000..7445271aa83 --- /dev/null +++ b/packages/search/package.json @@ -0,0 +1,25 @@ +{ + "name": "@linode/search", + "version": "0.0.1", + "description": "Search query parser for Linode API filtering", + "type": "module", + "main": "src/search.ts", + "module": "src/search.ts", + "types": "src/search.ts", + "license": "Apache-2.0", + "scripts": { + "test": "vitest run", + "test:watch": "vitest", + "precommit": "tsc" + }, + "dependencies": { + "peggy": "^4.0.3" + }, + "peerDependencies": { + "@linode/api-v4": "*", + "vite": "*" + }, + "devDependencies": { + "vitest": "^1.6.0" + } +} diff --git a/packages/search/src/search.peggy b/packages/search/src/search.peggy new file mode 100644 index 00000000000..7a28192bd2b --- /dev/null +++ b/packages/search/src/search.peggy @@ -0,0 +1,99 @@ +start + = orQuery + +orQuery + = left:andQuery Or right:orQuery { return { "+or": [left, right] }; } + / andQuery + / DefaultQuery + +andQuery + = left:subQuery And right:andQuery { return { "+and": [left, right] }; } + / subQuery + +subQuery + = '(' ws* query:orQuery ws* ')' { return query; } + / EqualQuery + / ContainsQuery + / NotEqualQuery + / LessThanQuery + / LessThenOrEqualTo + / GreaterThanQuery + / GreaterThanOrEqualTo + +DefaultQuery + = input:String { + const keys = options.searchableFieldsWithoutOperator; + return { "+or": keys.map((key) => ({ [key]: { "+contains": input } })) }; + } + +EqualQuery + = key:FilterableField ws* Equal ws* value:Number { return { [key]: value }; } + / key:FilterableField ws* Equal ws* value:String { return { [key]: value }; } + +ContainsQuery + = key:FilterableField ws* Contains ws* value:String { return { [key]: { "+contains": value } }; } + +TagQuery + = "tag" ws* Equal ws* value:String { return { "tags": { "+contains": value } }; } + +NotEqualQuery + = Not key:FilterableField ws* Equal ws* value:String { return { [key]: { "+neq": value } }; } + +LessThanQuery + = key:FilterableField ws* Less ws* value:Number { return { [key]: { "+lt": value } }; } + +GreaterThanQuery + = key:FilterableField ws* Greater ws* value:Number { return { [key]: { "+gt": value } }; } + +GreaterThanOrEqualTo + = key:FilterableField ws* Gte ws* value:Number { return { [key]: { "+gte": value } }; } + +LessThenOrEqualTo + = key:FilterableField ws* Lte ws* value:Number { return { [key]: { "+lte": value } }; } + +Or + = ws+ 'or'i ws+ + / ws* '||' ws* + / ws* '|' ws* + +And + = ws+ 'and'i ws+ + / ws* '&&' ws* + / ws* '&' ws* + / ws + +Not + = '!' + / '-' + +Less + = '<' + +Greater + = '>' + +Gte + = '>=' + +Lte + = '<=' + +Equal + = "=" + +Contains + = "~" + / ":" + +FilterableField "filterable field" + = [a-zA-Z0-9\-\.]+ { return text(); } + +String "search value" + = [a-zA-Z0-9\-\.]+ { return text(); } + +Number "numeric search value" + = number:[0-9\.]+ { return parseFloat(number.join("")); } + / number:[0-9]+ { return parseInt(number.join(""), 10); } + +ws "whitespace" + = [ \t\r\n] \ No newline at end of file diff --git a/packages/search/src/search.test.ts b/packages/search/src/search.test.ts new file mode 100644 index 00000000000..0726cba626a --- /dev/null +++ b/packages/search/src/search.test.ts @@ -0,0 +1,145 @@ +import { describe, expect, it } from 'vitest'; +import { getAPIFilterFromQuery } from './search'; + +describe("getAPIFilterFromQuery", () => { + it("handles +contains", () => { + const query = "label: my-linode"; + + expect(getAPIFilterFromQuery(query, { searchableFieldsWithoutOperator: [] })).toEqual({ + filter: { + label: { "+contains": "my-linode" }, + }, + error: null, + }); + }); + + it("handles +eq with strings", () => { + const query = "label = my-linode"; + + expect(getAPIFilterFromQuery(query, { searchableFieldsWithoutOperator: [] })).toEqual({ + filter: { + label: "my-linode", + }, + error: null, + }); + }); + + it("handles +eq with numbers", () => { + const query = "id = 100"; + + expect(getAPIFilterFromQuery(query, { searchableFieldsWithoutOperator: [] })).toEqual({ + filter: { + id: 100, + }, + error: null, + }); + }); + + it("handles +lt", () => { + const query = "size < 20"; + + expect(getAPIFilterFromQuery(query, { searchableFieldsWithoutOperator: [] })).toEqual({ + filter: { + size: { '+lt': 20 } + }, + error: null, + }); + }); + + it("handles +gt", () => { + const query = "size > 20"; + + expect(getAPIFilterFromQuery(query, { searchableFieldsWithoutOperator: [] })).toEqual({ + filter: { + size: { '+gt': 20 } + }, + error: null, + }); + }); + + it("handles +gte", () => { + const query = "size >= 20"; + + expect(getAPIFilterFromQuery(query, { searchableFieldsWithoutOperator: [] })).toEqual({ + filter: { + size: { '+gte': 20 } + }, + error: null, + }); + }); + + it("handles +lte", () => { + const query = "size <= 20"; + + expect(getAPIFilterFromQuery(query, { searchableFieldsWithoutOperator: [] })).toEqual({ + filter: { + size: { '+lte': 20 } + }, + error: null, + }); + }); + + it("handles an 'and' search", () => { + const query = "label: my-linode-1 and tags: production"; + + expect(getAPIFilterFromQuery(query, { searchableFieldsWithoutOperator: [] })).toEqual({ + filter: { + ["+and"]: [ + { label: { "+contains": "my-linode-1" } }, + { tags: { '+contains': "production" } }, + ], + }, + error: null, + }); + }); + + it("handles an 'or' search", () => { + const query = "label: prod or size >= 20"; + + expect(getAPIFilterFromQuery(query, { searchableFieldsWithoutOperator: [] })).toEqual({ + filter: { + ["+or"]: [ + { label: { "+contains": "prod" } }, + { size: { '+gte': 20 } }, + ], + }, + error: null, + }); + }); + + it("handles nested queries", () => { + const query = "(label: prod and size >= 20) or (label: staging and size < 50)"; + + expect(getAPIFilterFromQuery(query, { searchableFieldsWithoutOperator: [] })).toEqual({ + filter: { + ["+or"]: [ + { ["+and"]: [{ label: { '+contains': 'prod' } }, { size: { '+gte': 20 } }] }, + { ["+and"]: [{ label: { '+contains': 'staging' } }, { size: { '+lt': 50 } }] }, + ], + }, + error: null, + }); + }); + + it("returns a default query based on the 'defaultSearchKeys' provided", () => { + const query = "my-linode-1"; + + expect(getAPIFilterFromQuery(query, { searchableFieldsWithoutOperator: ['label', 'tags'] })).toEqual({ + filter: { + ["+or"]: [ + { label: { "+contains": "my-linode-1" } }, + { tags: { '+contains': "my-linode-1" } }, + ], + }, + error: null, + }); + }); + + it("returns an error for an incomplete search query", () => { + const query = "label: "; + + expect( + getAPIFilterFromQuery(query, { searchableFieldsWithoutOperator: [] }).error?.message + ).toEqual("Expected search value or whitespace but end of input found."); + }); +}); \ No newline at end of file diff --git a/packages/search/src/search.ts b/packages/search/src/search.ts new file mode 100644 index 00000000000..3cfb368c29a --- /dev/null +++ b/packages/search/src/search.ts @@ -0,0 +1,35 @@ +import { generate } from 'peggy'; +import type { Filter } from '@linode/api-v4'; +import grammar from './search.peggy?raw'; + +const parser = generate(grammar); + +interface Options { + /** + * Defines the API fields filtered against (currently using +contains) + * when the search query contains no operators. + * + * @example ['label', 'tags'] + */ + searchableFieldsWithoutOperator: string[]; +} + +/** + * Takes a search query and returns a valid X-Filter for Linode API v4 + */ +export function getAPIFilterFromQuery(query: string | null | undefined, options: Options) { + if (!query) { + return { filter: {}, error: null }; + } + + let filter: Filter = {}; + let error: SyntaxError | null = null; + + try { + filter = parser.parse(query, options); + } catch (e) { + error = e as SyntaxError; + } + + return { filter, error }; +} \ No newline at end of file diff --git a/packages/search/src/vite-env.d.ts b/packages/search/src/vite-env.d.ts new file mode 100644 index 00000000000..11f02fe2a00 --- /dev/null +++ b/packages/search/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/search/tsconfig.json b/packages/search/tsconfig.json new file mode 100644 index 00000000000..134d0055fe4 --- /dev/null +++ b/packages/search/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "skipLibCheck": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "forceConsistentCasingInFileNames": true, + "incremental": true + }, + "include": ["src"], +} diff --git a/yarn.lock b/yarn.lock index ecda695659e..c4094ed6d0a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2502,6 +2502,13 @@ dependencies: hi-base32 "^0.5.0" +"@peggyjs/from-mem@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@peggyjs/from-mem/-/from-mem-1.3.0.tgz#16470cf7dfa22fc75ca217a4e064a5f0c4e1111b" + integrity sha512-kzGoIRJjkg3KuGI4bopz9UvF3KguzfxalHRDEIdqEZUe45xezsQ6cx30e0RKuxPUexojQRBfu89Okn7f4/QXsw== + dependencies: + semver "7.6.0" + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" @@ -6004,6 +6011,11 @@ commander@11.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-11.0.0.tgz#43e19c25dbedc8256203538e8d7e9346877a6f67" integrity sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ== +commander@^12.1.0: + version "12.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" + integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== + commander@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" @@ -11144,6 +11156,15 @@ peek-stream@^1.1.0: duplexify "^3.5.0" through2 "^2.0.3" +peggy@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/peggy/-/peggy-4.0.3.tgz#7bcd47718483ab405c960350c5250e3e487dec74" + integrity sha512-v7/Pt6kGYsfXsCrfb52q7/yg5jaAwiVaUMAPLPvy4DJJU6Wwr72t6nDIqIDkGfzd1B4zeVuTnQT0RGeOhe/uSA== + dependencies: + "@peggyjs/from-mem" "1.3.0" + commander "^12.1.0" + source-map-generator "0.8.0" + pend@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" @@ -12451,7 +12472,7 @@ semver-compare@^1.0.0: resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow== -"semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.6.0, semver@^6.0.0, semver@^6.1.0, semver@^6.1.2, semver@^6.3.1, semver@^7.2.1, semver@^7.3.2, semver@^7.3.7, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4: +"semver@2 || 3 || 4 || 5", semver@7.6.0, semver@^5.5.0, semver@^5.6.0, semver@^6.0.0, semver@^6.1.0, semver@^6.1.2, semver@^6.3.1, semver@^7.2.1, semver@^7.3.2, semver@^7.3.7, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4: version "7.6.2" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== @@ -12723,6 +12744,11 @@ sonic-forest@^1.0.0: dependencies: tree-dump "^1.0.0" +source-map-generator@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/source-map-generator/-/source-map-generator-0.8.0.tgz#10d5ca0651e2c9302ea338739cbd4408849c5d00" + integrity sha512-psgxdGMwl5MZM9S3FWee4EgsEaIjahYV5AzGnwUvPhWeITz/j6rKpysQHlQ4USdxvINlb8lKfWGIXwfkrgtqkA== + source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" From d436114544480e58c88d4fefe4e820b3f7294bba Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Mon, 1 Jul 2024 11:45:43 -0400 Subject: [PATCH 20/37] upcoming: [M3-8282] - Prevent Linode Create v2 from toggling mid-creation (#10611) * hold flag value in state so it does not change * Added changeset: Prevent Linode Create v2 from toggling mid-creation --------- Co-authored-by: Banks Nussman --- .../pr-10611-upcoming-features-1719332278758.md | 5 +++++ packages/manager/src/features/Linodes/index.tsx | 11 +++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 packages/manager/.changeset/pr-10611-upcoming-features-1719332278758.md diff --git a/packages/manager/.changeset/pr-10611-upcoming-features-1719332278758.md b/packages/manager/.changeset/pr-10611-upcoming-features-1719332278758.md new file mode 100644 index 00000000000..c3bc81d8170 --- /dev/null +++ b/packages/manager/.changeset/pr-10611-upcoming-features-1719332278758.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Prevent Linode Create v2 from toggling mid-creation ([#10611](https://github.com/linode/manager/pull/10611)) diff --git a/packages/manager/src/features/Linodes/index.tsx b/packages/manager/src/features/Linodes/index.tsx index 9d8c7a7312a..db48cf3d1e3 100644 --- a/packages/manager/src/features/Linodes/index.tsx +++ b/packages/manager/src/features/Linodes/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { useState } from 'react'; import { Redirect, Route, Switch } from 'react-router-dom'; import { SuspenseLoader } from 'src/components/SuspenseLoader'; @@ -25,13 +25,16 @@ const LinodesCreatev2 = React.lazy(() => const LinodesRoutes = () => { const flags = useFlags(); + + // Hold this feature flag in state so that the user's Linode creation + // isn't interupted when the flag is toggled. + const [isLinodeCreateV2Enabled] = useState(flags.linodeCreateRefactor); + return ( }> From 1211de45d4f1a0268accac6221e3aa3d982bec82 Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:47:33 -0400 Subject: [PATCH 21/37] upcoming: [M3-8310] - Add Validation to Linode Create v2 Marketplace Tab (#10629) * add validation for marketplace tab * Added changeset: Add Validation to Linode Create v2 Marketplace Tab --------- Co-authored-by: Banks Nussman --- ...r-10629-upcoming-features-1719848495146.md | 5 ++++ .../Tabs/Marketplace/AppSelect.tsx | 10 ++++++++ .../Linodes/LinodeCreatev2/resolvers.ts | 25 +++++++++++++++++-- .../Linodes/LinodeCreatev2/schemas.ts | 9 +++++++ 4 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 packages/manager/.changeset/pr-10629-upcoming-features-1719848495146.md diff --git a/packages/manager/.changeset/pr-10629-upcoming-features-1719848495146.md b/packages/manager/.changeset/pr-10629-upcoming-features-1719848495146.md new file mode 100644 index 00000000000..16ba99d5879 --- /dev/null +++ b/packages/manager/.changeset/pr-10629-upcoming-features-1719848495146.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add Validation to Linode Create v2 Marketplace Tab ([#10629](https://github.com/linode/manager/pull/10629)) diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Marketplace/AppSelect.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Marketplace/AppSelect.tsx index a804f3e21ff..43082645282 100644 --- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Marketplace/AppSelect.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Marketplace/AppSelect.tsx @@ -1,8 +1,10 @@ import React, { useState } from 'react'; +import { useFormContext } from 'react-hook-form'; import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; import { Box } from 'src/components/Box'; import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField'; +import { Notice } from 'src/components/Notice/Notice'; import { Paper } from 'src/components/Paper'; import { Stack } from 'src/components/Stack'; import { Typography } from 'src/components/Typography'; @@ -11,6 +13,7 @@ import { useMarketplaceAppsQuery } from 'src/queries/stackscripts'; import { AppsList } from './AppsList'; import { categoryOptions } from './utilities'; +import type { LinodeCreateFormValues } from '../../utilities'; import type { AppCategory } from 'src/features/OneClickApps/types'; interface Props { @@ -23,6 +26,10 @@ interface Props { export const AppSelect = (props: Props) => { const { onOpenDetailsDrawer } = props; + const { + formState: { errors }, + } = useFormContext(); + const { isLoading } = useMarketplaceAppsQuery(true); const [query, setQuery] = useState(''); @@ -32,6 +39,9 @@ export const AppSelect = (props: Props) => { Select an App + {errors.stackscript_id?.message && ( + + )} = async ( @@ -52,6 +53,26 @@ export const stackscriptResolver: Resolver = async ( return { errors: {}, values }; }; +export const marketplaceResolver: Resolver = async ( + values, + context, + options +) => { + const transformedValues = getLinodeCreatePayload(structuredClone(values)); + + const { errors } = await yupResolver( + CreateLinodeFromMarketplaceAppSchema, + {}, + { mode: 'async', rawValues: true } + )(transformedValues, context, options); + + if (errors) { + return { errors, values }; + } + + return { errors: {}, values }; +}; + export const cloneResolver: Resolver = async ( values, context, @@ -107,6 +128,6 @@ export const linodeCreateResolvers: Record< 'Clone Linode': cloneResolver, Distributions: resolver, Images: resolver, - 'One-Click': stackscriptResolver, + 'One-Click': marketplaceResolver, StackScripts: stackscriptResolver, }; diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/schemas.ts b/packages/manager/src/features/Linodes/LinodeCreatev2/schemas.ts index d7e826c1a52..b97945a9868 100644 --- a/packages/manager/src/features/Linodes/LinodeCreatev2/schemas.ts +++ b/packages/manager/src/features/Linodes/LinodeCreatev2/schemas.ts @@ -27,3 +27,12 @@ export const CreateLinodeFromStackScriptSchema = CreateLinodeSchema.concat( stackscript_id: number().required('You must select a StackScript.'), }) ); + +/** + * Extends the Linode Create schema to make stackscript_id required for the Marketplace tab + */ +export const CreateLinodeFromMarketplaceAppSchema = CreateLinodeSchema.concat( + object({ + stackscript_id: number().required('You must select a Marketplace App.'), + }) +); From e612d3a25c11eaa7507ab8b90138330d91e4a3c4 Mon Sep 17 00:00:00 2001 From: Hana Xu <115299789+hana-linode@users.noreply.github.com> Date: Tue, 2 Jul 2024 10:41:50 -0400 Subject: [PATCH 22/37] fix distirbuted regions not displaying in linode create flow (#10631) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description 📝 Looks like there was a change made to the `isDistributedRegionSupported` function which caused a regression in the distributed regions displaying in the Linode Create flow ## How to test 🧪 ### Prerequisites (How to setup test environment) - Ensure your account has the `new-dc-testing`, `edge_testing` and `edge_compute` customer tags ### Reproduction steps (How to reproduce the issue, if applicable) - Go to the remote dev environment and observe no Distributed regions in the Linode Create flow ### Verification steps (How to verify changes) - Either locally or in the preview link, go to the Linode Create flow. Distributed regions should be displaying again --- .../manager/src/components/RegionSelect/RegionSelect.utils.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/manager/src/components/RegionSelect/RegionSelect.utils.tsx b/packages/manager/src/components/RegionSelect/RegionSelect.utils.tsx index 324accde315..7d7c21d8e9b 100644 --- a/packages/manager/src/components/RegionSelect/RegionSelect.utils.tsx +++ b/packages/manager/src/components/RegionSelect/RegionSelect.utils.tsx @@ -134,6 +134,7 @@ export const isDistributedRegionSupported = (createType: LinodeCreateType) => { 'Distributions', 'StackScripts', 'Images', + undefined, // /linodes/create route ]; return supportedDistributedRegionTypes.includes(createType); }; From 98ccce71ebaad8c1800ccb8131f79e873507496c Mon Sep 17 00:00:00 2001 From: Azure-akamai Date: Tue, 2 Jul 2024 12:37:09 -0400 Subject: [PATCH 23/37] test: [M3-6620] - Add assertions for created LKE cluster in Cypress LKE tests (#10593) * Add assertions for lke tests * update comments * Added changeset: Add assertions for created LKE cluster in Cypress LKE tests * update after reviews --- .../pr-10593-tests-1718736692043.md | 5 +++ .../e2e/core/kubernetes/lke-create.spec.ts | 37 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 packages/manager/.changeset/pr-10593-tests-1718736692043.md diff --git a/packages/manager/.changeset/pr-10593-tests-1718736692043.md b/packages/manager/.changeset/pr-10593-tests-1718736692043.md new file mode 100644 index 00000000000..7fb89a5ac1d --- /dev/null +++ b/packages/manager/.changeset/pr-10593-tests-1718736692043.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Add assertions for created LKE cluster in Cypress LKE tests ([#10593](https://github.com/linode/manager/pull/10593)) 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 fd495ecc0c0..bff073133a9 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts @@ -78,6 +78,7 @@ describe('LKE Cluster Creation', () => { * - Confirms that user is redirected to new LKE cluster summary page. * - Confirms that new LKE cluster summary page shows expected node pools. * - Confirms that new LKE cluster is shown on LKE clusters landing page. + * - Confirms that correct information is shown on the LKE cluster summary page */ it('can create an LKE cluster', () => { const clusterLabel = randomLabel(); @@ -114,6 +115,11 @@ describe('LKE Cluster Creation', () => { cy.get('[data-testid="ha-radio-button-yes"]').should('be.visible').click(); + let totalCpu = 0; + let totalMemory = 0; + let totalStorage = 0; + let monthPrice = 0; + // Add a node pool for each randomly selected plan, and confirm that the // selected node pool plan is added to the checkout bar. clusterPlans.forEach((clusterPlan) => { @@ -150,7 +156,29 @@ describe('LKE Cluster Creation', () => { // instance of the pool appears in the checkout bar. cy.findAllByText(checkoutName).first().should('be.visible'); }); + + // Expected information on the LKE cluster summary page. + if (clusterPlan.size == 2 && clusterPlan.type == 'Linode') { + totalCpu = totalCpu + nodeCount * 1; + totalMemory = totalMemory + nodeCount * 2; + totalStorage = totalStorage + nodeCount * 50; + monthPrice = monthPrice + nodeCount * 12; + } + if (clusterPlan.size == 4 && clusterPlan.type == 'Linode') { + totalCpu = totalCpu + nodeCount * 2; + totalMemory = totalMemory + nodeCount * 4; + totalStorage = totalStorage + nodeCount * 80; + monthPrice = monthPrice + nodeCount * 24; + } + if (clusterPlan.size == 4 && clusterPlan.type == 'Dedicated') { + totalCpu = totalCpu + nodeCount * 2; + totalMemory = totalMemory + nodeCount * 4; + totalStorage = totalStorage + nodeCount * 80; + monthPrice = monthPrice + nodeCount * 36; + } }); + // $60.00/month for enabling HA control plane + const totalPrice = monthPrice + 60; // Create LKE cluster. cy.get('[data-testid="kube-checkout-bar"]') @@ -184,6 +212,15 @@ describe('LKE Cluster Creation', () => { const similarNodePoolCount = getSimilarPlans(clusterPlan, clusterPlans) .length; + //Confirm that the cluster created with the expected parameters. + cy.findAllByText(`${clusterRegion.label}`).should('be.visible'); + cy.findAllByText(`${totalCpu} CPU Cores`).should('be.visible'); + cy.findAllByText(`${totalMemory} GB RAM`).should('be.visible'); + cy.findAllByText(`${totalStorage} GB Storage`).should('be.visible'); + cy.findAllByText(`$${totalPrice}.00/month`).should('be.visible'); + cy.contains('Kubernetes API Endpoint').should('be.visible'); + cy.contains('linodelke.net:443').should('be.visible'); + cy.findAllByText(nodePoolLabel, { selector: 'h2' }) .should('have.length', similarNodePoolCount) .first() From 18cd9f5089fb7d3113e63a208cf3025fc8c9050c Mon Sep 17 00:00:00 2001 From: Azure-akamai Date: Tue, 2 Jul 2024 12:38:47 -0400 Subject: [PATCH 24/37] test: [M3-7318] - Combine vpc details page subset create, edit, and delete tests (#10612) * combine vpc details page subset create, edit, and delete tests * Added changeset: Combine VPC details page subnet create, edit, and delete Cypress tests --- .../pr-10612-tests-1719343415784.md | 5 + .../e2e/core/vpc/vpc-details-page.spec.ts | 155 ++++++------------ 2 files changed, 54 insertions(+), 106 deletions(-) create mode 100644 packages/manager/.changeset/pr-10612-tests-1719343415784.md diff --git a/packages/manager/.changeset/pr-10612-tests-1719343415784.md b/packages/manager/.changeset/pr-10612-tests-1719343415784.md new file mode 100644 index 00000000000..8c3b73cf285 --- /dev/null +++ b/packages/manager/.changeset/pr-10612-tests-1719343415784.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Combine VPC details page subnet create, edit, and delete Cypress tests ([#10612](https://github.com/linode/manager/pull/10612)) diff --git a/packages/manager/cypress/e2e/core/vpc/vpc-details-page.spec.ts b/packages/manager/cypress/e2e/core/vpc/vpc-details-page.spec.ts index cc8341ef251..e7672f90c3c 100644 --- a/packages/manager/cypress/e2e/core/vpc/vpc-details-page.spec.ts +++ b/packages/manager/cypress/e2e/core/vpc/vpc-details-page.spec.ts @@ -120,87 +120,18 @@ describe('VPC details page', () => { cy.findByText('Create a private and isolated network'); }); - /** - * - Confirms Subnets section and table is shown on the VPC details page - * - Confirms UI flow when deleting a subnet from a VPC's detail page - */ - it('can delete a subnet from the VPC details page', () => { - const mockSubnet = subnetFactory.build({ - id: randomNumber(), - label: randomLabel(), - linodes: [], - }); - const mockVPC = vpcFactory.build({ - id: randomNumber(), - label: randomLabel(), - subnets: [mockSubnet], - }); - - const mockVPCAfterSubnetDeletion = vpcFactory.build({ - ...mockVPC, - subnets: [], - }); - - mockAppendFeatureFlags({ - vpc: makeFeatureFlagData(true), - }).as('getFeatureFlags'); - - mockGetVPC(mockVPC).as('getVPC'); - mockGetFeatureFlagClientstream().as('getClientStream'); - mockGetSubnets(mockVPC.id, [mockSubnet]).as('getSubnets'); - mockDeleteSubnet(mockVPC.id, mockSubnet.id).as('deleteSubnet'); - - cy.visitWithLogin(`/vpcs/${mockVPC.id}`); - cy.wait(['@getFeatureFlags', '@getClientStream', '@getVPC', '@getSubnets']); - - // confirm that vpc and subnet details get displayed - cy.findByText(mockVPC.label).should('be.visible'); - cy.findByText('Subnets (1)').should('be.visible'); - cy.findByText(mockSubnet.label).should('be.visible'); - - // confirm that subnet can be deleted and that page reflects changes - ui.actionMenu - .findByTitle(`Action menu for Subnet ${mockSubnet.label}`) - .should('be.visible') - .click(); - ui.actionMenuItem.findByTitle('Delete').should('be.visible').click(); - - mockGetVPC(mockVPCAfterSubnetDeletion).as('getVPC'); - mockGetSubnets(mockVPC.id, []).as('getSubnets'); - - ui.dialog - .findByTitle(`Delete Subnet ${mockSubnet.label}`) - .should('be.visible') - .within(() => { - cy.findByLabelText('Subnet Label') - .should('be.visible') - .click() - .type(mockSubnet.label); - - ui.button - .findByTitle('Delete') - .should('be.visible') - .should('be.enabled') - .click(); - }); - - cy.wait(['@deleteSubnet', '@getVPC', '@getSubnets']); - - // confirm that user should still be on VPC's detail page - // confirm there are no remaining subnets - cy.url().should('endWith', `/${mockVPC.id}`); - cy.findByText('Subnets (0)'); - cy.findByText('No Subnets are assigned.'); - cy.findByText(mockSubnet.label).should('not.exist'); - }); - /** * - Confirms UI flow when creating a subnet on a VPC's detail page. + * - Confirms UI flow for editing a subnet. + * - Confirms Subnets section and table is shown on the VPC details page. + * - Confirms UI flow when deleting a subnet from a VPC's detail page. */ - it('can create a subnet', () => { + it('can create, edit, and delete a subnet from the VPC details page', () => { + // create a subnet const mockSubnet = subnetFactory.build({ id: randomNumber(), label: randomLabel(), + linodes: [], }); const mockVPC = vpcFactory.build({ @@ -256,22 +187,8 @@ describe('VPC details page', () => { cy.findByText(mockVPC.label).should('be.visible'); cy.findByText('Subnets (1)').should('be.visible'); cy.findByText(mockSubnet.label).should('be.visible'); - }); - - /** - * - Confirms UI flow for editing a subnet - */ - it('can edit a subnet', () => { - const mockSubnet = subnetFactory.build({ - id: randomNumber(), - label: randomLabel(), - }); - const mockVPC = vpcFactory.build({ - id: randomNumber(), - label: randomLabel(), - subnets: [mockSubnet], - }); + // edit a subnet const mockEditedSubnet = subnetFactory.build({ ...mockSubnet, label: randomLabel(), @@ -282,22 +199,6 @@ describe('VPC details page', () => { subnets: [mockEditedSubnet], }); - mockAppendFeatureFlags({ - vpc: makeFeatureFlagData(true), - }).as('getFeatureFlags'); - - mockGetVPC(mockVPC).as('getVPC'); - mockGetFeatureFlagClientstream().as('getClientStream'); - mockGetSubnets(mockVPC.id, [mockSubnet]).as('getSubnets'); - - cy.visitWithLogin(`/vpcs/${mockVPC.id}`); - cy.wait(['@getFeatureFlags', '@getClientStream', '@getVPC', '@getSubnets']); - - // confirm that vpc and subnet details get displayed - cy.findByText(mockVPC.label).should('be.visible'); - cy.findByText('Subnets (1)').should('be.visible'); - cy.findByText(mockSubnet.label).should('be.visible'); - // confirm that subnet can be edited and that page reflects changes mockEditSubnet(mockVPC.id, mockEditedSubnet.id, mockEditedSubnet).as( 'editSubnet' @@ -336,5 +237,47 @@ describe('VPC details page', () => { cy.findByText(mockVPC.label).should('be.visible'); cy.findByText('Subnets (1)').should('be.visible'); cy.findByText(mockEditedSubnet.label).should('be.visible'); + + // delete a subnet + const mockVPCAfterSubnetDeletion = vpcFactory.build({ + ...mockVPC, + subnets: [], + }); + mockDeleteSubnet(mockVPC.id, mockEditedSubnet.id).as('deleteSubnet'); + + // confirm that subnet can be deleted and that page reflects changes + ui.actionMenu + .findByTitle(`Action menu for Subnet ${mockEditedSubnet.label}`) + .should('be.visible') + .click(); + ui.actionMenuItem.findByTitle('Delete').should('be.visible').click(); + + mockGetVPC(mockVPCAfterSubnetDeletion).as('getVPC'); + mockGetSubnets(mockVPC.id, []).as('getSubnets'); + + ui.dialog + .findByTitle(`Delete Subnet ${mockEditedSubnet.label}`) + .should('be.visible') + .within(() => { + cy.findByLabelText('Subnet Label') + .should('be.visible') + .click() + .type(mockEditedSubnet.label); + + ui.button + .findByTitle('Delete') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + cy.wait(['@deleteSubnet', '@getVPC', '@getSubnets']); + + // confirm that user should still be on VPC's detail page + // confirm there are no remaining subnets + cy.url().should('endWith', `/${mockVPC.id}`); + cy.findByText('Subnets (0)'); + cy.findByText('No Subnets are assigned.'); + cy.findByText(mockEditedSubnet.label).should('not.exist'); }); }); From 3a207aec6a9998aa1a303a1121b139bffde204dc Mon Sep 17 00:00:00 2001 From: Jaalah Ramos <125309814+jaalah-akamai@users.noreply.github.com> Date: Tue, 2 Jul 2024 13:01:01 -0400 Subject: [PATCH 25/37] refactor: [M3-8299] - Fix Notification Toast in Dark Mode (#10637) Co-authored-by: Jaalah Ramos --- ...r-10637-upcoming-features-1719936723714.md | 5 ++++ .../src/components/Snackbar/Snackbar.tsx | 25 ++++++++++-------- .../manager/src/foundations/themes/dark.ts | 26 +++++++++++++++++++ .../manager/src/foundations/themes/index.ts | 25 +++++++++++++----- .../manager/src/foundations/themes/light.ts | 26 +++++++++++++++++++ 5 files changed, 90 insertions(+), 17 deletions(-) create mode 100644 packages/manager/.changeset/pr-10637-upcoming-features-1719936723714.md diff --git a/packages/manager/.changeset/pr-10637-upcoming-features-1719936723714.md b/packages/manager/.changeset/pr-10637-upcoming-features-1719936723714.md new file mode 100644 index 00000000000..70820ca8f4c --- /dev/null +++ b/packages/manager/.changeset/pr-10637-upcoming-features-1719936723714.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Fix Notification Toast in Dark Mode ([#10637](https://github.com/linode/manager/pull/10637)) diff --git a/packages/manager/src/components/Snackbar/Snackbar.tsx b/packages/manager/src/components/Snackbar/Snackbar.tsx index 3a02f8ad24d..e8ee778d665 100644 --- a/packages/manager/src/components/Snackbar/Snackbar.tsx +++ b/packages/manager/src/components/Snackbar/Snackbar.tsx @@ -12,31 +12,34 @@ import type { SnackbarProviderProps } from 'notistack'; const StyledMaterialDesignContent = styled(MaterialDesignContent)( ({ theme }: { theme: Theme }) => ({ '&.notistack-MuiContent-error': { - backgroundColor: theme.palette.error.light, - borderLeft: `6px solid ${theme.palette.error.dark}`, + backgroundColor: theme.notificationToast.error.backgroundColor, + borderLeft: theme.notificationToast.error.borderLeft, }, '&.notistack-MuiContent-info': { - backgroundColor: theme.palette.info.light, - borderLeft: `6px solid ${theme.palette.primary.main}`, + backgroundColor: theme.notificationToast.info.backgroundColor, + borderLeft: theme.notificationToast.info.borderLeft, }, '&.notistack-MuiContent-success': { - backgroundColor: theme.palette.success.light, - borderLeft: `6px solid ${theme.palette.success.dark}`, + backgroundColor: theme.notificationToast.success.backgroundColor, + borderLeft: theme.notificationToast.success.borderLeft, }, '&.notistack-MuiContent-warning': { - backgroundColor: theme.palette.warning.light, - borderLeft: `6px solid ${theme.palette.warning.dark}`, + backgroundColor: theme.notificationToast.warning.backgroundColor, + borderLeft: theme.notificationToast.warning.borderLeft, }, }) ); const useStyles = makeStyles()((theme: Theme) => ({ root: { - '& div': { - backgroundColor: `transparent`, - color: theme.palette.text.primary, + '& .notistack-MuiContent': { + color: theme.notificationToast.default.color, fontSize: '0.875rem', }, + '& .notistack-MuiContent-default': { + backgroundColor: theme.notificationToast.default.backgroundColor, + borderLeft: theme.notificationToast.default.borderLeft, + }, [theme.breakpoints.down('md')]: { '& .SnackbarItem-contentRoot': { flexWrap: 'nowrap', diff --git a/packages/manager/src/foundations/themes/dark.ts b/packages/manager/src/foundations/themes/dark.ts index 4ba65b5e97f..262d166cd5a 100644 --- a/packages/manager/src/foundations/themes/dark.ts +++ b/packages/manager/src/foundations/themes/dark.ts @@ -5,6 +5,7 @@ import { Color, Dropdown, Interaction, + NotificationToast, Select, TextField, } from '@linode/design-language-system/themes/dark'; @@ -96,6 +97,30 @@ export const customDarkModeOptions = { }, } as const; +export const notificationToast = { + default: { + backgroundColor: NotificationToast.Informative.Background, + borderLeft: `6px solid ${NotificationToast.Informative.Border}`, + color: NotificationToast.Text, + }, + error: { + backgroundColor: NotificationToast.Error.Background, + borderLeft: `6px solid ${NotificationToast.Error.Border}`, + }, + info: { + backgroundColor: NotificationToast.Informative.Background, + borderLeft: `6px solid ${NotificationToast.Informative.Border}`, + }, + success: { + backgroundColor: NotificationToast.Success.Background, + borderLeft: `6px solid ${NotificationToast.Success.Border}`, + }, + warning: { + backgroundColor: NotificationToast.Warning.Background, + borderLeft: `6px solid ${NotificationToast.Warning.Border}`, + }, +} as const; + const iconCircleAnimation = { '& .circle': { fill: primaryColors.main, @@ -826,6 +851,7 @@ export const darkTheme: ThemeOptions = { }, }, name: 'dark', + notificationToast, palette: { background: { default: customDarkModeOptions.bg.app, diff --git a/packages/manager/src/foundations/themes/index.ts b/packages/manager/src/foundations/themes/index.ts index 74c596ed386..113dd754683 100644 --- a/packages/manager/src/foundations/themes/index.ts +++ b/packages/manager/src/foundations/themes/index.ts @@ -1,18 +1,23 @@ import { createTheme } from '@mui/material/styles'; -import { latoWeb } from 'src/foundations/fonts'; // Themes & Brands import { darkTheme } from 'src/foundations/themes/dark'; -// Types & Interfaces -import { customDarkModeOptions } from 'src/foundations/themes/dark'; import { lightTheme } from 'src/foundations/themes/light'; -import { +import { deepMerge } from 'src/utilities/deepMerge'; + +import type { latoWeb } from 'src/foundations/fonts'; +// Types & Interfaces +import type { + customDarkModeOptions, + notificationToast as notificationToastDark, +} from 'src/foundations/themes/dark'; +import type { bg, borderColors, color, + notificationToast, textColors, } from 'src/foundations/themes/light'; -import { deepMerge } from 'src/utilities/deepMerge'; export type ThemeName = 'dark' | 'light'; @@ -38,9 +43,15 @@ type TextColors = MergeTypes; type LightModeBorderColors = typeof borderColors; type DarkModeBorderColors = typeof customDarkModeOptions.borderColors; - type BorderColors = MergeTypes; +type LightNotificationToast = typeof notificationToast; +type DarkNotificationToast = typeof notificationToastDark; +type NotificationToast = MergeTypes< + LightNotificationToast, + DarkNotificationToast +>; + /** * Augmenting the Theme and ThemeOptions. * This allows us to add custom fields to the theme. @@ -60,6 +71,7 @@ declare module '@mui/material/styles/createTheme' { graphs: any; inputStyles: any; name: ThemeName; + notificationToast: NotificationToast; textColors: TextColors; visually: any; } @@ -77,6 +89,7 @@ declare module '@mui/material/styles/createTheme' { graphs?: any; inputStyles?: any; name: ThemeName; + notificationToast?: NotificationToast; textColors?: DarkModeTextColors | LightModeTextColors; visually?: any; } diff --git a/packages/manager/src/foundations/themes/light.ts b/packages/manager/src/foundations/themes/light.ts index 94506a29c20..5b8dfa3338b 100644 --- a/packages/manager/src/foundations/themes/light.ts +++ b/packages/manager/src/foundations/themes/light.ts @@ -5,6 +5,7 @@ import { Color, Dropdown, Interaction, + NotificationToast, Select, } from '@linode/design-language-system'; @@ -100,6 +101,30 @@ export const borderColors = { dividerDark: Color.Neutrals[80], } as const; +export const notificationToast = { + default: { + backgroundColor: NotificationToast.Informative.Background, + borderLeft: `6px solid ${NotificationToast.Informative.Border}`, + color: NotificationToast.Text, + }, + error: { + backgroundColor: NotificationToast.Error.Background, + borderLeft: `6px solid ${NotificationToast.Error.Border}`, + }, + info: { + backgroundColor: NotificationToast.Informative.Background, + borderLeft: `6px solid ${NotificationToast.Informative.Border}`, + }, + success: { + backgroundColor: NotificationToast.Success.Background, + borderLeft: `6px solid ${NotificationToast.Success.Border}`, + }, + warning: { + backgroundColor: NotificationToast.Warning.Background, + borderLeft: `6px solid ${NotificationToast.Warning.Border}`, + }, +} as const; + const iconCircleAnimation = { '& .circle': { fill: primaryColors.main, @@ -1547,6 +1572,7 @@ export const lightTheme: ThemeOptions = { }, }, name: 'light', // @todo remove this because we leverage pallete.mode now + notificationToast, palette: { background: { default: bg.app, From 921c863b8bd665a5401d077a3fad06d750e9b18f Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Tue, 2 Jul 2024 13:25:08 -0400 Subject: [PATCH 26/37] upcoming: [M3-8066] - Debounce the Linode Create v2 `VLANSelect` (#10628) * debounce input value so less requests are made * add unit test * changeset and improve test name * add a default debounce of `500` ms --------- Co-authored-by: Banks Nussman --- ...r-10628-upcoming-features-1719846490755.md | 5 +++ .../manager/src/components/VLANSelect.tsx | 5 ++- .../src/hooks/useDebouncedValue.test.ts | 32 +++++++++++++++++++ .../manager/src/hooks/useDebouncedValue.ts | 17 ++++++++++ 4 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 packages/manager/.changeset/pr-10628-upcoming-features-1719846490755.md create mode 100644 packages/manager/src/hooks/useDebouncedValue.test.ts create mode 100644 packages/manager/src/hooks/useDebouncedValue.ts diff --git a/packages/manager/.changeset/pr-10628-upcoming-features-1719846490755.md b/packages/manager/.changeset/pr-10628-upcoming-features-1719846490755.md new file mode 100644 index 00000000000..4334c6b6240 --- /dev/null +++ b/packages/manager/.changeset/pr-10628-upcoming-features-1719846490755.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add debouncing to the Linode Create v2 `VLANSelect` ([#10628](https://github.com/linode/manager/pull/10628)) diff --git a/packages/manager/src/components/VLANSelect.tsx b/packages/manager/src/components/VLANSelect.tsx index f95f584a680..560b744ee19 100644 --- a/packages/manager/src/components/VLANSelect.tsx +++ b/packages/manager/src/components/VLANSelect.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from 'react'; +import { useDebouncedValue } from 'src/hooks/useDebouncedValue'; import { useVLANsInfiniteQuery } from 'src/queries/vlans'; import { Autocomplete } from './Autocomplete/Autocomplete'; @@ -59,9 +60,11 @@ export const VLANSelect = (props: Props) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [value]); + const debouncedInputValue = useDebouncedValue(inputValue); + const apiFilter = getVLANSelectFilter({ defaultFilter: filter, - inputValue, + inputValue: debouncedInputValue, }); const { diff --git a/packages/manager/src/hooks/useDebouncedValue.test.ts b/packages/manager/src/hooks/useDebouncedValue.test.ts new file mode 100644 index 00000000000..cf560b93944 --- /dev/null +++ b/packages/manager/src/hooks/useDebouncedValue.test.ts @@ -0,0 +1,32 @@ +import { act, renderHook } from '@testing-library/react'; + +import { useDebouncedValue } from './useDebouncedValue'; + +describe('useDebouncedValue', () => { + it('debounces the provided value by the given delay', () => { + vi.useFakeTimers(); + + const { rerender, result } = renderHook( + ({ value }) => useDebouncedValue(value, 500), + { initialProps: { value: 'test' } } + ); + + expect(result.current).toBe('test'); + + rerender({ value: 'test-1' }); + + expect(result.current).toBe('test'); + + act(() => { + vi.advanceTimersByTime(400); + }); + + expect(result.current).toBe('test'); + + act(() => { + vi.advanceTimersByTime(100); + }); + + expect(result.current).toBe('test-1'); + }); +}); diff --git a/packages/manager/src/hooks/useDebouncedValue.ts b/packages/manager/src/hooks/useDebouncedValue.ts new file mode 100644 index 00000000000..526ed0a470a --- /dev/null +++ b/packages/manager/src/hooks/useDebouncedValue.ts @@ -0,0 +1,17 @@ +import { useEffect, useState } from 'react'; + +export const useDebouncedValue = (value: T, delay: number = 500) => { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedValue; +}; From 34c9d7bc80d0a3130f3f214e00b9e8aaed40bed5 Mon Sep 17 00:00:00 2001 From: cliu-akamai <126020611+cliu-akamai@users.noreply.github.com> Date: Tue, 2 Jul 2024 13:31:03 -0400 Subject: [PATCH 27/37] test: [M3-7957] - Add Cypress integration test for SSH key update and delete (#10542) * M3-7957 Cypress integration test for SSH key update and delete * Fixed comments * Added changeset: Cypress integration test for SSH key update and delete --- .../pr-10542-tests-1719335118630.md | 5 + .../cypress/e2e/core/account/ssh-keys.spec.ts | 179 ++++++++++++++++++ .../cypress/support/intercepts/profile.ts | 26 +++ 3 files changed, 210 insertions(+) create mode 100644 packages/manager/.changeset/pr-10542-tests-1719335118630.md diff --git a/packages/manager/.changeset/pr-10542-tests-1719335118630.md b/packages/manager/.changeset/pr-10542-tests-1719335118630.md new file mode 100644 index 00000000000..b5a04fd34d0 --- /dev/null +++ b/packages/manager/.changeset/pr-10542-tests-1719335118630.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Cypress integration test for SSH key update and delete ([#10542](https://github.com/linode/manager/pull/10542)) diff --git a/packages/manager/cypress/e2e/core/account/ssh-keys.spec.ts b/packages/manager/cypress/e2e/core/account/ssh-keys.spec.ts index 59b34a59101..d0cf29ac00d 100644 --- a/packages/manager/cypress/e2e/core/account/ssh-keys.spec.ts +++ b/packages/manager/cypress/e2e/core/account/ssh-keys.spec.ts @@ -2,7 +2,9 @@ import { sshKeyFactory } from 'src/factories'; import { mockCreateSSHKey, mockCreateSSHKeyError, + mockDeleteSSHKey, mockGetSSHKeys, + mockUpdateSSHKey, } from 'support/intercepts/profile'; import { ui } from 'support/ui'; import { randomLabel, randomString } from 'support/util/random'; @@ -169,4 +171,181 @@ describe('SSH keys', () => { // When the API responds with an error (e.g. a 400 response), the API response error message is displayed on the form cy.findByText(errorMessage); }); + + /* + * - Validates SSH key update flow using mock data. + * - Confirms that the drawer opens when clicking. + * - Confirms that a form validation error appears when the label is not present. + * - Confirms UI flow when user updates an SSH key. + */ + it('updates an SSH key via Profile page as expected', () => { + const randomKey = randomString(400, { + uppercase: true, + lowercase: true, + numbers: true, + spaces: false, + symbols: false, + }); + const mockSSHKey = sshKeyFactory.build({ + label: randomLabel(), + ssh_key: `ssh-rsa e2etestkey${randomKey} e2etest@linode`, + }); + const newSSHKeyLabel = randomLabel(); + const modifiedSSHKey = sshKeyFactory.build({ + ...mockSSHKey, + label: newSSHKeyLabel, + }); + + mockGetSSHKeys([mockSSHKey]).as('getSSHKeys'); + + // Navigate to SSH key landing page. + cy.visitWithLogin('/profile/keys'); + cy.wait('@getSSHKeys'); + + // When a user clicks "Edit" button on SSH key landing page (/profile/keys), the "Edit SSH Key" drawer opens + ui.button + .findByTitle('Edit') + .should('be.visible') + .should('be.enabled') + .click(); + ui.drawer + .findByTitle(`Edit SSH Key ${mockSSHKey.label}`) + .should('be.visible') + .within(() => { + // When the label is unchanged, the 'Save' button is diabled + ui.button + .findByTitle('Save') + .should('be.visible') + .should('be.disabled'); + + // When a user tries to update an SSH key without a label, a form validation error appears + cy.get('[id="label"]').clear(); + ui.button + .findByTitle('Save') + .should('be.visible') + .should('be.enabled') + .click(); + cy.findByText('Label is required.'); + + // SSH label is not modified when the operation is cancelled + cy.get('[id="label"]').clear().type(newSSHKeyLabel); + ui.button + .findByTitle('Cancel') + .should('be.visible') + .should('be.enabled') + .click(); + }); + cy.findAllByText(mockSSHKey.label).should('be.visible'); + + mockGetSSHKeys([modifiedSSHKey]).as('getSSHKeys'); + mockUpdateSSHKey(mockSSHKey.id, modifiedSSHKey).as('updateSSHKey'); + + ui.button + .findByTitle('Edit') + .should('be.visible') + .should('be.enabled') + .click(); + ui.drawer + .findByTitle(`Edit SSH Key ${mockSSHKey.label}`) + .should('be.visible') + .within(() => { + // Update a new ssh key + cy.get('[id="label"]').clear().type(newSSHKeyLabel); + ui.button + .findByTitle('Save') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + cy.wait(['@updateSSHKey', '@getSSHKeys']); + + // When a user updates an SSH key, a toast notification appears that says "Successfully updated SSH key." + ui.toast.assertMessage('Successfully updated SSH key.'); + + // When a user updates an SSH key, the list of SSH keys for each user updates to show the new key for the signed in user + cy.findAllByText(modifiedSSHKey.label).should('be.visible'); + }); + + /* + * - Vaildates SSH key delete flow using mock data. + * - Confirms that the dialog opens when clicking. + * - Confirms UI flow when user deletes an SSH key. + */ + it('deletes an SSH key via Profile page as expected', () => { + const mockSSHKeys = sshKeyFactory.buildList(2); + + mockGetSSHKeys(mockSSHKeys).as('getSSHKeys'); + + // Navigate to SSH key landing page. + cy.visitWithLogin('/profile/keys'); + cy.wait('@getSSHKeys'); + + mockDeleteSSHKey(mockSSHKeys[0].id).as('deleteSSHKey'); + mockGetSSHKeys([mockSSHKeys[1]]).as('getUpdatedSSHKeys'); + + // When a user clicks "Delete" button on SSH key landing page (/profile/keys), the "Delete SSH Key" dialog opens + cy.findAllByText(`${mockSSHKeys[0].label}`) + .should('be.visible') + .closest('tr') + .within(() => { + ui.button + .findByTitle('Delete') + .should('be.visible') + .should('be.enabled') + .click(); + }); + ui.dialog + .findByTitle('Delete SSH Key') + .should('be.visible') + .within(() => { + cy.findAllByText( + `Are you sure you want to delete SSH key ${mockSSHKeys[0].label}?` + ).should('be.visible'); + ui.button + .findByTitle('Delete') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + cy.wait(['@deleteSSHKey', '@getUpdatedSSHKeys']); + + // When a user deletes an SSH key, the SSH key is removed from the list + cy.findAllByText(mockSSHKeys[0].label).should('not.exist'); + + mockDeleteSSHKey(mockSSHKeys[1].id).as('deleteSSHKey'); + mockGetSSHKeys([]).as('getUpdatedSSHKeys'); + + // When a user clicks "Delete" button on SSH key landing page (/profile/keys), the "Delete SSH Key" dialog opens + cy.findAllByText(`${mockSSHKeys[1].label}`) + .should('be.visible') + .closest('tr') + .within(() => { + ui.button + .findByTitle('Delete') + .should('be.visible') + .should('be.enabled') + .click(); + }); + ui.dialog + .findByTitle('Delete SSH Key') + .should('be.visible') + .within(() => { + cy.findAllByText( + `Are you sure you want to delete SSH key ${mockSSHKeys[1].label}?` + ).should('be.visible'); + ui.button + .findByTitle('Delete') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + cy.wait(['@deleteSSHKey', '@getUpdatedSSHKeys']); + + // When a user deletes the last SSH key, the list of SSH keys updates to show "No items to display." + cy.findAllByText(mockSSHKeys[1].label).should('not.exist'); + cy.findAllByText('No items to display.').should('be.visible'); + }); }); diff --git a/packages/manager/cypress/support/intercepts/profile.ts b/packages/manager/cypress/support/intercepts/profile.ts index 11aee4b2c76..e2493f09868 100644 --- a/packages/manager/cypress/support/intercepts/profile.ts +++ b/packages/manager/cypress/support/intercepts/profile.ts @@ -458,3 +458,29 @@ export const mockCreateSSHKeyError = ( makeErrorResponse(errorMessage, status) ); }; + +/** + * Intercepts PUT request to update an SSH key and mocks response. + * + * @param sshKeyId - The SSH key ID to update + * @param sshKey - An SSH key with which to update. + * + * @returns Cypress chainable. + */ +export const mockUpdateSSHKey = ( + sshKeyId: number, + sshKey: SSHKey +): Cypress.Chainable => { + return cy.intercept('PUT', apiMatcher(`profile/sshkeys/${sshKeyId}`), sshKey); +}; + +/** + * Intercepts DELETE request to delete an SSH key and mocks response. + * + * @param sshKeyId - The SSH key ID to delete + * + * @returns Cypress chainable. + */ +export const mockDeleteSSHKey = (sshKeyId: number): Cypress.Chainable => { + return cy.intercept('DELETE', apiMatcher(`profile/sshkeys/${sshKeyId}`), {}); +}; From ecbc63fcee6a21db92b15dc3e21facba80216ba7 Mon Sep 17 00:00:00 2001 From: Dajahi Wiley <114682940+dwiley-akamai@users.noreply.github.com> Date: Tue, 2 Jul 2024 13:34:44 -0400 Subject: [PATCH 28/37] =?UTF-8?q?fix:=20[M3-7590,=20M3-7886]=20=E2=80=93?= =?UTF-8?q?=20Improve=20UX=20for=20Linode=20Resize=20dialog=20when=20linod?= =?UTF-8?q?e=20data=20is=20being=20loaded=20or=20there=20is=20a=20form=20e?= =?UTF-8?q?rror=20(#10618)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pr-10618-fixed-1719521792524.md | 5 + .../LinodeResize/LinodeResize.tsx | 295 +++++++++--------- 2 files changed, 156 insertions(+), 144 deletions(-) create mode 100644 packages/manager/.changeset/pr-10618-fixed-1719521792524.md diff --git a/packages/manager/.changeset/pr-10618-fixed-1719521792524.md b/packages/manager/.changeset/pr-10618-fixed-1719521792524.md new file mode 100644 index 00000000000..ebceb5f44ad --- /dev/null +++ b/packages/manager/.changeset/pr-10618-fixed-1719521792524.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Linode Resize dialog UX when linode data is loading or there is an error ([#10618](https://github.com/linode/manager/pull/10618)) diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeResize/LinodeResize.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeResize/LinodeResize.tsx index 6d47d62e872..877137b03ff 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeResize/LinodeResize.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeResize/LinodeResize.tsx @@ -1,7 +1,3 @@ -import { - MigrationTypes, - ResizeLinodePayload, -} from '@linode/api-v4/lib/linodes'; import { useTheme } from '@mui/material/styles'; import { useFormik } from 'formik'; import { useSnackbar } from 'notistack'; @@ -10,6 +6,7 @@ import * as React from 'react'; import { Box } from 'src/components/Box'; import { Button } from 'src/components/Button/Button'; import { Checkbox } from 'src/components/Checkbox'; +import { CircleProgress } from 'src/components/CircleProgress/CircleProgress'; import { Dialog } from 'src/components/Dialog/Dialog'; import { Divider } from 'src/components/Divider'; import { Link } from 'src/components/Link'; @@ -30,7 +27,7 @@ import { usePreferences } from 'src/queries/profile/preferences'; import { useRegionsQuery } from 'src/queries/regions/regions'; import { useAllTypes } from 'src/queries/types'; import { extendType } from 'src/utilities/extendType'; -import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; +import { scrollErrorIntoViewV2 } from 'src/utilities/scrollErrorIntoViewV2'; import { HostMaintenanceError } from '../HostMaintenanceError'; import { LinodePermissionsError } from '../LinodePermissionsError'; @@ -41,6 +38,11 @@ import { } from './LinodeResize.utils'; import { UnifiedMigrationPanel } from './LinodeResizeUnifiedMigrationPanel'; +import type { + MigrationTypes, + ResizeLinodePayload, +} from '@linode/api-v4/lib/linodes'; + interface Props { linodeId?: number; linodeLabel?: string; @@ -57,7 +59,7 @@ export const LinodeResize = (props: Props) => { const { linodeId, onClose, open } = props; const theme = useTheme(); - const { data: linode } = useLinodeQuery( + const { data: linode, isLoading: isLinodeDataLoading } = useLinodeQuery( linodeId ?? -1, linodeId !== undefined && open ); @@ -77,6 +79,8 @@ export const LinodeResize = (props: Props) => { const [hasResizeError, setHasResizeError] = React.useState(false); + const formRef = React.useRef(null); + const { error: resizeError, isLoading, @@ -125,6 +129,7 @@ export const LinodeResize = (props: Props) => { }); onClose(); }, + validate: () => scrollErrorIntoViewV2(formRef), }); React.useEffect(() => { @@ -154,8 +159,6 @@ export const LinodeResize = (props: Props) => { React.useEffect(() => { if (resizeError) { setHasResizeError(true); - // Set to "block: end" since the sticky header would otherwise interfere. - scrollErrorIntoView(undefined, { block: 'end' }); } }, [resizeError]); @@ -190,148 +193,152 @@ export const LinodeResize = (props: Props) => { maxWidth="md" onClose={onClose} open={open} - title={`Resize Linode ${linode?.label}`} + title={`Resize Linode ${linode?.label ?? ''}`} > -
- {isLinodesGrantReadOnly && } - {hostMaintenance && } - {disksError && ( - - )} - {hasResizeError && {error}} - - If you’re expecting a temporary burst of traffic to your - website, or if you’re not using your Linode as much as you - thought, you can temporarily or permanently resize your Linode to a - different plan.{' '} - - Learn more. - - - - div': { - padding: 0, - }, - marginBottom: theme.spacing(3), - marginTop: theme.spacing(5), - }} - > - formik.setFieldValue('type', type)} - regionsData={regionsData} - selectedId={formik.values.type} - selectedRegionID={linode?.region} - types={currentTypes.map(extendType)} - /> - - - - Auto Resize Disk - {disksError ? ( - + ) : ( + + {isLinodesGrantReadOnly && } + {hostMaintenance && } + {disksError && ( + - ) : isSmaller ? ( - {error}} + + If you’re expecting a temporary burst of traffic to your + website, or if you’re not using your Linode as much as you + thought, you can temporarily or permanently resize your Linode to a + different plan.{' '} + + Learn more. + + + + div': { + padding: 0, + }, + marginBottom: theme.spacing(3), + marginTop: theme.spacing(5), + }} + > + formik.setFieldValue('type', type)} + regionsData={regionsData} + selectedId={formik.values.type} + selectedRegionID={linode?.region} + types={currentTypes.map(extendType)} /> - ) : !_shouldEnableAutoResizeDiskOption ? ( - + + + Auto Resize Disk + {disksError ? ( + + ) : isSmaller ? ( + + ) : !_shouldEnableAutoResizeDiskOption ? ( + - ) : null} - - - formik.setFieldValue('allow_auto_disk_resize', checked) - } - text={ - - Would you like{' '} - {_shouldEnableAutoResizeDiskOption ? ( - {diskToResize} - ) : ( - 'your disk' - )}{' '} - to be automatically scaled with this Linode’s new size?{' '} -
- We recommend you keep this option enabled when available. -
- } - disabled={!_shouldEnableAutoResizeDiskOption || isSmaller} - /> - - - - To confirm these changes, type the label of the Linode ( - {linode?.label}) in the field below: - + status="help" + /> + ) : null} +
+ - - - - - + text={ + + Would you like{' '} + {_shouldEnableAutoResizeDiskOption ? ( + {diskToResize} + ) : ( + 'your disk' + )}{' '} + to be automatically scaled with this Linode’s new size?{' '} +
+ We recommend you keep this option enabled when available. +
+ } + disabled={!_shouldEnableAutoResizeDiskOption || isSmaller} + /> + + + + To confirm these changes, type the label of the Linode ( + {linode?.label}) in the field below: + + } + hideLabel + label="Linode Label" + onChange={setConfirmationText} + textFieldStyle={{ marginBottom: 16 }} + title="Confirm" + typographyStyle={{ marginBottom: 8 }} + value={confirmationText} + visible={preferences?.type_to_confirm} + /> + + + + + + )} ); }; From bae20b838accb23c4275f680109ec073f77e736f Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Tue, 2 Jul 2024 13:36:24 -0400 Subject: [PATCH 29/37] upcoming: [M3-8267] - Add Marketplace Cluster pricing support to Linode Create v2 (#10623) * add cluster pricing to summary * Added changeset: Add Marketplace Cluster pricing support to Linode Create v2 * clean up and make more robust --------- Co-authored-by: Banks Nussman --- ...r-10623-upcoming-features-1719595144995.md | 5 +++ .../{ => Summary}/Summary.test.tsx | 35 +++++++++++++++- .../LinodeCreatev2/{ => Summary}/Summary.tsx | 14 ++++--- .../LinodeCreatev2/Summary/utilities.test.ts | 33 +++++++++++++++ .../LinodeCreatev2/Summary/utilities.ts | 42 +++++++++++++++++++ .../features/Linodes/LinodeCreatev2/index.tsx | 6 +-- 6 files changed, 124 insertions(+), 11 deletions(-) create mode 100644 packages/manager/.changeset/pr-10623-upcoming-features-1719595144995.md rename packages/manager/src/features/Linodes/LinodeCreatev2/{ => Summary}/Summary.test.tsx (87%) rename packages/manager/src/features/Linodes/LinodeCreatev2/{ => Summary}/Summary.tsx (93%) create mode 100644 packages/manager/src/features/Linodes/LinodeCreatev2/Summary/utilities.test.ts create mode 100644 packages/manager/src/features/Linodes/LinodeCreatev2/Summary/utilities.ts diff --git a/packages/manager/.changeset/pr-10623-upcoming-features-1719595144995.md b/packages/manager/.changeset/pr-10623-upcoming-features-1719595144995.md new file mode 100644 index 00000000000..7ac2be855fa --- /dev/null +++ b/packages/manager/.changeset/pr-10623-upcoming-features-1719595144995.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add Marketplace Cluster pricing support to Linode Create v2 ([#10623](https://github.com/linode/manager/pull/10623)) diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Summary.test.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Summary/Summary.test.tsx similarity index 87% rename from packages/manager/src/features/Linodes/LinodeCreatev2/Summary.test.tsx rename to packages/manager/src/features/Linodes/LinodeCreatev2/Summary/Summary.test.tsx index 6663aa287a7..05994a8951b 100644 --- a/packages/manager/src/features/Linodes/LinodeCreatev2/Summary.test.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Summary/Summary.test.tsx @@ -85,7 +85,7 @@ describe('Linode Create v2 Summary', () => { await findByText(region.label); }); - it('should render a plan (type) label if a type is selected', async () => { + it('should render a plan (type) label if a region and type are selected', async () => { const type = typeFactory.build(); server.use( @@ -96,7 +96,9 @@ describe('Linode Create v2 Summary', () => { const { findByText } = renderWithThemeAndHookFormContext({ component: , - useFormOptions: { defaultValues: { type: type.id } }, + useFormOptions: { + defaultValues: { region: 'fake-region', type: type.id }, + }, }); await findByText(type.label); @@ -233,4 +235,33 @@ describe('Linode Create v2 Summary', () => { expect(getByText('Encrypted')).toBeVisible(); }); + + it('should render correct pricing for Marketplace app cluster deployments', async () => { + const type = typeFactory.build({ + price: { hourly: 0.5, monthly: 2 }, + }); + + server.use( + http.get('*/v4/linode/types/*', () => { + return HttpResponse.json(type); + }) + ); + + const { + findByText, + } = renderWithThemeAndHookFormContext({ + component: , + useFormOptions: { + defaultValues: { + region: 'fake-region', + stackscript_data: { + cluster_size: 5, + }, + type: type.id, + }, + }, + }); + + await findByText(`5 Nodes - $10/month $2.50/hr`); + }); }); diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Summary.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Summary/Summary.tsx similarity index 93% rename from packages/manager/src/features/Linodes/LinodeCreatev2/Summary.tsx rename to packages/manager/src/features/Linodes/LinodeCreatev2/Summary/Summary.tsx index 0ce13e3c03b..73277af3bc7 100644 --- a/packages/manager/src/features/Linodes/LinodeCreatev2/Summary.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Summary/Summary.tsx @@ -13,7 +13,8 @@ import { useTypeQuery } from 'src/queries/types'; import { formatStorageUnits } from 'src/utilities/formatStorageUnits'; import { getMonthlyBackupsPrice } from 'src/utilities/pricing/backups'; import { renderMonthlyPriceToCorrectDecimalPlace } from 'src/utilities/pricing/dynamicPricing'; -import { getLinodeRegionPrice } from 'src/utilities/pricing/linodes'; + +import { getLinodePrice } from './utilities'; import type { CreateLinodeRequest } from '@linode/api-v4'; @@ -35,6 +36,7 @@ export const Summary = () => { vlanLabel, vpcId, diskEncryption, + clusterSize, ] = useWatch({ control, name: [ @@ -49,6 +51,7 @@ export const Summary = () => { 'interfaces.1.label', 'interfaces.0.vpc_id', 'disk_encryption', + 'stackscript_data.cluster_size', ], }); @@ -58,13 +61,12 @@ export const Summary = () => { const region = regions?.find((r) => r.id === regionId); - // @todo handle marketplace cluster pricing (support many nodes by looking at UDF data) - const price = getLinodeRegionPrice(type, regionId); - const backupsPrice = renderMonthlyPriceToCorrectDecimalPlace( getMonthlyBackupsPrice({ region: regionId, type }) ); + const price = getLinodePrice({ type, regionId, clusterSize }); + const summaryItems = [ { item: { @@ -80,10 +82,10 @@ export const Summary = () => { }, { item: { - details: `$${price?.monthly}/month`, + details: price, title: type ? formatStorageUnits(type.label) : typeId, }, - show: Boolean(typeId), + show: price, }, { item: { diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Summary/utilities.test.ts b/packages/manager/src/features/Linodes/LinodeCreatev2/Summary/utilities.test.ts new file mode 100644 index 00000000000..1da6463ccd4 --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Summary/utilities.test.ts @@ -0,0 +1,33 @@ +import { linodeTypeFactory } from 'src/factories'; + +import { getLinodePrice } from './utilities'; + +describe('getLinodePrice', () => { + it('gets a price for a normal Linode', () => { + const type = linodeTypeFactory.build({ + price: { hourly: 0.1, monthly: 5 }, + }); + + const result = getLinodePrice({ + clusterSize: undefined, + regionId: 'fake-region-id', + type, + }); + + expect(result).toBe('$5/month'); + }); + + it('gets a price for a Marketplace Cluster deployment', () => { + const type = linodeTypeFactory.build({ + price: { hourly: 0.2, monthly: 5 }, + }); + + const result = getLinodePrice({ + clusterSize: '3', + regionId: 'fake-region-id', + type, + }); + + expect(result).toBe('3 Nodes - $15/month $0.60/hr'); + }); +}); diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Summary/utilities.ts b/packages/manager/src/features/Linodes/LinodeCreatev2/Summary/utilities.ts new file mode 100644 index 00000000000..9fc3df07966 --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Summary/utilities.ts @@ -0,0 +1,42 @@ +import { renderMonthlyPriceToCorrectDecimalPlace } from 'src/utilities/pricing/dynamicPricing'; +import { getLinodeRegionPrice } from 'src/utilities/pricing/linodes'; + +import type { LinodeType } from '@linode/api-v4'; + +interface LinodePriceOptions { + clusterSize: string | undefined; + regionId: string | undefined; + type: LinodeType | undefined; +} + +export const getLinodePrice = (options: LinodePriceOptions) => { + const { clusterSize, regionId, type } = options; + const price = getLinodeRegionPrice(type, regionId); + + const isCluster = clusterSize !== undefined; + + if ( + regionId === undefined || + price === undefined || + price.monthly === null || + price.hourly === null + ) { + return undefined; + } + + if (isCluster) { + const numberOfNodes = Number(clusterSize); + + const totalMonthlyPrice = renderMonthlyPriceToCorrectDecimalPlace( + price.monthly * numberOfNodes + ); + + const totalHourlyPrice = renderMonthlyPriceToCorrectDecimalPlace( + price.hourly * numberOfNodes + ); + + return `${numberOfNodes} Nodes - $${totalMonthlyPrice}/month $${totalHourlyPrice}/hr`; + } + + return `$${renderMonthlyPriceToCorrectDecimalPlace(price.monthly)}/month`; +}; diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx index f476eaef074..b59ad02d374 100644 --- a/packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreatev2/index.tsx @@ -17,7 +17,6 @@ import { } from 'src/queries/linodes/linodes'; import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; -import { Security } from './Security'; import { Actions } from './Actions'; import { Addons } from './Addons/Addons'; import { Details } from './Details/Details'; @@ -26,7 +25,8 @@ import { Firewall } from './Firewall'; import { Plan } from './Plan'; import { Region } from './Region'; import { linodeCreateResolvers } from './resolvers'; -import { Summary } from './Summary'; +import { Security } from './Security'; +import { Summary } from './Summary/Summary'; import { Backups } from './Tabs/Backups/Backups'; import { Clone } from './Tabs/Clone/Clone'; import { Distributions } from './Tabs/Distributions'; @@ -35,7 +35,6 @@ import { Marketplace } from './Tabs/Marketplace/Marketplace'; import { StackScripts } from './Tabs/StackScripts/StackScripts'; import { UserData } from './UserData/UserData'; import { - LinodeCreateFormValues, defaultValues, defaultValuesMap, getLinodeCreatePayload, @@ -46,6 +45,7 @@ import { import { VLAN } from './VLAN'; import { VPC } from './VPC/VPC'; +import type { LinodeCreateFormValues } from './utilities'; import type { SubmitHandler } from 'react-hook-form'; export const LinodeCreatev2 = () => { From a67822809b5596c17ebe14e8f82ec1db3a48101f Mon Sep 17 00:00:00 2001 From: jdamore-linode <97627410+jdamore-linode@users.noreply.github.com> Date: Tue, 2 Jul 2024 15:08:20 -0400 Subject: [PATCH 30/37] test: [M3-8122] - Disable access to internet for Linodes created during Cypress tests (#10633) * Place Linodes created by Cypress tests behind VLAN, increase Clone timeout * Add changeset --- .../pr-10633-tests-1719934397905.md | 5 ++++ .../e2e/core/linodes/clone-linode.spec.ts | 8 ++---- .../e2e/core/linodes/create-linode.spec.ts | 2 +- .../core/linodes/legacy-create-linode.spec.ts | 10 +++++-- .../core/oneClickApps/one-click-apps.spec.ts | 2 +- .../stackscripts/create-stackscripts.spec.ts | 7 +++-- .../smoke-community-stackscrips.spec.ts | 2 +- .../cypress/support/intercepts/linodes.ts | 11 +++++++- .../manager/cypress/support/util/linodes.ts | 28 +++++++++++++------ 9 files changed, 52 insertions(+), 23 deletions(-) create mode 100644 packages/manager/.changeset/pr-10633-tests-1719934397905.md diff --git a/packages/manager/.changeset/pr-10633-tests-1719934397905.md b/packages/manager/.changeset/pr-10633-tests-1719934397905.md new file mode 100644 index 00000000000..4acac81daa3 --- /dev/null +++ b/packages/manager/.changeset/pr-10633-tests-1719934397905.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Improve security of Linodes created during tests ([#10633](https://github.com/linode/manager/pull/10633)) 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 29273aab7f3..cd9a78f2258 100644 --- a/packages/manager/cypress/e2e/core/linodes/clone-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/clone-linode.spec.ts @@ -33,8 +33,8 @@ const getLinodeCloneUrl = (linode: Linode): string => { return `/linodes/create?linodeID=${linode.id}${regionQuery}&type=Clone+Linode${typeQuery}`; }; -/* Timeout after 3 minutes while waiting for clone. */ -const CLONE_TIMEOUT = 180_000; +/* Timeout after 4 minutes while waiting for clone. */ +const CLONE_TIMEOUT = 240_000; authenticate(); describe('clone linode', () => { @@ -47,7 +47,7 @@ describe('clone linode', () => { * - Confirms that Linode can be cloned successfully. */ it('can clone a Linode from Linode details page', () => { - const linodeRegion = chooseRegion(); + const linodeRegion = chooseRegion({ capabilities: ['Vlans'] }); const linodePayload = createLinodeRequestFactory.build({ label: randomLabel(), region: linodeRegion.id, @@ -64,8 +64,6 @@ describe('clone linode', () => { cy.defer(() => createTestLinode(linodePayload, { securityMethod: 'vlan_no_internet' }) ).then((linode: Linode) => { - const linodeRegion = getRegionById(linodePayload.region!); - interceptCloneLinode(linode.id).as('cloneLinode'); cy.visitWithLogin(`/linodes/${linode.id}`); diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts index 1945263f5e0..be6fad33848 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts @@ -69,7 +69,7 @@ describe('Create Linode', () => { */ it(`creates a ${planConfig.planType} Linode`, () => { const linodeRegion = chooseRegion({ - capabilities: ['Linodes', 'Premium Plans'], + capabilities: ['Linodes', 'Premium Plans', 'Vlans'], }); const linodeLabel = randomLabel(); diff --git a/packages/manager/cypress/e2e/core/linodes/legacy-create-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/legacy-create-linode.spec.ts index 0f9e5fc2238..cf92b39a21b 100644 --- a/packages/manager/cypress/e2e/core/linodes/legacy-create-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/legacy-create-linode.spec.ts @@ -12,7 +12,6 @@ import { getVisible, } from 'support/helpers'; import { ui } from 'support/ui'; -import { apiMatcher } from 'support/util/intercepts'; import { randomString, randomLabel, randomNumber } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; import { getRegionById } from 'support/util/regions'; @@ -39,6 +38,7 @@ import { import { mockGetVLANs } from 'support/intercepts/vlans'; import { mockGetLinodeConfigs } from 'support/intercepts/configs'; import { + interceptCreateLinode, mockCreateLinode, mockGetLinodeType, mockGetLinodeTypes, @@ -155,10 +155,14 @@ describe('create linode', () => { // intercept request cy.visitWithLogin('/linodes/create'); cy.get('[data-qa-deploy-linode]'); - cy.intercept('POST', apiMatcher('linode/instances')).as('linodeCreated'); + interceptCreateLinode().as('linodeCreated'); cy.get('[data-qa-header="Create"]').should('have.text', 'Create'); ui.regionSelect.find().click(); - ui.regionSelect.findItemByRegionLabel(chooseRegion().label).click(); + ui.regionSelect + .findItemByRegionLabel( + chooseRegion({ capabilities: ['Vlans', 'Linodes'] }).label + ) + .click(); fbtClick('Shared CPU'); getClick('[id="g6-nanode-1"]'); getClick('#linode-label').clear().type(linodeLabel); 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 88b3caf6623..3a08199557f 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 @@ -162,7 +162,7 @@ describe('OneClick Apps (OCA)', () => { const password = randomString(16); const image = 'linode/ubuntu22.04'; const rootPassword = randomString(16); - const region = chooseRegion(); + const region = chooseRegion({ capabilities: ['Vlans'] }); const linodeLabel = randomLabel(); const levelName = 'Get the enderman!'; 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 9afa75c6ce6..2733a68d940 100644 --- a/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts +++ b/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts @@ -169,7 +169,7 @@ describe('Create stackscripts', () => { const stackscriptImageTag = 'alpine3.19'; const linodeLabel = randomLabel(); - const linodeRegion = chooseRegion(); + const linodeRegion = chooseRegion({ capabilities: ['Vlans'] }); interceptCreateStackScript().as('createStackScript'); interceptGetStackScripts().as('getStackScripts'); @@ -372,7 +372,10 @@ describe('Create stackscripts', () => { .click(); interceptCreateLinode().as('createLinode'); - fillOutLinodeForm(linodeLabel, chooseRegion().label); + fillOutLinodeForm( + linodeLabel, + chooseRegion({ capabilities: ['Vlans'] }).label + ); ui.button .findByTitle('Create Linode') .should('be.visible') diff --git a/packages/manager/cypress/e2e/core/stackscripts/smoke-community-stackscrips.spec.ts b/packages/manager/cypress/e2e/core/stackscripts/smoke-community-stackscrips.spec.ts index e96645f3116..9bc796d2c6e 100644 --- a/packages/manager/cypress/e2e/core/stackscripts/smoke-community-stackscrips.spec.ts +++ b/packages/manager/cypress/e2e/core/stackscripts/smoke-community-stackscrips.spec.ts @@ -260,7 +260,7 @@ describe('Community Stackscripts integration tests', () => { const fairPassword = 'Akamai123'; const rootPassword = randomString(16); const image = 'AlmaLinux 9'; - const region = chooseRegion(); + const region = chooseRegion({ capabilities: ['Vlans'] }); const linodeLabel = randomLabel(); interceptGetStackScripts().as('getStackScripts'); diff --git a/packages/manager/cypress/support/intercepts/linodes.ts b/packages/manager/cypress/support/intercepts/linodes.ts index 2a4a898068c..8dfa481462a 100644 --- a/packages/manager/cypress/support/intercepts/linodes.ts +++ b/packages/manager/cypress/support/intercepts/linodes.ts @@ -6,16 +6,25 @@ import { makeErrorResponse } from 'support/util/errors'; import { apiMatcher } from 'support/util/intercepts'; import { paginateResponse } from 'support/util/paginate'; import { makeResponse } from 'support/util/response'; +import { linodeVlanNoInternetConfig } from 'support/util/linodes'; import type { Disk, Kernel, Linode, LinodeType, Volume } from '@linode/api-v4'; /** * Intercepts POST request to create a Linode. * + * The outgoing request payload is modified to create a Linode without access + * to the internet. + * * @returns Cypress chainable. */ export const interceptCreateLinode = (): Cypress.Chainable => { - return cy.intercept('POST', apiMatcher('linode/instances')); + return cy.intercept('POST', apiMatcher('linode/instances'), (req) => { + req.body = { + ...req.body, + interfaces: linodeVlanNoInternetConfig, + }; + }); }; /** diff --git a/packages/manager/cypress/support/util/linodes.ts b/packages/manager/cypress/support/util/linodes.ts index 76186a2ccc0..ad6e6b538d4 100644 --- a/packages/manager/cypress/support/util/linodes.ts +++ b/packages/manager/cypress/support/util/linodes.ts @@ -7,9 +7,26 @@ import { chooseRegion } from 'support/util/regions'; import { depaginate } from './paginate'; import { pageSize } from 'support/constants/api'; -import type { Config, CreateLinodeRequest, Linode } from '@linode/api-v4'; +import type { + Config, + CreateLinodeRequest, + InterfacePayload, + Linode, +} from '@linode/api-v4'; import { findOrCreateDependencyFirewall } from 'support/api/firewalls'; +/** + * Linode create interface to configure a Linode with no public internet access. + */ +export const linodeVlanNoInternetConfig: InterfacePayload[] = [ + { + purpose: 'vlan', + primary: false, + label: randomLabel(), + ipam_address: null, + }, +]; + /** * Methods used to secure test Linodes. * @@ -77,14 +94,7 @@ export const createTestLinode = async ( case 'vlan_no_internet': return { - interfaces: [ - { - purpose: 'vlan', - primary: false, - label: randomLabel(), - ipam_address: null, - }, - ], + interfaces: linodeVlanNoInternetConfig, }; case 'powered_off': From 95ef99f7bb490d151b2f06d4e43ffdb21d7d0b69 Mon Sep 17 00:00:00 2001 From: Alban Bailly <130582365+abailly-akamai@users.noreply.github.com> Date: Tue, 2 Jul 2024 16:26:01 -0400 Subject: [PATCH 31/37] fix: [M3-8303] - Remove Paper border in dark theme (#10638) * Allow passing SX on PlanPanel * missed db resize * revert initial changes and remove paper border in dark mode --- packages/manager/src/foundations/themes/dark.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/src/foundations/themes/dark.ts b/packages/manager/src/foundations/themes/dark.ts index 262d166cd5a..5307ee7aad4 100644 --- a/packages/manager/src/foundations/themes/dark.ts +++ b/packages/manager/src/foundations/themes/dark.ts @@ -599,7 +599,7 @@ export const darkTheme: ThemeOptions = { root: { backgroundColor: Color.Neutrals[90], backgroundImage: 'none', // I have no idea why MUI defaults to setting a background image... - border: `1px solid ${Color.Neutrals[80]}`, + border: 0, }, }, }, From bc14f6d1d167151f3750fdd6d17b5cc2b7614b2e Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Wed, 3 Jul 2024 13:43:50 -0400 Subject: [PATCH 32/37] change: [M3-8323] - Remove Region Helper Text on Image Upload (#10642) * remove region helper text * Added changeset: Region helper text on the Image Upload page * remove extra prop --------- Co-authored-by: Banks Nussman --- .../manager/.changeset/pr-10642-removed-1720016794302.md | 5 +++++ .../manager/src/features/Images/ImagesCreate/ImageUpload.tsx | 2 -- 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 packages/manager/.changeset/pr-10642-removed-1720016794302.md diff --git a/packages/manager/.changeset/pr-10642-removed-1720016794302.md b/packages/manager/.changeset/pr-10642-removed-1720016794302.md new file mode 100644 index 00000000000..405ae306216 --- /dev/null +++ b/packages/manager/.changeset/pr-10642-removed-1720016794302.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Removed +--- + +Region helper text on the Image Upload page ([#10642](https://github.com/linode/manager/pull/10642)) diff --git a/packages/manager/src/features/Images/ImagesCreate/ImageUpload.tsx b/packages/manager/src/features/Images/ImagesCreate/ImageUpload.tsx index bfef25f2a31..51bf538c31e 100644 --- a/packages/manager/src/features/Images/ImagesCreate/ImageUpload.tsx +++ b/packages/manager/src/features/Images/ImagesCreate/ImageUpload.tsx @@ -251,14 +251,12 @@ export const ImageUpload = () => { isImageCreateRestricted || form.formState.isSubmitting } textFieldProps={{ - helperTextPosition: 'top', inputRef: field.ref, onBlur: field.onBlur, }} currentCapability={undefined} disableClearable errorText={fieldState.error?.message} - helperText="For fastest initial upload, select the region that is geographically closest to you. Once uploaded, you will be able to deploy the image to other regions." label="Region" onChange={(e, region) => field.onChange(region.id)} regionFilter="core" // Images service will not be supported for Gecko Beta From c7842f06f7fb2c49628b370364b0315b997c8ac7 Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Wed, 3 Jul 2024 13:55:46 -0400 Subject: [PATCH 33/37] upcoming: [M3-8021] - Manage Image Regions Drawer (#10617) * save progress * save progress * save progress * begin adding unit testing * add more unit testing * update how image is passed via props * add test and adjust regions filter * dial in * more testing * add cypress test * fix spelling * Added changeset: Add Manage Image Regions Drawer * update cypress jsdoc * Added changeset: Update `updateImageRegions` to accept `UpdateImageRegionsPayload` instead of `regions: string[]` * add `Readonly` utility type to `IMAGE_REGION_STATUS_TO_STATUS_ICON_STATUS` * rearchitecture so that `useEffect` is not needed * update unit tests --------- Co-authored-by: Banks Nussman --- .../pr-10617-changed-1719590161430.md | 5 + packages/api-v4/src/images/images.ts | 15 +- packages/api-v4/src/images/types.ts | 9 +- ...r-10617-upcoming-features-1719524941884.md | 5 + .../core/images/manage-image-regions.spec.ts | 213 ++++++++++++++++++ .../cypress/support/intercepts/images.ts | 24 +- .../RegionSelect/RegionMultiSelect.tsx | 2 + .../Images/ImagesLanding/EditImageDrawer.tsx | 19 +- .../ImageRegions/ImageRegionRow.test.tsx | 42 ++++ .../ImageRegions/ImageRegionRow.tsx | 64 ++++++ .../ManageImageRegionsForm.test.tsx | 108 +++++++++ .../ImageRegions/ManageImageRegionsForm.tsx | 150 ++++++++++++ .../Images/ImagesLanding/ImagesLanding.tsx | 187 +++++++-------- .../ImagesLanding/RebuildImageDrawer.tsx | 5 +- packages/manager/src/mocks/serverHandlers.ts | 16 ++ packages/manager/src/queries/images.ts | 17 ++ 16 files changed, 760 insertions(+), 121 deletions(-) create mode 100644 packages/api-v4/.changeset/pr-10617-changed-1719590161430.md create mode 100644 packages/manager/.changeset/pr-10617-upcoming-features-1719524941884.md create mode 100644 packages/manager/cypress/e2e/core/images/manage-image-regions.spec.ts create mode 100644 packages/manager/src/features/Images/ImagesLanding/ImageRegions/ImageRegionRow.test.tsx create mode 100644 packages/manager/src/features/Images/ImagesLanding/ImageRegions/ImageRegionRow.tsx create mode 100644 packages/manager/src/features/Images/ImagesLanding/ImageRegions/ManageImageRegionsForm.test.tsx create mode 100644 packages/manager/src/features/Images/ImagesLanding/ImageRegions/ManageImageRegionsForm.tsx diff --git a/packages/api-v4/.changeset/pr-10617-changed-1719590161430.md b/packages/api-v4/.changeset/pr-10617-changed-1719590161430.md new file mode 100644 index 00000000000..1f7c25a4e35 --- /dev/null +++ b/packages/api-v4/.changeset/pr-10617-changed-1719590161430.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Changed +--- + +Update `updateImageRegions` to accept `UpdateImageRegionsPayload` instead of `regions: string[]` ([#10617](https://github.com/linode/manager/pull/10617)) diff --git a/packages/api-v4/src/images/images.ts b/packages/api-v4/src/images/images.ts index b012fe396fa..110f19158ed 100644 --- a/packages/api-v4/src/images/images.ts +++ b/packages/api-v4/src/images/images.ts @@ -18,6 +18,7 @@ import type { Image, ImageUploadPayload, UpdateImagePayload, + UpdateImageRegionsPayload, UploadImageResponse, } from './types'; @@ -99,16 +100,14 @@ export const uploadImage = (data: ImageUploadPayload) => { }; /** - * Selects the regions to which this image will be replicated. + * updateImageRegions * - * @param imageId { string } ID of the Image to look up. - * @param regions { string[] } ID of regions to replicate to. Must contain at least one valid region. + * Selects the regions to which this image will be replicated. */ -export const updateImageRegions = (imageId: string, regions: string[]) => { - const data = { - regions, - }; - +export const updateImageRegions = ( + imageId: string, + data: UpdateImageRegionsPayload +) => { return Request( setURL(`${API_ROOT}/images/${encodeURIComponent(imageId)}/regions`), setMethod('POST'), diff --git a/packages/api-v4/src/images/types.ts b/packages/api-v4/src/images/types.ts index e25fb28f9a2..cd3b34db673 100644 --- a/packages/api-v4/src/images/types.ts +++ b/packages/api-v4/src/images/types.ts @@ -8,7 +8,7 @@ export type ImageCapabilities = 'cloud-init' | 'distributed-images'; type ImageType = 'manual' | 'automatic'; -type ImageRegionStatus = +export type ImageRegionStatus = | 'creating' | 'pending' | 'available' @@ -154,3 +154,10 @@ export interface ImageUploadPayload extends BaseImagePayload { label: string; region: string; } + +export interface UpdateImageRegionsPayload { + /** + * An array of region ids + */ + regions: string[]; +} diff --git a/packages/manager/.changeset/pr-10617-upcoming-features-1719524941884.md b/packages/manager/.changeset/pr-10617-upcoming-features-1719524941884.md new file mode 100644 index 00000000000..5047c2d920a --- /dev/null +++ b/packages/manager/.changeset/pr-10617-upcoming-features-1719524941884.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add Manage Image Regions Drawer ([#10617](https://github.com/linode/manager/pull/10617)) diff --git a/packages/manager/cypress/e2e/core/images/manage-image-regions.spec.ts b/packages/manager/cypress/e2e/core/images/manage-image-regions.spec.ts new file mode 100644 index 00000000000..663125cd190 --- /dev/null +++ b/packages/manager/cypress/e2e/core/images/manage-image-regions.spec.ts @@ -0,0 +1,213 @@ +import { imageFactory, regionFactory } from 'src/factories'; +import { + mockGetCustomImages, + mockGetRecoveryImages, + mockUpdateImageRegions, +} from 'support/intercepts/images'; +import { mockGetRegions } from 'support/intercepts/regions'; +import { ui } from 'support/ui'; +import type { Image } from '@linode/api-v4'; + +describe('Manage Image Regions', () => { + /** + * Adds two new regions to an Image (region3 and region4) + * and removes one existing region (region 1). + */ + it("updates an Image's regions", () => { + const region1 = regionFactory.build({ site_type: 'core' }); + const region2 = regionFactory.build({ site_type: 'core' }); + const region3 = regionFactory.build({ site_type: 'core' }); + const region4 = regionFactory.build({ site_type: 'core' }); + + const image = imageFactory.build({ + size: 50, + total_size: 100, + capabilities: ['distributed-images'], + regions: [ + { region: region1.id, status: 'available' }, + { region: region2.id, status: 'available' }, + ], + }); + + mockGetRegions([region1, region2, region3, region4]).as('getRegions'); + mockGetCustomImages([image]).as('getImages'); + mockGetRecoveryImages([]); + + cy.visitWithLogin('/images'); + cy.wait(['@getImages', '@getRegions']); + + cy.findByText(image.label) + .closest('tr') + .within(() => { + // Verify total size is rendered + cy.findByText(`${image.total_size} MB`).should('be.visible'); + + // Verify capabilities are rendered + cy.findByText('Distributed').should('be.visible'); + + // Verify the first region is rendered + cy.findByText(region1.label + ',').should('be.visible'); + + // Click the "+1" + cy.findByText('+1').should('be.visible').should('be.enabled').click(); + }); + + // Verify the Manage Regions drawer opens and contains basic content + ui.drawer + .findByTitle(`Manage Regions for ${image.label}`) + .should('be.visible') + .within(() => { + // Verify the Image regions render + cy.findByText(region1.label).should('be.visible'); + cy.findByText(region2.label).should('be.visible'); + + cy.findByText('Image will be available in these regions (2)').should( + 'be.visible' + ); + + // Verify the "Save" button is disabled because no changes have been made + ui.button + .findByTitle('Save') + .should('be.visible') + .should('be.disabled'); + + // Close the Manage Regions drawer + ui.button + .findByTitle('Cancel') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + cy.findByText(image.label) + .closest('tr') + .within(() => { + // Open the Image's action menu + ui.actionMenu + .findByTitle(`Action menu for Image ${image.label}`) + .should('be.visible') + .should('be.enabled') + .click(); + }); + + // Click "Manage Regions" option in the action menu + ui.actionMenuItem + .findByTitle('Manage Regions') + .should('be.visible') + .should('be.enabled') + .click(); + + // Open the Regions Multi-Select + cy.findByLabelText('Add Regions') + .should('be.visible') + .should('be.enabled') + .click(); + + // Verify "Select All" shows up as an option + ui.autocompletePopper + .findByTitle('Select All') + .should('be.visible') + .should('be.enabled'); + + // Verify region3 shows up as an option and select it + ui.autocompletePopper + .findByTitle(`${region3.label} (${region3.id})`) + .should('be.visible') + .should('be.enabled') + .click(); + + // Verify region4 shows up as an option and select it + ui.autocompletePopper + .findByTitle(`${region4.label} (${region4.id})`) + .should('be.visible') + .should('be.enabled') + .click(); + + const updatedImage: Image = { + ...image, + total_size: 150, + regions: [ + { region: region2.id, status: 'available' }, + { region: region3.id, status: 'pending replication' }, + { region: region4.id, status: 'pending replication' }, + ], + }; + + // mock the POST /v4/images/:id:regions response + mockUpdateImageRegions(image.id, updatedImage); + + // mock the updated paginated response + mockGetCustomImages([updatedImage]); + + // Click outside of the Region Multi-Select to commit the selection to the list + ui.drawer + .findByTitle(`Manage Regions for ${image.label}`) + .click() + .within(() => { + // Verify the existing image regions render + cy.findByText(region1.label).should('be.visible'); + cy.findByText(region2.label).should('be.visible'); + + // Verify the newly selected image regions render + cy.findByText(region3.label).should('be.visible'); + cy.findByText(region4.label).should('be.visible'); + cy.findAllByText('unsaved').should('be.visible'); + + // Verify the count is now 3 + cy.findByText('Image will be available in these regions (4)').should( + 'be.visible' + ); + + // Verify the "Save" button is enabled because a new region is selected + ui.button.findByTitle('Save').should('be.visible').should('be.enabled'); + + // Remove region1 + cy.findByLabelText(`Remove ${region1.id}`) + .should('be.visible') + .should('be.enabled') + .click(); + + // Verify the image isn't shown in the list after being removed + cy.findByText(region1.label).should('not.exist'); + + // Verify the count is now 2 + cy.findByText('Image will be available in these regions (3)').should( + 'be.visible' + ); + + // Save changes + ui.button + .findByTitle('Save') + .should('be.visible') + .should('be.enabled') + .click(); + + // "Unsaved" regions should transition to "pending replication" because + // they are now returned by the API + cy.findAllByText('pending replication').should('be.visible'); + + // The save button should become disabled now that changes have been saved + ui.button.findByTitle('Save').should('be.disabled'); + + // The save button should become disabled now that changes have been saved + ui.button.findByTitle('Save').should('be.disabled'); + + cy.findByLabelText('Close drawer').click(); + }); + + ui.toast.assertMessage('Image regions successfully updated.'); + + cy.findByText(image.label) + .closest('tr') + .within(() => { + // Verify the new size is shown + cy.findByText('150 MB'); + + // Verify the first region is rendered + cy.findByText(region2.label + ',').should('be.visible'); + + // Verify the regions count is now "+2" + cy.findByText('+2').should('be.visible').should('be.enabled'); + }); + }); +}); diff --git a/packages/manager/cypress/support/intercepts/images.ts b/packages/manager/cypress/support/intercepts/images.ts index a9a38804c06..9e4a0f7a2bb 100644 --- a/packages/manager/cypress/support/intercepts/images.ts +++ b/packages/manager/cypress/support/intercepts/images.ts @@ -54,9 +54,7 @@ export const mockGetCustomImages = ( const filters = getFilters(req); if (filters?.type === 'manual') { req.reply(paginateResponse(images)); - return; } - req.continue(); }); }; @@ -74,9 +72,7 @@ export const mockGetRecoveryImages = ( const filters = getFilters(req); if (filters?.type === 'automatic') { req.reply(paginateResponse(images)); - return; } - req.continue(); }); }; @@ -130,3 +126,23 @@ export const mockDeleteImage = (id: string): Cypress.Chainable => { const encodedId = encodeURIComponent(id); return cy.intercept('DELETE', apiMatcher(`images/${encodedId}`), {}); }; + +/** + * Intercepts POST request to update an image's regions and mocks the response. + * + * @param id - ID of image + * @param updatedImage - Updated image with which to mock response. + * + * @returns Cypress chainable. + */ +export const mockUpdateImageRegions = ( + id: string, + updatedImage: Image +): Cypress.Chainable => { + const encodedId = encodeURIComponent(id); + return cy.intercept( + 'POST', + apiMatcher(`images/${encodedId}/regions`), + updatedImage + ); +}; diff --git a/packages/manager/src/components/RegionSelect/RegionMultiSelect.tsx b/packages/manager/src/components/RegionSelect/RegionMultiSelect.tsx index 4ba6d5879a7..2d3126e008a 100644 --- a/packages/manager/src/components/RegionSelect/RegionMultiSelect.tsx +++ b/packages/manager/src/components/RegionSelect/RegionMultiSelect.tsx @@ -67,6 +67,7 @@ export const RegionMultiSelect = React.memo((props: RegionMultiSelectProps) => { selectedIds, sortRegionOptions, width, + onClose, } = props; const { @@ -171,6 +172,7 @@ export const RegionMultiSelect = React.memo((props: RegionMultiSelectProps) => { options={regionOptions} placeholder={placeholder ?? 'Select Regions'} value={selectedRegions} + onClose={onClose} /> {selectedRegions.length > 0 && SelectedRegionsList && ( diff --git a/packages/manager/src/features/Images/ImagesLanding/EditImageDrawer.tsx b/packages/manager/src/features/Images/ImagesLanding/EditImageDrawer.tsx index 582a7738462..09c2d02e8b2 100644 --- a/packages/manager/src/features/Images/ImagesLanding/EditImageDrawer.tsx +++ b/packages/manager/src/features/Images/ImagesLanding/EditImageDrawer.tsx @@ -8,7 +8,6 @@ import { Drawer } from 'src/components/Drawer'; import { Notice } from 'src/components/Notice/Notice'; import { TagsInput } from 'src/components/TagsInput/TagsInput'; import { TextField } from 'src/components/TextField'; -import { usePrevious } from 'src/hooks/usePrevious'; import { useUpdateImageMutation } from 'src/queries/images'; import { useImageAndLinodeGrantCheck } from '../utils'; @@ -18,18 +17,17 @@ import type { APIError, Image, UpdateImagePayload } from '@linode/api-v4'; interface Props { image: Image | undefined; onClose: () => void; + open: boolean; } export const EditImageDrawer = (props: Props) => { - const { image, onClose } = props; + const { image, onClose, open } = props; const { canCreateImage } = useImageAndLinodeGrantCheck(); - // Prevent content from disappearing when closing drawer - const prevImage = usePrevious(image); const defaultValues = { - description: image?.description ?? prevImage?.description ?? undefined, - label: image?.label ?? prevImage?.label, - tags: image?.tags ?? prevImage?.tags, + description: image?.description ?? undefined, + label: image?.label, + tags: image?.tags, }; const { @@ -78,12 +76,7 @@ export const EditImageDrawer = (props: Props) => { }); return ( - + {!canCreateImage && ( { + it('renders a region label and status', async () => { + const region = regionFactory.build({ id: 'us-east', label: 'Newark, NJ' }); + + server.use( + http.get('*/v4/regions', () => { + return HttpResponse.json(makeResourcePage([region])); + }) + ); + + const { findByText, getByText } = renderWithTheme( + + ); + + expect(getByText('creating')).toBeVisible(); + expect(await findByText('Newark, NJ')).toBeVisible(); + }); + + it('calls onRemove when the remove button is clicked', async () => { + const onRemove = vi.fn(); + + const { getByLabelText } = renderWithTheme( + + ); + + const removeButton = getByLabelText('Remove us-east'); + + await userEvent.click(removeButton); + + expect(onRemove).toHaveBeenCalled(); + }); +}); diff --git a/packages/manager/src/features/Images/ImagesLanding/ImageRegions/ImageRegionRow.tsx b/packages/manager/src/features/Images/ImagesLanding/ImageRegions/ImageRegionRow.tsx new file mode 100644 index 00000000000..a3a1ccd292b --- /dev/null +++ b/packages/manager/src/features/Images/ImagesLanding/ImageRegions/ImageRegionRow.tsx @@ -0,0 +1,64 @@ +import Close from '@mui/icons-material/Close'; +import React from 'react'; + +import { Box } from 'src/components/Box'; +import { Flag } from 'src/components/Flag'; +import { IconButton } from 'src/components/IconButton'; +import { Stack } from 'src/components/Stack'; +import { StatusIcon } from 'src/components/StatusIcon/StatusIcon'; +import { Typography } from 'src/components/Typography'; +import { useRegionsQuery } from 'src/queries/regions/regions'; + +import type { ImageRegionStatus } from '@linode/api-v4'; +import type { Status } from 'src/components/StatusIcon/StatusIcon'; + +type ExtendedImageRegionStatus = 'unsaved' | ImageRegionStatus; + +interface Props { + onRemove: () => void; + region: string; + status: ExtendedImageRegionStatus; +} + +export const ImageRegionRow = (props: Props) => { + const { onRemove, region, status } = props; + + const { data: regions } = useRegionsQuery(); + + const actualRegion = regions?.find((r) => r.id === region); + + return ( + + + + {actualRegion?.label ?? region} + + + {status} + + + + + + + ); +}; + +const IMAGE_REGION_STATUS_TO_STATUS_ICON_STATUS: Readonly< + Record +> = { + available: 'active', + creating: 'other', + pending: 'other', + 'pending deletion': 'other', + 'pending replication': 'inactive', + replicating: 'other', + timedout: 'inactive', + unsaved: 'inactive', +}; diff --git a/packages/manager/src/features/Images/ImagesLanding/ImageRegions/ManageImageRegionsForm.test.tsx b/packages/manager/src/features/Images/ImagesLanding/ImageRegions/ManageImageRegionsForm.test.tsx new file mode 100644 index 00000000000..c3623e4d789 --- /dev/null +++ b/packages/manager/src/features/Images/ImagesLanding/ImageRegions/ManageImageRegionsForm.test.tsx @@ -0,0 +1,108 @@ +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import { imageFactory, regionFactory } from 'src/factories'; +import { makeResourcePage } from 'src/mocks/serverHandlers'; +import { HttpResponse, http, server } from 'src/mocks/testServer'; +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { ManageImageRegionsForm } from './ManageImageRegionsForm'; + +describe('ManageImageRegionsDrawer', () => { + it('should render a save button and a cancel button', () => { + const image = imageFactory.build(); + const { getByText } = renderWithTheme( + + ); + + const cancelButton = getByText('Cancel').closest('button'); + const saveButton = getByText('Save').closest('button'); + + expect(cancelButton).toBeVisible(); + expect(cancelButton).toBeEnabled(); + + expect(saveButton).toBeVisible(); + expect(saveButton).toBeDisabled(); // The save button should become enabled when regions are changed + }); + + it('should render existing regions and their statuses', async () => { + const region1 = regionFactory.build({ id: 'us-east', label: 'Newark, NJ' }); + const region2 = regionFactory.build({ id: 'us-west', label: 'Place, CA' }); + + const image = imageFactory.build({ + regions: [ + { + region: 'us-east', + status: 'available', + }, + { + region: 'us-west', + status: 'pending replication', + }, + ], + }); + + server.use( + http.get('*/v4/regions', () => { + return HttpResponse.json(makeResourcePage([region1, region2])); + }) + ); + + const { findByText } = renderWithTheme( + + ); + + await findByText('Newark, NJ'); + await findByText('available'); + await findByText('Place, CA'); + await findByText('pending replication'); + }); + + it('should render a status of "unsaved" when a new region is selected', async () => { + const region1 = regionFactory.build({ id: 'us-east', label: 'Newark, NJ' }); + const region2 = regionFactory.build({ id: 'us-west', label: 'Place, CA' }); + + const image = imageFactory.build({ + regions: [ + { + region: 'us-east', + status: 'available', + }, + ], + }); + + server.use( + http.get('*/v4/regions', () => { + return HttpResponse.json(makeResourcePage([region1, region2])); + }) + ); + + const { findByText, getByLabelText, getByText } = renderWithTheme( + + ); + + const saveButton = getByText('Save').closest('button'); + + expect(saveButton).toBeVisible(); + + // Verify the save button is disabled because no changes have been made + expect(saveButton).toBeDisabled(); + + const regionSelect = getByLabelText('Add Regions'); + + // Open the Region Select + await userEvent.click(regionSelect); + + // Select new region + await userEvent.click(await findByText('us-west', { exact: false })); + + // Close the Region Multi-Select to that selections are committed to the list + await userEvent.type(regionSelect, '{escape}'); + + expect(getByText('Place, CA')).toBeVisible(); + expect(getByText('unsaved')).toBeVisible(); + + // Verify the save button is enabled because changes have been made + expect(saveButton).toBeEnabled(); + }); +}); diff --git a/packages/manager/src/features/Images/ImagesLanding/ImageRegions/ManageImageRegionsForm.tsx b/packages/manager/src/features/Images/ImagesLanding/ImageRegions/ManageImageRegionsForm.tsx new file mode 100644 index 00000000000..f50c82a36aa --- /dev/null +++ b/packages/manager/src/features/Images/ImagesLanding/ImageRegions/ManageImageRegionsForm.tsx @@ -0,0 +1,150 @@ +import { yupResolver } from '@hookform/resolvers/yup'; +import { updateImageRegionsSchema } from '@linode/validation'; +import { useSnackbar } from 'notistack'; +import React, { useState } from 'react'; +import { useForm } from 'react-hook-form'; + +import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; +import { Link } from 'src/components/Link'; +import { Notice } from 'src/components/Notice/Notice'; +import { Paper } from 'src/components/Paper'; +import { RegionMultiSelect } from 'src/components/RegionSelect/RegionMultiSelect'; +import { Stack } from 'src/components/Stack'; +import { Typography } from 'src/components/Typography'; +import { useUpdateImageRegionsMutation } from 'src/queries/images'; +import { useRegionsQuery } from 'src/queries/regions/regions'; + +import { ImageRegionRow } from './ImageRegionRow'; + +import type { Image, UpdateImageRegionsPayload } from '@linode/api-v4'; + +interface Props { + image: Image | undefined; + onClose: () => void; +} + +export const ManageImageRegionsForm = (props: Props) => { + const { image, onClose } = props; + + const imageRegionIds = image?.regions.map(({ region }) => region) ?? []; + + const { enqueueSnackbar } = useSnackbar(); + const { data: regions } = useRegionsQuery(); + const { mutateAsync } = useUpdateImageRegionsMutation(image?.id ?? ''); + + const [selectedRegions, setSelectedRegions] = useState([]); + + const { + formState: { errors, isDirty, isSubmitting }, + handleSubmit, + setError, + setValue, + watch, + } = useForm({ + defaultValues: { regions: imageRegionIds }, + resolver: yupResolver(updateImageRegionsSchema), + values: { regions: imageRegionIds }, + }); + + const onSubmit = async (data: UpdateImageRegionsPayload) => { + try { + await mutateAsync(data); + + enqueueSnackbar('Image regions successfully updated.', { + variant: 'success', + }); + } catch (errors) { + for (const error of errors) { + if (error.field) { + setError(error.field, { message: error.reason }); + } else { + setError('root', { message: error.reason }); + } + } + } + }; + + const values = watch(); + + return ( +
+ {errors.root?.message && ( + + )} + + Custom images are billed monthly, at $.10/GB. Check out{' '} + + this guide + {' '} + for details on managing your Linux system's disk space. + + { + setValue('regions', [...values.regions, ...selectedRegions], { + shouldDirty: true, + shouldValidate: true, + }); + setSelectedRegions([]); + }} + regions={(regions ?? []).filter( + (r) => !values.regions.includes(r.id) && r.site_type === 'core' + )} + currentCapability={undefined} + errorText={errors.regions?.message} + label="Add Regions" + onChange={setSelectedRegions} + placeholder="Select Regions" + selectedIds={selectedRegions} + /> + + Image will be available in these regions ({values.regions.length}) + + ({ + backgroundColor: theme.palette.background.paper, + p: 2, + py: 1, + })} + variant="outlined" + > + + {values.regions.length === 0 && ( + + No Regions Selected + + )} + {values.regions.map((regionId) => ( + + setValue( + 'regions', + values.regions.filter((r) => r !== regionId), + { shouldDirty: true, shouldValidate: true } + ) + } + status={ + image?.regions.find( + (regionItem) => regionItem.region === regionId + )?.status ?? 'unsaved' + } + key={regionId} + region={regionId} + /> + ))} + + + + + ); +}; diff --git a/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.tsx b/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.tsx index 039a71711c7..28d72c8abb5 100644 --- a/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.tsx +++ b/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.tsx @@ -10,6 +10,7 @@ import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { CircleProgress } from 'src/components/CircleProgress'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; +import { Drawer } from 'src/components/Drawer'; import { ErrorState } from 'src/components/ErrorState/ErrorState'; import { Hidden } from 'src/components/Hidden'; import { IconButton } from 'src/components/IconButton'; @@ -24,7 +25,6 @@ import { TableCell } from 'src/components/TableCell'; import { TableHead } from 'src/components/TableHead'; import { TableRow } from 'src/components/TableRow'; import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty'; -import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading'; import { TableSortCell } from 'src/components/TableSortCell'; import { TextField } from 'src/components/TextField'; import { Typography } from 'src/components/Typography'; @@ -45,13 +45,13 @@ import { getErrorStringOrDefault } from 'src/utilities/errorUtils'; import { getEventsForImages } from '../utils'; import { EditImageDrawer } from './EditImageDrawer'; +import { ManageImageRegionsForm } from './ImageRegions/ManageImageRegionsForm'; import { ImageRow } from './ImageRow'; import { ImagesLandingEmptyState } from './ImagesLandingEmptyState'; import { RebuildImageDrawer } from './RebuildImageDrawer'; import type { Handlers as ImageHandlers } from './ImagesActionMenu'; -import type { Image, ImageStatus } from '@linode/api-v4'; -import type { APIError } from '@linode/api-v4/lib/types'; +import type { ImageStatus } from '@linode/api-v4'; import type { Theme } from '@mui/material/styles'; const searchQueryKey = 'query'; @@ -212,13 +212,18 @@ export const ImagesLanding = () => { imageEvents ); + const [selectedImageId, setSelectedImageId] = React.useState(); + const [ - // @ts-expect-error This will be unused until the regions drawer is implemented - manageRegionsDrawerImage, - setManageRegionsDrawerImage, - ] = React.useState(); - const [editDrawerImage, setEditDrawerImage] = React.useState(); - const [rebuildDrawerImage, setRebuildDrawerImage] = React.useState(); + isManageRegionsDrawerOpen, + setIsManageRegionsDrawerOpen, + ] = React.useState(false); + const [isEditDrawerOpen, setIsEditDrawerOpen] = React.useState(false); + const [isRebuildDrawerOpen, setIsRebuildDrawerOpen] = React.useState(false); + + const selectedImage = + manualImages?.data.find((i) => i.id === selectedImageId) ?? + automaticImages?.data.find((i) => i.id === selectedImageId); const [dialog, setDialogState] = React.useState( defaultDialogState @@ -312,24 +317,6 @@ export const ImagesLanding = () => { }); }; - const getActions = () => { - return ( - - ); - }; - const resetSearch = () => { queryParams.delete(searchQueryKey); history.push({ search: queryParams.toString() }); @@ -345,61 +332,44 @@ export const ImagesLanding = () => { onCancelFailed: onCancelFailedClick, onDelete: openDialog, onDeploy: deployNewLinode, - onEdit: setEditDrawerImage, + onEdit: (image) => { + setSelectedImageId(image.id); + setIsEditDrawerOpen(true); + }, onManageRegions: multiRegionsEnabled - ? setManageRegionsDrawerImage + ? (image) => { + setSelectedImageId(image.id); + setIsManageRegionsDrawerOpen(true); + } : undefined, - onRestore: setRebuildDrawerImage, + onRestore: (image) => { + setSelectedImageId(image.id); + setIsRebuildDrawerOpen(true); + }, onRetry: onRetryClick, }; - const renderError = (_: APIError[]) => { + if (manualImagesLoading || automaticImagesLoading) { + return ; + } + + if (manualImagesError || automaticImagesError) { return ( ); - }; - - const renderLoading = () => { - return ; - }; - - const renderEmpty = () => { - return ; - }; - - if (manualImagesLoading || automaticImagesLoading) { - return renderLoading(); - } - - /** Error State */ - if (manualImagesError) { - return renderError(manualImagesError); - } - - if (automaticImagesError) { - return renderError(automaticImagesError); } - /** Empty States */ if ( - !manualImages.data.length && - !automaticImages.data.length && + manualImages.results === 0 && + automaticImages.results === 0 && !imageLabelFromParam ) { - return renderEmpty(); + return ; } - const noManualImages = ( - - ); - - const noAutomaticImages = ( - - ); - const isFetching = manualImagesIsFetching || automaticImagesIsFetching; return ( @@ -501,17 +471,21 @@ export const ImagesLanding = () => { - {manualImages.data.length > 0 - ? manualImages.data.map((manualImage) => ( - - )) - : noManualImages} + {manualImages.results === 0 && ( + + )} + {manualImages.data.map((manualImage) => ( + + ))}
{ - {isFetching ? ( - - ) : automaticImages.data.length > 0 ? ( - automaticImages.data.map((automaticImage) => ( - - )) - ) : ( - noAutomaticImages + {automaticImages.results === 0 && ( + )} + {automaticImages.data.map((automaticImage) => ( + + ))} { /> setEditDrawerImage(undefined)} + image={selectedImage} + onClose={() => setIsEditDrawerOpen(false)} + open={isEditDrawerOpen} /> setRebuildDrawerImage(undefined)} + image={selectedImage} + onClose={() => setIsRebuildDrawerOpen(false)} + open={isRebuildDrawerOpen} /> + setIsManageRegionsDrawerOpen(false)} + open={isManageRegionsDrawerOpen} + title={`Manage Regions for ${selectedImage?.label}`} + > + setIsManageRegionsDrawerOpen(false)} + /> + + } title={ dialogAction === 'cancel' ? 'Cancel Upload' : `Delete Image ${dialog.image}` } - actions={getActions} onClose={closeDialog} open={dialog.open} > diff --git a/packages/manager/src/features/Images/ImagesLanding/RebuildImageDrawer.tsx b/packages/manager/src/features/Images/ImagesLanding/RebuildImageDrawer.tsx index c5d49a70a8a..dc2bf134a93 100644 --- a/packages/manager/src/features/Images/ImagesLanding/RebuildImageDrawer.tsx +++ b/packages/manager/src/features/Images/ImagesLanding/RebuildImageDrawer.tsx @@ -18,10 +18,11 @@ import type { Image } from '@linode/api-v4'; interface Props { image: Image | undefined; onClose: () => void; + open: boolean; } export const RebuildImageDrawer = (props: Props) => { - const { image, onClose } = props; + const { image, onClose, open } = props; const history = useHistory(); const { @@ -54,7 +55,7 @@ export const RebuildImageDrawer = (props: Props) => { diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index 19113ef57f0..198d63e9244 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -107,6 +107,7 @@ import type { ObjectStorageKeyRequest, SecurityQuestionsPayload, TokenRequest, + UpdateImageRegionsPayload, User, VolumeStatus, } from '@linode/api-v4'; @@ -697,6 +698,21 @@ export const handlers = [ return HttpResponse.json(makeResourcePage(images)); }), + http.post( + '*/v4/images/:id/regions', + async ({ request }) => { + const data = await request.json(); + + const image = imageFactory.build(); + + image.regions = data.regions.map((regionId) => ({ + region: regionId, + status: 'pending replication', + })); + + return HttpResponse.json(image); + } + ), http.get('*/linode/types', () => { return HttpResponse.json( diff --git a/packages/manager/src/queries/images.ts b/packages/manager/src/queries/images.ts index 93d2717850b..66cc2eab3d4 100644 --- a/packages/manager/src/queries/images.ts +++ b/packages/manager/src/queries/images.ts @@ -2,12 +2,14 @@ import { CreateImagePayload, Image, ImageUploadPayload, + UpdateImageRegionsPayload, UploadImageResponse, createImage, deleteImage, getImage, getImages, updateImage, + updateImageRegions, uploadImage, } from '@linode/api-v4'; import { @@ -134,6 +136,21 @@ export const useUploadImageMutation = () => { }); }; +export const useUpdateImageRegionsMutation = (imageId: string) => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (data) => updateImageRegions(imageId, data), + onSuccess(image) { + queryClient.invalidateQueries(imageQueries.paginated._def); + queryClient.invalidateQueries(imageQueries.all._def); + queryClient.setQueryData( + imageQueries.image(image.id).queryKey, + image + ); + }, + }); +}; + export const imageEventsHandler = ({ event, queryClient, From 4a452db307c278acb98e4031ffa0add67e3bd524 Mon Sep 17 00:00:00 2001 From: Alban Bailly <130582365+abailly-akamai@users.noreply.github.com> Date: Wed, 3 Jul 2024 14:39:41 -0400 Subject: [PATCH 34/37] change: [M3-8322] - Add Design Update Global Notification Banner (#10640) * initial commit: save progress * update with dynamic flag data * Added changeset: Design update dismissible banner --- .../pr-10640-added-1720026539138.md | 5 +++ packages/manager/src/featureFlags.ts | 7 ++++ .../GlobalNotifications.tsx | 3 ++ .../TokensUpdateBanner.tsx | 39 +++++++++++++++++++ 4 files changed, 54 insertions(+) create mode 100644 packages/manager/.changeset/pr-10640-added-1720026539138.md create mode 100644 packages/manager/src/features/GlobalNotifications/TokensUpdateBanner.tsx diff --git a/packages/manager/.changeset/pr-10640-added-1720026539138.md b/packages/manager/.changeset/pr-10640-added-1720026539138.md new file mode 100644 index 00000000000..c52317f7c4b --- /dev/null +++ b/packages/manager/.changeset/pr-10640-added-1720026539138.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Added +--- + +Design update dismissible banner ([#10640](https://github.com/linode/manager/pull/10640)) diff --git a/packages/manager/src/featureFlags.ts b/packages/manager/src/featureFlags.ts index ff4ae7b05d1..632dcedfac4 100644 --- a/packages/manager/src/featureFlags.ts +++ b/packages/manager/src/featureFlags.ts @@ -66,13 +66,20 @@ interface AclpFlag { interface gpuV2 { planDivider: boolean; } + type OneClickApp = Record; +interface DesignUpdatesBannerFlag extends BaseFeatureFlag { + key: string; + link: string; +} + export interface Flags { aclb: boolean; aclbFullCreateFlow: boolean; aclp: AclpFlag; apiMaintenance: APIMaintenance; + cloudManagerDesignUpdatesBanner: DesignUpdatesBannerFlag; databaseBeta: boolean; databaseResize: boolean; databases: boolean; diff --git a/packages/manager/src/features/GlobalNotifications/GlobalNotifications.tsx b/packages/manager/src/features/GlobalNotifications/GlobalNotifications.tsx index 756ad8b912e..2d600740b1b 100644 --- a/packages/manager/src/features/GlobalNotifications/GlobalNotifications.tsx +++ b/packages/manager/src/features/GlobalNotifications/GlobalNotifications.tsx @@ -17,7 +17,9 @@ import { ComplianceUpdateModal } from './ComplianceUpdateModal'; import { EmailBounceNotificationSection } from './EmailBounce'; import { RegionStatusBanner } from './RegionStatusBanner'; import { TaxCollectionBanner } from './TaxCollectionBanner'; +import { DesignUpdateBanner } from './TokensUpdateBanner'; import { VerificationDetailsBanner } from './VerificationDetailsBanner'; + export const GlobalNotifications = () => { const flags = useFlags(); const { data: profile } = useProfile(); @@ -51,6 +53,7 @@ export const GlobalNotifications = () => { return ( <> + diff --git a/packages/manager/src/features/GlobalNotifications/TokensUpdateBanner.tsx b/packages/manager/src/features/GlobalNotifications/TokensUpdateBanner.tsx new file mode 100644 index 00000000000..73efd621c61 --- /dev/null +++ b/packages/manager/src/features/GlobalNotifications/TokensUpdateBanner.tsx @@ -0,0 +1,39 @@ +import * as React from 'react'; + +import { DismissibleBanner } from 'src/components/DismissibleBanner/DismissibleBanner'; +import { Link } from 'src/components/Link'; +import { Typography } from 'src/components/Typography'; +import { useFlags } from 'src/hooks/useFlags'; + +export const DesignUpdateBanner = () => { + const flags = useFlags(); + const designUpdateFlag = flags.cloudManagerDesignUpdatesBanner; + + if (!designUpdateFlag || !designUpdateFlag.enabled) { + return null; + } + const { key, link } = designUpdateFlag; + + /** + * This banner is a reusable banner for future Cloud Manager design updates. + * Since this banner is dismissible, we want to be able to dynamically change the key, + * so we can show it again as needed to users who have dismissed it in the past in the case of a new series of UI updates. + * + * Flag shape is as follows: + * + * { + * "enabled": boolean, + * "key": "some-key", + * "link": "link to docs" + * } + * + */ + return ( + + + We are improving the Cloud Manager experience for our users.{' '} + Read more about recent updates. + + + ); +}; From f707c4dd5b17375f65a8db2c4ea4cedbe1d8c645 Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Wed, 3 Jul 2024 15:30:56 -0400 Subject: [PATCH 35/37] upcoming: [M3-8320] - Add Image distributed compatibility notice to Linode Create (#10636) * add notice to both create flows with unit tests * fix import * fix error prop * Added changeset: Add Image distributed compatibility notice to Linode Create * Apply suggestions from code review Co-authored-by: Mariah Jacobs <114685994+mjac0bs@users.noreply.github.com> * update tooltip to say `distributed compute regions` insted of `distributed regions` --------- Co-authored-by: Banks Nussman Co-authored-by: Mariah Jacobs <114685994+mjac0bs@users.noreply.github.com> --- ...r-10636-upcoming-features-1719935270528.md | 5 ++ .../components/ImageSelect/ImageOption.tsx | 2 +- .../ImageSelect/ImageSelect.test.tsx | 27 ++++++- .../components/ImageSelect/ImageSelect.tsx | 70 ++++++++++++------- .../ImageSelectv2/ImageOptionv2.test.tsx | 4 +- .../ImageSelectv2/ImageOptionv2.tsx | 2 +- .../LinodeCreatev2/Tabs/Images.test.tsx | 26 +++++++ .../Linodes/LinodeCreatev2/Tabs/Images.tsx | 43 +++++++++--- 8 files changed, 142 insertions(+), 37 deletions(-) create mode 100644 packages/manager/.changeset/pr-10636-upcoming-features-1719935270528.md diff --git a/packages/manager/.changeset/pr-10636-upcoming-features-1719935270528.md b/packages/manager/.changeset/pr-10636-upcoming-features-1719935270528.md new file mode 100644 index 00000000000..a8f6817c173 --- /dev/null +++ b/packages/manager/.changeset/pr-10636-upcoming-features-1719935270528.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add Image distributed compatibility notice to Linode Create ([#10636](https://github.com/linode/manager/pull/10636)) diff --git a/packages/manager/src/components/ImageSelect/ImageOption.tsx b/packages/manager/src/components/ImageSelect/ImageOption.tsx index b1da36086ea..c619d845fa2 100644 --- a/packages/manager/src/components/ImageSelect/ImageOption.tsx +++ b/packages/manager/src/components/ImageSelect/ImageOption.tsx @@ -74,7 +74,7 @@ export const ImageOption = (props: ImageOptionProps) => { {data.isDistributedCompatible && ( - +
diff --git a/packages/manager/src/components/ImageSelect/ImageSelect.test.tsx b/packages/manager/src/components/ImageSelect/ImageSelect.test.tsx index 65eb904c708..76b63511e1d 100644 --- a/packages/manager/src/components/ImageSelect/ImageSelect.test.tsx +++ b/packages/manager/src/components/ImageSelect/ImageSelect.test.tsx @@ -1,8 +1,10 @@ import { DateTime } from 'luxon'; +import React from 'react'; import { imageFactory } from 'src/factories'; +import { renderWithTheme } from 'src/utilities/testHelpers'; -import { imagesToGroupedItems } from './ImageSelect'; +import { ImageSelect, imagesToGroupedItems } from './ImageSelect'; describe('imagesToGroupedItems', () => { it('should filter deprecated images when end of life is past beyond 6 months ', () => { @@ -94,3 +96,26 @@ describe('imagesToGroupedItems', () => { expect(imagesToGroupedItems(images)).toStrictEqual(expected); }); }); + +describe('ImageSelect', () => { + it('renders a "Indicates compatibility with distributed compute regions." notice if the user has at least one image with the distributed capability', async () => { + const images = [ + imageFactory.build({ capabilities: [] }), + imageFactory.build({ capabilities: ['distributed-images'] }), + imageFactory.build({ capabilities: [] }), + ]; + + const { getByText } = renderWithTheme( + + ); + + expect( + getByText('Indicates compatibility with distributed compute regions.') + ).toBeVisible(); + }); +}); diff --git a/packages/manager/src/components/ImageSelect/ImageSelect.tsx b/packages/manager/src/components/ImageSelect/ImageSelect.tsx index f2636bc9422..1fdbb9a4498 100644 --- a/packages/manager/src/components/ImageSelect/ImageSelect.tsx +++ b/packages/manager/src/components/ImageSelect/ImageSelect.tsx @@ -1,13 +1,11 @@ -import { Image } from '@linode/api-v4/lib/images'; -import Grid from '@mui/material/Unstable_Grid2'; import produce from 'immer'; import { DateTime } from 'luxon'; import { equals, groupBy } from 'ramda'; import * as React from 'react'; -import Select, { GroupType, Item } from 'src/components/EnhancedSelect'; +import DistributedRegionIcon from 'src/assets/icons/entityIcons/distributed-region.svg'; +import Select from 'src/components/EnhancedSelect'; import { _SingleValue } from 'src/components/EnhancedSelect/components/SingleValue'; -import { BaseSelectProps } from 'src/components/EnhancedSelect/Select'; import { ImageOption } from 'src/components/ImageSelect/ImageOption'; import { Paper } from 'src/components/Paper'; import { Typography } from 'src/components/Typography'; @@ -17,7 +15,13 @@ import { arePropsEqual } from 'src/utilities/arePropsEqual'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { getSelectedOptionFromGroupedOptions } from 'src/utilities/getSelectedOptionFromGroupedOptions'; +import { Box } from '../Box'; import { distroIcons } from '../DistributionIcon'; +import { Stack } from '../Stack'; + +import type { Image } from '@linode/api-v4/lib/images'; +import type { GroupType, Item } from 'src/components/EnhancedSelect'; +import type { BaseSelectProps } from 'src/components/EnhancedSelect/Select'; export type Variant = 'all' | 'private' | 'public'; @@ -151,12 +155,12 @@ export const ImageSelect = React.memo((props: ImageSelectProps) => { const { classNames, disabled, + error: errorText, handleSelectImage, images, selectedImageID, title, variant, - ...reactSelectProps } = props; // Check for loading status and request errors in React Query @@ -203,31 +207,47 @@ export const ImageSelect = React.memo((props: ImageSelectProps) => { return handleSelectImage(selection.value, selectedImage); }; + const showDistributedCapabilityNotice = + variant === 'private' && + filteredImages.some((image) => + image.capabilities.includes('distributed-images') + ); + return ( {title} - - - + {showDistributedCapabilityNotice && ( + + + + Indicates compatibility with distributed compute regions. + + + )} + ); }, isMemo); diff --git a/packages/manager/src/components/ImageSelectv2/ImageOptionv2.test.tsx b/packages/manager/src/components/ImageSelectv2/ImageOptionv2.test.tsx index 33923a9f889..67da3a0bbf6 100644 --- a/packages/manager/src/components/ImageSelectv2/ImageOptionv2.test.tsx +++ b/packages/manager/src/components/ImageSelectv2/ImageOptionv2.test.tsx @@ -44,7 +44,9 @@ describe('ImageOptionv2', () => { ); expect( - getByLabelText('This image is compatible with distributed regions.') + getByLabelText( + 'This image is compatible with distributed compute regions.' + ) ).toBeVisible(); }); }); diff --git a/packages/manager/src/components/ImageSelectv2/ImageOptionv2.tsx b/packages/manager/src/components/ImageSelectv2/ImageOptionv2.tsx index d8ceb098d02..c1d8c139f3f 100644 --- a/packages/manager/src/components/ImageSelectv2/ImageOptionv2.tsx +++ b/packages/manager/src/components/ImageSelectv2/ImageOptionv2.tsx @@ -40,7 +40,7 @@ export const ImageOptionv2 = ({ image, isSelected, listItemProps }: Props) => {
{image.capabilities.includes('distributed-images') && ( - +
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Images.test.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Images.test.tsx index 4ba95eac43a..b2565653537 100644 --- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Images.test.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Images.test.tsx @@ -3,6 +3,9 @@ import React from 'react'; import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers'; import { Images } from './Images'; +import { HttpResponse, http, server } from 'src/mocks/testServer'; +import { imageFactory } from 'src/factories'; +import { makeResourcePage } from 'src/mocks/serverHandlers'; describe('Images', () => { it('renders a header', () => { @@ -27,4 +30,27 @@ describe('Images', () => { expect(getByLabelText('Images')).toBeVisible(); expect(getByPlaceholderText('Choose an image')).toBeVisible(); }); + + it('renders a "Indicates compatibility with distributed compute regions." notice if the user has at least one image with the distributed capability', async () => { + server.use( + http.get('*/v4/images', () => { + const images = [ + imageFactory.build({ capabilities: [] }), + imageFactory.build({ capabilities: ['distributed-images'] }), + imageFactory.build({ capabilities: [] }), + ]; + return HttpResponse.json(makeResourcePage(images)); + }) + ); + + const { findByText } = renderWithThemeAndHookFormContext({ + component: , + }); + + expect( + await findByText( + 'Indicates compatibility with distributed compute regions.' + ) + ).toBeVisible(); + }); }); diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Images.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Images.tsx index 0962cb7549b..17542f0e313 100644 --- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Images.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Images.tsx @@ -1,10 +1,15 @@ import React from 'react'; import { useController, useFormContext, useWatch } from 'react-hook-form'; +import DistributedRegionIcon from 'src/assets/icons/entityIcons/distributed-region.svg'; +import { Box } from 'src/components/Box'; import { ImageSelectv2 } from 'src/components/ImageSelectv2/ImageSelectv2'; +import { getAPIFilterForImageSelect } from 'src/components/ImageSelectv2/utilities'; import { Paper } from 'src/components/Paper'; +import { Stack } from 'src/components/Stack'; import { Typography } from 'src/components/Typography'; import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; +import { useAllImagesQuery } from 'src/queries/images'; import { useRegionsQuery } from 'src/queries/regions/regions'; import type { LinodeCreateFormValues } from '../utilities'; @@ -32,6 +37,7 @@ export const Images = () => { // Non-"distributed compatible" Images must only be deployed to core sites. // Clear the region field if the currently selected region is a distributed site and the Image is only core compatible. + // @todo: delete this logic when all Images are "distributed compatible" if ( image && !image.capabilities.includes('distributed-images') && @@ -41,17 +47,38 @@ export const Images = () => { } }; + const { data: images } = useAllImagesQuery( + {}, + getAPIFilterForImageSelect('private') + ); + + // @todo: delete this logic when all Images are "distributed compatible" + const showDistributedCapabilityNotice = images?.some((image) => + image.capabilities.includes('distributed-images') + ); + return ( Choose an Image - + + + {showDistributedCapabilityNotice && ( + + + + Indicates compatibility with distributed compute regions. + + + )} + ); }; From 5eec0df2af2bc6df1f491339e31b662ba3bb61fe Mon Sep 17 00:00:00 2001 From: Dajahi Wiley Date: Mon, 8 Jul 2024 10:29:10 -0400 Subject: [PATCH 36/37] Cloud version 1.123.0, API v4 version 0.121.0, and Validation version 0.49.0 --- ...r-10589-upcoming-features-1718971604339.md | 5 -- .../pr-10617-changed-1719590161430.md | 5 -- packages/api-v4/CHANGELOG.md | 10 ++++ packages/api-v4/package.json | 2 +- .../pr-10022-added-1703703204947.md | 5 -- ...r-10479-upcoming-features-1718906242285.md | 5 -- .../pr-10542-tests-1719335118630.md | 5 -- .../pr-10557-tech-stories-1718728411161.md | 5 -- .../pr-10579-tests-1718297804893.md | 5 -- .../pr-10584-fixed-1718376872460.md | 5 -- ...r-10589-upcoming-features-1718716331830.md | 5 -- .../pr-10593-tests-1718736692043.md | 5 -- .../pr-10594-changed-1718748465150.md | 5 -- .../pr-10598-tech-stories-1718897422896.md | 5 -- .../pr-10599-fixed-1718922229335.md | 5 -- .../pr-10602-tests-1718985699307.md | 5 -- .../pr-10604-changed-1719003322690.md | 5 -- ...r-10607-upcoming-features-1719247291579.md | 5 -- .../pr-10609-tests-1719255666019.md | 5 -- ...r-10611-upcoming-features-1719332278758.md | 5 -- .../pr-10612-tests-1719343415784.md | 5 -- ...r-10613-upcoming-features-1719355193187.md | 5 -- .../pr-10614-changed-1719356418613.md | 5 -- .../pr-10615-tests-1719412831217.md | 5 -- ...r-10617-upcoming-features-1719524941884.md | 5 -- .../pr-10618-fixed-1719521792524.md | 5 -- .../pr-10619-tech-stories-1719459021760.md | 5 -- .../pr-10622-tests-1719519067716.md | 5 -- ...r-10623-upcoming-features-1719595144995.md | 5 -- ...r-10628-upcoming-features-1719846490755.md | 5 -- ...r-10629-upcoming-features-1719848495146.md | 5 -- .../pr-10633-tests-1719934397905.md | 5 -- ...r-10636-upcoming-features-1719935270528.md | 5 -- ...r-10637-upcoming-features-1719936723714.md | 5 -- .../pr-10640-added-1720026539138.md | 5 -- .../pr-10642-removed-1720016794302.md | 5 -- packages/manager/CHANGELOG.md | 54 +++++++++++++++++++ packages/manager/package.json | 4 +- .../pr-10557-added-1718728514265.md | 5 -- packages/validation/CHANGELOG.md | 14 +++-- packages/validation/package.json | 2 +- 41 files changed, 74 insertions(+), 187 deletions(-) delete mode 100644 packages/api-v4/.changeset/pr-10589-upcoming-features-1718971604339.md delete mode 100644 packages/api-v4/.changeset/pr-10617-changed-1719590161430.md delete mode 100644 packages/manager/.changeset/pr-10022-added-1703703204947.md delete mode 100644 packages/manager/.changeset/pr-10479-upcoming-features-1718906242285.md delete mode 100644 packages/manager/.changeset/pr-10542-tests-1719335118630.md delete mode 100644 packages/manager/.changeset/pr-10557-tech-stories-1718728411161.md delete mode 100644 packages/manager/.changeset/pr-10579-tests-1718297804893.md delete mode 100644 packages/manager/.changeset/pr-10584-fixed-1718376872460.md delete mode 100644 packages/manager/.changeset/pr-10589-upcoming-features-1718716331830.md delete mode 100644 packages/manager/.changeset/pr-10593-tests-1718736692043.md delete mode 100644 packages/manager/.changeset/pr-10594-changed-1718748465150.md delete mode 100644 packages/manager/.changeset/pr-10598-tech-stories-1718897422896.md delete mode 100644 packages/manager/.changeset/pr-10599-fixed-1718922229335.md delete mode 100644 packages/manager/.changeset/pr-10602-tests-1718985699307.md delete mode 100644 packages/manager/.changeset/pr-10604-changed-1719003322690.md delete mode 100644 packages/manager/.changeset/pr-10607-upcoming-features-1719247291579.md delete mode 100644 packages/manager/.changeset/pr-10609-tests-1719255666019.md delete mode 100644 packages/manager/.changeset/pr-10611-upcoming-features-1719332278758.md delete mode 100644 packages/manager/.changeset/pr-10612-tests-1719343415784.md delete mode 100644 packages/manager/.changeset/pr-10613-upcoming-features-1719355193187.md delete mode 100644 packages/manager/.changeset/pr-10614-changed-1719356418613.md delete mode 100644 packages/manager/.changeset/pr-10615-tests-1719412831217.md delete mode 100644 packages/manager/.changeset/pr-10617-upcoming-features-1719524941884.md delete mode 100644 packages/manager/.changeset/pr-10618-fixed-1719521792524.md delete mode 100644 packages/manager/.changeset/pr-10619-tech-stories-1719459021760.md delete mode 100644 packages/manager/.changeset/pr-10622-tests-1719519067716.md delete mode 100644 packages/manager/.changeset/pr-10623-upcoming-features-1719595144995.md delete mode 100644 packages/manager/.changeset/pr-10628-upcoming-features-1719846490755.md delete mode 100644 packages/manager/.changeset/pr-10629-upcoming-features-1719848495146.md delete mode 100644 packages/manager/.changeset/pr-10633-tests-1719934397905.md delete mode 100644 packages/manager/.changeset/pr-10636-upcoming-features-1719935270528.md delete mode 100644 packages/manager/.changeset/pr-10637-upcoming-features-1719936723714.md delete mode 100644 packages/manager/.changeset/pr-10640-added-1720026539138.md delete mode 100644 packages/manager/.changeset/pr-10642-removed-1720016794302.md delete mode 100644 packages/validation/.changeset/pr-10557-added-1718728514265.md diff --git a/packages/api-v4/.changeset/pr-10589-upcoming-features-1718971604339.md b/packages/api-v4/.changeset/pr-10589-upcoming-features-1718971604339.md deleted file mode 100644 index 31b076dc1d7..00000000000 --- a/packages/api-v4/.changeset/pr-10589-upcoming-features-1718971604339.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/api-v4": Upcoming Features ---- - -Added types needed for DashboardSelect component ([#10589](https://github.com/linode/manager/pull/10589)) diff --git a/packages/api-v4/.changeset/pr-10617-changed-1719590161430.md b/packages/api-v4/.changeset/pr-10617-changed-1719590161430.md deleted file mode 100644 index 1f7c25a4e35..00000000000 --- a/packages/api-v4/.changeset/pr-10617-changed-1719590161430.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/api-v4": Changed ---- - -Update `updateImageRegions` to accept `UpdateImageRegionsPayload` instead of `regions: string[]` ([#10617](https://github.com/linode/manager/pull/10617)) diff --git a/packages/api-v4/CHANGELOG.md b/packages/api-v4/CHANGELOG.md index 75ab194a0c6..9af57c6599f 100644 --- a/packages/api-v4/CHANGELOG.md +++ b/packages/api-v4/CHANGELOG.md @@ -1,3 +1,13 @@ +## [2024-07-08] - v0.121.0 + +### Changed: + +- Update `updateImageRegions` to accept `UpdateImageRegionsPayload` instead of `regions: string[]` ([#10617](https://github.com/linode/manager/pull/10617)) + +### Upcoming Features: + +- Added types needed for DashboardSelect component ([#10589](https://github.com/linode/manager/pull/10589)) + ## [2024-06-24] - v0.120.0 ### Added: diff --git a/packages/api-v4/package.json b/packages/api-v4/package.json index 99d23eaffec..792784321bb 100644 --- a/packages/api-v4/package.json +++ b/packages/api-v4/package.json @@ -1,6 +1,6 @@ { "name": "@linode/api-v4", - "version": "0.120.0", + "version": "0.121.0", "homepage": "https://github.com/linode/manager/tree/develop/packages/api-v4", "bugs": { "url": "https://github.com/linode/manager/issues" diff --git a/packages/manager/.changeset/pr-10022-added-1703703204947.md b/packages/manager/.changeset/pr-10022-added-1703703204947.md deleted file mode 100644 index 1a6769e2894..00000000000 --- a/packages/manager/.changeset/pr-10022-added-1703703204947.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Added ---- - -Added Design Tokens (CDS 2.0) ([#10022](https://github.com/linode/manager/pull/10022)) diff --git a/packages/manager/.changeset/pr-10479-upcoming-features-1718906242285.md b/packages/manager/.changeset/pr-10479-upcoming-features-1718906242285.md deleted file mode 100644 index 63d0181b024..00000000000 --- a/packages/manager/.changeset/pr-10479-upcoming-features-1718906242285.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Gecko GA Region Select ([#10479](https://github.com/linode/manager/pull/10479)) diff --git a/packages/manager/.changeset/pr-10542-tests-1719335118630.md b/packages/manager/.changeset/pr-10542-tests-1719335118630.md deleted file mode 100644 index b5a04fd34d0..00000000000 --- a/packages/manager/.changeset/pr-10542-tests-1719335118630.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tests ---- - -Cypress integration test for SSH key update and delete ([#10542](https://github.com/linode/manager/pull/10542)) diff --git a/packages/manager/.changeset/pr-10557-tech-stories-1718728411161.md b/packages/manager/.changeset/pr-10557-tech-stories-1718728411161.md deleted file mode 100644 index 4dbee382763..00000000000 --- a/packages/manager/.changeset/pr-10557-tech-stories-1718728411161.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tech Stories ---- - -Refactor `SupportTicketDialog` with React Hook Form ([#10557](https://github.com/linode/manager/pull/10557)) diff --git a/packages/manager/.changeset/pr-10579-tests-1718297804893.md b/packages/manager/.changeset/pr-10579-tests-1718297804893.md deleted file mode 100644 index ecd89ebaf74..00000000000 --- a/packages/manager/.changeset/pr-10579-tests-1718297804893.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tests ---- - -Refactor Cypress Longview test to use mock API data/events ([#10579](https://github.com/linode/manager/pull/10579)) diff --git a/packages/manager/.changeset/pr-10584-fixed-1718376872460.md b/packages/manager/.changeset/pr-10584-fixed-1718376872460.md deleted file mode 100644 index 586c1a96686..00000000000 --- a/packages/manager/.changeset/pr-10584-fixed-1718376872460.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Fixed ---- - -Potential runtime issue with conditional hook ([#10584](https://github.com/linode/manager/pull/10584)) diff --git a/packages/manager/.changeset/pr-10589-upcoming-features-1718716331830.md b/packages/manager/.changeset/pr-10589-upcoming-features-1718716331830.md deleted file mode 100644 index 2d179125d88..00000000000 --- a/packages/manager/.changeset/pr-10589-upcoming-features-1718716331830.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Added Dashboard Selection component inside the Global Filters of CloudPulse view. ([#10589](https://github.com/linode/manager/pull/10589)) diff --git a/packages/manager/.changeset/pr-10593-tests-1718736692043.md b/packages/manager/.changeset/pr-10593-tests-1718736692043.md deleted file mode 100644 index 7fb89a5ac1d..00000000000 --- a/packages/manager/.changeset/pr-10593-tests-1718736692043.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tests ---- - -Add assertions for created LKE cluster in Cypress LKE tests ([#10593](https://github.com/linode/manager/pull/10593)) diff --git a/packages/manager/.changeset/pr-10594-changed-1718748465150.md b/packages/manager/.changeset/pr-10594-changed-1718748465150.md deleted file mode 100644 index 4f2629f6b4f..00000000000 --- a/packages/manager/.changeset/pr-10594-changed-1718748465150.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Changed ---- - -Rebuild Linode drawer ([#10594](https://github.com/linode/manager/pull/10594)) diff --git a/packages/manager/.changeset/pr-10598-tech-stories-1718897422896.md b/packages/manager/.changeset/pr-10598-tech-stories-1718897422896.md deleted file mode 100644 index 9dfffac3dd4..00000000000 --- a/packages/manager/.changeset/pr-10598-tech-stories-1718897422896.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tech Stories ---- - -Query Key Factory for ACLB ([#10598](https://github.com/linode/manager/pull/10598)) diff --git a/packages/manager/.changeset/pr-10599-fixed-1718922229335.md b/packages/manager/.changeset/pr-10599-fixed-1718922229335.md deleted file mode 100644 index fc9d4f21be1..00000000000 --- a/packages/manager/.changeset/pr-10599-fixed-1718922229335.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Fixed ---- - -Visual bug inside Node Pools Table ([#10599](https://github.com/linode/manager/pull/10599)) diff --git a/packages/manager/.changeset/pr-10602-tests-1718985699307.md b/packages/manager/.changeset/pr-10602-tests-1718985699307.md deleted file mode 100644 index 82ccb0de864..00000000000 --- a/packages/manager/.changeset/pr-10602-tests-1718985699307.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tests ---- - -Update Object Storage tests to mock account capabilities as needed for multi cluster ([#10602](https://github.com/linode/manager/pull/10602)) diff --git a/packages/manager/.changeset/pr-10604-changed-1719003322690.md b/packages/manager/.changeset/pr-10604-changed-1719003322690.md deleted file mode 100644 index 58c69476395..00000000000 --- a/packages/manager/.changeset/pr-10604-changed-1719003322690.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Changed ---- - -Auto-populate Image label based on Linode and Disk names ([#10604](https://github.com/linode/manager/pull/10604)) diff --git a/packages/manager/.changeset/pr-10607-upcoming-features-1719247291579.md b/packages/manager/.changeset/pr-10607-upcoming-features-1719247291579.md deleted file mode 100644 index 695c55b6a9a..00000000000 --- a/packages/manager/.changeset/pr-10607-upcoming-features-1719247291579.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Conditionally disable regions based on the selected image on Linode Create ([#10607](https://github.com/linode/manager/pull/10607)) diff --git a/packages/manager/.changeset/pr-10609-tests-1719255666019.md b/packages/manager/.changeset/pr-10609-tests-1719255666019.md deleted file mode 100644 index 36fd2352e8a..00000000000 --- a/packages/manager/.changeset/pr-10609-tests-1719255666019.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tests ---- - -Fix OBJ test failure caused by visiting hardcoded and out-of-date URL ([#10609](https://github.com/linode/manager/pull/10609)) diff --git a/packages/manager/.changeset/pr-10611-upcoming-features-1719332278758.md b/packages/manager/.changeset/pr-10611-upcoming-features-1719332278758.md deleted file mode 100644 index c3bc81d8170..00000000000 --- a/packages/manager/.changeset/pr-10611-upcoming-features-1719332278758.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Prevent Linode Create v2 from toggling mid-creation ([#10611](https://github.com/linode/manager/pull/10611)) diff --git a/packages/manager/.changeset/pr-10612-tests-1719343415784.md b/packages/manager/.changeset/pr-10612-tests-1719343415784.md deleted file mode 100644 index 8c3b73cf285..00000000000 --- a/packages/manager/.changeset/pr-10612-tests-1719343415784.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tests ---- - -Combine VPC details page subnet create, edit, and delete Cypress tests ([#10612](https://github.com/linode/manager/pull/10612)) diff --git a/packages/manager/.changeset/pr-10613-upcoming-features-1719355193187.md b/packages/manager/.changeset/pr-10613-upcoming-features-1719355193187.md deleted file mode 100644 index 410b25c641b..00000000000 --- a/packages/manager/.changeset/pr-10613-upcoming-features-1719355193187.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Add new search query parser to Linode Create v2 StackScripts tab ([#10613](https://github.com/linode/manager/pull/10613)) diff --git a/packages/manager/.changeset/pr-10614-changed-1719356418613.md b/packages/manager/.changeset/pr-10614-changed-1719356418613.md deleted file mode 100644 index 96847464667..00000000000 --- a/packages/manager/.changeset/pr-10614-changed-1719356418613.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Changed ---- - -Update Linode disk action menu ([#10614](https://github.com/linode/manager/pull/10614)) diff --git a/packages/manager/.changeset/pr-10615-tests-1719412831217.md b/packages/manager/.changeset/pr-10615-tests-1719412831217.md deleted file mode 100644 index 0c93492cabc..00000000000 --- a/packages/manager/.changeset/pr-10615-tests-1719412831217.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tests ---- - -De-Parameterize Cypress Domain Record Create Tests ([#10615](https://github.com/linode/manager/pull/10615)) diff --git a/packages/manager/.changeset/pr-10617-upcoming-features-1719524941884.md b/packages/manager/.changeset/pr-10617-upcoming-features-1719524941884.md deleted file mode 100644 index 5047c2d920a..00000000000 --- a/packages/manager/.changeset/pr-10617-upcoming-features-1719524941884.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Add Manage Image Regions Drawer ([#10617](https://github.com/linode/manager/pull/10617)) diff --git a/packages/manager/.changeset/pr-10618-fixed-1719521792524.md b/packages/manager/.changeset/pr-10618-fixed-1719521792524.md deleted file mode 100644 index ebceb5f44ad..00000000000 --- a/packages/manager/.changeset/pr-10618-fixed-1719521792524.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Fixed ---- - -Linode Resize dialog UX when linode data is loading or there is an error ([#10618](https://github.com/linode/manager/pull/10618)) diff --git a/packages/manager/.changeset/pr-10619-tech-stories-1719459021760.md b/packages/manager/.changeset/pr-10619-tech-stories-1719459021760.md deleted file mode 100644 index 7bd3fb5ae3d..00000000000 --- a/packages/manager/.changeset/pr-10619-tech-stories-1719459021760.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tech Stories ---- - -Make `Factory.each` start incrementing at 1 instead of 0 ([#10619](https://github.com/linode/manager/pull/10619)) diff --git a/packages/manager/.changeset/pr-10622-tests-1719519067716.md b/packages/manager/.changeset/pr-10622-tests-1719519067716.md deleted file mode 100644 index db48d82236e..00000000000 --- a/packages/manager/.changeset/pr-10622-tests-1719519067716.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tests ---- - -De-Parameterize Cypress Deep Link Smoke Tests ([#10622](https://github.com/linode/manager/pull/10622)) diff --git a/packages/manager/.changeset/pr-10623-upcoming-features-1719595144995.md b/packages/manager/.changeset/pr-10623-upcoming-features-1719595144995.md deleted file mode 100644 index 7ac2be855fa..00000000000 --- a/packages/manager/.changeset/pr-10623-upcoming-features-1719595144995.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Add Marketplace Cluster pricing support to Linode Create v2 ([#10623](https://github.com/linode/manager/pull/10623)) diff --git a/packages/manager/.changeset/pr-10628-upcoming-features-1719846490755.md b/packages/manager/.changeset/pr-10628-upcoming-features-1719846490755.md deleted file mode 100644 index 4334c6b6240..00000000000 --- a/packages/manager/.changeset/pr-10628-upcoming-features-1719846490755.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Add debouncing to the Linode Create v2 `VLANSelect` ([#10628](https://github.com/linode/manager/pull/10628)) diff --git a/packages/manager/.changeset/pr-10629-upcoming-features-1719848495146.md b/packages/manager/.changeset/pr-10629-upcoming-features-1719848495146.md deleted file mode 100644 index 16ba99d5879..00000000000 --- a/packages/manager/.changeset/pr-10629-upcoming-features-1719848495146.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Add Validation to Linode Create v2 Marketplace Tab ([#10629](https://github.com/linode/manager/pull/10629)) diff --git a/packages/manager/.changeset/pr-10633-tests-1719934397905.md b/packages/manager/.changeset/pr-10633-tests-1719934397905.md deleted file mode 100644 index 4acac81daa3..00000000000 --- a/packages/manager/.changeset/pr-10633-tests-1719934397905.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tests ---- - -Improve security of Linodes created during tests ([#10633](https://github.com/linode/manager/pull/10633)) diff --git a/packages/manager/.changeset/pr-10636-upcoming-features-1719935270528.md b/packages/manager/.changeset/pr-10636-upcoming-features-1719935270528.md deleted file mode 100644 index a8f6817c173..00000000000 --- a/packages/manager/.changeset/pr-10636-upcoming-features-1719935270528.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Add Image distributed compatibility notice to Linode Create ([#10636](https://github.com/linode/manager/pull/10636)) diff --git a/packages/manager/.changeset/pr-10637-upcoming-features-1719936723714.md b/packages/manager/.changeset/pr-10637-upcoming-features-1719936723714.md deleted file mode 100644 index 70820ca8f4c..00000000000 --- a/packages/manager/.changeset/pr-10637-upcoming-features-1719936723714.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Fix Notification Toast in Dark Mode ([#10637](https://github.com/linode/manager/pull/10637)) diff --git a/packages/manager/.changeset/pr-10640-added-1720026539138.md b/packages/manager/.changeset/pr-10640-added-1720026539138.md deleted file mode 100644 index c52317f7c4b..00000000000 --- a/packages/manager/.changeset/pr-10640-added-1720026539138.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Added ---- - -Design update dismissible banner ([#10640](https://github.com/linode/manager/pull/10640)) diff --git a/packages/manager/.changeset/pr-10642-removed-1720016794302.md b/packages/manager/.changeset/pr-10642-removed-1720016794302.md deleted file mode 100644 index 405ae306216..00000000000 --- a/packages/manager/.changeset/pr-10642-removed-1720016794302.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Removed ---- - -Region helper text on the Image Upload page ([#10642](https://github.com/linode/manager/pull/10642)) diff --git a/packages/manager/CHANGELOG.md b/packages/manager/CHANGELOG.md index 220f3ccf276..1edbc296bda 100644 --- a/packages/manager/CHANGELOG.md +++ b/packages/manager/CHANGELOG.md @@ -4,6 +4,60 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [2024-07-08] - v1.123.0 + +### Added: + +- Design Tokens (CDS 2.0) ([#10022](https://github.com/linode/manager/pull/10022)) +- Design update dismissible banner ([#10640](https://github.com/linode/manager/pull/10640)) + +### Changed: + +- Rebuild Linode drawer ([#10594](https://github.com/linode/manager/pull/10594)) +- Auto-populate Image label based on Linode and Disk names ([#10604](https://github.com/linode/manager/pull/10604)) +- Update Linode disk action menu ([#10614](https://github.com/linode/manager/pull/10614)) + +### Fixed: + +- Potential runtime issue with conditional hook ([#10584](https://github.com/linode/manager/pull/10584)) +- Visual bug inside Node Pools table ([#10599](https://github.com/linode/manager/pull/10599)) +- Linode Resize dialog UX when linode data is loading or there is an error ([#10618](https://github.com/linode/manager/pull/10618)) + +### Removed: + +- Region helper text on the Image Upload page ([#10642](https://github.com/linode/manager/pull/10642)) + +### Tech Stories: + +- Refactor `SupportTicketDialog` with React Hook Form ([#10557](https://github.com/linode/manager/pull/10557)) +- Query Key Factory for ACLB ([#10598](https://github.com/linode/manager/pull/10598)) +- Make `Factory.each` start incrementing at 1 instead of 0 ([#10619](https://github.com/linode/manager/pull/10619)) + +### Tests: + +- Cypress integration test for SSH key update and delete ([#10542](https://github.com/linode/manager/pull/10542)) +- Refactor Cypress Longview test to use mock API data/events ([#10579](https://github.com/linode/manager/pull/10579)) +- Add assertions for created LKE cluster in Cypress LKE tests ([#10593](https://github.com/linode/manager/pull/10593)) +- Update Object Storage tests to mock account capabilities as needed for Multicluster ([#10602](https://github.com/linode/manager/pull/10602)) +- Fix OBJ test failure caused by visiting hardcoded and out-of-date URL ([#10609](https://github.com/linode/manager/pull/10609)) +- Combine VPC details page subnet create, edit, and delete Cypress tests ([#10612](https://github.com/linode/manager/pull/10612)) +- De-parameterize Cypress Domain Record Create tests ([#10615](https://github.com/linode/manager/pull/10615)) +- De-parameterize Cypress Deep Link smoke tests ([#10622](https://github.com/linode/manager/pull/10622)) +- Improve security of Linodes created during tests ([#10633](https://github.com/linode/manager/pull/10633)) + +### Upcoming Features: + +- Gecko GA Region Select ([#10479](https://github.com/linode/manager/pull/10479)) +- Add Dashboard Selection component inside the Global Filters of CloudPulse view ([#10589](https://github.com/linode/manager/pull/10589)) +- Conditionally disable regions based on the selected image on Linode Create ([#10607](https://github.com/linode/manager/pull/10607)) +- Prevent Linode Create v2 from toggling mid-creation ([#10611](https://github.com/linode/manager/pull/10611)) +- Add new search query parser to Linode Create v2 StackScripts tab ([#10613](https://github.com/linode/manager/pull/10613)) +- Add ‘Manage Image Regions’ Drawer ([#10617](https://github.com/linode/manager/pull/10617)) +- Add Marketplace Cluster pricing support to Linode Create v2 ([#10623](https://github.com/linode/manager/pull/10623)) +- Add debouncing to the Linode Create v2 `VLANSelect` ([#10628](https://github.com/linode/manager/pull/10628)) +- Add Validation to Linode Create v2 Marketplace Tab ([#10629](https://github.com/linode/manager/pull/10629)) +- Add Image distributed compatibility notice to Linode Create ([#10636](https://github.com/linode/manager/pull/10636)) + ## [2024-06-24] - v1.122.0 ### Added: diff --git a/packages/manager/package.json b/packages/manager/package.json index 2bc53708ac9..9c03007ac00 100644 --- a/packages/manager/package.json +++ b/packages/manager/package.json @@ -2,7 +2,7 @@ "name": "linode-manager", "author": "Linode", "description": "The Linode Manager website", - "version": "1.122.0", + "version": "1.123.0", "private": true, "type": "module", "bugs": { @@ -223,4 +223,4 @@ "Firefox ESR", "not ie < 9" ] -} \ No newline at end of file +} diff --git a/packages/validation/.changeset/pr-10557-added-1718728514265.md b/packages/validation/.changeset/pr-10557-added-1718728514265.md deleted file mode 100644 index c4532414d45..00000000000 --- a/packages/validation/.changeset/pr-10557-added-1718728514265.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/validation": Added ---- - -`createSMTPSupportTicketSchema` to support schemas ([#10557](https://github.com/linode/manager/pull/10557)) diff --git a/packages/validation/CHANGELOG.md b/packages/validation/CHANGELOG.md index b0ba34e3bbc..0f7aed6d817 100644 --- a/packages/validation/CHANGELOG.md +++ b/packages/validation/CHANGELOG.md @@ -1,5 +1,10 @@ -## [2024-06-10] - v0.48.0 +## [2024-07-08] - v0.49.0 + +### Added: +- `createSMTPSupportTicketSchema` to support schemas ([#10557](https://github.com/linode/manager/pull/10557)) + +## [2024-06-10] - v0.48.0 ### Added: @@ -8,7 +13,6 @@ ## [2024-05-28] - v0.47.0 - ### Added: - `tags` to `createImageSchema` ([#10471](https://github.com/linode/manager/pull/10471)) @@ -18,19 +22,15 @@ - Adjust DiskEncryptionSchema so it is not an object ([#10462](https://github.com/linode/manager/pull/10462)) - Improve Image `label` validation ([#10471](https://github.com/linode/manager/pull/10471)) - ## [2024-05-13] - v0.46.0 - ### Changed: - Include disk_encryption in CreateLinodeSchema and RebuildLinodeSchema ([#10413](https://github.com/linode/manager/pull/10413)) - Allow `backup_id` to be nullable in `CreateLinodeSchema` ([#10421](https://github.com/linode/manager/pull/10421)) - ## [2024-04-29] - v0.45.0 - ### Changed: - Improved VPC `ip_ranges` validation in `LinodeInterfaceSchema` ([#10354](https://github.com/linode/manager/pull/10354)) @@ -52,12 +52,10 @@ ## [2024-03-18] - v0.42.0 - ### Changed: - Update TCP rules to not include a `match_condition` ([#10264](https://github.com/linode/manager/pull/10264)) - ## [2024-03-04] - v0.41.0 ### Upcoming Features: diff --git a/packages/validation/package.json b/packages/validation/package.json index 06dcd0ca568..34d55b981b4 100644 --- a/packages/validation/package.json +++ b/packages/validation/package.json @@ -1,6 +1,6 @@ { "name": "@linode/validation", - "version": "0.48.0", + "version": "0.49.0", "description": "Yup validation schemas for use with the Linode APIv4", "type": "module", "main": "lib/index.cjs", From 719841bb070c967ba09494e57ab28739782391b2 Mon Sep 17 00:00:00 2001 From: Alban Bailly <130582365+abailly-akamai@users.noreply.github.com> Date: Mon, 8 Jul 2024 12:25:11 -0400 Subject: [PATCH 37/37] Release v1.123.0 fixes (#10650) * token updates: Fix chip bg/color * token updates: Fix chip bg/color --- packages/manager/src/foundations/themes/dark.ts | 3 +++ packages/manager/src/foundations/themes/light.ts | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/manager/src/foundations/themes/dark.ts b/packages/manager/src/foundations/themes/dark.ts index 5307ee7aad4..4a374ee9d1b 100644 --- a/packages/manager/src/foundations/themes/dark.ts +++ b/packages/manager/src/foundations/themes/dark.ts @@ -354,6 +354,9 @@ export const darkTheme: ThemeOptions = { color: 'primary', }, styleOverrides: { + clickable: { + color: Color.Brand[100], + }, colorError: { backgroundColor: Badge.Bold.Red.Background, color: Badge.Bold.Red.Text, diff --git a/packages/manager/src/foundations/themes/light.ts b/packages/manager/src/foundations/themes/light.ts index 5b8dfa3338b..548cfb375cb 100644 --- a/packages/manager/src/foundations/themes/light.ts +++ b/packages/manager/src/foundations/themes/light.ts @@ -555,10 +555,10 @@ export const lightTheme: ThemeOptions = { styleOverrides: { clickable: { '&:focus': { - bbackgroundColor: Color.Brand[40], // TODO: This was the closest color according to our palette + backgroundColor: Color.Brand[30], // TODO: This was the closest color according to our palette }, '&:hover': { - bbackgroundColor: Color.Brand[40], // TODO: This was the closest color according to our palette + backgroundColor: Color.Brand[30], // TODO: This was the closest color according to our palette }, backgroundColor: Color.Brand[10], // TODO: This was the closest color according to our palette },