diff --git a/.gitignore b/.gitignore index 9b96da463da..734136b05e8 100644 --- a/.gitignore +++ b/.gitignore @@ -127,7 +127,12 @@ packages/manager/test-report.xml **/manager/cypress/videos/ **/manager/cypress/downloads/ **/manager/cypress/results/ -**/manager/cypress/screenshots/ + +# ignore all screenshots except records +# we ignore the png files, not the whole folder recursively +# or the record files are ignored too +**/manager/cypress/screenshots/**/*.png +!**/manager/cypress/screenshots/**/record*.png packages/manager/cypress/fixtures/example.json @@ -137,4 +142,4 @@ packages/manager/bundle_analyzer_report.html **/manager/src/dev-tools/*.local.* # vitepress -docs/.vitepress/cache +docs/.vitepress/cache \ No newline at end of file diff --git a/Jenkinsfile-component-tests.groovy b/Jenkinsfile-component-tests.groovy deleted file mode 100644 index 90ce63039e7..00000000000 --- a/Jenkinsfile-component-tests.groovy +++ /dev/null @@ -1,3 +0,0 @@ -library 'ui-builder' - -testManagerComponents() diff --git a/docker-compose.yml b/docker-compose.yml index 5285fdd036d..51f2f01e6a7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -43,9 +43,6 @@ x-e2e-env: CY_TEST_SPLIT_RUN_TOTAL: ${CY_TEST_SPLIT_RUN_TOTAL} CY_TEST_SPLIT_RUN_INDEX: ${CY_TEST_SPLIT_RUN_INDEX} - # Cypress performance. - CY_TEST_ACCOUNT_CACHE_DIR: ${CY_TEST_ACCOUNT_CACHE_DIR} - # Cypress reporting. CY_TEST_JUNIT_REPORT: ${CY_TEST_JUNIT_REPORT} CY_TEST_USER_REPORT: ${CY_TEST_USER_REPORT} @@ -66,7 +63,6 @@ x-e2e-env: x-e2e-volumes: &default-volumes - ./.git:/home/node/app/.git - - ./cache:/home/node/app/cache - ./packages/manager:/home/node/app/packages/manager - ./packages/validation:/home/node/app/packages/validation - ./packages/api-v4:/home/node/app/packages/api-v4 @@ -87,8 +83,6 @@ x-e2e-runners: condition: service_healthy env_file: ./packages/manager/.env volumes: *default-volumes - # TODO Stop using entrypoint, use CMD instead. - # (Or just make `yarn` the entrypoint, but either way stop forcing `cy:e2e`). entrypoint: ['yarn', 'cy:e2e'] services: @@ -120,16 +114,6 @@ services: <<: *default-env MANAGER_OAUTH: ${MANAGER_OAUTH} - # Component test runner. - # Does not require any Cloud Manager environment to run. - component: - <<: *default-runner - depends_on: [] - environment: - CY_TEST_DISABLE_RETRIES: ${CY_TEST_DISABLE_RETRIES} - CY_TEST_JUNIT_REPORT: ${CY_TEST_JUNIT_REPORT} - entrypoint: ['yarn', 'cy:component:run'] - # End-to-end test runner for Cloud's synthetic monitoring tests. # Configured to run against a remote Cloud instance hosted at some URL. e2e_heimdall: diff --git a/docs/development-guide/08-testing.md b/docs/development-guide/08-testing.md index 5635774c867..805729ae65d 100644 --- a/docs/development-guide/08-testing.md +++ b/docs/development-guide/08-testing.md @@ -220,14 +220,6 @@ Environment variables related to Cypress logging and reporting, as well as repor | `CY_TEST_FAIL_ON_MANAGED` | Fail affected tests when Managed is enabled | `1` | Unset; disabled by default | | `CY_TEST_GENWEIGHTS` | Generate and output test weights to the given path | `./weights.json` | Unset; disabled by default | -###### Performance -Environment variables that can be used to improve test performance in some scenarios. - -| Environment Variable | Description | Example | Default | -|---------------------------------|-----------------------------------------------|--------------------|----------------------------| -| `CY_TEST_ACCOUNT_CACHE_DIR` | Directory containing test account cache data | `./cache/accounts` | Unset; disabled by default | - - ### Writing End-to-End Tests 1. Look here for [Cypress Best Practices](https://docs.cypress.io/guides/references/best-practices) diff --git a/package.json b/package.json index 8a2e4147fcc..02710752bc4 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ "cy:ci": "yarn cy:e2e", "cy:debug": "yarn workspace linode-manager cy:debug", "cy:component": "yarn workspace linode-manager cy:component", - "cy:component:run": "yarn workspace linode-manager cy:component:run", "cy:rec-snap": "yarn workspace linode-manager cy:rec-snap", "changeset": "node scripts/changelog/changeset.mjs", "generate-changelogs": "node scripts/changelog/generate-changelogs.mjs", diff --git a/packages/api-v4/.changeset/pr-10920-added-1726070878408.md b/packages/api-v4/.changeset/pr-10920-added-1726070878408.md deleted file mode 100644 index 003c71787b7..00000000000 --- a/packages/api-v4/.changeset/pr-10920-added-1726070878408.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/api-v4": Added ---- - -LinodeCapabilities type used for `capabilities` property of Linode interface ([#10920](https://github.com/linode/manager/pull/10920)) diff --git a/packages/api-v4/src/linodes/types.ts b/packages/api-v4/src/linodes/types.ts index aa74b71ac70..dcc4eaebba2 100644 --- a/packages/api-v4/src/linodes/types.ts +++ b/packages/api-v4/src/linodes/types.ts @@ -19,7 +19,7 @@ export interface Linode { id: number; alerts: LinodeAlerts; backups: LinodeBackups; - capabilities?: LinodeCapabilities[]; // @TODO BSE: Remove optionality once BSE is fully rolled out + capabilities?: string[]; // @TODO BSE: Remove optionality once BSE is fully rolled out created: string; disk_encryption?: EncryptionStatus; // @TODO LDE: Remove optionality once LDE is fully rolled out region: string; @@ -54,8 +54,6 @@ export interface LinodeBackups { last_successful: string | null; } -export type LinodeCapabilities = 'Block Storage Encryption'; - export type Window = | 'Scheduling' | 'W0' diff --git a/packages/manager/.changeset/pr-10825-tests-1724428903440.md b/packages/manager/.changeset/pr-10825-tests-1724428903440.md deleted file mode 100644 index 6c1c721ee8e..00000000000 --- a/packages/manager/.changeset/pr-10825-tests-1724428903440.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tests ---- - -Add tests for NodeBalancer Create flow ([#10825](https://github.com/linode/manager/pull/10825)) diff --git a/packages/manager/.changeset/pr-10852-tests-1724937535185.md b/packages/manager/.changeset/pr-10852-tests-1724937535185.md deleted file mode 100644 index a82c165e853..00000000000 --- a/packages/manager/.changeset/pr-10852-tests-1724937535185.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tests ---- - -Add new tests for for selecting "All" Scopes ([#10852](https://github.com/linode/manager/pull/10852)) diff --git a/packages/manager/.changeset/pr-10859-added-1725550540714.md b/packages/manager/.changeset/pr-10859-added-1725550540714.md deleted file mode 100644 index c56813e96f2..00000000000 --- a/packages/manager/.changeset/pr-10859-added-1725550540714.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Added ---- - -Gravatar sunset banner for existing Gravatar users ([#10859](https://github.com/linode/manager/pull/10859)) diff --git a/packages/manager/.changeset/pr-10859-changed-1725550568902.md b/packages/manager/.changeset/pr-10859-changed-1725550568902.md deleted file mode 100644 index f7be9157e66..00000000000 --- a/packages/manager/.changeset/pr-10859-changed-1725550568902.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Changed ---- - -Avatars for users without Gravatars ([#10859](https://github.com/linode/manager/pull/10859)) diff --git a/packages/manager/.changeset/pr-10867-tests-1725460656929.md b/packages/manager/.changeset/pr-10867-tests-1725460656929.md deleted file mode 100644 index a59c63b37dc..00000000000 --- a/packages/manager/.changeset/pr-10867-tests-1725460656929.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tests ---- - -Add `CY_TEST_ACCOUNT_CACHE_DIR` environment variable to enable retrieval of test account cache data ([#10867](https://github.com/linode/manager/pull/10867)) diff --git a/packages/manager/.changeset/pr-10867-tests-1725482924721.md b/packages/manager/.changeset/pr-10867-tests-1725482924721.md deleted file mode 100644 index 5ab76d7861f..00000000000 --- a/packages/manager/.changeset/pr-10867-tests-1725482924721.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tests ---- - -Allow tests to fall back on cached account data when API request fails ([#10867](https://github.com/linode/manager/pull/10867)) diff --git a/packages/manager/.changeset/pr-10868-upcoming-features-1725546685590.md b/packages/manager/.changeset/pr-10868-upcoming-features-1725546685590.md deleted file mode 100644 index 8677ed9acc1..00000000000 --- a/packages/manager/.changeset/pr-10868-upcoming-features-1725546685590.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Add conditional client library update required reboot notice to Volume Create page ([#10868](https://github.com/linode/manager/pull/10868)) diff --git a/packages/manager/.changeset/pr-10887-tech-stories-1725467686328.md b/packages/manager/.changeset/pr-10887-tech-stories-1725467686328.md deleted file mode 100644 index 48bf6c14c7f..00000000000 --- a/packages/manager/.changeset/pr-10887-tech-stories-1725467686328.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tech Stories ---- - -Resolve "Incomplete string escape or encoding" codeQL alert in `generate-ansibleConfig.ts` ([#10887](https://github.com/linode/manager/pull/10887)) diff --git a/packages/manager/.changeset/pr-10889-tests-1725485099504.md b/packages/manager/.changeset/pr-10889-tests-1725485099504.md deleted file mode 100644 index 0145a26624d..00000000000 --- a/packages/manager/.changeset/pr-10889-tests-1725485099504.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tests ---- - -Update remaining Linode Create Cypress tests run against Linode Create v2 ([#10889](https://github.com/linode/manager/pull/10889)) diff --git a/packages/manager/.changeset/pr-10892-tests-1725546783401.md b/packages/manager/.changeset/pr-10892-tests-1725546783401.md deleted file mode 100644 index 359a0df0caf..00000000000 --- a/packages/manager/.changeset/pr-10892-tests-1725546783401.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tests ---- - -Clean up feature flag mocks ([#10892](https://github.com/linode/manager/pull/10892)) diff --git a/packages/manager/.changeset/pr-10895-fixed-1725556985673.md b/packages/manager/.changeset/pr-10895-fixed-1725556985673.md deleted file mode 100644 index dbd46afeffd..00000000000 --- a/packages/manager/.changeset/pr-10895-fixed-1725556985673.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Fixed ---- - -Typo with toast success notification when updating Reverse DNS ([#10895](https://github.com/linode/manager/pull/10895)) diff --git a/packages/manager/.changeset/pr-10895-tests-1725557096432.md b/packages/manager/.changeset/pr-10895-tests-1725557096432.md deleted file mode 100644 index 4e9a7a53d68..00000000000 --- a/packages/manager/.changeset/pr-10895-tests-1725557096432.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tests ---- - -Add cypress test to confirm toast when updating RDNS, add unit tests for RDNS drawers ([#10895](https://github.com/linode/manager/pull/10895)) diff --git a/packages/manager/.changeset/pr-10899-changed-1725595024930.md b/packages/manager/.changeset/pr-10899-changed-1725595024930.md deleted file mode 100644 index 9d38fbc00dd..00000000000 --- a/packages/manager/.changeset/pr-10899-changed-1725595024930.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Changed ---- - -Lower Events historical data fetching to 7 days ([#10899](https://github.com/linode/manager/pull/10899)) diff --git a/packages/manager/.changeset/pr-10904-added-1725873382370.md b/packages/manager/.changeset/pr-10904-added-1725873382370.md deleted file mode 100644 index 66b5e4a75f6..00000000000 --- a/packages/manager/.changeset/pr-10904-added-1725873382370.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Added ---- - -DisplayPrice Story ([#10904](https://github.com/linode/manager/pull/10904)) diff --git a/packages/manager/.changeset/pr-10905-added-1725893781288.md b/packages/manager/.changeset/pr-10905-added-1725893781288.md deleted file mode 100644 index c75821523f9..00000000000 --- a/packages/manager/.changeset/pr-10905-added-1725893781288.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Added ---- - -CheckoutSummary Story in Storybook ([#10905](https://github.com/linode/manager/pull/10905)) diff --git a/packages/manager/.changeset/pr-10907-tests-1725898460559.md b/packages/manager/.changeset/pr-10907-tests-1725898460559.md deleted file mode 100644 index d4a32549afa..00000000000 --- a/packages/manager/.changeset/pr-10907-tests-1725898460559.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tests ---- - -Cypress integration test for Object Storage Gen2: E1 Endpoint ([#10907](https://github.com/linode/manager/pull/10907)) diff --git a/packages/manager/.changeset/pr-10909-tests-1725903174019.md b/packages/manager/.changeset/pr-10909-tests-1725903174019.md deleted file mode 100644 index 3cf118d08bc..00000000000 --- a/packages/manager/.changeset/pr-10909-tests-1725903174019.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tests ---- - -Add unit tests for AttachVolumeDrawer component ([#10909](https://github.com/linode/manager/pull/10909)) diff --git a/packages/manager/.changeset/pr-10909-upcoming-features-1725903122030.md b/packages/manager/.changeset/pr-10909-upcoming-features-1725903122030.md deleted file mode 100644 index 5d3e9651650..00000000000 --- a/packages/manager/.changeset/pr-10909-upcoming-features-1725903122030.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Add 'Encrypt Volume' checkbox in Attach Volume drawer ([#10909](https://github.com/linode/manager/pull/10909)) diff --git a/packages/manager/.changeset/pr-10910-changed-1725910840891.md b/packages/manager/.changeset/pr-10910-changed-1725910840891.md deleted file mode 100644 index afad60fe57b..00000000000 --- a/packages/manager/.changeset/pr-10910-changed-1725910840891.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Changed ---- - - "contact support" links to new support ticket in event messages ([#10910](https://github.com/linode/manager/pull/10910)) diff --git a/packages/manager/.changeset/pr-10911-tests-1725912717549.md b/packages/manager/.changeset/pr-10911-tests-1725912717549.md deleted file mode 100644 index a542ab6e47c..00000000000 --- a/packages/manager/.changeset/pr-10911-tests-1725912717549.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tests ---- - -Add unit tests for NodeBalancersLanding package ([#10911](https://github.com/linode/manager/pull/10911)) diff --git a/packages/manager/.changeset/pr-10912-added-1725913446437.md b/packages/manager/.changeset/pr-10912-added-1725913446437.md deleted file mode 100644 index 1f268b9a61c..00000000000 --- a/packages/manager/.changeset/pr-10912-added-1725913446437.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Added ---- - -CopyableTextField story and clean up components ([#10912](https://github.com/linode/manager/pull/10912)) diff --git a/packages/manager/.changeset/pr-10913-fixed-1725955539025.md b/packages/manager/.changeset/pr-10913-fixed-1725955539025.md deleted file mode 100644 index d0db3d09c1d..00000000000 --- a/packages/manager/.changeset/pr-10913-fixed-1725955539025.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Fixed ---- - -DisplayPrice story crash when Currency component's minimumFractionDigits is negative ([#10913](https://github.com/linode/manager/pull/10913)) diff --git a/packages/manager/.changeset/pr-10914-fixed-1725988850881.md b/packages/manager/.changeset/pr-10914-fixed-1725988850881.md deleted file mode 100644 index 0df44d2fb12..00000000000 --- a/packages/manager/.changeset/pr-10914-fixed-1725988850881.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Fixed ---- - -Linode Create v2 not handling deprecated and EOL Images ([#10914](https://github.com/linode/manager/pull/10914)) diff --git a/packages/manager/.changeset/pr-10915-tests-1725992312603.md b/packages/manager/.changeset/pr-10915-tests-1725992312603.md deleted file mode 100644 index 6d7703fd558..00000000000 --- a/packages/manager/.changeset/pr-10915-tests-1725992312603.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tests ---- - -Tag cypress tests by adding the "method:e2e" and "purpose:dcTesting" ([#10915](https://github.com/linode/manager/pull/10915)) diff --git a/packages/manager/.changeset/pr-10918-fixed-1726068911721.md b/packages/manager/.changeset/pr-10918-fixed-1726068911721.md deleted file mode 100644 index 2cecf77576e..00000000000 --- a/packages/manager/.changeset/pr-10918-fixed-1726068911721.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Fixed ---- - -API Tokens Table Style Regression ([#10918](https://github.com/linode/manager/pull/10918)) diff --git a/packages/manager/.changeset/pr-10920-upcoming-features-1726068029277.md b/packages/manager/.changeset/pr-10920-upcoming-features-1726068029277.md deleted file mode 100644 index 058c2fbf6d7..00000000000 --- a/packages/manager/.changeset/pr-10920-upcoming-features-1726068029277.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Update BSE capability for Linodes to be `Block Storage Encryption` instead of `blockstorage_encryption` ([#10920](https://github.com/linode/manager/pull/10920)) diff --git a/packages/manager/.changeset/pr-10921-tech-stories-1726073448179.md b/packages/manager/.changeset/pr-10921-tech-stories-1726073448179.md deleted file mode 100644 index 526c5492a2b..00000000000 --- a/packages/manager/.changeset/pr-10921-tech-stories-1726073448179.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tech Stories ---- - -Remove `linodeCreateRefactor` feature flag ([#10921](https://github.com/linode/manager/pull/10921)) diff --git a/packages/manager/.changeset/pr-10923-fixed-1726086511137.md b/packages/manager/.changeset/pr-10923-fixed-1726086511137.md deleted file mode 100644 index 037784342fe..00000000000 --- a/packages/manager/.changeset/pr-10923-fixed-1726086511137.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Fixed ---- - -Incorrect avatar displaying in Notification Center for a small subset of events ([#10923](https://github.com/linode/manager/pull/10923)) diff --git a/packages/manager/.changeset/pr-10926-tests-1726156342482.md b/packages/manager/.changeset/pr-10926-tests-1726156342482.md deleted file mode 100644 index eee72e2ce39..00000000000 --- a/packages/manager/.changeset/pr-10926-tests-1726156342482.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tests ---- - -Support running component tests via CI ([#10926](https://github.com/linode/manager/pull/10926)) diff --git a/packages/manager/.changeset/pr-10928-removed-1726148104546.md b/packages/manager/.changeset/pr-10928-removed-1726148104546.md deleted file mode 100644 index 3549df67a4a..00000000000 --- a/packages/manager/.changeset/pr-10928-removed-1726148104546.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Changed ---- - -Invalid Tax Id Notification ([#10928](https://github.com/linode/manager/pull/10928)) diff --git a/packages/manager/.changeset/pr-10929-upcoming-features-1726156754302.md b/packages/manager/.changeset/pr-10929-upcoming-features-1726156754302.md deleted file mode 100644 index 70e24c98b29..00000000000 --- a/packages/manager/.changeset/pr-10929-upcoming-features-1726156754302.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Fix 'Create Volume' button state on Volume Create page when 'Encrypt Volume' checkbox is checked ([#10929](https://github.com/linode/manager/pull/10929)) diff --git a/packages/manager/cypress.config.ts b/packages/manager/cypress.config.ts index 09322be48ba..48e5938ec3a 100644 --- a/packages/manager/cypress.config.ts +++ b/packages/manager/cypress.config.ts @@ -58,14 +58,6 @@ export default defineConfig({ specPattern: './cypress/component/**/*.spec.tsx', viewportWidth: 500, viewportHeight: 500, - - setupNodeEvents(on, config) { - return setupPlugins(on, config, [ - loadEnvironmentConfig, - discardPassedTestRecordings, - enableJunitReport('Component', true), - ]); - }, }, e2e: { @@ -93,7 +85,7 @@ export default defineConfig({ regionOverrideCheck, logTestTagInfo, splitCypressRun, - enableJunitReport(), + enableJunitReport, generateTestWeights, ]); }, diff --git a/packages/manager/cypress/component/poc/region-select.spec.tsx b/packages/manager/cypress/component/poc/region-select.spec.tsx index e15411dece2..d3ad57eae07 100644 --- a/packages/manager/cypress/component/poc/region-select.spec.tsx +++ b/packages/manager/cypress/component/poc/region-select.spec.tsx @@ -397,9 +397,9 @@ componentTests('RegionSelect', (mount) => { .should('be.visible'); }); regionsWithoutObj.forEach((region) => { - ui.autocompletePopper.find().within(() => { - cy.findByText(`${region.label} (${region.id})`).should('not.exist'); - }); + ui.autocompletePopper + .findByTitle(`${region.label} (${region.id})`) + .should('not.exist'); }); }); diff --git a/packages/manager/cypress/e2e/core/account/account-logout.spec.ts b/packages/manager/cypress/e2e/core/account/account-logout.spec.ts index 550c4b39c98..bb72c9795b4 100644 --- a/packages/manager/cypress/e2e/core/account/account-logout.spec.ts +++ b/packages/manager/cypress/e2e/core/account/account-logout.spec.ts @@ -4,7 +4,7 @@ import { ui } from 'support/ui'; describe('Logout Test', () => { beforeEach(() => { - cy.tag('purpose:syntheticTesting', 'method:e2e'); + cy.tag('purpose:syntheticTesting'); }); /* diff --git a/packages/manager/cypress/e2e/core/account/personal-access-tokens.spec.ts b/packages/manager/cypress/e2e/core/account/personal-access-tokens.spec.ts index 7df2637bbc7..270f5568f23 100644 --- a/packages/manager/cypress/e2e/core/account/personal-access-tokens.spec.ts +++ b/packages/manager/cypress/e2e/core/account/personal-access-tokens.spec.ts @@ -24,8 +24,6 @@ describe('Personal access tokens', () => { * - Confirms that user is shown the token secret upon successful PAT creation * - Confirms that new personal access token is shown in list * - Confirms that user can open and close "View Scopes" drawer - * - Confirm that the “Child account access” grant is not visible in the list of permissions. - * - Upon clicking “Create Token”, assert that the outgoing API request payload contains "scopes" value as defined in token. */ it('can create personal access tokens', () => { const token = appTokenFactory.build({ @@ -65,9 +63,6 @@ describe('Personal access tokens', () => { .findByTitle('Add Personal Access Token') .should('be.visible') .within(() => { - // Confirm that the “Child account access” grant is not visible in the list of permissions. - cy.findAllByText('Child Account Access').should('not.exist'); - // Confirm submit button is disabled without specifying scopes. ui.buttonGroup .findButtonByTitle('Create Token') @@ -152,12 +147,7 @@ describe('Personal access tokens', () => { }); // Confirm that new PAT is shown in list and "View Scopes" drawer works. - // Upon clicking “Create Token”, assert that the outgoing API request payload contains "scopes" value as defined in token. - cy.wait('@getTokens').then((xhr) => { - const actualTokenData = xhr.response?.body.data; - const actualTokenScopes = actualTokenData[0].scopes; - expect(actualTokenScopes).to.equal(token.scopes); - }); + cy.wait('@getTokens'); cy.findByText(token.label) .should('be.visible') .closest('tr') diff --git a/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts b/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts index 51c3059d59f..71428f889ea 100644 --- a/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts +++ b/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts @@ -121,7 +121,6 @@ describe('Account service transfers', () => { * - Confirms user can navigate to service transfer page via user menu. */ it('can navigate to service transfers landing page', () => { - cy.tag('method:e2e'); cy.visitWithLogin('/'); cy.findByLabelText('Profile & Account').should('be.visible').click(); @@ -245,7 +244,6 @@ describe('Account service transfers', () => { * - Confirms that users can cancel a service transfer */ it('can initiate and cancel a service transfer', () => { - cy.tag('method:e2e'); // Create a Linode to transfer. const setupLinode = async (): Promise => { const payload = createLinodeRequestFactory.build({ diff --git a/packages/manager/cypress/e2e/core/billing/billing-contact.spec.ts b/packages/manager/cypress/e2e/core/billing/billing-contact.spec.ts index 346023aa089..c73550b7a13 100644 --- a/packages/manager/cypress/e2e/core/billing/billing-contact.spec.ts +++ b/packages/manager/cypress/e2e/core/billing/billing-contact.spec.ts @@ -2,8 +2,13 @@ import { mockGetAccount, mockUpdateAccount } from 'support/intercepts/account'; import { accountFactory } from 'src/factories/account'; import type { Account } from '@linode/api-v4'; import { ui } from 'support/ui'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; import { TAX_ID_HELPER_TEXT } from 'src/features/Billing/constants'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; +import type { Flags } from 'src/featureFlags'; /* eslint-disable sonarjs/no-duplicate-string */ const accountData = accountFactory.build({ @@ -67,10 +72,11 @@ const checkAccountContactDisplay = (accountInfo: Account) => { describe('Billing Contact', () => { beforeEach(() => { mockAppendFeatureFlags({ - taxId: { + taxId: makeFeatureFlagData({ enabled: true, - }, + }), }); + mockGetFeatureFlagClientstream(); }); it('Edit Contact Info', () => { // mock the user's account data and confirm that it is displayed correctly upon page load diff --git a/packages/manager/cypress/e2e/core/domains/smoke-clone-domain.spec.ts b/packages/manager/cypress/e2e/core/domains/smoke-clone-domain.spec.ts index 4e9b28fb0b9..d45a2bf886d 100644 --- a/packages/manager/cypress/e2e/core/domains/smoke-clone-domain.spec.ts +++ b/packages/manager/cypress/e2e/core/domains/smoke-clone-domain.spec.ts @@ -14,9 +14,6 @@ describe('Clone a Domain', () => { before(() => { cleanUp('domains'); }); - beforeEach(() => { - cy.tag('method:e2e'); - }); /* * - Clicks "Clone" action menu item for domain but cancels operation. diff --git a/packages/manager/cypress/e2e/core/domains/smoke-create-domain-records.spec.ts b/packages/manager/cypress/e2e/core/domains/smoke-create-domain-records.spec.ts index 950719ce399..3b1cb74e422 100644 --- a/packages/manager/cypress/e2e/core/domains/smoke-create-domain-records.spec.ts +++ b/packages/manager/cypress/e2e/core/domains/smoke-create-domain-records.spec.ts @@ -4,9 +4,6 @@ import { interceptCreateDomainRecord } from 'support/intercepts/domains'; import { createDomainRecords } from 'support/constants/domains'; authenticate(); -beforeEach(() => { - cy.tag('method:e2e'); -}); describe('Creates Domains records with Form', () => { it('Adds domain records to a newly created Domain', () => { diff --git a/packages/manager/cypress/e2e/core/domains/smoke-create-domain.spec.ts b/packages/manager/cypress/e2e/core/domains/smoke-create-domain.spec.ts index e3d8d513de1..04d206a0bc8 100644 --- a/packages/manager/cypress/e2e/core/domains/smoke-create-domain.spec.ts +++ b/packages/manager/cypress/e2e/core/domains/smoke-create-domain.spec.ts @@ -16,7 +16,6 @@ describe('Create a Domain', () => { }); it('Creates first Domain', () => { - cy.tag('method:e2e'); // Mock Domains to modify incoming response. const mockDomains = new Array(2).fill(null).map( (_item: null, index: number): Domain => { diff --git a/packages/manager/cypress/e2e/core/domains/smoke-delete-domain.spec.ts b/packages/manager/cypress/e2e/core/domains/smoke-delete-domain.spec.ts index 4e9223cca14..80d9b632aa2 100644 --- a/packages/manager/cypress/e2e/core/domains/smoke-delete-domain.spec.ts +++ b/packages/manager/cypress/e2e/core/domains/smoke-delete-domain.spec.ts @@ -7,9 +7,6 @@ import { createDomain } from '@linode/api-v4/lib/domains'; import { ui } from 'support/ui'; authenticate(); -beforeEach(() => { - cy.tag('method:e2e'); -}); describe('Delete a Domain', () => { /* * - Clicks "Delete" action menu item for domain but cancels operation. diff --git a/packages/manager/cypress/e2e/core/firewalls/create-firewall.spec.ts b/packages/manager/cypress/e2e/core/firewalls/create-firewall.spec.ts index 3007167215a..4da8d8c2dab 100644 --- a/packages/manager/cypress/e2e/core/firewalls/create-firewall.spec.ts +++ b/packages/manager/cypress/e2e/core/firewalls/create-firewall.spec.ts @@ -12,9 +12,6 @@ describe('create firewall', () => { before(() => { cleanUp(['lke-clusters', 'linodes', 'firewalls']); }); - beforeEach(() => { - cy.tag('method:e2e'); - }); /* * - Creates a firewall that is not assigned to a Linode. diff --git a/packages/manager/cypress/e2e/core/firewalls/delete-firewall.spec.ts b/packages/manager/cypress/e2e/core/firewalls/delete-firewall.spec.ts index 7be2db7cd11..2cbedb29e5f 100644 --- a/packages/manager/cypress/e2e/core/firewalls/delete-firewall.spec.ts +++ b/packages/manager/cypress/e2e/core/firewalls/delete-firewall.spec.ts @@ -11,9 +11,6 @@ describe('delete firewall', () => { before(() => { cleanUp('firewalls'); }); - beforeEach(() => { - cy.tag('method:e2e'); - }); /* * - Clicks "Delete" action menu item for firewall but cancels operation. diff --git a/packages/manager/cypress/e2e/core/firewalls/migrate-linode-with-firewall.spec.ts b/packages/manager/cypress/e2e/core/firewalls/migrate-linode-with-firewall.spec.ts index 2074a0785db..f8feabaf6db 100644 --- a/packages/manager/cypress/e2e/core/firewalls/migrate-linode-with-firewall.spec.ts +++ b/packages/manager/cypress/e2e/core/firewalls/migrate-linode-with-firewall.spec.ts @@ -144,7 +144,6 @@ describe('Migrate Linode With Firewall', () => { * - Uses real API data to create a Firewall, attach a Linode to it, then migrate the Linode. */ it('migrates linode with firewall - real data', () => { - cy.tag('method:e2e', 'purpose:dcTesting'); const [migrationRegionStart, migrationRegionEnd] = chooseRegions(2); const firewallLabel = randomLabel(); const linodePayload = createLinodeRequestFactory.build({ diff --git a/packages/manager/cypress/e2e/core/firewalls/update-firewall.spec.ts b/packages/manager/cypress/e2e/core/firewalls/update-firewall.spec.ts index 7269f369631..d67ac52fd3a 100644 --- a/packages/manager/cypress/e2e/core/firewalls/update-firewall.spec.ts +++ b/packages/manager/cypress/e2e/core/firewalls/update-firewall.spec.ts @@ -170,9 +170,6 @@ describe('update firewall', () => { before(() => { cleanUp('firewalls'); }); - beforeEach(() => { - cy.tag('method:e2e'); - }); /* * - Confirms that a linode can be added and removed from a firewall. diff --git a/packages/manager/cypress/e2e/core/general/account-activation.spec.ts b/packages/manager/cypress/e2e/core/general/account-activation.spec.ts index 1c5273e999b..c6ab1764182 100644 --- a/packages/manager/cypress/e2e/core/general/account-activation.spec.ts +++ b/packages/manager/cypress/e2e/core/general/account-activation.spec.ts @@ -1,8 +1,5 @@ import { apiMatcher } from 'support/util/intercepts'; -beforeEach(() => { - cy.tag('method:e2e'); -}); describe('account activation', () => { /** * The API will return 403 with the body below for most endpoint except `/v4/profile`. diff --git a/packages/manager/cypress/e2e/core/general/gdpr-agreement.spec.ts b/packages/manager/cypress/e2e/core/general/gdpr-agreement.spec.ts index 282a1bf48f5..089e766fb93 100644 --- a/packages/manager/cypress/e2e/core/general/gdpr-agreement.spec.ts +++ b/packages/manager/cypress/e2e/core/general/gdpr-agreement.spec.ts @@ -1,10 +1,16 @@ import { ui } from 'support/ui'; -import { linodeFactory, regionFactory } from '@src/factories'; +import { fbtClick, getClick } from 'support/helpers'; +import { regionFactory } from '@src/factories'; import { randomString, randomLabel } from 'support/util/random'; import { mockGetRegions } from 'support/intercepts/regions'; import { mockGetAccountAgreements } from 'support/intercepts/account'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; + import type { Region } from '@linode/api-v4'; -import { mockCreateLinode } from 'support/intercepts/linodes'; const mockRegions: Region[] = [ regionFactory.build({ @@ -96,7 +102,15 @@ describe('GDPR agreement', () => { cy.get('[data-testid="eu-agreement-checkbox"]').should('not.exist'); }); - it('needs the agreement checked to submit the form', () => { + it('needs the agreement checked to validate the form', () => { + // This test does not apply to Linode Create v2 because + // Linode Create v2 allows you to press "Create Linode" + // without checking the GDPR checkbox. (The user will + // get a validation error if they have not agreed). + mockAppendFeatureFlags({ + linodeCreateRefactor: makeFeatureFlagData(false), + }); + mockGetFeatureFlagClientstream(); mockGetRegions(mockRegions).as('getRegions'); mockGetAccountAgreements({ privacy_policy: false, @@ -106,50 +120,26 @@ describe('GDPR agreement', () => { const linodeLabel = randomLabel(); cy.visitWithLogin('/linodes/create'); - cy.wait(['@getRegions']); + cy.wait(['@getAgreements', '@getRegions']); // Paris should have the agreement ui.regionSelect.find().click(); ui.regionSelect.findItemByRegionId('fr-par').click(); + cy.get('[data-testid="eu-agreement-checkbox"]').should('be.visible'); - cy.wait('@getAgreements'); - - cy.findByText('Shared CPU').click(); - - cy.get('[id="g6-nanode-1"]').click(); - - cy.findByLabelText('Linode Label').clear().type(linodeLabel); - - cy.findByLabelText('Root Password').type(rootpass); - - cy.get('[data-testid="eu-agreement-checkbox"]') - .scrollIntoView() - .should('be.visible'); - - cy.findByText('Create Linode') - .scrollIntoView() - .should('be.enabled') - .should('be.visible') - .click(); + // Fill out the form + fbtClick('Shared CPU'); + getClick('[id="g6-nanode-1"]'); + getClick('#linode-label').clear().type(linodeLabel); + cy.get('#root-password').type(rootpass); - cy.findByText( - 'You must agree to the EU agreement to deploy to this region.' - ).should('be.visible'); + // expect the button to be disabled + cy.get('[data-qa-deploy-linode="true"]').should('be.disabled'); // check the agreement - cy.get('#gdpr-checkbox').click(); - - cy.findByText( - 'You must agree to the EU agreement to deploy to this region.' - ).should('not.exist'); - - mockCreateLinode(linodeFactory.build()).as('createLinode'); - - cy.findByText('Create Linode') - .should('be.enabled') - .should('be.visible') - .click(); + getClick('#gdpr-checkbox'); - cy.wait('@createLinode'); + // expect the button to be enabled + cy.get('[data-qa-deploy-linode="true"]').should('not.be.disabled'); }); }); diff --git a/packages/manager/cypress/e2e/core/general/smoke-deep-link.spec.ts b/packages/manager/cypress/e2e/core/general/smoke-deep-link.spec.ts index e11f5707ae6..bc9f2af2951 100644 --- a/packages/manager/cypress/e2e/core/general/smoke-deep-link.spec.ts +++ b/packages/manager/cypress/e2e/core/general/smoke-deep-link.spec.ts @@ -1,9 +1,7 @@ import { pages } from 'support/ui/constants'; + import type { Page } from 'support/ui/constants'; -beforeEach(() => { - cy.tag('method:e2e'); -}); describe('smoke - deep links', () => { beforeEach(() => { cy.visitWithLogin('/null'); diff --git a/packages/manager/cypress/e2e/core/helpAndSupport/close-support-ticket.spec.ts b/packages/manager/cypress/e2e/core/helpAndSupport/close-support-ticket.spec.ts index 46f3867d7a1..f9eb2a840cb 100644 --- a/packages/manager/cypress/e2e/core/helpAndSupport/close-support-ticket.spec.ts +++ b/packages/manager/cypress/e2e/core/helpAndSupport/close-support-ticket.spec.ts @@ -1,5 +1,9 @@ import 'cypress-file-upload'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; import { ui } from 'support/ui'; import { randomItem, @@ -44,8 +48,9 @@ describe('close support tickets', () => { } mockAppendFeatureFlags({ - supportTicketSeverity: true, + supportTicketSeverity: makeFeatureFlagData(true), }); + mockGetFeatureFlagClientstream(); mockGetSupportTickets([mockTicket]); mockGetSupportTicket(mockTicket).as('getSupportTicket'); mockGetSupportTicketReplies(mockTicket.id, []).as('getReplies'); @@ -102,8 +107,9 @@ describe('close support tickets', () => { } mockAppendFeatureFlags({ - supportTicketSeverity: true, + supportTicketSeverity: makeFeatureFlagData(true), }); + mockGetFeatureFlagClientstream(); mockGetSupportTickets([mockTicket]); mockGetSupportTicket(mockTicket).as('getSupportTicket'); mockGetSupportTicketReplies(mockTicket.id, []).as('getReplies'); diff --git a/packages/manager/cypress/e2e/core/helpAndSupport/open-support-ticket.spec.ts b/packages/manager/cypress/e2e/core/helpAndSupport/open-support-ticket.spec.ts index 5644f5deea4..bffc53d6cef 100644 --- a/packages/manager/cypress/e2e/core/helpAndSupport/open-support-ticket.spec.ts +++ b/packages/manager/cypress/e2e/core/helpAndSupport/open-support-ticket.spec.ts @@ -2,7 +2,11 @@ /* eslint-disable sonarjs/no-duplicate-string */ import 'cypress-file-upload'; import { interceptGetProfile } from 'support/intercepts/profile'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; import { ui } from 'support/ui'; import { randomItem, @@ -65,8 +69,9 @@ describe('open support tickets', () => { */ it('can open a support ticket', () => { mockAppendFeatureFlags({ - supportTicketSeverity: false, + supportTicketSeverity: makeFeatureFlagData(false), }); + mockGetFeatureFlagClientstream(); const image = 'test_screenshot.png'; const ticketDescription = 'this is a test ticket'; @@ -154,8 +159,9 @@ describe('open support tickets', () => { } mockAppendFeatureFlags({ - supportTicketSeverity: true, + supportTicketSeverity: makeFeatureFlagData(true), }); + mockGetFeatureFlagClientstream(); mockCreateSupportTicket(mockTicket).as('createTicket'); mockGetSupportTickets([]); mockGetSupportTicket(mockTicket); diff --git a/packages/manager/cypress/e2e/core/helpAndSupport/support-tickets-landing-page.spec.ts b/packages/manager/cypress/e2e/core/helpAndSupport/support-tickets-landing-page.spec.ts index 7fc84b7a74b..2c511d9810f 100644 --- a/packages/manager/cypress/e2e/core/helpAndSupport/support-tickets-landing-page.spec.ts +++ b/packages/manager/cypress/e2e/core/helpAndSupport/support-tickets-landing-page.spec.ts @@ -1,5 +1,9 @@ import { interceptGetProfile } from 'support/intercepts/profile'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; import { randomItem, randomLabel, @@ -34,8 +38,9 @@ describe('support tickets landing page', () => { */ it('shows the empty message when there are no tickets.', () => { mockAppendFeatureFlags({ - supportTicketSeverity: false, + supportTicketSeverity: makeFeatureFlagData(false), }); + mockGetFeatureFlagClientstream(); interceptGetProfile().as('getProfile'); @@ -87,8 +92,9 @@ describe('support tickets landing page', () => { const mockTickets = [mockTicket, mockAnotherTicket]; mockAppendFeatureFlags({ - supportTicketSeverity: true, + supportTicketSeverity: makeFeatureFlagData(true), }); + mockGetFeatureFlagClientstream(); mockGetSupportTickets(mockTickets); cy.visitWithLogin('/support/tickets'); @@ -150,8 +156,9 @@ describe('support tickets landing page', () => { } mockAppendFeatureFlags({ - supportTicketSeverity: true, + supportTicketSeverity: makeFeatureFlagData(true), }); + mockGetFeatureFlagClientstream(); mockGetSupportTickets([mockTicket]); mockGetSupportTicket(mockTicket).as('getSupportTicket'); mockGetSupportTicketReplies(mockTicket.id, []).as('getReplies'); @@ -244,8 +251,9 @@ describe('support tickets landing page', () => { } mockAppendFeatureFlags({ - supportTicketSeverity: true, + supportTicketSeverity: makeFeatureFlagData(true), }); + mockGetFeatureFlagClientstream(); mockGetSupportTickets([mockTicket]); mockGetSupportTicket(mockTicket).as('getSupportTicket'); mockGetSupportTicketReplies(mockTicket.id, []).as('getReplies'); diff --git a/packages/manager/cypress/e2e/core/images/create-image.spec.ts b/packages/manager/cypress/e2e/core/images/create-image.spec.ts index 8ea89934bab..1415f28d93c 100644 --- a/packages/manager/cypress/e2e/core/images/create-image.spec.ts +++ b/packages/manager/cypress/e2e/core/images/create-image.spec.ts @@ -5,6 +5,7 @@ import { mockGetAccount } from 'support/intercepts/account'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; import { createTestLinode } from 'support/util/linodes'; import { randomLabel, randomPhrase } from 'support/util/random'; import { mockGetRegions } from 'support/intercepts/regions'; @@ -49,7 +50,6 @@ describe('create image (e2e)', () => { }); it('create image from a linode', () => { - cy.tag('method:e2e'); const label = randomLabel(); const description = randomPhrase(); @@ -127,7 +127,7 @@ describe('create image (e2e)', () => { it('displays notice informing user that Images are not encrypted, provided the LDE feature is enabled and the selected linode is not in an Edge region', () => { // Mock feature flag -- @TODO LDE: Remove feature flag once LDE is fully rolled out mockAppendFeatureFlags({ - linodeDiskEncryption: true, + linodeDiskEncryption: makeFeatureFlagData(true), }).as('getFeatureFlags'); // Mock responses @@ -164,7 +164,7 @@ describe('create image (e2e)', () => { it('does not display a notice informing user that Images are not encrypted if the LDE feature is disabled', () => { // Mock feature flag -- @TODO LDE: Remove feature flag once LDE is fully rolled out mockAppendFeatureFlags({ - linodeDiskEncryption: false, + linodeDiskEncryption: makeFeatureFlagData(false), }).as('getFeatureFlags'); // Mock responses @@ -201,7 +201,7 @@ describe('create image (e2e)', () => { it('does not display a notice informing user that Images are not encrypted if the selected linode is in an Edge region', () => { // Mock feature flag -- @TODO LDE: Remove feature flag once LDE is fully rolled out mockAppendFeatureFlags({ - linodeDiskEncryption: true, + linodeDiskEncryption: makeFeatureFlagData(true), }).as('getFeatureFlags'); // Mock responses diff --git a/packages/manager/cypress/e2e/core/images/search-images.spec.ts b/packages/manager/cypress/e2e/core/images/search-images.spec.ts index 8da716b8d33..9620a2312e7 100644 --- a/packages/manager/cypress/e2e/core/images/search-images.spec.ts +++ b/packages/manager/cypress/e2e/core/images/search-images.spec.ts @@ -13,9 +13,6 @@ describe('Search Images', () => { before(() => { cleanUp(['linodes', 'images']); }); - beforeEach(() => { - cy.tag('method:e2e'); - }); /* * - Confirm that images are API searchable and filtered in the UI. diff --git a/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts b/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts index f354607d4a0..bff073133a9 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts @@ -81,7 +81,6 @@ describe('LKE Cluster Creation', () => { * - Confirms that correct information is shown on the LKE cluster summary page */ it('can create an LKE cluster', () => { - cy.tag('method:e2e', 'purpose:dcTesting'); const clusterLabel = randomLabel(); const clusterRegion = chooseRegion(); const clusterVersion = '1.27'; diff --git a/packages/manager/cypress/e2e/core/kubernetes/lke-landing-page.spec.ts b/packages/manager/cypress/e2e/core/kubernetes/lke-landing-page.spec.ts index b97a4b77baf..24c7fcbfeb0 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/lke-landing-page.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/lke-landing-page.spec.ts @@ -12,15 +12,20 @@ import { import { getRegionById } from 'support/util/regions'; import { readDownload } from 'support/util/downloads'; import { ui } from 'support/ui'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; import { mockGetAccount } from 'support/intercepts/account'; describe('LKE landing page', () => { it('does not display a Disk Encryption info banner if the LDE feature is disabled', () => { // Mock feature flag -- @TODO LDE: Remove feature flag once LDE is fully rolled out mockAppendFeatureFlags({ - linodeDiskEncryption: false, + linodeDiskEncryption: makeFeatureFlagData(false), }).as('getFeatureFlags'); + mockGetFeatureFlagClientstream().as('getClientStream'); // Mock responses const mockAccount = accountFactory.build({ @@ -46,8 +51,9 @@ describe('LKE landing page', () => { it('displays a Disk Encryption info banner if the LDE feature is enabled', () => { // Mock feature flag -- @TODO LDE: Remove feature flag once LDE is fully rolled out mockAppendFeatureFlags({ - linodeDiskEncryption: true, + linodeDiskEncryption: makeFeatureFlagData(true), }).as('getFeatureFlags'); + mockGetFeatureFlagClientstream().as('getClientStream'); // Mock responses const mockAccount = accountFactory.build({ diff --git a/packages/manager/cypress/e2e/core/linodes/backup-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/backup-linode.spec.ts index 54eaf02e5b6..f973a37651d 100644 --- a/packages/manager/cypress/e2e/core/linodes/backup-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/backup-linode.spec.ts @@ -41,7 +41,6 @@ describe('linode backups', () => { * - Confirms that Linode details page updates to reflect that backups are enabled. */ it('can enable backups', () => { - cy.tag('method:e2e'); // Skip or optionally fail if test account has Managed enabled. // This is necessary because Managed accounts have backups enabled implicitly. expectManagedDisabled(); @@ -108,7 +107,6 @@ describe('linode backups', () => { * - Confirms that backups page content updates to reflect new snapshot. */ it('can capture a manual snapshot', () => { - cy.tag('method:e2e'); // Create a Linode that is not booted and which has backups enabled. const createLinodeRequest = createLinodeRequestFactory.build({ label: randomLabel(), diff --git a/packages/manager/cypress/e2e/core/linodes/clone-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/clone-linode.spec.ts index 55a1ce53a76..c64e43abba6 100644 --- a/packages/manager/cypress/e2e/core/linodes/clone-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/clone-linode.spec.ts @@ -47,7 +47,6 @@ describe('clone linode', () => { * - Confirms that Linode can be cloned successfully. */ it('can clone a Linode from Linode details page', () => { - cy.tag('method:e2e', 'purpose:dcTesting'); const linodeRegion = chooseRegion({ capabilities: ['Vlans'] }); const linodePayload = createLinodeRequestFactory.build({ label: randomLabel(), diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-mobile.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-mobile.spec.ts index 1cc4bfa5f59..bf7774cc0bd 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-mobile.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-mobile.spec.ts @@ -7,10 +7,23 @@ import { MOBILE_VIEWPORTS } from 'support/constants/environment'; import { linodeCreatePage } from 'support/ui/pages'; import { randomLabel, randomNumber, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; import { ui } from 'support/ui'; import { mockCreateLinode } from 'support/intercepts/linodes'; describe('Linode create mobile smoke', () => { + // TODO Remove feature flag mocks when `linodeCreateRefactor` flag is retired. + beforeEach(() => { + mockAppendFeatureFlags({ + linodeCreateRefactor: makeFeatureFlagData(true), + }); + mockGetFeatureFlagClientstream(); + }); + MOBILE_VIEWPORTS.forEach((viewport) => { /* * - Confirms Linode create flow can be completed on common mobile screen sizes diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-region-select.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-region-select.spec.ts deleted file mode 100644 index 069fc806b06..00000000000 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-region-select.spec.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { ui } from 'support/ui'; -import { regionFactory } from '@src/factories'; -import { mockGetRegions } from 'support/intercepts/regions'; -import { extendRegion } from 'support/util/regions'; - -import type { ExtendedRegion } from 'support/util/regions'; - -const mockRegions: ExtendedRegion[] = [ - extendRegion( - regionFactory.build({ - capabilities: ['Linodes'], - country: 'uk', - id: 'eu-west', - label: 'London, UK', - }) - ), - extendRegion( - regionFactory.build({ - capabilities: ['Linodes'], - country: 'sg', - id: 'ap-south', - label: 'Singapore, SG', - }) - ), - extendRegion( - regionFactory.build({ - capabilities: ['Linodes'], - id: 'us-east', - label: 'Newark, NJ', - }) - ), - extendRegion( - regionFactory.build({ - capabilities: ['Linodes'], - id: 'us-central', - label: 'Dallas, TX', - }) - ), -]; - -describe('Linode Create Region Select', () => { - /* - * Region select test. - * - * TODO: Cypress - * Move this to cypress component testing once the setup is complete - see https://github.com/linode/manager/pull/10134 - * - * - Confirms that region select dropdown is visible and interactive. - * - Confirms that region select dropdown is populated with expected regions. - * - Confirms that region select dropdown is sorted alphabetically by region, with North America first. - * - Confirms that region select dropdown is populated with expected DCs, sorted alphabetically. - */ - it('region select', () => { - mockGetRegions(mockRegions).as('getRegions'); - - cy.visitWithLogin('linodes/create'); - - cy.wait('@getRegions'); - - // Confirm that region select dropdown is visible and interactive. - ui.regionSelect.find().click(); - cy.get('[data-qa-autocomplete-popper="true"]').should('be.visible'); - - // Confirm that region select dropdown are grouped by region, - // sorted alphabetically, with North America first. - cy.get('.MuiAutocomplete-groupLabel') - .should('have.length', 3) - .should((group) => { - expect(group[0]).to.contain('North America'); - expect(group[1]).to.contain('Asia'); - expect(group[2]).to.contain('Europe'); - }); - - // Confirm that region select dropdown is populated with expected regions, sorted alphabetically. - cy.get('[data-qa-option]').should('exist').should('have.length', 4); - mockRegions.forEach((region) => { - cy.get('[data-qa-option]').contains(region.label); - }); - - // Select an option - cy.findByTestId('eu-west').click(); - // Confirm the popper is closed - cy.get('[data-qa-autocomplete-popper="true"]').should('not.exist'); - - // Confirm that the selected region is displayed in the input field. - cy.findByLabelText('Region').should('have.value', 'UK, London (eu-west)'); - - // Confirm that selecting a valid region updates the Plan Selection panel. - expect(cy.get('[data-testid="table-row-empty"]').should('not.exist')); - }); -}); diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-view-code-snippet.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-view-code-snippet.spec.ts index 0c3fb2ed059..d801c3046b7 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-view-code-snippet.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-view-code-snippet.spec.ts @@ -6,8 +6,12 @@ import { ui } from 'support/ui'; import { randomLabel, randomString } from 'support/util/random'; import { linodeCreatePage } from 'support/ui/pages'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; -import { chooseRegion } from 'support/util/regions'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; + +import { makeFeatureFlagData } from 'support/util/feature-flags'; describe('Create Linode', () => { /* @@ -18,8 +22,10 @@ describe('Create Linode', () => { // TODO Delete these mocks once `apicliDxToolsAdditions` feature flag is retired. beforeEach(() => { mockAppendFeatureFlags({ - apicliDxToolsAdditions: true, + apicliDxToolsAdditions: makeFeatureFlagData(true), + linodeCreateRefactor: makeFeatureFlagData(true), }); + mockGetFeatureFlagClientstream(); }); it(`view code snippets in create linode flow`, () => { const linodeLabel = randomLabel(); @@ -143,14 +149,15 @@ describe('Create Linode', () => { }); }); }); - describe('Create Linode flow with apicliDxToolsAdditions disabled', () => { // Enable the `apicliDxToolsAdditions` feature flag. // TODO Delete these mocks and test once `apicliDxToolsAdditions` feature flag is retired. beforeEach(() => { mockAppendFeatureFlags({ - apicliDxToolsAdditions: false, + apicliDxToolsAdditions: makeFeatureFlagData(false), + linodeCreateRefactor: makeFeatureFlagData(true), }); + mockGetFeatureFlagClientstream(); }); it(`view code snippets in create linode flow`, () => { const linodeLabel = randomLabel(); @@ -196,75 +203,4 @@ describe('Create Linode', () => { }); }); }); - - it('creates a linode via CLI', () => { - const linodeLabel = randomLabel(); - const linodePass = randomString(32); - const linodeRegion = chooseRegion(); - - cy.visitWithLogin('/linodes/create'); - - ui.regionSelect.find().click(); - ui.autocompletePopper - .findByTitle(`${linodeRegion.label} (${linodeRegion.id})`) - .should('exist') - .click(); - - cy.get('[id="g6-dedicated-2"]').click(); - - cy.findByLabelText('Linode Label').should( - 'have.value', - `debian-${linodeRegion.id}` - ); - - cy.findByLabelText('Linode Label') - .should('be.visible') - .should('be.enabled') - .clear() - .type(linodeLabel); - - cy.findByLabelText('Root Password') - .should('be.visible') - .should('be.enabled') - .type(linodePass); - - ui.button - .findByTitle('Create using command line') - .should('be.visible') - .should('be.enabled') - .click(); - - ui.dialog - .findByTitle('Create Linode') - .should('be.visible') - .within(() => { - // Switch to cURL view if necessary. - cy.findByText('cURL').should('be.visible').click(); - - // Confirm that cURL command has expected details. - [ - `"region": "${linodeRegion.id}"`, - `"type": "g6-dedicated-2"`, - `"label": "${linodeLabel}"`, - `"root_pass": "${linodePass}"`, - ].forEach((line: string) => - cy.findByText(line, { exact: false }).should('be.visible') - ); - - cy.findByText('Linode CLI').should('be.visible').click(); - - [ - `--region ${linodeRegion.id}`, - '--type g6-dedicated-2', - `--label ${linodeLabel}`, - `--root_pass ${linodePass}`, - ].forEach((line: string) => cy.contains(line).should('be.visible')); - - ui.buttonGroup - .findButtonByTitle('Close') - .should('be.visible') - .should('be.enabled') - .click(); - }); - }); }); diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-add-ons.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-add-ons.spec.ts index c6d8befe082..23d62bf666f 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-add-ons.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-add-ons.spec.ts @@ -1,14 +1,27 @@ import { linodeFactory } from 'src/factories'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; import { mockCreateLinode, mockGetLinodeDetails, } from 'support/intercepts/linodes'; import { ui } from 'support/ui'; import { linodeCreatePage } from 'support/ui/pages'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; import { randomLabel, randomNumber, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; describe('Create Linode with Add-ons', () => { + // TODO Remove feature flag mocks when `linodeCreateRefactor` flag is retired. + beforeEach(() => { + mockAppendFeatureFlags({ + linodeCreateRefactor: makeFeatureFlagData(true), + }); + mockGetFeatureFlagClientstream(); + }); + /* * - Confirms UI flow to create a Linode with backups using mock API data. * - Confirms that backups is reflected in create summary section. diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-dc-specific-pricing.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-dc-specific-pricing.spec.ts deleted file mode 100644 index bb2b40b205d..00000000000 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-dc-specific-pricing.spec.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { ui } from 'support/ui'; -import { randomLabel } from 'support/util/random'; -import { getRegionById } from 'support/util/regions'; -import { linodeFactory } from '@src/factories'; -import { - dcPricingPlanPlaceholder, - dcPricingMockLinodeTypes, - dcPricingDocsLabel, - dcPricingDocsUrl, -} from 'support/constants/dc-specific-pricing'; -import { - mockCreateLinode, - mockGetLinodeType, - mockGetLinodeTypes, -} from 'support/intercepts/linodes'; - -describe('Create Linode with DC-specific pricing', () => { - /* - * - Confirms DC-specific pricing UI flow works as expected during Linode creation. - * - Confirms that pricing docs link is shown in "Region" section. - * - Confirms that backups pricing is correct when selecting a region with a different price structure. - */ - it('shows DC-specific pricing information during create flow', () => { - const linodeLabel = randomLabel(); - const initialRegion = getRegionById('us-west'); - const newRegion = getRegionById('us-east'); - - const mockLinode = linodeFactory.build({ - label: linodeLabel, - region: initialRegion.id, - type: dcPricingMockLinodeTypes[0].id, - }); - - const currentPrice = dcPricingMockLinodeTypes[0].region_prices.find( - (regionPrice) => regionPrice.id === initialRegion.id - )!; - const currentBackupPrice = dcPricingMockLinodeTypes[0].addons.backups.region_prices.find( - (regionPrice) => regionPrice.id === initialRegion.id - )!; - const newPrice = dcPricingMockLinodeTypes[1].region_prices.find( - (linodeType) => linodeType.id === newRegion.id - )!; - const newBackupPrice = dcPricingMockLinodeTypes[1].addons.backups.region_prices.find( - (regionPrice) => regionPrice.id === newRegion.id - )!; - - // Mock requests to get individual types. - mockGetLinodeType(dcPricingMockLinodeTypes[0]); - mockGetLinodeType(dcPricingMockLinodeTypes[1]); - mockGetLinodeTypes(dcPricingMockLinodeTypes).as('getLinodeTypes'); - - // intercept request - cy.visitWithLogin('/linodes/create'); - cy.wait(['@getLinodeTypes']); - - mockCreateLinode(mockLinode).as('linodeCreated'); - - cy.get('[data-qa-header="Create"]').should('have.text', 'Create'); - - ui.button.findByTitle('Create Linode').click(); - - // A message is shown to instruct users to select a region in order to view plans and prices - cy.get('[data-qa-tp="Linode Plan"]').should( - 'contain.text', - 'Plan is required.' - ); - cy.get('[data-qa-tp="Linode Plan"]').should( - 'contain.text', - dcPricingPlanPlaceholder - ); - - // Check the 'Backups' add on - cy.get('[data-testid="backups"]').should('be.visible').click(); - ui.regionSelect.find().click(); - ui.regionSelect.findItemByRegionLabel(initialRegion.label).click(); - cy.findByText('Shared CPU').click(); - cy.get(`[id="${dcPricingMockLinodeTypes[0].id}"]`).click(); - // Confirm that the backup prices are displayed as expected. - cy.get('[data-qa-add-ons="true"]').within(() => { - cy.findByText(`$${currentBackupPrice.monthly}`).should('be.visible'); - cy.findByText('per month').should('be.visible'); - }); - // Confirm that the checkout summary at the bottom of the page reflects the correct price. - cy.get('[data-qa-linode-create-summary="true"]').within(() => { - cy.findByText(`$${currentPrice.monthly!.toFixed(2)}/month`).should( - 'be.visible' - ); - cy.findByText('Backups').should('be.visible'); - cy.findByText(`$${currentBackupPrice.monthly!.toFixed(2)}/month`).should( - 'be.visible' - ); - }); - - // Confirm there is a docs link to the pricing page. - cy.findByText(dcPricingDocsLabel) - .should('be.visible') - .should('have.attr', 'href', dcPricingDocsUrl); - - ui.regionSelect.find().click().type(`${newRegion.label} {enter}`); - cy.findByText('Shared CPU').click(); - cy.get(`[id="${dcPricingMockLinodeTypes[0].id}"]`).click(); - // Confirm that the backup prices are displayed as expected. - cy.get('[data-qa-add-ons="true"]').within(() => { - cy.findByText(`$${newBackupPrice.monthly}`).should('be.visible'); - cy.findByText('per month').should('be.visible'); - }); - // Confirms that the summary updates to reflect price changes if the user changes their region and plan selection. - cy.get('[data-qa-linode-create-summary="true"]').within(() => { - cy.findByText(`$${newPrice.monthly!.toFixed(2)}/month`).should( - 'be.visible' - ); - cy.findByText('Backups').should('be.visible'); - cy.findByText(`$${newBackupPrice.monthly!.toFixed(2)}/month`).should( - 'be.visible' - ); - }); - }); -}); diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-disk-encryption.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-disk-encryption.spec.ts deleted file mode 100644 index cf8707c1ec9..00000000000 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-disk-encryption.spec.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { ui } from 'support/ui'; -import { accountFactory, regionFactory } from '@src/factories'; -import { mockGetRegions } from 'support/intercepts/regions'; -import { mockGetAccount } from 'support/intercepts/account'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; -import { makeFeatureFlagData } from 'support/util/feature-flags'; -import { - checkboxTestId, - headerTestId, -} from 'src/components/Encryption/Encryption'; - -describe('Create Linode with Disk Encryption', () => { - it('should not have a "Disk Encryption" section visible if the feature flag is off and user does not have capability', () => { - // Mock feature flag -- @TODO LDE: Remove feature flag once LDE is fully rolled out - mockAppendFeatureFlags({ - linodeDiskEncryption: makeFeatureFlagData(false), - }).as('getFeatureFlags'); - - // Mock account response - const mockAccount = accountFactory.build({ - capabilities: ['Linodes'], - }); - - mockGetAccount(mockAccount).as('getAccount'); - - // intercept request - cy.visitWithLogin('/linodes/create'); - cy.wait(['@getFeatureFlags', '@getAccount']); - - // Check if section is visible - cy.get(`[data-testid=${headerTestId}]`).should('not.exist'); - }); - - it('should have a "Disk Encryption" section visible if feature flag is on and user has the capability', () => { - // Mock feature flag -- @TODO LDE: Remove feature flag once LDE is fully rolled out - mockAppendFeatureFlags({ - linodeDiskEncryption: makeFeatureFlagData(true), - }).as('getFeatureFlags'); - - // Mock account response - const mockAccount = accountFactory.build({ - capabilities: ['Linodes', 'Disk Encryption'], - }); - - const mockRegion = regionFactory.build({ - capabilities: ['Linodes', 'Disk Encryption'], - }); - - const mockRegionWithoutDiskEncryption = regionFactory.build({ - capabilities: ['Linodes'], - }); - - const mockRegions = [mockRegion, mockRegionWithoutDiskEncryption]; - - mockGetAccount(mockAccount).as('getAccount'); - mockGetRegions(mockRegions); - - // intercept request - cy.visitWithLogin('/linodes/create'); - cy.wait(['@getFeatureFlags', '@getAccount']); - - // Check if section is visible - cy.get(`[data-testid="${headerTestId}"]`).should('exist'); - - // "Encrypt Disk" checkbox should be disabled if a region that does not support LDE is selected - ui.regionSelect.find().click(); - ui.select - .findItemByText( - `${mockRegionWithoutDiskEncryption.label} (${mockRegionWithoutDiskEncryption.id})` - ) - .click(); - - cy.get(`[data-testid="${checkboxTestId}"]`).should('be.disabled'); - - ui.regionSelect.find().click(); - ui.select.findItemByText(`${mockRegion.label} (${mockRegion.id})`).click(); - - cy.get(`[data-testid="${checkboxTestId}"]`).should('be.enabled'); - }); -}); diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-firewall.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-firewall.spec.ts index 70a759c140a..1659a07189c 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-firewall.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-firewall.spec.ts @@ -3,6 +3,10 @@ import { firewallFactory, firewallTemplateFactory, } from 'src/factories'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; import { mockCreateLinode, mockGetLinodeDetails, @@ -15,10 +19,19 @@ import { } from 'support/intercepts/firewalls'; import { ui } from 'support/ui'; import { linodeCreatePage } from 'support/ui/pages'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; import { randomLabel, randomNumber, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; describe('Create Linode with Firewall', () => { + // TODO Remove feature flag mocks when `linodeCreateRefactor` flag is retired. + beforeEach(() => { + mockAppendFeatureFlags({ + linodeCreateRefactor: makeFeatureFlagData(true), + }); + mockGetFeatureFlagClientstream(); + }); + /* * - Confirms UI flow to create a Linode with an existing Firewall using mock API data. * - Confirms that Firewall is reflected in create summary section. diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-ssh-key.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-ssh-key.spec.ts index b613f8cf384..07a04310671 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-ssh-key.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-ssh-key.spec.ts @@ -3,6 +3,11 @@ import { linodeFactory, sshKeyFactory, } from 'src/factories'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; import { randomLabel, randomNumber, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; import { mockGetUser, mockGetUsers } from 'support/intercepts/account'; @@ -12,6 +17,14 @@ import { ui } from 'support/ui'; import { mockCreateSSHKey } from 'support/intercepts/profile'; describe('Create Linode with SSH Key', () => { + // TODO Remove feature flag mocks when `linodeCreateRefactor` flag is retired. + beforeEach(() => { + mockAppendFeatureFlags({ + linodeCreateRefactor: makeFeatureFlagData(true), + }); + mockGetFeatureFlagClientstream(); + }); + /* * - Confirms UI flow when creating a Linode with an authorized SSH key. * - Confirms that existing SSH keys are listed on page and can be selected. diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts index 8951ed66e9b..21096becdf3 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts @@ -1,4 +1,8 @@ import { imageFactory, linodeFactory, regionFactory } from 'src/factories'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; import { mockGetAllImages, mockGetImage } from 'support/intercepts/images'; import { mockCreateLinode, @@ -7,10 +11,18 @@ import { import { mockGetRegions } from 'support/intercepts/regions'; import { ui } from 'support/ui'; import { linodeCreatePage } from 'support/ui/pages'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; import { randomLabel, randomNumber, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; describe('Create Linode with user data', () => { + beforeEach(() => { + mockAppendFeatureFlags({ + linodeCreateRefactor: makeFeatureFlagData(true), + }); + mockGetFeatureFlagClientstream(); + }); + /* * - Confirms UI flow to create a Linode with cloud-init user data specified. * - Confirms that outgoing API request contains expected user data payload. @@ -118,8 +130,6 @@ describe('Create Linode with user data', () => { vendor: 'Debian', // `cloud-init` is omitted from Image capabilities. capabilities: [], - // null eol so that the image is not deprecated - eol: null, }); mockGetImage(mockImage.id, mockImage); diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-vlan.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-vlan.spec.ts index d80d2a963b9..65361c25d01 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-vlan.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-vlan.spec.ts @@ -2,6 +2,11 @@ import { linodeFactory, regionFactory, VLANFactory } from 'src/factories'; import { mockGetRegions } from 'support/intercepts/regions'; import { ui } from 'support/ui'; import { linodeCreatePage } from 'support/ui/pages'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; import { chooseRegion } from 'support/util/regions'; import { randomIp, @@ -13,6 +18,14 @@ import { mockGetVLANs } from 'support/intercepts/vlans'; import { mockCreateLinode } from 'support/intercepts/linodes'; describe('Create Linode with VLANs', () => { + // TODO Remove feature flag mocks when `linodeCreateRefactor` flag is retired. + beforeEach(() => { + mockAppendFeatureFlags({ + linodeCreateRefactor: makeFeatureFlagData(true), + }); + mockGetFeatureFlagClientstream(); + }); + /* * - Uses mock API data to confirm VLAN attachment UI flow during Linode create. * - Confirms that outgoing Linode create API request contains expected data for VLAN. diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-vpc.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-vpc.spec.ts index 44041fc9dc6..238be908e06 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-vpc.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-vpc.spec.ts @@ -4,6 +4,10 @@ import { subnetFactory, vpcFactory, } from 'src/factories'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; import { mockCreateLinode, mockGetLinodeDetails, @@ -17,6 +21,7 @@ import { } from 'support/intercepts/vpc'; import { ui } from 'support/ui'; import { linodeCreatePage, vpcCreateDrawer } from 'support/ui/pages'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; import { randomIp, randomLabel, @@ -27,6 +32,14 @@ import { import { chooseRegion } from 'support/util/regions'; describe('Create Linode with VPCs', () => { + // TODO Remove feature flag mocks when `linodeCreateRefactor` flag is retired. + beforeEach(() => { + mockAppendFeatureFlags({ + linodeCreateRefactor: makeFeatureFlagData(true), + }); + mockGetFeatureFlagClientstream(); + }); + /* * - Confirms UI flow to create a Linode with an existing VPC assigned using mock API data. * - Confirms that VPC assignment is reflected in create summary section. 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 f57807153d6..bf86857ff8c 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts @@ -9,10 +9,15 @@ import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; import { cleanUp } from 'support/util/cleanup'; import { linodeCreatePage } from 'support/ui/pages'; import { authenticate } from 'support/api/authentication'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; import { interceptCreateLinode, mockCreateLinodeError, } from 'support/intercepts/linodes'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; import { interceptGetProfile } from 'support/intercepts/profile'; import { Region, VLAN, Config, Disk } from '@linode/api-v4'; import { getRegionById } from 'support/util/regions'; @@ -57,6 +62,15 @@ describe('Create Linode', () => { cleanUp('ssh-keys'); }); + // Enable the `linodeCreateRefactor` feature flag. + // TODO Delete these mocks once `linodeCreateRefactor` feature flag is retired. + beforeEach(() => { + mockAppendFeatureFlags({ + linodeCreateRefactor: makeFeatureFlagData(true), + }); + mockGetFeatureFlagClientstream(); + }); + /* * End-to-end tests to create Linodes for each available plan type. */ 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 new file mode 100644 index 00000000000..48ece191c37 --- /dev/null +++ b/packages/manager/cypress/e2e/core/linodes/legacy-create-linode.spec.ts @@ -0,0 +1,609 @@ +/** + * @file Integration tests and end-to-end tests for legacy Linode Create flow. + */ +// TODO Delete this test file when `linodeCreateRefactor` feature flag is retired. +// Move out any tests (e.g. region select test) for flows that aren't covered by new tests in the meantime. + +import { + containsVisible, + fbtClick, + fbtVisible, + getClick, + getVisible, +} from 'support/helpers'; +import { ui } from 'support/ui'; +import { randomString, randomLabel, randomNumber } from 'support/util/random'; +import { chooseRegion } from 'support/util/regions'; +import { getRegionById } from 'support/util/regions'; +import { + accountFactory, + subnetFactory, + vpcFactory, + linodeFactory, + linodeConfigFactory, + regionFactory, + VLANFactory, + LinodeConfigInterfaceFactory, + LinodeConfigInterfaceFactoryWithVPC, +} from '@src/factories'; +import { authenticate } from 'support/api/authentication'; +import { cleanUp } from 'support/util/cleanup'; +import { mockGetRegions } from 'support/intercepts/regions'; +import { + dcPricingPlanPlaceholder, + dcPricingMockLinodeTypes, + dcPricingDocsLabel, + dcPricingDocsUrl, +} from 'support/constants/dc-specific-pricing'; +import { mockGetVLANs } from 'support/intercepts/vlans'; +import { mockGetLinodeConfigs } from 'support/intercepts/configs'; +import { + interceptCreateLinode, + mockCreateLinode, + mockGetLinodeType, + mockGetLinodeTypes, + mockGetLinodeDisks, + mockGetLinodeVolumes, +} from 'support/intercepts/linodes'; +import { mockGetAccount } from 'support/intercepts/account'; +import { mockGetVPC, mockGetVPCs } from 'support/intercepts/vpc'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; +import { + checkboxTestId, + headerTestId, +} from 'src/components/Encryption/Encryption'; +import { extendRegion } from 'support/util/regions'; + +import type { Config, VLAN, Disk, Region } from '@linode/api-v4'; +import type { ExtendedRegion } from 'support/util/regions'; + +const mockRegions: ExtendedRegion[] = [ + extendRegion( + regionFactory.build({ + capabilities: ['Linodes'], + country: 'uk', + id: 'eu-west', + label: 'London, UK', + }) + ), + extendRegion( + regionFactory.build({ + capabilities: ['Linodes'], + country: 'sg', + id: 'ap-south', + label: 'Singapore, SG', + }) + ), + extendRegion( + regionFactory.build({ + capabilities: ['Linodes'], + id: 'us-east', + label: 'Newark, NJ', + }) + ), + extendRegion( + regionFactory.build({ + capabilities: ['Linodes'], + id: 'us-central', + label: 'Dallas, TX', + }) + ), +]; + +authenticate(); +describe('create linode', () => { + before(() => { + cleanUp('linodes'); + }); + + beforeEach(() => { + mockAppendFeatureFlags({ + linodeCreateRefactor: makeFeatureFlagData(false), + apicliDxToolsAdditions: makeFeatureFlagData(false), + }); + }); + + /* + * Region select test. + * + * TODO: Cypress + * Move this to cypress component testing once the setup is complete - see https://github.com/linode/manager/pull/10134 + * + * - Confirms that region select dropdown is visible and interactive. + * - Confirms that region select dropdown is populated with expected regions. + * - Confirms that region select dropdown is sorted alphabetically by region, with North America first. + * - Confirms that region select dropdown is populated with expected DCs, sorted alphabetically. + */ + it('region select', () => { + mockGetRegions(mockRegions).as('getRegions'); + + cy.visitWithLogin('linodes/create'); + + cy.wait(['@getRegions']); + + // Confirm that region select dropdown is visible and interactive. + ui.regionSelect.find().click(); + cy.get('[data-qa-autocomplete-popper="true"]').should('be.visible'); + + // Confirm that region select dropdown are grouped by region, + // sorted alphabetically, with North America first. + cy.get('.MuiAutocomplete-groupLabel') + .should('have.length', 3) + .should((group) => { + expect(group[0]).to.contain('North America'); + expect(group[1]).to.contain('Asia'); + expect(group[2]).to.contain('Europe'); + }); + + // Confirm that region select dropdown is populated with expected regions, sorted alphabetically. + cy.get('[data-qa-option]').should('exist').should('have.length', 4); + mockRegions.forEach((region) => { + cy.get('[data-qa-option]').contains(region.label); + }); + + // Select an option + cy.findByTestId('eu-west').click(); + // Confirm the popper is closed + cy.get('[data-qa-autocomplete-popper="true"]').should('not.exist'); + // Confirm that the selected region is displayed in the input field. + cy.get('[data-testid="textfield-input"]').should( + 'have.value', + 'UK, London (eu-west)' + ); + + // Confirm that selecting a valid region updates the Plan Selection panel. + expect(cy.get('[data-testid="table-row-empty"]').should('not.exist')); + }); + + it('creates a nanode', () => { + const rootpass = randomString(32); + const linodeLabel = randomLabel(); + // intercept request + cy.visitWithLogin('/linodes/create'); + cy.get('[data-qa-deploy-linode]'); + interceptCreateLinode().as('linodeCreated'); + cy.get('[data-qa-header="Create"]').should('have.text', 'Create'); + ui.regionSelect.find().click(); + ui.regionSelect + .findItemByRegionLabel( + chooseRegion({ capabilities: ['Vlans', 'Linodes'] }).label + ) + .click(); + fbtClick('Shared CPU'); + getClick('[id="g6-nanode-1"]'); + getClick('#linode-label').clear().type(linodeLabel); + cy.get('#root-password').type(rootpass); + getClick('[data-qa-deploy-linode]'); + cy.wait('@linodeCreated').its('response.statusCode').should('eq', 200); + ui.toast.assertMessage(`Your Linode ${linodeLabel} is being created.`); + containsVisible('PROVISIONING'); + fbtVisible(linodeLabel); + cy.contains('RUNNING', { timeout: 300000 }).should('be.visible'); + }); + + it('creates a linode via CLI', () => { + const linodeLabel = randomLabel(); + const linodePass = randomString(32); + const linodeRegion = chooseRegion(); + + cy.visitWithLogin('/linodes/create'); + + ui.regionSelect.find().click(); + ui.autocompletePopper + .findByTitle(`${linodeRegion.label} (${linodeRegion.id})`) + .should('exist') + .click(); + + cy.get('[id="g6-dedicated-2"]').click(); + + cy.findByLabelText('Linode Label') + .should('be.visible') + .should('be.enabled') + .clear() + .type(linodeLabel); + + cy.findByLabelText('Root Password') + .should('be.visible') + .should('be.enabled') + .type(linodePass); + + ui.button + .findByTitle('Create using command line') + .should('be.visible') + .should('be.enabled') + .click(); + + ui.dialog + .findByTitle('Create Linode') + .should('be.visible') + .within(() => { + // Switch to cURL view if necessary. + cy.findByText('cURL') + .should('be.visible') + .should('have.attr', 'data-selected'); + + // Confirm that cURL command has expected details. + [ + `"region": "${linodeRegion.id}"`, + `"type": "g6-dedicated-2"`, + `"label": "${linodeLabel}"`, + `"root_pass": "${linodePass}"`, + '"booted": true', + ].forEach((line: string) => + cy.findByText(line, { exact: false }).should('be.visible') + ); + + cy.findByText('Linode CLI').should('be.visible').click(); + + [ + `--region ${linodeRegion.id}`, + '--type g6-dedicated-2', + `--label ${linodeLabel}`, + `--root_pass ${linodePass}`, + `--booted true`, + ].forEach((line: string) => cy.contains(line).should('be.visible')); + + ui.buttonGroup + .findButtonByTitle('Close') + .should('be.visible') + .should('be.enabled') + .click(); + }); + }); + + /* + * - Confirms DC-specific pricing UI flow works as expected during Linode creation. + * - Confirms that pricing docs link is shown in "Region" section. + * - Confirms that backups pricing is correct when selecting a region with a different price structure. + */ + it('shows DC-specific pricing information during create flow', () => { + const rootpass = randomString(32); + const linodeLabel = randomLabel(); + const initialRegion = getRegionById('us-west'); + const newRegion = getRegionById('us-east'); + + const mockLinode = linodeFactory.build({ + label: linodeLabel, + region: initialRegion.id, + type: dcPricingMockLinodeTypes[0].id, + }); + + const currentPrice = dcPricingMockLinodeTypes[0].region_prices.find( + (regionPrice) => regionPrice.id === initialRegion.id + )!; + const currentBackupPrice = dcPricingMockLinodeTypes[0].addons.backups.region_prices.find( + (regionPrice) => regionPrice.id === initialRegion.id + )!; + const newPrice = dcPricingMockLinodeTypes[1].region_prices.find( + (linodeType) => linodeType.id === newRegion.id + )!; + const newBackupPrice = dcPricingMockLinodeTypes[1].addons.backups.region_prices.find( + (regionPrice) => regionPrice.id === newRegion.id + )!; + + // Mock requests to get individual types. + mockGetLinodeType(dcPricingMockLinodeTypes[0]); + mockGetLinodeType(dcPricingMockLinodeTypes[1]); + mockGetLinodeTypes(dcPricingMockLinodeTypes).as('getLinodeTypes'); + + // intercept request + cy.visitWithLogin('/linodes/create'); + cy.wait(['@getLinodeTypes']); + + mockCreateLinode(mockLinode).as('linodeCreated'); + cy.get('[data-qa-header="Create"]').should('have.text', 'Create'); + getClick('[data-qa-deploy-linode]'); + + // A message is shown to instruct users to select a region in order to view plans and prices + cy.get('[data-qa-tp="Linode Plan"]').should( + 'contain.text', + 'Plan is required.' + ); + cy.get('[data-qa-tp="Linode Plan"]').should( + 'contain.text', + dcPricingPlanPlaceholder + ); + + // Check the 'Backups' add on + cy.get('[data-testid="backups"]').should('be.visible').click(); + ui.regionSelect.find().click(); + ui.regionSelect.findItemByRegionLabel(initialRegion.label).click(); + fbtClick('Shared CPU'); + getClick(`[id="${dcPricingMockLinodeTypes[0].id}"]`); + // Confirm that the backup prices are displayed as expected. + cy.get('[data-qa-add-ons="true"]') + .eq(1) + .within(() => { + cy.findByText(`$${currentBackupPrice.monthly}`).should('be.visible'); + cy.findByText('per month').should('be.visible'); + }); + // Confirm that the checkout summary at the bottom of the page reflects the correct price. + cy.get('[data-qa-summary="true"]').within(() => { + cy.findByText(`$${currentPrice.monthly!.toFixed(2)}/month`).should( + 'be.visible' + ); + cy.findByText('Backups').should('be.visible'); + cy.findByText(`$${currentBackupPrice.monthly!.toFixed(2)}/month`).should( + 'be.visible' + ); + }); + + // Confirm there is a docs link to the pricing page. + cy.findByText(dcPricingDocsLabel) + .should('be.visible') + .should('have.attr', 'href', dcPricingDocsUrl); + + ui.regionSelect.find().click().type(`${newRegion.label} {enter}`); + fbtClick('Shared CPU'); + getClick(`[id="${dcPricingMockLinodeTypes[0].id}"]`); + // Confirm that the backup prices are displayed as expected. + cy.get('[data-qa-add-ons="true"]') + .eq(1) + .within(() => { + cy.findByText(`$${newBackupPrice.monthly}`).should('be.visible'); + cy.findByText('per month').should('be.visible'); + }); + // Confirms that the summary updates to reflect price changes if the user changes their region and plan selection. + cy.get('[data-qa-summary="true"]').within(() => { + cy.findByText(`$${newPrice.monthly!.toFixed(2)}/month`).should( + 'be.visible' + ); + cy.findByText('Backups').should('be.visible'); + cy.findByText(`$${newBackupPrice.monthly!.toFixed(2)}/month`).should( + 'be.visible' + ); + }); + + getClick('#linode-label').clear().type(linodeLabel); + cy.get('#root-password').type(rootpass); + getClick('[data-qa-deploy-linode]'); + cy.wait('@linodeCreated').its('response.statusCode').should('eq', 200); + fbtVisible(linodeLabel); + cy.contains('RUNNING', { timeout: 300000 }).should('be.visible'); + }); + + it("prevents a VPC from being assigned in a region that doesn't support VPCs during the Linode Create flow", () => { + const region: Region = getRegionById('us-southeast'); + const mockNoVPCRegion = regionFactory.build({ + id: region.id, + label: region.label, + capabilities: ['Linodes'], + }); + + // Mock requests to get individual types. + mockGetLinodeType(dcPricingMockLinodeTypes[0]); + mockGetLinodeType(dcPricingMockLinodeTypes[1]); + mockGetLinodeTypes(dcPricingMockLinodeTypes).as('getLinodeTypes'); + mockGetRegions([mockNoVPCRegion]).as('getRegions'); + + // intercept request + cy.visitWithLogin('/linodes/create'); + cy.wait('@getLinodeTypes'); + + cy.get('[data-qa-header="Create"]').should('have.text', 'Create'); + + // Check the 'Backups' add on + cy.get('[data-testid="backups"]').should('be.visible').click(); + ui.regionSelect.find().click().type(`${region.label} {enter}`); + fbtClick('Shared CPU'); + getClick(`[id="${dcPricingMockLinodeTypes[0].id}"]`); + + // the "VPC" section is present + getVisible('[data-testid="vpc-panel"]').within(() => { + containsVisible( + 'Allow Linode to communicate in an isolated environment.' + ); + // Helper text appears if VPC is not available in selected region. + containsVisible('VPC is not available in the selected region.'); + }); + }); + + it('assigns a VPC to the linode during create flow', () => { + const rootpass = randomString(32); + const linodeLabel = randomLabel(); + const region: Region = getRegionById('us-southeast'); + const diskLabel: string = 'Debian 10 Disk'; + const mockLinode = linodeFactory.build({ + label: linodeLabel, + region: region.id, + type: dcPricingMockLinodeTypes[0].id, + }); + const mockVLANs: VLAN[] = VLANFactory.buildList(2); + const mockSubnet = subnetFactory.build({ + id: randomNumber(2), + label: randomLabel(), + }); + const mockVPC = vpcFactory.build({ + id: randomNumber(), + region: 'us-southeast', + subnets: [mockSubnet], + label: randomLabel(), + }); + const mockVPCRegion = regionFactory.build({ + id: region.id, + label: region.label, + capabilities: ['Linodes', 'VPCs', 'Vlans'], + }); + const mockPublicConfigInterface = LinodeConfigInterfaceFactory.build({ + ipam_address: null, + purpose: 'public', + }); + const mockVlanConfigInterface = LinodeConfigInterfaceFactory.build(); + const mockVpcConfigInterface = LinodeConfigInterfaceFactoryWithVPC.build({ + vpc_id: mockVPC.id, + purpose: 'vpc', + active: true, + }); + const mockConfig: Config = linodeConfigFactory.build({ + id: randomNumber(), + interfaces: [ + // The order of this array is significant. Index 0 (eth0) should be public. + mockPublicConfigInterface, + mockVlanConfigInterface, + mockVpcConfigInterface, + ], + }); + const mockDisks: Disk[] = [ + { + id: 44311273, + status: 'ready', + label: diskLabel, + created: '2020-08-21T17:26:14', + updated: '2020-08-21T17:26:30', + filesystem: 'ext4', + size: 81408, + }, + { + id: 44311274, + status: 'ready', + label: '512 MB Swap Image', + created: '2020-08-21T17:26:14', + updated: '2020-08-21T17:26:31', + filesystem: 'swap', + size: 512, + }, + ]; + + // Mock requests to get individual types. + mockGetLinodeType(dcPricingMockLinodeTypes[0]); + mockGetLinodeType(dcPricingMockLinodeTypes[1]); + mockGetLinodeTypes(dcPricingMockLinodeTypes).as('getLinodeTypes'); + + mockAppendFeatureFlags({ + apicliDxToolsAdditions: makeFeatureFlagData(false), + }).as('getFeatureFlags'); + mockGetFeatureFlagClientstream().as('getClientStream'); + + mockGetRegions([mockVPCRegion]).as('getRegions'); + + mockGetVLANs(mockVLANs); + mockGetVPC(mockVPC).as('getVPC'); + mockGetVPCs([mockVPC]).as('getVPCs'); + mockCreateLinode(mockLinode).as('linodeCreated'); + mockGetLinodeConfigs(mockLinode.id, [mockConfig]).as('getLinodeConfigs'); + mockGetLinodeDisks(mockLinode.id, mockDisks).as('getDisks'); + mockGetLinodeVolumes(mockLinode.id, []).as('getVolumes'); + + // intercept request + cy.visitWithLogin('/linodes/create'); + cy.wait(['@getLinodeTypes', '@getVPCs']); + + cy.get('[data-qa-header="Create"]').should('have.text', 'Create'); + + // Check the 'Backups' add on + cy.get('[data-testid="backups"]').should('be.visible').click(); + ui.regionSelect.find().click().type(`${region.label} {enter}`); + fbtClick('Shared CPU'); + getClick(`[id="${dcPricingMockLinodeTypes[0].id}"]`); + + // the "VPC" section is present, and the VPC in the same region of + // the linode can be selected. + getVisible('[data-testid="vpc-panel"]').within(() => { + containsVisible('Assign this Linode to an existing VPC.'); + // select VPC + cy.findByLabelText('Assign VPC') + .should('be.visible') + .focus() + .clear() + .type(`${mockVPC.label}{downArrow}{enter}`); + // select subnet + cy.findByPlaceholderText('Select Subnet') + .should('be.visible') + .type(`${mockSubnet.label}{downArrow}{enter}`); + }); + + getClick('#linode-label').clear().type(linodeLabel); + cy.get('#root-password').type(rootpass); + + ui.button.findByTitle('Create Linode').click(); + + cy.wait('@linodeCreated').its('response.statusCode').should('eq', 200); + fbtVisible(linodeLabel); + cy.contains('RUNNING', { timeout: 300000 }).should('be.visible'); + + fbtClick('Configurations'); + //cy.wait(['@getLinodeConfigs', '@getVPC', '@getDisks', '@getVolumes']); + + // Confirm that VLAN and VPC have been assigned. + cy.findByLabelText('List of Configurations').within(() => { + cy.get('tr').should('have.length', 2); + containsVisible(`${mockConfig.label} – GRUB 2`); + containsVisible('eth0 – Public Internet'); + containsVisible(`eth2 – VPC: ${mockVPC.label}`); + }); + }); + + it('should not have a "Disk Encryption" section visible if the feature flag is off and user does not have capability', () => { + // Mock feature flag -- @TODO LDE: Remove feature flag once LDE is fully rolled out + mockAppendFeatureFlags({ + linodeDiskEncryption: makeFeatureFlagData(false), + apicliDxToolsAdditions: makeFeatureFlagData(false), + }).as('getFeatureFlags'); + + // Mock account response + const mockAccount = accountFactory.build({ + capabilities: ['Linodes'], + }); + + mockGetAccount(mockAccount).as('getAccount'); + + // intercept request + cy.visitWithLogin('/linodes/create'); + cy.wait(['@getFeatureFlags', '@getAccount']); + + // Check if section is visible + cy.get(`[data-testid=${headerTestId}]`).should('not.exist'); + }); + + it('should have a "Disk Encryption" section visible if feature flag is on and user has the capability', () => { + // Mock feature flag -- @TODO LDE: Remove feature flag once LDE is fully rolled out + mockAppendFeatureFlags({ + linodeDiskEncryption: makeFeatureFlagData(true), + apicliDxToolsAdditions: makeFeatureFlagData(false), + }).as('getFeatureFlags'); + + // Mock account response + const mockAccount = accountFactory.build({ + capabilities: ['Linodes', 'Disk Encryption'], + }); + + const mockRegion = regionFactory.build({ + capabilities: ['Linodes', 'Disk Encryption'], + }); + + const mockRegionWithoutDiskEncryption = regionFactory.build({ + capabilities: ['Linodes'], + }); + + const mockRegions = [mockRegion, mockRegionWithoutDiskEncryption]; + + mockGetAccount(mockAccount).as('getAccount'); + mockGetRegions(mockRegions); + + // intercept request + cy.visitWithLogin('/linodes/create'); + cy.wait(['@getFeatureFlags', '@getAccount']); + + // Check if section is visible + cy.get(`[data-testid="${headerTestId}"]`).should('exist'); + + // "Encrypt Disk" checkbox should be disabled if a region that does not support LDE is selected + ui.regionSelect.find().click(); + ui.select + .findItemByText( + `${mockRegionWithoutDiskEncryption.label} (${mockRegionWithoutDiskEncryption.id})` + ) + .click(); + + cy.get(`[data-testid="${checkboxTestId}"]`).should('be.disabled'); + + ui.regionSelect.find().click(); + ui.select.findItemByText(`${mockRegion.label} (${mockRegion.id})`).click(); + + cy.get(`[data-testid="${checkboxTestId}"]`).should('be.enabled'); + }); +}); diff --git a/packages/manager/cypress/e2e/core/linodes/linode-config.spec.ts b/packages/manager/cypress/e2e/core/linodes/linode-config.spec.ts index 68ec28b9701..2e73ca80de0 100644 --- a/packages/manager/cypress/e2e/core/linodes/linode-config.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/linode-config.spec.ts @@ -91,9 +91,6 @@ describe('Linode Config management', () => { } ); }); - beforeEach(() => { - cy.tag('method:e2e'); - }); /* * - Tests Linode config creation end-to-end using real API requests. diff --git a/packages/manager/cypress/e2e/core/linodes/linode-network.spec.ts b/packages/manager/cypress/e2e/core/linodes/linode-network.spec.ts deleted file mode 100644 index d17336e1110..00000000000 --- a/packages/manager/cypress/e2e/core/linodes/linode-network.spec.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { linodeFactory, ipAddressFactory } from '@src/factories'; - -import { - mockGetLinodeDetails, - mockGetLinodeIPAddresses, - mockGetLinodeFirewalls, -} from 'support/intercepts/linodes'; -import { mockUpdateIPAddress } from 'support/intercepts/networking'; -import { ui } from 'support/ui'; - -describe('linode networking', () => { - /** - * - Confirms the success toast message after editing RDNS - */ - it('checks for the toast message upon editing an RDNS', () => { - const mockLinode = linodeFactory.build(); - const linodeIPv4 = mockLinode.ipv4[0]; - const mockRDNS = `${linodeIPv4}.ip.linodeusercontent.com`; - const ipAddress = ipAddressFactory.build({ - address: linodeIPv4, - linode_id: mockLinode.id, - rdns: mockRDNS, - }); - - mockGetLinodeDetails(mockLinode.id, mockLinode).as('getLinode'); - mockGetLinodeFirewalls(mockLinode.id, []).as('getLinodeFirewalls'); - mockGetLinodeIPAddresses(mockLinode.id, { - ipv4: { - public: [ipAddress], - private: [], - shared: [], - reserved: [], - }, - }).as('getLinodeIPAddresses'); - mockUpdateIPAddress(linodeIPv4, mockRDNS).as('updateIPAddress'); - - cy.visitWithLogin(`linodes/${mockLinode.id}/networking`); - cy.wait(['@getLinode', '@getLinodeFirewalls', '@getLinodeIPAddresses']); - - cy.findByLabelText('IPv4 Addresses') - .should('be.visible') - .within(() => { - // confirm table headers - cy.get('thead').findByText('Address').should('be.visible'); - cy.get('thead').findByText('Type').should('be.visible'); - cy.get('thead').findByText('Default Gateway').should('be.visible'); - cy.get('thead').findByText('Subnet Mask').should('be.visible'); - cy.get('thead').findByText('Reverse DNS').should('be.visible'); - }); - - // confirm row for Linode's (first) IPv4 address exists and open up the RDNS drawer - cy.get(`[data-qa-ip="${linodeIPv4}"]`) - .should('be.visible') - .closest('tr') - .within(() => { - cy.findByText('IPv4 – Public').should('be.visible'); - cy.findByText(mockRDNS).should('be.visible'); - - // open up the edit RDNS drawer - ui.button.findByTitle('Edit RDNS').should('be.visible').click(); - }); - - // confirm RDNS drawer is visible - ui.drawer - .findByTitle('Edit Reverse DNS') - .should('be.visible') - .within(() => { - cy.findByText('Leave this field blank to reset RDNS').should( - 'be.visible' - ); - - // click Save button - this test is only to confirm the toast message - // and intentionally doesn't edit the RDNS form. Note - although we're using - // mocks here, with actual data, I would get an error each time I tried to edit the RDNS - cy.findByText('Save').should('be.visible').should('be.enabled').click(); - }); - - cy.wait(['@updateIPAddress']); - - // confirm RDNS toast message - ui.toast.assertMessage(`Successfully updated RDNS for ${linodeIPv4}`); - }); -}); diff --git a/packages/manager/cypress/e2e/core/linodes/linode-storage.spec.ts b/packages/manager/cypress/e2e/core/linodes/linode-storage.spec.ts index 9403d258e72..0e256bbbb41 100644 --- a/packages/manager/cypress/e2e/core/linodes/linode-storage.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/linode-storage.spec.ts @@ -5,11 +5,7 @@ import { createTestLinode } from 'support/util/linodes'; import { containsVisible, fbtClick, fbtVisible } from 'support/helpers'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; -import { - interceptDeleteDisks, - interceptAddDisks, - interceptResizeDisks, -} from 'support/intercepts/linodes'; +import { apiMatcher } from 'support/util/intercepts'; // 3 minutes. const LINODE_PROVISION_TIMEOUT = 180_000; @@ -101,9 +97,6 @@ const addDisk = (diskName: string) => { }; authenticate(); -beforeEach(() => { - cy.tag('method:e2e'); -}); describe('linode storage tab', () => { before(() => { cleanUp(['linodes', 'lke-clusters']); @@ -112,7 +105,10 @@ describe('linode storage tab', () => { it('try to delete in use disk', () => { const diskName = 'Debian 11 Disk'; cy.defer(() => createTestLinode({ booted: true })).then((linode) => { - interceptDeleteDisks(linode.id).as('deleteDisk'); + cy.intercept( + 'DELETE', + apiMatcher(`linode/instances/${linode.id}/disks/*`) + ).as('deleteDisk'); cy.visitWithLogin(`linodes/${linode.id}/storage`); containsVisible('RUNNING'); fbtVisible(diskName); @@ -132,8 +128,14 @@ describe('linode storage tab', () => { it('delete disk', () => { const diskName = 'cy-test-disk'; cy.defer(() => createTestLinode({ image: null })).then((linode) => { - interceptDeleteDisks(linode.id).as('deleteDisk'); - interceptAddDisks(linode.id).as('addDisk'); + cy.intercept( + 'DELETE', + apiMatcher(`linode/instances/${linode.id}/disks/*`) + ).as('deleteDisk'); + cy.intercept( + 'POST', + apiMatcher(`linode/instances/${linode.id}/disks`) + ).as('addDisk'); cy.visitWithLogin(`/linodes/${linode.id}/storage`); addDisk(diskName); fbtVisible(diskName); @@ -158,7 +160,10 @@ describe('linode storage tab', () => { it('add a disk', () => { const diskName = 'cy-test-disk'; cy.defer(() => createTestLinode({ image: null })).then((linode: Linode) => { - interceptAddDisks(linode.id).as('addDisk'); + cy.intercept( + 'POST', + apiMatcher(`/linode/instances/${linode.id}/disks`) + ).as('addDisk'); cy.visitWithLogin(`/linodes/${linode.id}/storage`); addDisk(diskName); fbtVisible(diskName); @@ -169,8 +174,14 @@ describe('linode storage tab', () => { it('resize disk', () => { const diskName = 'Debian 10 Disk'; cy.defer(() => createTestLinode({ image: null })).then((linode: Linode) => { - interceptAddDisks(linode.id).as('addDisk'); - interceptResizeDisks(linode.id).as('resizeDisk'); + cy.intercept( + 'POST', + apiMatcher(`linode/instances/${linode.id}/disks`) + ).as('addDisk'); + cy.intercept( + 'POST', + apiMatcher(`linode/instances/${linode.id}/disks/*/resize`) + ).as('resizeDisk'); cy.visitWithLogin(`/linodes/${linode.id}/storage`); addDisk(diskName); fbtVisible(diskName); diff --git a/packages/manager/cypress/e2e/core/linodes/rebuild-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/rebuild-linode.spec.ts index dd0314af1b0..2c2c98ce89b 100644 --- a/packages/manager/cypress/e2e/core/linodes/rebuild-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/rebuild-linode.spec.ts @@ -110,7 +110,6 @@ describe('rebuild linode', () => { * - Confirms that password complexity */ it('rebuilds a linode from Image', () => { - cy.tag('method:e2e'); const weakPassword = 'abc123'; const fairPassword = 'Akamai123'; @@ -165,7 +164,6 @@ describe('rebuild linode', () => { * - Confirms that a Linode can be rebuilt using a Community StackScript. */ it('rebuilds a linode from Community StackScript', () => { - cy.tag('method:e2e'); const stackScriptId = '443929'; const stackScriptName = 'OpenLiteSpeed-WordPress'; const image = 'AlmaLinux 9'; @@ -228,7 +226,6 @@ describe('rebuild linode', () => { * - Confirms that a Linode can be rebuilt using an Account StackScript. */ it('rebuilds a linode from Account StackScript', () => { - cy.tag('method:e2e'); const image = 'Alpine'; const region = 'us-east'; diff --git a/packages/manager/cypress/e2e/core/linodes/rescue-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/rescue-linode.spec.ts index 07e1c262ff0..533eac2535f 100644 --- a/packages/manager/cypress/e2e/core/linodes/rescue-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/rescue-linode.spec.ts @@ -38,7 +38,6 @@ describe('Rescue Linodes', () => { * - Confirms that toast appears confirming successful reboot into rescue mode. */ it('Can reboot a Linode into rescue mode', () => { - cy.tag('method:e2e'); const linodePayload = createLinodeRequestFactory.build({ label: randomLabel(), region: chooseRegion().id, diff --git a/packages/manager/cypress/e2e/core/linodes/resize-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/resize-linode.spec.ts index d3ab5c2bd09..ba2107cd62a 100644 --- a/packages/manager/cypress/e2e/core/linodes/resize-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/resize-linode.spec.ts @@ -3,16 +3,18 @@ import { containsVisible, fbtVisible, getClick } from 'support/helpers'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; import { authenticate } from 'support/api/authentication'; +import { mockGetFeatureFlagClientstream } from 'support/intercepts/feature-flags'; import { interceptLinodeResize } from 'support/intercepts/linodes'; authenticate(); describe('resize linode', () => { beforeEach(() => { cleanUp(['linodes']); - cy.tag('method:e2e'); }); it('resizes a linode by increasing size: warm migration', () => { + mockGetFeatureFlagClientstream().as('getClientStream'); + // Use `vlan_no_internet` security method. // This works around an issue where the Linode API responds with a 400 // when attempting to interact with it shortly after booting up when the @@ -38,6 +40,7 @@ describe('resize linode', () => { }); it('resizes a linode by increasing size: cold migration', () => { + mockGetFeatureFlagClientstream().as('getClientStream'); // Use `vlan_no_internet` security method. // This works around an issue where the Linode API responds with a 400 // when attempting to interact with it shortly after booting up when the @@ -64,6 +67,7 @@ describe('resize linode', () => { }); it('resizes a linode by increasing size when offline: cold migration', () => { + mockGetFeatureFlagClientstream().as('getClientStream'); // Use `vlan_no_internet` security method. // This works around an issue where the Linode API responds with a 400 // when attempting to interact with it shortly after booting up when the diff --git a/packages/manager/cypress/e2e/core/linodes/smoke-delete-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/smoke-delete-linode.spec.ts index 5520041418f..13ad0efaf4b 100644 --- a/packages/manager/cypress/e2e/core/linodes/smoke-delete-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/smoke-delete-linode.spec.ts @@ -67,9 +67,6 @@ describe('delete linode', () => { before(() => { cleanUp(['linodes', 'lke-clusters']); }); - beforeEach(() => { - cy.tag('method:e2e'); - }); it('deletes linode from linode details page', () => { const linodeCreatePayload = createLinodeRequestFactory.build({ diff --git a/packages/manager/cypress/e2e/core/linodes/switch-linode-state.spec.ts b/packages/manager/cypress/e2e/core/linodes/switch-linode-state.spec.ts index c0e03a30ea6..7375e3e1e27 100644 --- a/packages/manager/cypress/e2e/core/linodes/switch-linode-state.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/switch-linode-state.spec.ts @@ -8,7 +8,6 @@ authenticate(); describe('switch linode state', () => { beforeEach(() => { cleanUp(['linodes']); - cy.tag('method:e2e'); }); /* diff --git a/packages/manager/cypress/e2e/core/linodes/update-linode-labels.spec.ts b/packages/manager/cypress/e2e/core/linodes/update-linode-labels.spec.ts index 08c14da2a33..4e87aa948a2 100644 --- a/packages/manager/cypress/e2e/core/linodes/update-linode-labels.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/update-linode-labels.spec.ts @@ -8,7 +8,6 @@ authenticate(); describe('update linode label', () => { beforeEach(() => { cleanUp(['linodes']); - cy.tag('method:e2e'); }); it('updates a linode label from details page', () => { diff --git a/packages/manager/cypress/e2e/core/nodebalancers/nodebalancers-create-in-complex-form.spec.ts b/packages/manager/cypress/e2e/core/nodebalancers/nodebalancers-create-in-complex-form.spec.ts deleted file mode 100644 index 81b60e72f1a..00000000000 --- a/packages/manager/cypress/e2e/core/nodebalancers/nodebalancers-create-in-complex-form.spec.ts +++ /dev/null @@ -1,251 +0,0 @@ -import { entityTag } from 'support/constants/cypress'; -import { createTestLinode } from 'support/util/linodes'; -import { randomLabel } from 'support/util/random'; -import { chooseRegion, getRegionById } from 'support/util/regions'; -import { ui } from 'support/ui'; -import { cleanUp } from 'support/util/cleanup'; -import { authenticate } from 'support/api/authentication'; -import type { Linode } from '@linode/api-v4'; -import { nodeBalancerFactory } from 'src/factories'; -import { interceptCreateNodeBalancer } from 'support/intercepts/nodebalancers'; - -authenticate(); -describe('create NodeBalancer to test the submission of multiple nodes and multiple configs', () => { - before(() => { - cleanUp(['tags', 'node-balancers', 'linodes']); - }); - - /* - * - Confirms NodeBalancer create flow when adding multiple Backend Nodes. - * - Confirms Summary field displays correct Node number. - */ - it('creates a NodeBalancer with multiple Backend Nodes', async () => { - const region = chooseRegion(); - const linodePayload = { - region: region.id, - // NodeBalancers require Linodes with private IPs. - private_ip: true, - }; - const linode: Linode = await createTestLinode(linodePayload); - const nodeBal = nodeBalancerFactory.build({ - label: randomLabel(), - region: region.id, - ipv4: linode.ipv4[1], - }); - - const linodePayload_2 = { - region: region.id, - private_ip: true, - }; - const linode_2: Linode = await createTestLinode(linodePayload_2); - const nodeBal_2 = nodeBalancerFactory.build({ - label: randomLabel(), - region: region.id, - ipv4: linode_2.ipv4[1], - }); - const regionName = getRegionById(nodeBal.region).label; - - // catch request - interceptCreateNodeBalancer().as('createNodeBalancer'); - cy.visitWithLogin('/nodebalancers/create'); - cy.get('[id="nodebalancer-label"]') - .should('be.visible') - .click() - .clear() - .type(nodeBal.label); - cy.findByPlaceholderText(/create a tag/i) - .click() - .type(entityTag); - - // this will create the NB in newark, where the default Linode was created - ui.regionSelect.find().click().clear().type(`${regionName}{enter}`); - - // node backend config - cy.findByText('Label').click().type(randomLabel()); - cy.findByLabelText('IP Address') - .should('be.visible') - .click() - .type(nodeBal.ipv4); - ui.autocompletePopper - .findByTitle(nodeBal.ipv4) - .should('be.visible') - .click(); - cy.findByLabelText('Weight') - .should('be.visible') - .click() - .clear() - .type('50'); - - // Add a backend node - cy.get('[data-testid="Button"]').contains('Add a Node').click(); - cy.findAllByText('Label').last().click().type(randomLabel()); - cy.findAllByText('IP Address') - .last() - .should('be.visible') - .click() - .type(nodeBal_2.ipv4); - ui.autocompletePopper - .findByTitle(nodeBal_2.ipv4) - .should('be.visible') - .click(); - cy.get('[data-testid="textfield-input"]') - .last() - .should('be.visible') - .click() - .clear() - .type('50'); - - // Confirm Summary info - cy.get('[data-qa-summary="true"]').within(() => { - cy.contains(`Nodes 2`).should('be.visible'); - }); - - cy.get('[data-qa-deploy-nodebalancer]').click(); - cy.wait('@createNodeBalancer').its('response.statusCode').should('eq', 200); - }); - - /* - * - Confirms NodeBalancer create flow when adding additional config. - * - Confirms Summary field displays correct Config number. - */ - it('creates a NodeBalancer with an additional config', async () => { - const region = chooseRegion(); - const linodePayload = { - region: region.id, - // NodeBalancers require Linodes with private IPs. - private_ip: true, - }; - const linode: Linode = await createTestLinode(linodePayload); - const nodeBal = nodeBalancerFactory.build({ - label: randomLabel(), - region: region.id, - ipv4: linode.ipv4[1], - }); - - const linodePayload_2 = { - region: region.id, - private_ip: true, - }; - const linode_2: Linode = await createTestLinode(linodePayload_2); - const nodeBal_2 = nodeBalancerFactory.build({ - label: randomLabel(), - region: region.id, - ipv4: linode_2.ipv4[1], - }); - const regionName = getRegionById(nodeBal.region).label; - - // catch request - interceptCreateNodeBalancer().as('createNodeBalancer'); - - cy.visitWithLogin('/nodebalancers/create'); - cy.get('[id="nodebalancer-label"]') - .should('be.visible') - .click() - .clear() - .type(nodeBal.label); - cy.findByPlaceholderText(/create a tag/i) - .click() - .type(entityTag); - - // This will create the NB in newark, where the default Linode was created - ui.regionSelect.find().click().clear().type(`${regionName}{enter}`); - - // Node backend config - cy.findByText('Label').click().type(randomLabel()); - cy.findByLabelText('IP Address') - .should('be.visible') - .click() - .type(nodeBal.ipv4); - ui.autocompletePopper - .findByTitle(nodeBal.ipv4) - .should('be.visible') - .click(); - - // Add another configuration - cy.get('[data-testid="Button"]') - .contains('Add another Configuration') - .click(); - cy.get('[data-qa-panel="Configuration - Port "]').within(() => { - cy.get('[data-testid="textfield-input"]').first().click().type('8080'); - }); - cy.findAllByText('Label').last().click().type(randomLabel()); - cy.findAllByText('IP Address') - .last() - .should('be.visible') - .click() - .type(nodeBal_2.ipv4); - ui.autocompletePopper - .findByTitle(nodeBal_2.ipv4) - .should('be.visible') - .click(); - - // Confirm Summary info - cy.get('[data-qa-summary="true"]').within(() => { - cy.contains('Configs 2').should('be.visible'); - }); - - cy.get('[data-qa-deploy-nodebalancer]').click(); - cy.wait('@createNodeBalancer').its('response.statusCode').should('eq', 200); - }); - - /* - * - Confirms Port field displays error if same port number used in additional config. - * - Confirms Label field displays error if label is empty in additional config. - * - Confirms IP field displays error if ip is empty in additional config. - */ - it('displays errors during adding new config', async () => { - const region = chooseRegion(); - const linodePayload = { - region: region.id, - // NodeBalancers require Linodes with private IPs. - private_ip: true, - }; - const linode: Linode = await createTestLinode(linodePayload); - const nodeBal = nodeBalancerFactory.build({ - label: randomLabel(), - region: region.id, - ipv4: linode.ipv4[1], - }); - const regionName = getRegionById(nodeBal.region).label; - - cy.visitWithLogin('/nodebalancers/create'); - cy.get('[id="nodebalancer-label"]') - .should('be.visible') - .click() - .clear() - .type(nodeBal.label); - cy.findByPlaceholderText(/create a tag/i) - .click() - .type(entityTag); - - // This will create the NB in newark, where the default Linode was created - ui.regionSelect.find().click().clear().type(`${regionName}{enter}`); - - // Node backend config - cy.findByText('Label').click().type(randomLabel()); - cy.findByLabelText('IP Address') - .should('be.visible') - .click() - .type(nodeBal.ipv4); - ui.autocompletePopper - .findByTitle(nodeBal.ipv4) - .should('be.visible') - .click(); - - // Add another configuration - cy.get('[data-testid="Button"]') - .contains('Add another Configuration') - .click(); - cy.get('[data-qa-panel="Configuration - Port "]').within(() => { - cy.get('[data-testid="textfield-input"]').first().click().type('80'); - }); - cy.get('[data-qa-deploy-nodebalancer]').click(); - - // Confirm error displays - cy.contains('Port must be unique').scrollIntoView().should('be.visible'); - cy.contains('Label is required').scrollIntoView().should('be.visible'); - cy.contains('Must be a valid private IPv4 address.') - .scrollIntoView() - .should('be.visible'); - }); -}); diff --git a/packages/manager/cypress/e2e/core/nodebalancers/smoke-create-nodebal.spec.ts b/packages/manager/cypress/e2e/core/nodebalancers/smoke-create-nodebal.spec.ts index 63c1e8080cf..3155ecc1d53 100644 --- a/packages/manager/cypress/e2e/core/nodebalancers/smoke-create-nodebal.spec.ts +++ b/packages/manager/cypress/e2e/core/nodebalancers/smoke-create-nodebal.spec.ts @@ -78,9 +78,6 @@ const createNodeBalancerWithUI = ( }; authenticate(); -beforeEach(() => { - cy.tag('method:e2e', 'purpose:dcTesting'); -}); describe('create NodeBalancer', () => { before(() => { cleanUp(['tags', 'node-balancers', 'linodes']); diff --git a/packages/manager/cypress/e2e/core/notificationsAndEvents/events-fetching.spec.ts b/packages/manager/cypress/e2e/core/notificationsAndEvents/events-fetching.spec.ts index afd0300cf3b..e1f3804b6a5 100644 --- a/packages/manager/cypress/e2e/core/notificationsAndEvents/events-fetching.spec.ts +++ b/packages/manager/cypress/e2e/core/notificationsAndEvents/events-fetching.spec.ts @@ -1,219 +1,7 @@ -/** - * @file Integration tests for Cloud Manager's events fetching and polling behavior. - */ - -import { mockGetEvents, mockGetEventsPolling } from 'support/intercepts/events'; -import { DateTime } from 'luxon'; import { eventFactory } from 'src/factories'; -import { randomNumber } from 'support/util/random'; -import { Interception } from 'cypress/types/net-stubbing'; +import { mockGetEvents } from 'support/intercepts/events'; import { mockGetVolumes } from 'support/intercepts/volumes'; -describe('Event fetching and polling', () => { - /** - * - Confirms that Cloud Manager makes a request to the events endpoint on page load. - * - Confirms API filters are applied to the request to limit the number and type of events retrieved. - */ - it('Makes initial fetch to events endpoint', () => { - const mockNow = DateTime.now(); - - mockGetEvents([]).as('getEvents'); - - cy.clock(mockNow.toJSDate()); - cy.visitWithLogin('/'); - cy.wait('@getEvents').then((xhr) => { - const filters = xhr.request.headers['x-filter']; - const lastWeekTimestamp = mockNow - .minus({ weeks: 1 }) - .toUTC() - .startOf('second') // Helps with matching the timestamp at the start of the second - .toFormat("yyyy-MM-dd'T'HH:mm:ss"); - - const timestampFilter = `"created":{"+gt":"${lastWeekTimestamp}"`; - - /* - * Confirm that initial fetch request contains filters to achieve - * each of the following behaviors: - * - * - Exclude `profile_update` events. - * - Retrieve a maximum of 25 events. - * - Sort events by their created date. - * - Only retrieve events created within the past week. - */ - expect(filters).to.contain(timestampFilter); - expect(filters).to.contain('"+neq":"profile_update"'); - expect(filters).to.contain('"+order_by":"id"'); - }); - }); - - /** - * - Confirms that Cloud Manager makes subsequent events requests after the initial request. - * - Confirms API filters are applied to polling requests which differ from the initial request. - */ - it('Polls events endpoint after initial fetch', () => { - const mockEvent = eventFactory.build({ - id: randomNumber(10000, 99999), - created: DateTime.now() - .minus({ minutes: 5 }) - .toUTC() - .startOf('second') // Helps with matching the timestamp at the start of the second - .toFormat("yyyy-MM-dd'T'HH:mm:ss"), - duration: null, - rate: null, - percent_complete: null, - }); - - mockGetEvents([mockEvent]).as('getEvents'); - cy.visitWithLogin('/'); - - cy.wait(['@getEvents', '@getEvents']); - cy.get('@getEvents.all').then((xhrRequests: unknown) => { - // Cypress types for `cy.get().then(...)` seem to be wrong. - // Types suggest that `cy.get()` can only yield a jQuery HTML element, but - // when the alias is an HTTP route it yields the request and response data. - const secondRequest = (xhrRequests as Interception[])[1]; - const filters = secondRequest.request.headers['x-filter']; - - /* - * Confirm that polling fetch request contains filters to achieve - * each of the following behaviors: - * - * - Exclude `profile_update` events. - * - Only retrieve events created more recently than the most recent event in the initial fetch. - * - Exclude the most recent event that was included in the initial fetch. - * - Sort events by their ID (TODO). - */ - expect(filters).to.contain('"action":{"+neq":"profile_update"}'); - expect(filters).to.contain(`"created":{"+gte":"${mockEvent.created}"}`); - expect(filters).to.contain(`{"id":{"+neq":${mockEvent.id}}}]`); - expect(filters).to.contain('"+order_by":"id"'); - }); - }); - - /** - * - Confirms that Cloud Manager polls the events endpoint 16 times per second. - * - Confirms that Cloud Manager makes a request to the events endpoint after 16 seconds. - * - Confirms that Cloud Manager does not make a request to the events endpoint before 16 seconds have passed. - * - Confirms Cloud polling rate when there are no in-progress events. - */ - it('Polls events at a 16-second interval', () => { - // Expect Cloud to poll the events endpoint every 16 seconds, - // and configure the test to check if a request has been made - // every simulated second for 16 samples total. - const expectedPollingInterval = 16_000; - const pollingSamples = 16; - const mockNow = DateTime.now(); - const mockNowTimestamp = mockNow - .toUTC() - .startOf('second') // Helps with matching the timestamp at the start of the second - .toFormat("yyyy-MM-dd'T'HH:mm:ss"); - - const mockEvent = eventFactory.build({ - id: randomNumber(10000, 99999), - created: DateTime.now() - .minus({ minutes: 5 }) - .toFormat("yyyy-MM-dd'T'HH:mm:ss"), - duration: null, - rate: null, - percent_complete: null, - }); - - mockGetEvents([mockEvent]).as('getEventsInitialFetches'); - - // We need access to the `clock` object directly since we cannot call `cy.clock()` inside - // a `should(() => {})` callback because Cypress commands are disallowed there. - cy.clock(mockNow.toJSDate()).then((clock) => { - cy.visitWithLogin('/'); - - // Confirm that Cloud manager polls the requests endpoint no more than - // once every 16 seconds. - mockGetEventsPolling([mockEvent], mockNowTimestamp).as('getEventsPoll'); - for (let i = 0; i < pollingSamples; i += 1) { - cy.log( - `Confirming that Cloud has not made events request... (${ - i + 1 - }/${pollingSamples})` - ); - cy.get('@getEventsPoll.all').should('have.length', 0); - cy.tick(expectedPollingInterval / pollingSamples, { log: false }); - } - - cy.tick(50); - cy.wait('@getEventsPoll'); - cy.get('@getEventsPoll.all').should('have.length', 1); - }); - }); - - /** - * - Confirms that Cloud Manager polls the events endpoint 2 times per second when there are in-progress events. - * - Confirms that Cloud Manager makes a request to the events endpoint after 2 seconds. - * - Confirms that Cloud Manager does not make a request to the events endpoint before 2 seconds have passed. - * - Confirms Cloud polling rate when there are in-progress events. - */ - it('Polls in-progress events at a 2-second interval', () => { - // When in-progress events are present, expect Cloud to poll the - // events endpoint every 2 seconds, and configure the test to check - // if a request has been made every simulated tenth of a second for - // 20 samples total. - const expectedPollingInterval = 2_000; - const pollingSamples = 20; - const mockNow = DateTime.now(); - const mockNowTimestamp = mockNow - .toUTC() - .startOf('second') // Helps with matching the timestamp at the start of the second - .toFormat("yyyy-MM-dd'T'HH:mm:ss"); - - const mockEventBasic = eventFactory.build({ - id: randomNumber(10000, 99999), - created: DateTime.now() - .minus({ minutes: 5 }) - .startOf('second') // Helps with matching the timestamp at the start of the second - .toFormat("yyyy-MM-dd'T'HH:mm:ss"), - duration: null, - rate: null, - percent_complete: null, - }); - - const mockEventInProgress = eventFactory.build({ - id: randomNumber(10000, 99999), - created: DateTime.now().minus({ minutes: 6 }).toISO(), - duration: 0, - rate: null, - percent_complete: 50, - }); - - const mockEvents = [mockEventBasic, mockEventInProgress]; - - // Visit Cloud Manager, and wait for Cloud to fire its first two - // requests to the `events` endpoint: the initial request, and the - // initial polling request. - mockGetEvents(mockEvents).as('getEventsInitialFetches'); - - // We need access to the `clock` object directly since we cannot call `cy.clock()` inside - // a `should(() => {})` callback because Cypress commands are disallowed there. - cy.clock(Date.now()).then((clock) => { - cy.visitWithLogin('/'); - - // Confirm that Cloud manager polls the requests endpoint no more than once - // every 2 seconds. - mockGetEventsPolling(mockEvents, mockNowTimestamp).as('getEventsPoll'); - for (let i = 0; i < pollingSamples; i += 1) { - cy.log( - `Confirming that Cloud has not made events request... (${ - i + 1 - }/${pollingSamples})` - ); - cy.get('@getEventsPoll.all').should('have.length', 0); - cy.tick(expectedPollingInterval / pollingSamples, { log: false }); - } - - cy.tick(50); - cy.wait('@getEventsPoll'); - cy.get('@getEventsPoll.all').should('have.length', 1); - }); - }); -}); - describe('Event Handlers', () => { it('invokes event handlers when new events are polled and makes the correct number of requests', () => { // See https://github.com/linode/manager/pull/10824 diff --git a/packages/manager/cypress/e2e/core/notificationsAndEvents/events-menu.spec.ts b/packages/manager/cypress/e2e/core/notificationsAndEvents/events-menu.spec.ts deleted file mode 100644 index 79c1638848f..00000000000 --- a/packages/manager/cypress/e2e/core/notificationsAndEvents/events-menu.spec.ts +++ /dev/null @@ -1,309 +0,0 @@ -/** - * @file Integration tests for Cloud Manager's events menu. - */ - -import { mockGetEvents, mockMarkEventSeen } from 'support/intercepts/events'; -import { ui } from 'support/ui'; -import { eventFactory } from 'src/factories'; -import { buildArray } from 'support/util/arrays'; -import { DateTime } from 'luxon'; -import { randomLabel, randomNumber } from 'support/util/random'; - -describe('Notifications Menu', () => { - /* - * - Confirms that the notification menu shows all events when 20 or fewer exist. - */ - it('Shows all recent events when there are 20 or fewer', () => { - const mockEvents = buildArray(randomNumber(1, 20), (index) => { - return eventFactory.build({ - action: 'linode_delete', - // The response from the API will be ordered by created date, descending. - created: DateTime.local().minus({ minutes: index }).toISO(), - percent_complete: null, - rate: null, - seen: false, - duration: null, - status: 'scheduled', - entity: { - id: 1000 + index, - label: `my-linode-${index}`, - type: 'linode', - url: `/v4/linode/instances/${1000 + index}`, - }, - username: randomLabel(), - }); - }); - - mockGetEvents(mockEvents).as('getEvents'); - cy.visitWithLogin('/'); - cy.wait('@getEvents'); - - ui.appBar.find().within(() => { - cy.findByLabelText('Notifications') - .should('be.visible') - .should('be.enabled') - .click(); - }); - - cy.get('[data-qa-notification-menu]') - .should('be.visible') - .within(() => { - // Confirm that all mocked events are shown in the notification menu. - mockEvents.forEach((event) => { - cy.get(`[data-qa-event="${event.id}"]`) - .scrollIntoView() - .should('be.visible'); - }); - }); - }); - - /* - * - Confirms that the notification menu shows no more than 20 events. - * - Confirms that only the most recently created events are shown. - */ - it('Shows the 20 most recently created events', () => { - const mockEvents = buildArray(25, (index) => { - return eventFactory.build({ - action: 'linode_delete', - // The response from the API will be ordered by created date, descending. - created: DateTime.local().minus({ minutes: index }).toISO(), - percent_complete: null, - rate: null, - seen: false, - duration: null, - status: 'scheduled', - entity: { - id: 1000 + index, - label: `my-linode-${index}`, - type: 'linode', - url: `/v4/linode/instances/${1000 + index}`, - }, - username: randomLabel(), - }); - }); - - const shownEvents = mockEvents.slice(0, 20); - const hiddenEvents = mockEvents.slice(20); - - mockGetEvents(mockEvents).as('getEvents'); - cy.visitWithLogin('/'); - cy.wait('@getEvents'); - - ui.appBar.find().within(() => { - cy.findByLabelText('Notifications') - .should('be.visible') - .should('be.enabled') - .click(); - }); - - cy.get('[data-qa-notification-menu]') - .should('be.visible') - .within(() => { - // Confirm that first 20 events in response are displayed. - shownEvents.forEach((event) => { - cy.get(`[data-qa-event="${event.id}"]`) - .scrollIntoView() - .should('be.visible'); - }); - - // Confirm that last 5 events in response are not displayed. - hiddenEvents.forEach((event) => { - cy.get(`[data-qa-event="${event.id}"]`).should('not.exist'); - }); - }); - }); - - /* - * - Confirms that notification menu contains a notice when no recent events exist. - */ - it('Shows notice when there are no recent events', () => { - mockGetEvents([]).as('getEvents'); - cy.visitWithLogin('/'); - cy.wait('@getEvents'); - - // Find and click Notifications button in Cloud's top app bar. - ui.appBar.find().within(() => { - cy.findByLabelText('Notifications') - .should('be.visible') - .should('be.enabled') - .click(); - }); - - cy.get('[data-qa-notification-menu]') - .should('be.visible') - .within(() => { - // Use RegEx here to account for cases where the period is and is not present. - // Period is displayed in Notifications Menu v2, but omitted in v1. - cy.findByText(/No recent events to display\.?/).should('be.visible'); - }); - }); - - /* - * - Confirms that events in menu are marked as seen upon viewing. - * - Uses typical mock data setup where IDs are ordered (descending) and all create dates are unique. - * - Confirms that events are reflected in the UI as being seen or unseen. - */ - it('Marks events in menu as seen', () => { - const mockEvents = buildArray(10, (index) => { - return eventFactory.build({ - // The event with the highest ID is expected to come first in the array. - id: 5000 - index, - action: 'linode_delete', - // The response from the API will be ordered by created date, descending. - created: DateTime.local().minus({ minutes: index }).toISO(), - percent_complete: null, - seen: false, - rate: null, - duration: null, - status: 'scheduled', - entity: { - id: 1000 + index, - label: `my-linode-${index}`, - type: 'linode', - url: `/v4/linode/instances/${1000 + index}`, - }, - username: randomLabel(), - }); - }); - - // In this case, we know that the first event in the mocked events response - // will contain the highest event ID. - const highestEventId = mockEvents[0].id; - - mockGetEvents(mockEvents).as('getEvents'); - mockMarkEventSeen(highestEventId).as('markEventsSeen'); - - cy.visitWithLogin('/'); - cy.wait('@getEvents'); - - // Find and click Notifications button in Cloud's top app bar. - ui.appBar.find().within(() => { - cy.findByLabelText('Notifications') - .should('be.visible') - .should('be.enabled') - .click(); - }); - - // Confirm notification menu opens - cy.get('[data-qa-notification-menu]') - .should('be.visible') - .within(() => { - // Confirm that UI reflects that every event is unseen. - cy.get('[data-qa-event-seen="false"]').should('have.length', 10); - }); - - // Dismiss the notifications menu by clicking the bell button again. - ui.appBar.find().within(() => { - // This time we have to pass `force: true` to cy.click() - // because otherwise Cypress thinks the element is blocked because - // of the notifications menu popover. - cy.findByLabelText('Notifications') - .should('be.visible') - .should('be.enabled') - .click({ force: true }); - }); - - // Confirm that Cloud fires a request to the `/events/:id/seen` endpoint, - // where `id` corresponds to the mocked event with the highest ID. - // If Cloud attempts to mark the wrong event ID as seen, this assertion - // will fail. - cy.log(`Waiting for request to '/events/${highestEventId}/seen'`); - cy.wait('@markEventsSeen'); - - ui.appBar.find().within(() => { - cy.findByLabelText('Notifications') - .should('be.visible') - .should('be.enabled') - .click(); - }); - - cy.get('[data-qa-notification-menu]') - .should('be.visible') - .within(() => { - // Confirm that UI reflects that every event is now seen. - cy.get('[data-qa-event-seen="true"]').should('have.length', 10); - }); - }); - - /* - * - Confirms event seen logic for non-typical event ordering edge case. - * - Confirms that Cloud marks the correct event as seen even when it's not the first result. - */ - it('Marks events in menu as seen with duplicate created dates and out-of-order IDs', () => { - /* - * When several events are triggered simultaneously, they may have the - * same `created` timestamp. Cloud asks for events to be sorted by created - * date when fetching from the API, but when events have identical timestamps, - * there is no guarantee in which order they will be returned. - * - * As a result, we have to account for cases where the most recent event - * in reality (e.g. as determined by its ID) is not returned first by the API. - * This is especially relevant when marking events as 'seen', as we have - * to explicitly mark the event with the highest ID as seen when the user - * closes their notification menu. - */ - const createTime = DateTime.local().minus({ minutes: 2 }).toISO(); - const mockEvents = buildArray(10, (index) => { - return eventFactory.build({ - // Events are not guaranteed to be ordered by ID; simulate this by using random IDs. - id: randomNumber(1000, 9999), - action: 'linode_delete', - // To simulate multiple events occurring simultaneously, give all - // events the same created timestamp. - created: createTime, - percent_complete: null, - seen: false, - rate: null, - duration: null, - status: 'scheduled', - entity: { - id: 1000 + index, - label: `my-linode-${index}`, - type: 'linode', - url: `/v4/linode/instances/${1000 + index}`, - }, - username: randomLabel(), - }); - }); - - // Sort the mockEvents array by id in descending order to simulate API response - mockEvents.sort((a, b) => b.id - a.id); - - const highestEventId = mockEvents[0].id; - - mockGetEvents(mockEvents).as('getEvents'); - mockMarkEventSeen(highestEventId).as('markEventsSeen'); - - cy.visitWithLogin('/'); - cy.wait('@getEvents'); - - // Find and click Notifications button in Cloud's top app bar. - ui.appBar.find().within(() => { - cy.findByLabelText('Notifications') - .should('be.visible') - .should('be.enabled') - .click(); - }); - - // Confirm notification menu opens; we don't care about its contents. - cy.get('[data-qa-notification-menu]').should('be.visible'); - - // Dismiss the notifications menu by clicking the bell button again. - ui.appBar.find().within(() => { - // This time we have to pass `force: true` to cy.click() - // because otherwise Cypress thinks the element is blocked because - // of the notifications menu popover. - cy.findByLabelText('Notifications') - .should('be.visible') - .should('be.enabled') - .click({ force: true }); - }); - - // Confirm that Cloud fires a request to the `/events/:id/seen` endpoint, - // where `id` corresponds to the mocked event with the highest ID. - // If Cloud attempts to mark the wrong event ID as seen, this assertion - // will fail. - cy.log(`Waiting for request to '/events/${highestEventId}/seen'`); - cy.wait('@markEventsSeen'); - }); -}); 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 17fda30faa9..aa7cf9ee38b 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 @@ -5,11 +5,15 @@ import { createObjectStorageBucketFactoryLegacy } from 'src/factories/objectStorage'; import { authenticate } from 'support/api/authentication'; import { createBucket } from '@linode/api-v4/lib/object-storage'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; import { interceptGetAccessKeys, interceptCreateAccessKey, } from 'support/intercepts/object-storage'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; import { randomLabel } from 'support/util/random'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; @@ -21,9 +25,6 @@ describe('object storage access key end-to-end tests', () => { before(() => { cleanUp(['obj-buckets', 'obj-access-keys']); }); - beforeEach(() => { - cy.tag('method:e2e'); - }); /* * - Creates an access key with unlimited access @@ -40,8 +41,9 @@ describe('object storage access key end-to-end tests', () => { mockGetAccount(accountFactory.build({ capabilities: [] })); mockAppendFeatureFlags({ - objMultiCluster: false, + objMultiCluster: makeFeatureFlagData(false), }); + mockGetFeatureFlagClientstream(); cy.visitWithLogin('/object-storage/access-keys'); cy.wait('@getKeys'); @@ -134,8 +136,9 @@ describe('object storage access key end-to-end tests', () => { mockGetAccount(accountFactory.build({ capabilities: [] })); mockAppendFeatureFlags({ - objMultiCluster: false, + objMultiCluster: makeFeatureFlagData(false), }); + mockGetFeatureFlagClientstream(); interceptGetAccessKeys().as('getKeys'); interceptCreateAccessKey().as('createKey'); 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 3182a4c5bed..ea45c89d99b 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 @@ -6,7 +6,10 @@ import { objectStorageKeyFactory, objectStorageBucketFactory, } from 'src/factories/objectStorage'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; import { mockCreateAccessKey, mockDeleteAccessKey, @@ -14,6 +17,7 @@ import { mockGetBucketsForRegion, mockUpdateAccessKey, } from 'support/intercepts/object-storage'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; import { randomDomainName, randomLabel, @@ -44,8 +48,9 @@ describe('object storage access keys smoke tests', () => { mockGetAccount(accountFactory.build({ capabilities: [] })); mockAppendFeatureFlags({ - objMultiCluster: false, + objMultiCluster: makeFeatureFlagData(false), }); + mockGetFeatureFlagClientstream(); mockGetAccessKeys([]).as('getKeys'); mockCreateAccessKey(mockAccessKey).as('createKey'); @@ -115,8 +120,9 @@ describe('object storage access keys smoke tests', () => { mockGetAccount(accountFactory.build({ capabilities: [] })); mockAppendFeatureFlags({ - objMultiCluster: false, + objMultiCluster: makeFeatureFlagData(false), }); + mockGetFeatureFlagClientstream(); // Mock initial GET request to include an access key. mockGetAccessKeys([accessKey]).as('getKeys'); @@ -166,8 +172,9 @@ describe('object storage access keys smoke tests', () => { }) ); mockAppendFeatureFlags({ - objMultiCluster: true, + objMultiCluster: makeFeatureFlagData(true), }); + mockGetFeatureFlagClientstream(); }); /* 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 5073da56424..240e88153bb 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 @@ -31,7 +31,11 @@ import { ui } from 'support/ui'; import { randomLabel } from 'support/util/random'; import { mockGetRegions } from 'support/intercepts/regions'; import { mockGetAccessKeys } from 'support/intercepts/object-storage'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; // Various messages, notes, and warnings that may be shown when enabling Object Storage // under different circumstances. @@ -58,8 +62,9 @@ describe('Object Storage enrollment', () => { it('can enroll in Object Storage', () => { mockGetAccount(accountFactory.build({ capabilities: [] })); mockAppendFeatureFlags({ - objMultiCluster: false, + objMultiCluster: makeFeatureFlagData(false), }); + mockGetFeatureFlagClientstream(); const mockAccountSettings = accountSettingsFactory.build({ managed: 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 8599f4a1621..4e6604f32ee 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 @@ -25,7 +25,11 @@ import { import { ui } from 'support/ui'; import { randomLabel } from 'support/util/random'; import { cleanUp } from 'support/util/cleanup'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; // Message shown on-screen when user navigates to an empty bucket. const emptyBucketMessage = 'This bucket is empty.'; @@ -155,9 +159,6 @@ const assertStatusForUrlAtAlias = ( }; authenticate(); -beforeEach(() => { - cy.tag('method:e2e'); -}); describe('object storage end-to-end tests', () => { before(() => { cleanUp('obj-buckets'); @@ -185,8 +186,9 @@ describe('object storage end-to-end tests', () => { mockGetAccount(accountFactory.build({ capabilities: [] })); mockAppendFeatureFlags({ - objMultiCluster: false, + objMultiCluster: makeFeatureFlagData(false), }).as('getFeatureFlags'); + mockGetFeatureFlagClientstream().as('getClientStream'); cy.visitWithLogin('/object-storage'); cy.wait(['@getFeatureFlags', '@getBuckets', '@getNetworkUtilization']); 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 e189ad3a87d..29947ac1374 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 @@ -16,7 +16,11 @@ import { mockUploadBucketObjectS3, mockCreateBucketError, } from 'support/intercepts/object-storage'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; import { randomLabel, randomString } from 'support/util/random'; import { ui } from 'support/ui'; import { accountFactory, regionFactory } from 'src/factories'; @@ -64,8 +68,9 @@ describe('object storage smoke tests', () => { }) ); mockAppendFeatureFlags({ - objMultiCluster: true, + objMultiCluster: makeFeatureFlagData(true), }).as('getFeatureFlags'); + mockGetFeatureFlagClientstream().as('getClientStream'); mockGetRegions(mockRegions).as('getRegions'); mockGetBuckets([]).as('getBuckets'); @@ -84,7 +89,7 @@ describe('object storage smoke tests', () => { .within(() => { // Enter label. cy.contains('Label').click().type(mockBucket.label); - cy.log(`${mockRegionWithObj.label}`); + cy.contains('Region').click().type(mockRegionWithObj.label); ui.autocompletePopper @@ -162,9 +167,10 @@ describe('object storage smoke tests', () => { mockGetAccount(accountFactory.build({ capabilities: [] })); mockAppendFeatureFlags({ - objMultiCluster: false, - gecko2: false, + objMultiCluster: makeFeatureFlagData(false), + gecko2: makeFeatureFlagData(false), }).as('getFeatureFlags'); + mockGetFeatureFlagClientstream().as('getClientStream'); mockGetBuckets([]).as('getBuckets'); @@ -299,8 +305,9 @@ describe('object storage smoke tests', () => { mockGetAccount(accountFactory.build({ capabilities: [] })); mockAppendFeatureFlags({ - objMultiCluster: false, + objMultiCluster: makeFeatureFlagData(false), }); + mockGetFeatureFlagClientstream(); mockGetBuckets([bucketMock]).as('getBuckets'); mockDeleteBucket(bucketLabel, bucketCluster).as('deleteBucket'); @@ -352,8 +359,9 @@ describe('object storage smoke tests', () => { }) ); mockAppendFeatureFlags({ - objMultiCluster: true, + objMultiCluster: makeFeatureFlagData(true), }); + mockGetFeatureFlagClientstream(); mockGetBuckets([bucketMock]).as('getBuckets'); mockDeleteBucket(bucketLabel, bucketMock.region!).as('deleteBucket'); diff --git a/packages/manager/cypress/e2e/core/objectStorageGen2/bucket-create-gen2.spec.ts b/packages/manager/cypress/e2e/core/objectStorageGen2/bucket-create-gen2.spec.ts index dce48e600f3..d70e3c33a3b 100644 --- a/packages/manager/cypress/e2e/core/objectStorageGen2/bucket-create-gen2.spec.ts +++ b/packages/manager/cypress/e2e/core/objectStorageGen2/bucket-create-gen2.spec.ts @@ -107,10 +107,18 @@ describe('Object Storage Gen2 create bucket tests', () => { * Confirms all endpoints are displayed regardless if there's multiple of the same type * Confirms S3 endpoint hostname displayed to differentiate between identical options in the dropdown */ - it('can create a bucket with E0 endpoint type', () => { + it('can create a bucket with endpoint type 0', () => { const endpointTypeE0 = 'Legacy (E0)'; const bucketLabel = randomLabel(); + //wait for the newly 'created' mocked bucket to appear + const mockBucket = objectStorageBucketFactoryGen2.build({ + label: bucketLabel, + region: mockRegion.id, + endpoint_type: 'E0', + s3_endpoint: undefined, + }); + mockGetBuckets([]).as('getBuckets'); mockDeleteBucket(bucketLabel, mockRegion.id).as('deleteBucket'); mockCreateBucket({ @@ -120,6 +128,11 @@ describe('Object Storage Gen2 create bucket tests', () => { region: mockRegion.id, }).as('createBucket'); + mockAppendFeatureFlags({ + objMultiCluster: true, + objectStorageGen2: { enabled: true }, + }).as('getFeatureFlags'); + mockGetObjectStorageEndpoints(mockEndpoints).as( 'getObjectStorageEndpoints' ); @@ -134,13 +147,6 @@ describe('Object Storage Gen2 create bucket tests', () => { '@getObjectStorageEndpoints', ]); - const mockBucket = objectStorageBucketFactoryGen2.build({ - label: bucketLabel, - region: mockRegion.id, - endpoint_type: 'E0', - s3_endpoint: undefined, - }); - ui.drawer .findByTitle('Create Bucket') .should('be.visible') @@ -185,8 +191,6 @@ describe('Object Storage Gen2 create bucket tests', () => { // Confirm bucket rate limit table should not exist when E0 endpoint is selected cy.get('[data-testid="bucket-rate-limit-table"]').should('not.exist'); - mockGetBuckets([mockBucket]).as('getBuckets'); - ui.buttonGroup .findButtonByTitle('Create Bucket') .should('be.visible') @@ -194,7 +198,7 @@ describe('Object Storage Gen2 create bucket tests', () => { .click(); }); - // Wait for the newly 'created' mocked bucket to appear + mockGetBuckets([mockBucket]).as('getBuckets'); cy.wait(['@getBuckets']); // Confirm request body has expected data @@ -235,127 +239,20 @@ describe('Object Storage Gen2 create bucket tests', () => { }); /** - * Confirms UI flow for creating a gen2 Object Storage bucket with endpoint E1 + * Confirms UI flow for creating a gen2 Object Storage bucket with endpoint E2 */ - it('can create a bucket with E1 endpoint type', () => { - const endpointTypeE1 = 'Standard (E1)'; + it('can create a bucket with endpoint type 2', () => { + const endpointTypeE2 = 'Standard (E2)'; const bucketLabel = randomLabel(); - mockGetBuckets([]).as('getBuckets'); - mockDeleteBucket(bucketLabel, mockRegion.id).as('deleteBucket'); - mockCreateBucket({ - label: bucketLabel, - endpoint_type: 'E1', - cors_enabled: true, - region: mockRegion.id, - }).as('createBucket'); - - mockGetObjectStorageEndpoints(mockEndpoints).as( - 'getObjectStorageEndpoints' - ); - - mockGetRegions(mockRegions); - - cy.visitWithLogin('/object-storage/buckets/create'); - cy.wait([ - '@getFeatureFlags', - '@getBuckets', - '@getAccount', - '@getObjectStorageEndpoints', - ]); - + //wait for the newly 'created' mocked bucket to appear const mockBucket = objectStorageBucketFactoryGen2.build({ label: bucketLabel, region: mockRegion.id, - endpoint_type: 'E1', - s3_endpoint: 'us-sea-1.linodeobjects.com', - }); - - ui.drawer - .findByTitle('Create Bucket') - .should('be.visible') - .within(() => { - cy.findByText('Label').click().type(bucketLabel); - ui.regionSelect.find().click().type(`${mockRegion.label}{enter}`); - cy.findByLabelText('Object Storage Endpoint Type') - .should('be.visible') - .click(); - - // Select E1 endpoint - ui.autocompletePopper - .findByTitle('Standard (E1) us-sea-1.linodeobjects.com') - .should('be.visible') - .should('be.enabled') - .click(); - - // Confirm bucket rate limits text for E1 endpoint - cy.findByText('Bucket Rate Limits').should('be.visible'); - cy.contains( - 'This endpoint type supports up to 750 Requests Per Second (RPS). Understand bucket rate limits' - ).should('be.visible'); - - // Confirm bucket rate limit table should not exist when E1 endpoint is selected - cy.get('[data-testid="bucket-rate-limit-table"]').should('not.exist'); - - mockGetBuckets([mockBucket]).as('getBuckets'); - - ui.buttonGroup - .findButtonByTitle('Create Bucket') - .should('be.visible') - .should('be.enabled') - .click(); - }); - - // Wait for the newly 'created' mocked bucket to appear - cy.wait(['@getBuckets']); - - // Confirm request body has expected data - cy.wait('@createBucket').then((xhr) => { - const requestPayload = xhr.request.body; - expect(requestPayload['endpoint_type']).to.equal('E1'); - expect(requestPayload['cors_enabled']).to.equal(true); - expect(requestPayload['s3_endpoint']).to.equal( - 'us-sea-1.linodeobjects.com' - ); + endpoint_type: 'E2', + s3_endpoint: undefined, }); - ui.drawer.find().should('not.exist'); - - // Confirm that bucket is created, initiate deletion for cleanup - cy.findByText(endpointTypeE1).should('be.visible'); - cy.findByText(bucketLabel) - .should('be.visible') - .closest('tr') - .within(() => { - cy.findByText(mockRegion.label).should('be.visible'); - ui.button.findByTitle('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.visible') - .should('be.enabled') - .click(); - }); - - // Confirm bucket gets deleted - mockGetBuckets([]).as('getBuckets'); - cy.wait(['@deleteBucket', '@getBuckets']); - cy.findByText(bucketLabel).should('not.exist'); - }); - - /** - * Confirms UI flow for creating a gen2 Object Storage bucket with endpoint E2 - */ - it('can create a bucket with E2 endpoint type', () => { - const endpointTypeE2 = 'Standard (E2)'; - const bucketLabel = randomLabel(); - mockGetBuckets([]).as('getBuckets'); mockDeleteBucket(bucketLabel, mockRegion.id).as('deleteBucket'); mockCreateBucket({ @@ -379,13 +276,6 @@ describe('Object Storage Gen2 create bucket tests', () => { '@getObjectStorageEndpoints', ]); - const mockBucket = objectStorageBucketFactoryGen2.build({ - label: bucketLabel, - region: mockRegion.id, - endpoint_type: 'E2', - s3_endpoint: undefined, - }); - ui.drawer .findByTitle('Create Bucket') .should('be.visible') @@ -415,8 +305,6 @@ describe('Object Storage Gen2 create bucket tests', () => { // Confirm that basic rate limits table is displayed checkRateLimitsTable(mockBucket.endpoint_type!); - mockGetBuckets([mockBucket]).as('getBuckets'); - ui.buttonGroup .findButtonByTitle('Create Bucket') .should('be.visible') @@ -424,7 +312,7 @@ describe('Object Storage Gen2 create bucket tests', () => { .click(); }); - // Wait for the newly 'created' mocked bucket to appear + mockGetBuckets([mockBucket]).as('getBuckets'); cy.wait(['@getBuckets']); // Confirm request body has expected data @@ -467,10 +355,18 @@ describe('Object Storage Gen2 create bucket tests', () => { /** * Confirms UI flow for creating a gen2 Object Storage bucket with endpoint E3 */ - it('can create a bucket with E3 endpoint type', () => { + it('can create a bucket with endpoint type 3', () => { const endpointTypeE3 = 'Standard (E3)'; const bucketLabel = randomLabel(); + //wait for the newly 'created' mocked bucket to appear + const mockBucket = objectStorageBucketFactoryGen2.build({ + label: bucketLabel, + region: mockRegion.id, + endpoint_type: 'E3', + s3_endpoint: undefined, + }); + mockGetBuckets([]).as('getBuckets'); mockDeleteBucket(bucketLabel, mockRegion.id).as('deleteBucket'); mockCreateBucket({ @@ -494,13 +390,6 @@ describe('Object Storage Gen2 create bucket tests', () => { '@getObjectStorageEndpoints', ]); - const mockBucket = objectStorageBucketFactoryGen2.build({ - label: bucketLabel, - region: mockRegion.id, - endpoint_type: 'E3', - s3_endpoint: undefined, - }); - ui.drawer .findByTitle('Create Bucket') .should('be.visible') @@ -531,8 +420,6 @@ describe('Object Storage Gen2 create bucket tests', () => { // Confirm that basic rate limits table is displayed checkRateLimitsTable(mockBucket.endpoint_type!); - mockGetBuckets([mockBucket]).as('getBuckets'); - ui.buttonGroup .findButtonByTitle('Create Bucket') .should('be.visible') @@ -540,7 +427,7 @@ describe('Object Storage Gen2 create bucket tests', () => { .click(); }); - // Wait for the newly 'created' mocked bucket to appear + mockGetBuckets([mockBucket]).as('getBuckets'); cy.wait(['@getBuckets']); // Confirm request body has expected data 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 a6961e86709..f7aa759ec34 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 @@ -1,29 +1,38 @@ -import { containsVisible } from 'support/helpers'; +import { containsClick, containsVisible } from 'support/helpers'; import { ui } from 'support/ui'; +import { authenticate } from 'support/api/authentication'; +import { cleanUp } from 'support/util/cleanup'; import { interceptGetStackScripts, - mockGetStackScript, mockGetStackScripts, } from 'support/intercepts/stackscripts'; -import { mockCreateLinode } from 'support/intercepts/linodes'; +import { interceptCreateLinode } from 'support/intercepts/linodes'; import { filterOneClickApps, handleAppLabel, } from 'src/features/Linodes/LinodesCreate/utilities'; import { randomLabel, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; import { mapStackScriptLabelToOCA } from 'src/features/OneClickApps/utils'; import { stackScriptFactory } from 'src/factories/stackscripts'; import { oneClickApps } from 'src/features/OneClickApps/oneClickAppsv2'; import type { StackScript } from '@linode/api-v4'; import type { OCA } from '@src/features/OneClickApps/types'; -import { imageFactory, linodeFactory } from 'src/factories'; -import { mockGetAllImages } from 'support/intercepts/images'; + +authenticate(); describe('OneClick Apps (OCA)', () => { + before(() => { + cleanUp(['linodes']); + }); + it('Lists all the OneClick Apps', () => { - cy.tag('method:e2e'); interceptGetStackScripts().as('getStackScripts'); cy.visitWithLogin(`/linodes/create?type=One-Click`); @@ -114,19 +123,9 @@ describe('OneClick Apps (OCA)', () => { }); it('Deploys a Linode from a One Click App', () => { - const images = [ - imageFactory.build({ - id: 'linode/ubuntu22.04', - label: 'Ubuntu 20.04', - }), - imageFactory.build({ - id: 'linode/debian11', - label: 'Debian 11', - }), - ]; - - const stackscript = stackScriptFactory.build({ - id: 0, + const stackscriptId = 401709; + const stackScripts = stackScriptFactory.build({ + id: stackscriptId, username: 'linode', user_gravatar_id: '9d4d301385af69ceb7ad658aad09c142', label: 'E2E Test App', @@ -161,25 +160,26 @@ describe('OneClick Apps (OCA)', () => { ], }); - const rootPassword = randomString(16); - const region = chooseRegion(); - const linodeLabel = randomLabel(); - - // UDF values const firstName = randomLabel(); const password = randomString(16); + const image = 'linode/ubuntu22.04'; + const rootPassword = randomString(16); + const region = chooseRegion({ capabilities: ['Vlans'] }); + const linodeLabel = randomLabel(); const levelName = 'Get the enderman!'; - const linode = linodeFactory.build({ - label: linodeLabel, - }); - - mockGetAllImages(images); - mockGetStackScripts([stackscript]).as('getStackScripts'); - mockGetStackScript(stackscript.id, stackscript); + mockGetStackScripts([stackScripts]).as('getStackScripts'); + mockAppendFeatureFlags({ + linodeCreateRefactor: makeFeatureFlagData(false), + oneClickApps: makeFeatureFlagData({ + 401709: 'E2E Test App', + }), + }).as('getFeatureFlags'); + mockGetFeatureFlagClientstream().as('getClientStream'); cy.visitWithLogin(`/linodes/create?type=One-Click`); + cy.wait('@getFeatureFlags'); cy.wait('@getStackScripts'); cy.findByTestId('one-click-apps-container').within(() => { @@ -188,43 +188,40 @@ describe('OneClick Apps (OCA)', () => { // Check that the app is listed and select it cy.get('[data-qa-selection-card="true"]').should('have.length', 3); - cy.findAllByText(stackscript.label).first().should('be.visible').click(); + cy.get(`[id=app-${stackscriptId}]`).first().should('be.visible').click(); }); - cy.findByLabelText( - "The username for the Linode's non-root admin/SSH user(must be lowercase) (required)" - ) - .should('be.visible') - .click() - .type(firstName); - - cy.findByLabelText( - "The password for the Linode's non-root admin/SSH user (required)" - ) - .should('be.visible') - .click() - .type(password); - - cy.findByLabelText('World Name (required)') - .should('be.visible') - .click() - .type(levelName); - - // Check each field should persist when moving onto another field - cy.findByLabelText( - "The username for the Linode's non-root admin/SSH user(must be lowercase) (required)" - ).should('have.value', firstName); + // Input the user defined fields + const userFieldId = + "the-username-for-the-linode's-non-root-admin/ssh-user(must-be-lowercase)"; + const passwordFieldId = + "the-password-for-the-linode's-non-root-admin/ssh-user"; + const levelNameFieldId = 'world-name'; - cy.findByLabelText( - "The password for the Linode's non-root admin/SSH user (required)" - ).should('have.value', password); + cy.findByTestId('user-defined-fields-panel').within(() => { + cy.get(`[id="${userFieldId}"]`) + .should('be.visible') + .click() + .type(`${firstName}{enter}`); + cy.get(`[id="${passwordFieldId}"]`) + .should('be.visible') + .click() + .type(`${password}{enter}`); + cy.get(`[id="${levelNameFieldId}"]`) + .should('be.visible') + .click() + .type(`${levelName}{enter}`); - cy.findByLabelText('World Name (required)').should('have.value', levelName); + // Check each field should persist when moving onto another field + cy.get(`[id="${userFieldId}"]`).should('have.value', firstName); + cy.get(`[id="${passwordFieldId}"]`).should('have.value', password); + cy.get(`[id="${levelNameFieldId}"]`).should('have.value', levelName); + }); // Choose an image - cy.findByPlaceholderText('Choose an image') - .click() - .type('{downArrow}{enter}'); + cy.get('[data-qa-enhanced-select="Choose an image"]').within(() => { + containsClick('Choose an image').type(`${image}{enter}`); + }); // Choose a region ui.regionSelect.find().click().type(`${region.id}{enter}`); @@ -240,14 +237,14 @@ describe('OneClick Apps (OCA)', () => { cy.findByText('Linode Label') .should('be.visible') .click() + .type('{selectAll}{backspace}') .type(linodeLabel); // Choose a Root Password cy.get('[id="root-password"]').type(rootPassword); // Create the Linode - mockCreateLinode(linode).as('createLinode'); - + interceptCreateLinode().as('createLinode'); ui.button .findByTitle('Create Linode') .should('be.visible') @@ -255,7 +252,6 @@ describe('OneClick Apps (OCA)', () => { .click(); cy.wait('@createLinode'); - - ui.toast.assertMessage(`Your Linode ${linode.label} is being created.`); + ui.toast.assertMessage(`Your Linode ${linodeLabel} is being created.`); }); }); diff --git a/packages/manager/cypress/e2e/core/parentChild/token-scopes.spec.ts b/packages/manager/cypress/e2e/core/parentChild/token-scopes.spec.ts deleted file mode 100644 index ddfbdab9566..00000000000 --- a/packages/manager/cypress/e2e/core/parentChild/token-scopes.spec.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { - accountFactory, - appTokenFactory, - profileFactory, -} from '@src/factories'; -import { accountUserFactory } from '@src/factories/accountUsers'; -import { DateTime } from 'luxon'; -import { - mockGetAccount, - mockGetChildAccounts, - mockGetUser, -} from 'support/intercepts/account'; -import { - mockCreatePersonalAccessToken, - mockGetAppTokens, - mockGetPersonalAccessTokens, - mockGetProfile, -} from 'support/intercepts/profile'; -import { ui } from 'support/ui'; -import { randomLabel, randomNumber, randomString } from 'support/util/random'; - -const mockParentAccount = accountFactory.build({ - company: 'Parent Company', -}); - -const mockParentProfile = profileFactory.build({ - username: randomLabel(), - user_type: 'parent', -}); - -const mockParentUser = accountUserFactory.build({ - username: mockParentProfile.username, - user_type: 'parent', -}); - -const mockChildAccount = accountFactory.build({ - company: 'Partner Company', -}); - -const mockParentAccountToken = appTokenFactory.build({ - id: randomNumber(), - created: DateTime.now().toISO(), - expiry: DateTime.now().plus({ minutes: 15 }).toISO(), - label: `${mockParentAccount.company}_proxy`, - scopes: '*', - token: randomString(32), - website: undefined, - thumbnail_url: undefined, -}); - -describe('Token scopes', () => { - /* - * Confirm that the “Child account access” grant is not visible in the list of permissions. - * Upon clicking “Create Token”, assert that the outgoing API request payload contains "scopes" value as defined in token. - */ - it('Token scopes for parent user with restricted access', () => { - mockGetProfile(mockParentProfile); - mockGetAccount(mockParentAccount); - mockGetChildAccounts([mockChildAccount]); - mockGetProfile({ ...mockParentProfile, restricted: true }); - mockGetUser(mockParentUser); - - mockGetPersonalAccessTokens([]).as('getTokens'); - mockGetAppTokens([]).as('getAppTokens'); - mockCreatePersonalAccessToken(mockParentAccountToken).as('createToken'); - - cy.visitWithLogin('/profile/tokens'); - cy.wait(['@getTokens', '@getAppTokens']); - - // Click create button, fill out and submit PAT create form. - ui.button - .findByTitle('Create a Personal Access Token') - .should('be.visible') - .should('be.enabled') - .click(); - - mockGetPersonalAccessTokens([mockParentAccountToken]).as('getTokens'); - ui.drawer - .findByTitle('Add Personal Access Token') - .should('be.visible') - .within(() => { - // Confirm that the “Child account access” grant is not visible in the list of permissions. - cy.findAllByText('Child Account Access').should('not.exist'); - - // Specify ALL scopes by selecting the "No Access" Select All radio button. - cy.get('[data-qa-perm-rw-radio]').click(); - cy.get('[data-qa-perm-rw-radio]').should( - 'have.attr', - 'data-qa-radio', - 'true' - ); - - // Specify a label and re-submit. - cy.findByLabelText('Label') - .scrollIntoView() - .should('be.visible') - .should('be.enabled') - .click() - .type(mockParentAccountToken.label); - - ui.buttonGroup - .findButtonByTitle('Create Token') - .scrollIntoView() - .should('be.visible') - .should('be.enabled') - .click(); - }); - - // Confirm that PAT secret dialog is shown and close it. - cy.wait('@createToken'); - ui.dialog - .findByTitle('Personal Access Token') - .should('be.visible') - .within(() => { - ui.button - .findByTitle('I Have Saved My Personal Access Token') - .should('be.visible') - .should('be.enabled') - .click(); - }); - - // Confirm that new PAT is shown in list and "View Scopes" drawer works. - // Upon clicking “Create Token”, assert that the outgoing API request payload contains "scopes" value as defined in token. - cy.wait('@getTokens').then((xhr) => { - const actualTokenData = xhr.response?.body.data; - const actualTokenScopes = actualTokenData[0].scopes; - expect(actualTokenScopes).to.equal(mockParentAccountToken.scopes); - }); - }); - - /* - * Confirm that the “Child account access” grant is visible in the list of permissions. - * Upon clicking “Create Token”, assert that the outgoing API request payload contains "scopes" value as defined in token. - */ - it('Token scopes for parent user with unrestricted access', () => { - mockGetProfile(mockParentProfile); - mockGetAccount(mockParentAccount); - mockGetChildAccounts([mockChildAccount]); - mockGetProfile({ ...mockParentProfile, restricted: false }); - mockGetUser(mockParentUser); - - mockGetPersonalAccessTokens([]).as('getTokens'); - mockGetAppTokens([]).as('getAppTokens'); - mockCreatePersonalAccessToken(mockParentAccountToken).as('createToken'); - - cy.visitWithLogin('/profile/tokens'); - cy.wait(['@getTokens', '@getAppTokens']); - - // Click create button, fill out and submit PAT create form. - ui.button - .findByTitle('Create a Personal Access Token') - .should('be.visible') - .should('be.enabled') - .click(); - - mockGetPersonalAccessTokens([mockParentAccountToken]).as('getTokens'); - ui.drawer - .findByTitle('Add Personal Access Token') - .should('be.visible') - .within(() => { - // Confirm that the “Child account access” grant is not visible in the list of permissions. - cy.findAllByText('Child Account Access') - .scrollIntoView() - .should('be.visible'); - - // Specify ALL scopes by selecting the "No Access" Select All radio button. - cy.get('[data-qa-perm-rw-radio]').click(); - cy.get('[data-qa-perm-rw-radio]').should( - 'have.attr', - 'data-qa-radio', - 'true' - ); - - // Specify a label and re-submit. - cy.findByLabelText('Label') - .scrollIntoView() - .should('be.visible') - .should('be.enabled') - .click() - .type(mockParentAccountToken.label); - - ui.buttonGroup - .findButtonByTitle('Create Token') - .scrollIntoView() - .should('be.visible') - .should('be.enabled') - .click(); - }); - - // Confirm that PAT secret dialog is shown and close it. - cy.wait('@createToken'); - ui.dialog - .findByTitle('Personal Access Token') - .should('be.visible') - .within(() => { - ui.button - .findByTitle('I Have Saved My Personal Access Token') - .should('be.visible') - .should('be.enabled') - .click(); - }); - - // Confirm that new PAT is shown in list and "View Scopes" drawer works. - // Upon clicking “Create Token”, assert that the outgoing API request payload contains "scopes" value as defined in token. - cy.wait('@getTokens').then((xhr) => { - const actualTokenData = xhr.response?.body.data; - const actualTokenScopes = actualTokenData[0].scopes; - expect(actualTokenScopes).to.equal(mockParentAccountToken.scopes); - }); - }); -}); diff --git a/packages/manager/cypress/e2e/core/placementGroups/create-linode-with-placement-groups.spec.ts b/packages/manager/cypress/e2e/core/placementGroups/create-linode-with-placement-groups.spec.ts index 2484db5c322..b1573679bdc 100644 --- a/packages/manager/cypress/e2e/core/placementGroups/create-linode-with-placement-groups.spec.ts +++ b/packages/manager/cypress/e2e/core/placementGroups/create-linode-with-placement-groups.spec.ts @@ -1,3 +1,8 @@ +import { + mockAppendFeatureFlags, + mockGetFeatureFlagClientstream, +} from 'support/intercepts/feature-flags'; +import { makeFeatureFlagData } from 'support/util/feature-flags'; import { mockGetAccount } from 'support/intercepts/account'; import { accountFactory, @@ -20,6 +25,8 @@ import { CANNOT_CHANGE_PLACEMENT_GROUP_POLICY_MESSAGE } from 'src/features/Place import { linodeCreatePage } from 'support/ui/pages'; import { extendRegion } from 'support/util/regions'; +import type { Flags } from 'src/featureFlags'; + const mockAccount = accountFactory.build(); const mockNewarkRegion = extendRegion( @@ -46,6 +53,13 @@ describe('Linode create flow with Placement Group', () => { beforeEach(() => { mockGetAccount(mockAccount); mockGetRegions(mockRegions).as('getRegions'); + // TODO Remove feature flag mocks when `placementGroups` flag is retired. + mockAppendFeatureFlags({ + linodeCreateRefactor: makeFeatureFlagData( + false + ), + }); + mockGetFeatureFlagClientstream(); }); /* @@ -180,16 +194,16 @@ describe('Linode create flow with Placement Group', () => { }); // Confirm the Placement group assignment is accounted for in the summary. - cy.findByText('Assigned to Placement Group') - .scrollIntoView() - .should('be.visible'); + cy.get('[data-qa-summary="true"]').within(() => { + cy.findByText('Assigned to Placement Group').should('be.visible'); + }); // Type in a label, password and submit the form. mockCreateLinode(mockLinode).as('createLinode'); cy.get('#linode-label').clear().type('linode-with-placement-group'); cy.get('#root-password').type(randomString(32)); - cy.findByText('Create Linode').should('be.enabled').click(); + cy.get('[data-qa-deploy-linode]').click(); // Wait for outgoing API request and confirm that payload contains expected data. cy.wait('@createLinode').then((xhr) => { @@ -251,9 +265,9 @@ describe('Linode create flow with Placement Group', () => { .click(); // Confirm the Placement group assignment is accounted for in the summary. - cy.findByText('Assigned to Placement Group') - .scrollIntoView() - .should('be.visible'); + cy.get('[data-qa-summary="true"]').within(() => { + cy.findByText('Assigned to Placement Group').should('be.visible'); + }); // Create Linode and confirm contents of outgoing API request payload. ui.button diff --git a/packages/manager/cypress/e2e/core/placementGroups/create-placement-groups.spec.ts b/packages/manager/cypress/e2e/core/placementGroups/create-placement-groups.spec.ts index e114bf576de..39069134862 100644 --- a/packages/manager/cypress/e2e/core/placementGroups/create-placement-groups.spec.ts +++ b/packages/manager/cypress/e2e/core/placementGroups/create-placement-groups.spec.ts @@ -2,6 +2,7 @@ import { mockGetAccount } from 'support/intercepts/account'; import { accountFactory, placementGroupFactory } from 'src/factories'; import { regionFactory } from 'src/factories'; import { ui } from 'support/ui/'; + import { mockGetRegions } from 'support/intercepts/regions'; import { mockCreatePlacementGroup, diff --git a/packages/manager/cypress/e2e/core/placementGroups/placement-groups-linode-assignment.spec.ts b/packages/manager/cypress/e2e/core/placementGroups/placement-groups-linode-assignment.spec.ts index 0c52a148848..90890095375 100644 --- a/packages/manager/cypress/e2e/core/placementGroups/placement-groups-linode-assignment.spec.ts +++ b/packages/manager/cypress/e2e/core/placementGroups/placement-groups-linode-assignment.spec.ts @@ -22,6 +22,7 @@ import { ui } from 'support/ui'; import { buildArray } from 'support/util/arrays'; import { randomLabel, randomNumber } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; + import type { Linode } from '@linode/api-v4'; const mockAccount = accountFactory.build(); 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 c2fa9e65557..cb14c8e9265 100644 --- a/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts +++ b/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts @@ -172,9 +172,6 @@ describe('Create stackscripts', () => { before(() => { cleanUp(['linodes', 'images', 'stackscripts']); }); - beforeEach(() => { - cy.tag('method:e2e', 'purpose:dcTesting'); - }); /* * - Creates a StackScript with user-defined fields. @@ -377,7 +374,7 @@ describe('Create stackscripts', () => { */ filteredImageData?.forEach((imageSample: Image) => { const imageLabel = imageSample.label; - cy.findAllByText(imageLabel, { exact: false }) + cy.findAllByText(imageLabel) .last() .scrollIntoView() .should('exist') diff --git a/packages/manager/cypress/e2e/core/stackscripts/smoke-community-stackscripts.spec.ts b/packages/manager/cypress/e2e/core/stackscripts/smoke-community-stackscripts.spec.ts index 26453578d7b..43ae5fa6dc0 100644 --- a/packages/manager/cypress/e2e/core/stackscripts/smoke-community-stackscripts.spec.ts +++ b/packages/manager/cypress/e2e/core/stackscripts/smoke-community-stackscripts.spec.ts @@ -190,7 +190,6 @@ describe('Community Stackscripts integration tests', () => { * - Confirms that pagination works as expected. */ it('pagination works with infinite scrolling', () => { - cy.tag('method:e2e'); interceptGetStackScripts().as('getStackScripts'); // Fetch all public Images to later use while filtering StackScripts. @@ -264,7 +263,6 @@ describe('Community Stackscripts integration tests', () => { * - Confirms that search can filter the expected results. */ it('search function filters results correctly', () => { - cy.tag('method:e2e'); const stackScript = mockStackScripts[0]; interceptGetStackScripts().as('getStackScripts'); diff --git a/packages/manager/cypress/e2e/core/volumes/attach-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/attach-volume.spec.ts index 41c53259a98..76c2ec63a0f 100644 --- a/packages/manager/cypress/e2e/core/volumes/attach-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/attach-volume.spec.ts @@ -50,9 +50,6 @@ describe('volume attach and detach flows', () => { before(() => { cleanUp(['volumes', 'linodes']); }); - beforeEach(() => { - cy.tag('method:e2e'); - }); /* * - Clicks "Attach" action menu item for volume, selects Linode with common region, and submits form. diff --git a/packages/manager/cypress/e2e/core/volumes/clone-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/clone-volume.spec.ts index e65a582243b..f589c9b979b 100644 --- a/packages/manager/cypress/e2e/core/volumes/clone-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/clone-volume.spec.ts @@ -35,9 +35,6 @@ describe('volume clone flow', () => { before(() => { cleanUp('volumes'); }); - beforeEach(() => { - cy.tag('method:e2e'); - }); /* * - Clicks "Clone" action menu item for volume, enters new label, and submits form. diff --git a/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts index ceac6fc7cf1..6dfd3bc07dd 100644 --- a/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts @@ -1,9 +1,6 @@ import type { Linode, Region } from '@linode/api-v4'; import { createTestLinode } from 'support/util/linodes'; -import { - createLinodeRequestFactory, - linodeFactory, -} from 'src/factories/linodes'; +import { createLinodeRequestFactory } from 'src/factories/linodes'; import { authenticate } from 'support/api/authentication'; import { cleanUp } from 'support/util/cleanup'; import { @@ -18,10 +15,6 @@ import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { accountFactory, regionFactory, volumeFactory } from 'src/factories'; import { mockGetAccount } from 'support/intercepts/account'; import { mockGetRegions } from 'support/intercepts/regions'; -import { - mockGetLinodeDetails, - mockGetLinodes, -} from 'support/intercepts/linodes'; // Local storage override to force volume table to list up to 100 items. // This is a workaround while we wait to get stuck volumes removed. @@ -53,7 +46,7 @@ describe('volume create flow', () => { * - Confirms that volume is listed correctly on volumes landing page. */ it('creates an unattached volume', () => { - cy.tag('purpose:syntheticTesting', 'method:e2e', 'purpose:dcTesting'); + cy.tag('purpose:syntheticTesting'); const region = chooseRegion(); const volume = { @@ -98,7 +91,6 @@ describe('volume create flow', () => { * - Confirms that volume is listed correctly on Linode 'Storage' details page. */ it('creates an attached volume', () => { - cy.tag('method:e2e', 'purpose:dcTesting'); const region = chooseRegion(); const linodeRequest = createLinodeRequestFactory.build({ @@ -140,10 +132,6 @@ describe('volume create flow', () => { .should('be.visible') .click(); - // @TODO BSE: once BSE is fully rolled out, check for the notice (selected linode doesn't have - // "Block Storage Encryption" capability + user checked "Encrypt Volume" checkbox) instead of the absence of it - cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('not.exist'); - cy.findByText('Create Volume').click(); cy.wait('@createVolume'); @@ -174,123 +162,10 @@ describe('volume create flow', () => { }); /* - * - Checks for Block Storage Encryption client library update notice on the Volume Create page. - */ - it('displays a warning notice on Volume Create page re: rebooting for client library updates under the appropriate conditions', () => { - // Conditions: Block Storage encryption feature flag is on; user has Block Storage Encryption capability; volume being created is encrypted and the - // selected Linode does not support Block Storage Encryption - - // Mock feature flag -- @TODO BSE: Remove feature flag once BSE is fully rolled out - mockAppendFeatureFlags({ - blockStorageEncryption: true, - }).as('getFeatureFlags'); - - // Mock account response - const mockAccount = accountFactory.build({ - capabilities: ['Linodes', 'Block Storage Encryption'], - }); - - mockGetAccount(mockAccount).as('getAccount'); - mockGetRegions(mockRegions).as('getRegions'); - - const linodeRequest = createLinodeRequestFactory.build({ - label: randomLabel(), - root_pass: randomString(16), - region: mockRegions[0].id, - booted: false, - }); - - cy.defer(() => createTestLinode(linodeRequest), 'creating Linode').then( - (linode: Linode) => { - cy.visitWithLogin('/volumes/create'); - cy.wait(['@getFeatureFlags', '@getAccount']); - - // Select a linode without the BSE capability - cy.findByLabelText('Linode') - .should('be.visible') - .click() - .type(linode.label); - - ui.autocompletePopper - .findByTitle(linode.label) - .should('be.visible') - .click(); - - // Check the "Encrypt Volume" checkbox - cy.get('[data-qa-checked]').should('be.visible').click(); - // }); - - // Ensure warning notice is displayed and "Create Volume" button is disabled - cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('be.visible'); - ui.button - .findByTitle('Create Volume') - .should('be.visible') - .should('be.disabled'); - } - ); - }); - - /* - * - Checks for absence of Block Storage Encryption client library update notice on the Volume Create page - * when selected linode supports BSE - */ - it('does not display a warning notice on Volume Create page re: rebooting for client library updates when selected linode supports BSE', () => { - // Conditions: Block Storage encryption feature flag is on; user has Block Storage Encryption capability; volume being created is encrypted and the - // selected Linode supports Block Storage Encryption - - // Mock feature flag -- @TODO BSE: Remove feature flag once BSE is fully rolled out - mockAppendFeatureFlags({ - blockStorageEncryption: true, - }).as('getFeatureFlags'); - - // Mock account response - const mockAccount = accountFactory.build({ - capabilities: ['Linodes', 'Block Storage Encryption'], - }); - - // Mock linode - const mockLinode = linodeFactory.build({ - region: mockRegions[0].id, - id: 123456, - capabilities: ['Block Storage Encryption'], - }); - - mockGetAccount(mockAccount).as('getAccount'); - mockGetRegions(mockRegions).as('getRegions'); - mockGetLinodes([mockLinode]).as('getLinodes'); - mockGetLinodeDetails(mockLinode.id, mockLinode); - - cy.visitWithLogin(`/volumes/create`); - cy.wait(['@getAccount', '@getRegions', '@getLinodes']); - - // Select a linode without the BSE capability - cy.findByLabelText('Linode') - .should('be.visible') - .click() - .type(mockLinode.label); - - ui.autocompletePopper - .findByTitle(mockLinode.label) - .should('be.visible') - .click(); - - // Check the "Encrypt Volume" checkbox - cy.get('[data-qa-checked]').should('be.visible').click(); - // }); - - // Ensure warning notice is not displayed and "Create Volume" button is enabled - cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('not.exist'); - ui.button - .findByTitle('Create Volume') - .should('be.visible') - .should('be.enabled'); - }); - - /* - * - Checks for Block Storage Encryption client library update notice in the Create/Attach Volume drawer from the + * - Checks for Block Storage Encryption notices in the Create/Attach Volume drawer from the 'Storage' details page of an existing Linode. */ - it('displays a warning notice re: rebooting for client library updates under the appropriate conditions in Create/Attach Volume drawer', () => { + it('displays a warning notice re: rebooting for client library updates under the appropriate conditions', () => { // Conditions: Block Storage encryption feature flag is on; user has Block Storage Encryption capability; Linode does not support Block Storage Encryption and the user is trying to attach an encrypted volume // Mock feature flag -- @TODO BSE: Remove feature flag once BSE is fully rolled out @@ -329,25 +204,18 @@ describe('volume create flow', () => { // Click "Add Volume" button cy.findByText('Add Volume').click(); - // Check "Encrypt Volume" checkbox cy.get('[data-qa-drawer="true"]').within(() => { cy.get('[data-qa-checked]').should('be.visible').click(); }); - // Ensure client library update notice is displayed and the "Create Volume" button is disabled cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('be.visible'); - ui.button.findByTitle('Create Volume').should('be.disabled'); // Ensure notice is cleared when switching views in drawer cy.get('[data-qa-radio="Attach Existing Volume"]').click(); cy.wait(['@getVolumes']); cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('not.exist'); - ui.button - .findByTitle('Attach Volume') - .should('be.visible') - .should('be.enabled'); - // Ensure notice is displayed in "Attach Existing Volume" view when an encrypted volume is selected, & that the "Attach Volume" button is disabled + // Ensure notice is displayed in "Attach Existing Volume" view when an encrypted volume is selected cy.findByPlaceholderText('Select a Volume') .should('be.visible') .click() @@ -358,10 +226,6 @@ describe('volume create flow', () => { .click(); cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('be.visible'); - ui.button - .findByTitle('Attach Volume') - .should('be.visible') - .should('be.disabled'); } ); }); @@ -372,7 +236,6 @@ describe('volume create flow', () => { * - Confirms that volume is listed correctly on Volumes landing page. */ it('creates a volume from an existing Linode', () => { - cy.tag('method:e2e'); const linodeRequest = createLinodeRequestFactory.build({ label: randomLabel(), root_pass: randomString(16), diff --git a/packages/manager/cypress/e2e/core/volumes/delete-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/delete-volume.spec.ts index 6828618fb70..7897d0c7f2c 100644 --- a/packages/manager/cypress/e2e/core/volumes/delete-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/delete-volume.spec.ts @@ -20,9 +20,6 @@ describe('volume delete flow', () => { before(() => { cleanUp('volumes'); }); - beforeEach(() => { - cy.tag('method:e2e'); - }); /* * - Clicks "Delete" action menu item for volume but cancels operation. diff --git a/packages/manager/cypress/e2e/core/volumes/resize-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/resize-volume.spec.ts index 74b9955ea76..f1ef8a5ddb7 100644 --- a/packages/manager/cypress/e2e/core/volumes/resize-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/resize-volume.spec.ts @@ -35,9 +35,6 @@ describe('volume resize flow', () => { before(() => { cleanUp('volumes'); }); - beforeEach(() => { - cy.tag('method:e2e'); - }); /* * - Clicks "Resize" action menu item for volume, enters new size, and submits form. diff --git a/packages/manager/cypress/e2e/core/volumes/search-volumes.spec.ts b/packages/manager/cypress/e2e/core/volumes/search-volumes.spec.ts index 4b95b1a407f..e6fc05b38b0 100644 --- a/packages/manager/cypress/e2e/core/volumes/search-volumes.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/search-volumes.spec.ts @@ -1,6 +1,7 @@ import { createVolume } from '@linode/api-v4/lib/volumes'; import { Volume } from '@linode/api-v4'; import { ui } from 'support/ui'; + import { authenticate } from 'support/api/authentication'; import { randomLabel } from 'support/util/random'; import { cleanUp } from 'support/util/cleanup'; @@ -10,9 +11,6 @@ describe('Search Volumes', () => { before(() => { cleanUp(['volumes']); }); - beforeEach(() => { - cy.tag('method:e2e'); - }); /* * - Confirm that volumes are API searchable and filtered in the UI. diff --git a/packages/manager/cypress/e2e/core/volumes/update-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/update-volume.spec.ts index 70f4840bbad..e5a3d9f5de0 100644 --- a/packages/manager/cypress/e2e/core/volumes/update-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/update-volume.spec.ts @@ -11,9 +11,6 @@ describe('volume update flow', () => { before(() => { cleanUp(['tags', 'volumes']); }); - beforeEach(() => { - cy.tag('method:e2e'); - }); /* * - Confirms that volume label and tags can be changed from the Volumes landing page. diff --git a/packages/manager/cypress/screenshots/smoke-visual-icons.spec.ts/record-Docs-View-1440-900.png b/packages/manager/cypress/screenshots/smoke-visual-icons.spec.ts/record-Docs-View-1440-900.png new file mode 100644 index 00000000000..4b9d2ca3b2b Binary files /dev/null and b/packages/manager/cypress/screenshots/smoke-visual-icons.spec.ts/record-Docs-View-1440-900.png differ diff --git a/packages/manager/cypress/screenshots/smoke-visual-icons.spec.ts/record-Running-Total-1440-900.png b/packages/manager/cypress/screenshots/smoke-visual-icons.spec.ts/record-Running-Total-1440-900.png new file mode 100644 index 00000000000..a6c99b9dea2 Binary files /dev/null and b/packages/manager/cypress/screenshots/smoke-visual-icons.spec.ts/record-Running-Total-1440-900.png differ diff --git a/packages/manager/cypress/screenshots/smoke-visual-icons.spec.ts/record-linode-offline-1440-900.png b/packages/manager/cypress/screenshots/smoke-visual-icons.spec.ts/record-linode-offline-1440-900.png new file mode 100644 index 00000000000..442e3c5c04e Binary files /dev/null and b/packages/manager/cypress/screenshots/smoke-visual-icons.spec.ts/record-linode-offline-1440-900.png differ diff --git a/packages/manager/cypress/screenshots/smoke-visual-icons.spec.ts/record-linode-running-1440-900.png b/packages/manager/cypress/screenshots/smoke-visual-icons.spec.ts/record-linode-running-1440-900.png new file mode 100644 index 00000000000..22b7c4d71fd Binary files /dev/null and b/packages/manager/cypress/screenshots/smoke-visual-icons.spec.ts/record-linode-running-1440-900.png differ diff --git a/packages/manager/cypress/support/component/index.html b/packages/manager/cypress/support/component/index.html index c3ef4d3fb82..02e600735b2 100644 --- a/packages/manager/cypress/support/component/index.html +++ b/packages/manager/cypress/support/component/index.html @@ -4,7 +4,6 @@ - Cloud Manager Components diff --git a/packages/manager/cypress/support/component/setup.tsx b/packages/manager/cypress/support/component/setup.tsx index cb1e039d375..ac19267791c 100644 --- a/packages/manager/cypress/support/component/setup.tsx +++ b/packages/manager/cypress/support/component/setup.tsx @@ -30,6 +30,9 @@ import { ThemeName } from 'src/foundations/themes'; import 'cypress-axe'; +// Load fonts using Vite rather than HTML ``. +import '../../../public/fonts/fonts.css'; + /** * Mounts a component with a Cloud Manager theme applied. * diff --git a/packages/manager/cypress/support/intercepts/events.ts b/packages/manager/cypress/support/intercepts/events.ts index 0e12a2f8fb9..eea7b5fac12 100644 --- a/packages/manager/cypress/support/intercepts/events.ts +++ b/packages/manager/cypress/support/intercepts/events.ts @@ -2,11 +2,9 @@ * @file Mocks and intercepts related to notification and event handling. */ +import type { Event, Notification } from '@linode/api-v4'; import { apiMatcher } from 'support/util/intercepts'; import { paginateResponse } from 'support/util/paginate'; -import { makeResponse } from 'support/util/response'; - -import type { Event, Notification } from '@linode/api-v4'; /** * Intercepts GET request to fetch events and mocks response. @@ -23,50 +21,6 @@ export const mockGetEvents = (events: Event[]): Cypress.Chainable => { ); }; -/** - * Intercepts polling GET request to fetch events and mocks response. - * - * Unlike `mockGetEvents`, this utility only intercepts outgoing requests that - * occur while Cloud Manager is polling for events. - * - * @param events - Array of Events with which to mock response. - * @param pollingTimestamp - Timestamp to find when identifying polling requests. - * - * @returns Cypress chainable. - */ -export const mockGetEventsPolling = ( - events: Event[], - pollingTimestamp: string -): Cypress.Chainable => { - return cy.intercept('GET', apiMatcher('account/events*'), (req) => { - console.log({ headers: req.headers }); - if ( - req.headers['x-filter'].includes( - `{"created":{"+gte":"${pollingTimestamp}"}}` - ) - ) { - req.reply(paginateResponse(events)); - } else { - req.continue(); - } - }); -}; - -/** - * Intercepts POST request to mark an event as seen and mocks response. - * - * @param eventId - ID of the event for which to intercept request. - * - * @returns Cypress chainable. - */ -export const mockMarkEventSeen = (eventId: number): Cypress.Chainable => { - return cy.intercept( - 'POST', - apiMatcher(`account/events/${eventId}/seen`), - makeResponse({}) - ); -}; - /** * Intercepts GET request to fetch notifications and mocks response. * diff --git a/packages/manager/cypress/support/intercepts/linodes.ts b/packages/manager/cypress/support/intercepts/linodes.ts index 2a665ac49bc..e3dd41d140a 100644 --- a/packages/manager/cypress/support/intercepts/linodes.ts +++ b/packages/manager/cypress/support/intercepts/linodes.ts @@ -4,19 +4,11 @@ import { makeErrorResponse } from 'support/util/errors'; import { apiMatcher } from 'support/util/intercepts'; -import { linodeVlanNoInternetConfig } from 'support/util/linodes'; import { paginateResponse } from 'support/util/paginate'; import { makeResponse } from 'support/util/response'; +import { linodeVlanNoInternetConfig } from 'support/util/linodes'; -import type { - Disk, - Firewall, - Kernel, - Linode, - LinodeIPsResponse, - LinodeType, - Volume, -} from '@linode/api-v4'; +import type { Disk, Kernel, Linode, LinodeType, Volume } from '@linode/api-v4'; /** * Intercepts POST request to create a Linode. @@ -295,51 +287,6 @@ export const mockGetLinodeDisks = ( ); }; -/** - * Intercepts DELETE request to delete a Linode's Disks - * - * @param linodeId - ID of Linode for intercepted request. - * - * @returns Cypress chainable. - */ -export const interceptDeleteDisks = ( - linodeId: number -): Cypress.Chainable => { - return cy.intercept( - 'DELETE', - apiMatcher(`linode/instances/${linodeId}/disks/*`) - ); -}; - -/** - * Intercepts POST request to add a Linode's Disks - * - * @param linodeId - ID of Linode for intercepted request. - * - * @returns Cypress chainable. - */ -export const interceptAddDisks = ( - linodeId: number -): Cypress.Chainable => { - return cy.intercept('POST', apiMatcher(`linode/instances/${linodeId}/disks`)); -}; - -/** - * Intercepts POST request to resize a Linode's Disks - * - * @param linodeId - ID of Linode for intercepted request. - * - * @returns Cypress chainable. - */ -export const interceptResizeDisks = ( - linodeId: number -): Cypress.Chainable => { - return cy.intercept( - 'POST', - apiMatcher(`linode/instances/${linodeId}/disks/*/resize`) - ); -}; - /** * Intercepts DELETE request to delete linode and mocks response. * @@ -535,8 +482,7 @@ export const mockGetLinodeKernel = ( ); }; -/** - * Intercepts POST request to get a Linode Resize. +/* Intercepts POST request to get a Linode Resize. * * @param linodeId - ID of Linode to fetch. * @@ -550,41 +496,3 @@ export const interceptLinodeResize = ( apiMatcher(`linode/instances/${linodeId}/resize`) ); }; - -/** - * Mocks GET request to get a Linode's firewalls. - * - * @param linodeId - ID of Linode to get firewalls associated with it. - * @param firewalls - the firewalls with which to mock the response. - * - * @returns Cypress Chainable. - */ -export const mockGetLinodeFirewalls = ( - linodeId: number, - firewalls: Firewall[] -): Cypress.Chainable => { - return cy.intercept( - 'GET', - apiMatcher(`linode/instances/${linodeId}/firewalls`), - paginateResponse(firewalls) - ); -}; - -/** - * Mocks GET request to get a Linode's IP addresses. - * - * @param linodeId - ID of Linode to get IP addresses for. - * @param ipAddresses: the IP Addresses with which to mock the response. - * - * @returns Cypress Chainable. - */ -export const mockGetLinodeIPAddresses = ( - linodeId: number, - ipAddresses: LinodeIPsResponse -): Cypress.Chainable => { - return cy.intercept( - 'GET', - apiMatcher(`linode/instances/${linodeId}/ips`), - makeResponse(ipAddresses) - ); -}; diff --git a/packages/manager/cypress/support/intercepts/networking.ts b/packages/manager/cypress/support/intercepts/networking.ts deleted file mode 100644 index 375183442c1..00000000000 --- a/packages/manager/cypress/support/intercepts/networking.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { apiMatcher } from 'support/util/intercepts'; - -/** - * Mocks PUT request to update an IP address. - * - * @param address - the IP address to update - * @param rdns - the updated RDNS of the IP address - * - * @returns Cypress chainable. - */ -export const mockUpdateIPAddress = ( - address: string, - rdns: string -): Cypress.Chainable => { - return cy.intercept('PUT', apiMatcher(`/networking/ips/${address}`), rdns); -}; diff --git a/packages/manager/cypress/support/plugins/fetch-account.ts b/packages/manager/cypress/support/plugins/fetch-account.ts index 0262c5d3f16..3de8553f7b8 100644 --- a/packages/manager/cypress/support/plugins/fetch-account.ts +++ b/packages/manager/cypress/support/plugins/fetch-account.ts @@ -1,14 +1,5 @@ import type { CypressPlugin } from './plugin'; -import { resolve, join } from 'path'; -import { getAccountInfo, getAccountSettings, getProfile } from '@linode/api-v4'; -import { readFileSync } from 'fs'; - -import type { Account } from '@linode/api-v4'; - -/** - * The name of the environment variable that controls account cache reading. - */ -const envVarName = 'CY_TEST_ACCOUNT_CACHE_DIR'; +import { getAccountInfo, getAccountSettings } from '@linode/api-v4'; /** * Fetches and caches Linode account info and settings. @@ -17,78 +8,16 @@ const envVarName = 'CY_TEST_ACCOUNT_CACHE_DIR'; * `cloudManagerAccountSettings` env, respectively. */ export const fetchAccount: CypressPlugin = async (_on, config) => { - // Fetch profile and account settings first, since there is a comparatively - // low likelihood that these requests will fail. - const [profile, accountSettings] = await Promise.all([ - getProfile(), + const [account, accountSettings] = await Promise.all([ + getAccountInfo(), getAccountSettings(), ]); - /** - * Cached account data for the desired test account if it is available, or `undefined`. - */ - const accountCacheData = (() => { - if (!config.env[envVarName]) { - return undefined; - } - - const accountCacheDir = config.env[envVarName]; - const accountCachePath = resolve( - join(accountCacheDir, `${profile.uid}.json`) - ); - - try { - const cacheJson = readFileSync(accountCachePath, 'utf8'); - const cacheData = JSON.parse(cacheJson); - - if ('account' in cacheData) { - const accountCache = cacheData['account'] as Account; - return accountCache; - } - } catch (e) { - // TODO Error message. - console.error(`Failed to read account cache file at ${accountCachePath}`); - if ('message' in e) { - console.error(e.message); - } - return undefined; - } - - return undefined; - })(); - - // Fetch account info, falling back to offline cached data if it is - // enabled and available. - let account: undefined | Account = undefined; - try { - account = await getAccountInfo(); - } catch (e) { - console.error( - 'An error occurred while retrieving test account information.' - ); - - // Re-throw the error if no cached account data is available, because the - // test run cannot continue. - if (!accountCacheData) { - throw e; - } - // Otherwise, note that the original account fetch failed and that the tests - // will be proceeding using cached data. - else { - if (e.message) { - console.error(e.message); - } - console.info( - 'Cached account data is available and will be used instead.' - ); - } - } - return { ...config, env: { ...config.env, - cloudManagerAccount: account || accountCacheData, + cloudManagerAccount: account, cloudManagerAccountSettings: accountSettings, }, }; diff --git a/packages/manager/cypress/support/plugins/junit-report.ts b/packages/manager/cypress/support/plugins/junit-report.ts index a60ded4521b..986eb6c6b24 100644 --- a/packages/manager/cypress/support/plugins/junit-report.ts +++ b/packages/manager/cypress/support/plugins/junit-report.ts @@ -8,42 +8,27 @@ const capitalize = (str: string): string => { }; /** - * Returns a plugin to enable JUnit reporting when `CY_TEST_JUNIT_REPORT` is defined. - * - * If no suite name is specified, this function will attempt to determine the - * suite name using the Cypress configuration object. - * - * @param suiteName - Optional suite name in the JUnit output. + * Enables and configures JUnit reporting when `CY_TEST_JUNIT_REPORT` is defined. * * @returns Cypress configuration object. */ -export const enableJunitReport = ( - suiteName?: string, - jenkinsMode: boolean = false -): CypressPlugin => { - return (_on, config) => { - if (!!config.env[envVarName]) { - // Use `suiteName` if it is specified. - // Otherwise, attempt to determine the test suite name using - // our Cypress configuration. - const testSuite = suiteName || config.env['cypress_test_suite'] || 'core'; - - const testSuiteName = `${capitalize(testSuite)} Test Suite`; +export const enableJunitReport: CypressPlugin = (_on, config) => { + if (!!config.env[envVarName]) { + const testSuite = config.env['cypress_test_suite'] || 'core'; + const testSuiteName = `${capitalize(testSuite)} Test Suite`; - // Cypress doesn't know to look for modules in the root `node_modules` - // directory, so we have to pass a relative path. - // See also: https://github.com/cypress-io/cypress/issues/6406 - config.reporter = '../../node_modules/mocha-junit-reporter'; + // Cypress doesn't know to look for modules in the root `node_modules` + // directory, so we have to pass a relative path. + // See also: https://github.com/cypress-io/cypress/issues/6406 + config.reporter = '../../node_modules/mocha-junit-reporter'; - // See also: https://www.npmjs.com/package/mocha-junit-reporter#full-configuration-options - config.reporterOptions = { - mochaFile: 'cypress/results/test-results-[hash].xml', - rootSuiteTitle: 'Cloud Manager Cypress Tests', - testsuitesTitle: testSuiteName, - jenkinsMode, - suiteTitleSeparatedBy: jenkinsMode ? '→' : ' ', - }; - } - return config; - }; + // See also: https://www.npmjs.com/package/mocha-junit-reporter#full-configuration-options + config.reporterOptions = { + mochaFile: 'cypress/results/test-results-[hash].xml', + rootSuiteTitle: 'Cloud Manager Cypress Tests', + testsuitesTitle: testSuiteName, + jenkinsMode: false, + }; + } + return config; }; diff --git a/packages/manager/cypress/support/ui/app-bar.ts b/packages/manager/cypress/support/ui/app-bar.ts deleted file mode 100644 index 163e395760b..00000000000 --- a/packages/manager/cypress/support/ui/app-bar.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * UI helpers for Cloud Manager top app bar. - */ -export const appBar = { - /** - * Finds the app bar. - * - * @returns Cypress chainable. - */ - find: () => { - return cy.get('[data-qa-appbar]'); - }, -}; diff --git a/packages/manager/cypress/support/ui/index.ts b/packages/manager/cypress/support/ui/index.ts index 1cba6746bcb..26d8b89ac17 100644 --- a/packages/manager/cypress/support/ui/index.ts +++ b/packages/manager/cypress/support/ui/index.ts @@ -1,6 +1,5 @@ import * as accordion from './accordion'; import * as actionMenu from './action-menu'; -import * as appBar from './app-bar'; import * as autocomplete from './autocomplete'; import * as breadcrumb from './breadcrumb'; import * as buttons from './buttons'; @@ -22,7 +21,6 @@ import * as userMenu from './user-menu'; export const ui = { ...accordion, ...actionMenu, - ...appBar, ...autocomplete, ...breadcrumb, ...buttons, diff --git a/packages/manager/package.json b/packages/manager/package.json index df8b2d070d0..d7b37e5f97a 100644 --- a/packages/manager/package.json +++ b/packages/manager/package.json @@ -100,7 +100,6 @@ "cy:e2e": "cypress run --headless -b chrome", "cy:debug": "cypress open --e2e", "cy:component": "cypress open --component", - "cy:component:run": "cypress run --component --headless -b chrome", "cy:rec-snap": "cypress run --headless -b chrome --env visualRegMode=record --spec ./cypress/integration/**/*visual*.spec.ts", "typecheck": "tsc --noEmit && tsc -p cypress --noEmit", "coverage": "vitest run --coverage && open coverage/index.html", diff --git a/packages/manager/src/assets/logo/akamai-wave.svg b/packages/manager/src/assets/logo/akamai-wave.svg deleted file mode 100644 index 423f77081c8..00000000000 --- a/packages/manager/src/assets/logo/akamai-wave.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/manager/src/components/AccessPanel/UserSSHKeyPanel.tsx b/packages/manager/src/components/AccessPanel/UserSSHKeyPanel.tsx index fa9ed19f713..c3c8d41ebf4 100644 --- a/packages/manager/src/components/AccessPanel/UserSSHKeyPanel.tsx +++ b/packages/manager/src/components/AccessPanel/UserSSHKeyPanel.tsx @@ -1,4 +1,4 @@ -import { useTheme } from '@mui/material/styles'; +import { Theme } from '@mui/material/styles'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; @@ -18,14 +18,10 @@ import { useAccountUsers } from 'src/queries/account/users'; import { useProfile, useSSHKeysQuery } from 'src/queries/profile/profile'; import { truncateAndJoinList } from 'src/utilities/stringUtils'; -import { Avatar } from '../Avatar/Avatar'; import { GravatarByEmail } from '../GravatarByEmail'; -import { GravatarOrAvatar } from '../GravatarOrAvatar'; import { PaginationFooter } from '../PaginationFooter/PaginationFooter'; import { TableRowLoading } from '../TableRowLoading/TableRowLoading'; -import type { Theme } from '@mui/material/styles'; - export const MAX_SSH_KEYS_DISPLAY = 25; const useStyles = makeStyles()((theme: Theme) => ({ @@ -61,7 +57,6 @@ interface Props { const UserSSHKeyPanel = (props: Props) => { const { classes } = useStyles(); - const theme = useTheme(); const { authorizedUsers, disabled, setAuthorizedUsers } = props; const [isCreateDrawerOpen, setIsCreateDrawerOpen] = React.useState( @@ -150,14 +145,9 @@ const UserSSHKeyPanel = (props: Props) => {
- - } - avatar={} + {profile.username}
@@ -187,25 +177,7 @@ const UserSSHKeyPanel = (props: Props) => {
- - } - gravatar={ - - } - /> + {user.username}
diff --git a/packages/manager/src/components/Avatar/Avatar.stories.tsx b/packages/manager/src/components/Avatar/Avatar.stories.tsx deleted file mode 100644 index 9817951902b..00000000000 --- a/packages/manager/src/components/Avatar/Avatar.stories.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import * as React from 'react'; - -import { Avatar } from 'src/components/Avatar/Avatar'; - -import type { Meta, StoryObj } from '@storybook/react'; -import type { AvatarProps } from 'src/components/Avatar/Avatar'; - -export const Default: StoryObj = { - render: (args) => , -}; - -export const System: StoryObj = { - render: (args) => , -}; - -const meta: Meta = { - args: { - color: '#0174bc', - height: 88, - sx: {}, - username: 'MyUsername', - width: 88, - }, - component: Avatar, - title: 'Components/Avatar', -}; -export default meta; diff --git a/packages/manager/src/components/Avatar/Avatar.test.tsx b/packages/manager/src/components/Avatar/Avatar.test.tsx deleted file mode 100644 index e1c553d2c15..00000000000 --- a/packages/manager/src/components/Avatar/Avatar.test.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import * as React from 'react'; - -import { profileFactory } from 'src/factories/profile'; -import { renderWithTheme } from 'src/utilities/testHelpers'; - -import { Avatar } from './Avatar'; - -import type { AvatarProps } from './Avatar'; - -const mockProps: AvatarProps = {}; - -const queryMocks = vi.hoisted(() => ({ - useProfile: vi.fn().mockReturnValue({}), -})); - -vi.mock('src/queries/profile/profile', async () => { - const actual = await vi.importActual('src/queries/profile/profile'); - return { - ...actual, - useProfile: queryMocks.useProfile, - }; -}); - -describe('Avatar', () => { - it('should render the first letter of a username from /profile with default background color', () => { - queryMocks.useProfile.mockReturnValue({ - data: profileFactory.build({ username: 'my-user' }), - }); - const { getByTestId } = renderWithTheme(); - const avatar = getByTestId('avatar'); - const avatarStyles = getComputedStyle(avatar); - - expect(getByTestId('avatar-letter')).toHaveTextContent('M'); - expect(avatarStyles.backgroundColor).toBe('rgb(1, 116, 188)'); // theme.color.primary.dark (#0174bc) - }); - - it('should render a background color from props', () => { - queryMocks.useProfile.mockReturnValue({ - data: profileFactory.build({ username: 'my-user' }), - }); - - const { getByTestId } = renderWithTheme( - - ); - const avatar = getByTestId('avatar'); - const avatarText = getByTestId('avatar-letter'); - const avatarStyles = getComputedStyle(avatar); - const avatarTextStyles = getComputedStyle(avatarText); - - // Confirm background color contrasts with text color. - expect(avatarStyles.backgroundColor).toBe('rgb(0, 0, 0)'); // black - expect(avatarTextStyles.color).toBe('rgb(255, 255, 255)'); // white - }); - - it('should render the first letter of username from props', async () => { - const { getByTestId } = renderWithTheme( - - ); - - expect(getByTestId('avatar-letter')).toHaveTextContent('T'); - }); - - it('should render an svg instead of first letter for system users', async () => { - const systemUsernames = ['Linode', 'lke-service-account-123']; - - systemUsernames.forEach((username, i) => { - const { getAllByRole, queryByTestId } = renderWithTheme( - - ); - expect(getAllByRole('img')[i]).toBeVisible(); - expect(queryByTestId('avatar-letter')).toBe(null); - }); - }); -}); diff --git a/packages/manager/src/components/Avatar/Avatar.tsx b/packages/manager/src/components/Avatar/Avatar.tsx deleted file mode 100644 index 968ec8b5834..00000000000 --- a/packages/manager/src/components/Avatar/Avatar.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { Typography, useTheme } from '@mui/material'; -import { default as _Avatar } from '@mui/material/Avatar'; -import * as React from 'react'; - -import AkamaiWave from 'src/assets/logo/akamai-wave.svg'; -import { usePreferences } from 'src/queries/profile/preferences'; -import { useProfile } from 'src/queries/profile/profile'; - -import type { SxProps } from '@mui/material'; - -export const DEFAULT_AVATAR_SIZE = 28; - -export interface AvatarProps { - /** - * Optional background color to override the color set in user preferences - * */ - color?: string; - /** - * Optional height - * @default 28px - * */ - height?: number; - /** - * Optional styles - * */ - sx?: SxProps; - /** - * Optional username to override the profile username; will display the first letter - * */ - username?: string; - /** - * Optional width - * @default 28px - * */ - width?: number; -} - -/** - * The Avatar component displays the first letter of a username on a solid background color. - * For system avatars associated with Akamai-generated events, an Akamai logo is displayed in place of a letter. - */ -export const Avatar = (props: AvatarProps) => { - const { - color, - height = DEFAULT_AVATAR_SIZE, - sx, - username, - width = DEFAULT_AVATAR_SIZE, - } = props; - - const theme = useTheme(); - - const { data: preferences } = usePreferences(); - const { data: profile } = useProfile(); - - const _username = username ?? profile?.username ?? ''; - const isAkamai = - _username === 'Linode' || _username.startsWith('lke-service-account'); - - const savedAvatarColor = - isAkamai || !preferences?.avatarColor - ? theme.palette.primary.dark - : preferences.avatarColor; - const avatarLetter = _username[0]?.toUpperCase() ?? ''; - - return ( - <_Avatar - sx={{ - '& svg': { - height: width / 2, - width: width / 2, - }, - bgcolor: color ?? savedAvatarColor, - height, - width, - ...sx, - }} - alt={`Avatar for user ${username ?? profile?.email ?? ''}`} - data-testid="avatar" - > - {isAkamai ? ( - - ) : ( - - {avatarLetter} - - )} - - ); -}; diff --git a/packages/manager/src/components/CheckoutSummary/CheckoutSummary.stories.tsx b/packages/manager/src/components/CheckoutSummary/CheckoutSummary.stories.tsx deleted file mode 100644 index f0593bcad69..00000000000 --- a/packages/manager/src/components/CheckoutSummary/CheckoutSummary.stories.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; - -import { Typography } from 'src/components/Typography'; - -import { CheckoutSummary } from './CheckoutSummary'; - -import type { Meta, StoryFn, StoryObj } from '@storybook/react'; - -type Story = StoryObj; - -const Item = ({ children }: { children?: React.ReactNode }) => ( - - {children} - -); - -const defaultArgs = { - displaySections: [ - { title: 'Debian 11' }, - { details: '$36/month', title: 'Dedicated 4GB' }, - ], - heading: 'Checkout Summary', -}; - -const meta: Meta = { - component: CheckoutSummary, - decorators: [ - (Story: StoryFn) => ( -
- -
- ), - ], - title: 'Components/CheckoutSummary', -}; - -export default meta; - -export const Default: Story = { - args: defaultArgs, -}; - -export const WithAgreement: Story = { - args: { - ...defaultArgs, - agreement: Agreement item can go here!, - }, -}; - -export const WithChildren: Story = { - args: { - ...defaultArgs, - children: Child items can go here!, - }, -}; diff --git a/packages/manager/src/components/CheckoutSummary/CheckoutSummary.tsx b/packages/manager/src/components/CheckoutSummary/CheckoutSummary.tsx index a272a3536c5..ada8a14e6bb 100644 --- a/packages/manager/src/components/CheckoutSummary/CheckoutSummary.tsx +++ b/packages/manager/src/components/CheckoutSummary/CheckoutSummary.tsx @@ -1,6 +1,6 @@ import { useTheme } from '@mui/material'; -import { styled } from '@mui/material/styles'; import Grid2 from '@mui/material/Unstable_Grid2/Grid2'; +import { Theme, styled } from '@mui/material/styles'; import useMediaQuery from '@mui/material/useMediaQuery'; import * as React from 'react'; @@ -8,24 +8,10 @@ import { Paper } from '../Paper'; import { Typography } from '../Typography'; import { SummaryItem } from './SummaryItem'; -import type { Theme } from '@mui/material/styles'; - -interface CheckoutSummaryProps { - /** - * JSX element to be displayed as an agreement section. - */ +interface Props { agreement?: JSX.Element; - /** - * JSX element for additional content to be rendered within the component. - */ children?: JSX.Element | null; - /** - * The sections to be displayed in the `CheckoutSumamry` - */ displaySections: SummaryItem[]; - /** - * The heading text to be displayed in the `CheckoutSummary`. - */ heading: string; } @@ -36,7 +22,7 @@ export interface SummaryItem { title?: string; } -export const CheckoutSummary = (props: CheckoutSummaryProps) => { +export const CheckoutSummary = (props: Props) => { const theme = useTheme(); const matchesSmDown = useMediaQuery(theme.breakpoints.down('md')); diff --git a/packages/manager/src/components/ColorPicker/ColorPicker.stories.tsx b/packages/manager/src/components/ColorPicker/ColorPicker.stories.tsx deleted file mode 100644 index aba8758b071..00000000000 --- a/packages/manager/src/components/ColorPicker/ColorPicker.stories.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; - -import { ColorPicker } from 'src/components/ColorPicker/ColorPicker'; - -import type { ColorPickerProps } from './ColorPicker'; -import type { Meta, StoryObj } from '@storybook/react'; - -const meta: Meta = { - args: { - defaultColor: '#0174bc', - label: 'Label for color picker', - onChange: () => undefined, - }, - component: ColorPicker, - title: 'Components/ColorPicker', -}; - -export const Default: StoryObj = { - render: (args) => { - return ; - }, -}; - -export default meta; diff --git a/packages/manager/src/components/ColorPicker/ColorPicker.tsx b/packages/manager/src/components/ColorPicker/ColorPicker.tsx deleted file mode 100644 index 6b611c91ee7..00000000000 --- a/packages/manager/src/components/ColorPicker/ColorPicker.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { useTheme } from '@mui/material'; -import React, { useState } from 'react'; - -import type { CSSProperties } from 'react'; - -export interface ColorPickerProps { - /** - * Optional color to specify as a default - * */ - defaultColor?: string; - /** - * Optional styles for the input element - * */ - inputStyles?: CSSProperties; - /** - * Visually hidden label to semantically describe the color picker for accessibility - * */ - label: string; - /** - * Function to update the color based on user selection - * */ - onChange: (color: string) => void; -} - -/** - * The ColorPicker component serves as a wrapper for the native HTML input color picker. - */ -export const ColorPicker = (props: ColorPickerProps) => { - const { defaultColor, inputStyles, label, onChange } = props; - - const theme = useTheme(); - const [color, setColor] = useState( - defaultColor ?? theme.palette.primary.dark - ); - - return ( - <> - - { - setColor(e.target.value); - onChange(e.target.value); - }} - color={color} - id="color-picker" - style={inputStyles} - type="color" - value={color} - /> - - ); -}; diff --git a/packages/manager/src/components/CopyTooltip/CopyTooltip.tsx b/packages/manager/src/components/CopyTooltip/CopyTooltip.tsx index 1a244e89044..ed83f09279e 100644 --- a/packages/manager/src/components/CopyTooltip/CopyTooltip.tsx +++ b/packages/manager/src/components/CopyTooltip/CopyTooltip.tsx @@ -3,11 +3,9 @@ import copy from 'copy-to-clipboard'; import * as React from 'react'; import FileCopy from 'src/assets/icons/copy.svg'; -import { Tooltip } from 'src/components/Tooltip'; +import { Tooltip, TooltipProps } from 'src/components/Tooltip'; import { omittedProps } from 'src/utilities/omittedProps'; -import type { TooltipProps } from 'src/components/Tooltip'; - export interface CopyTooltipProps { /** * Additional classes to be applied to the root element. @@ -64,7 +62,7 @@ export const CopyTooltip = (props: CopyTooltipProps) => { }; const CopyButton = ( - { {...props} > {copyableText ? text : } - + ); if (disabled) { @@ -93,8 +91,8 @@ export const CopyTooltip = (props: CopyTooltipProps) => { ); }; -export const StyledIconButton = styled('button', { - label: 'StyledIconButton', +const StyledCopyButton = styled('button', { + label: 'StyledCopyButton', shouldForwardProp: omittedProps(['copyableText', 'text', 'onClickCallback']), })>(({ theme, ...props }) => ({ '& svg': { diff --git a/packages/manager/src/components/CopyableAndDownloadableTextField.tsx b/packages/manager/src/components/CopyableAndDownloadableTextField.tsx new file mode 100644 index 00000000000..17f89731429 --- /dev/null +++ b/packages/manager/src/components/CopyableAndDownloadableTextField.tsx @@ -0,0 +1,72 @@ +import { Theme } from '@mui/material/styles'; +import * as React from 'react'; +import { makeStyles } from 'tss-react/mui'; + +import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; +import { DownloadTooltip } from 'src/components/DownloadTooltip'; +import { TextField, TextFieldProps } from 'src/components/TextField'; + +const useStyles = makeStyles()((theme: Theme) => ({ + copyIcon: { + '& svg': { + height: 14, + top: 1, + }, + marginRight: theme.spacing(0.5), + }, + removeDisabledStyles: { + '&.Mui-disabled': { + background: theme.bg.main, + borderColor: theme.name === 'light' ? '#ccc' : '#222', + color: theme.name === 'light' ? 'inherit' : '#fff !important', + opacity: 1, + }, + }, +})); + +interface Props extends TextFieldProps { + className?: string; + fileName?: string; + hideIcon?: boolean; +} + +export const CopyableAndDownloadableTextField = (props: Props) => { + const { classes } = useStyles(); + const { className, hideIcon, value, ...restProps } = props; + + const fileName = props.fileName ?? snakeCase(props.label); + + return ( + + + + + ), + }} + className={`${className} ${classes.removeDisabledStyles}`} + data-qa-copy-tooltip + disabled + /> + ); +}; + +const snakeCase = (str: string | undefined): string => { + if (!str) { + return ''; + } + + return str + .replace(/\W+/g, ' ') + .split(/ |\B(?=[A-Z])/) + .map((word) => word.toLowerCase()) + .join('_'); +}; diff --git a/packages/manager/src/components/CopyableTextField/CopyableTextField.stories.tsx b/packages/manager/src/components/CopyableTextField/CopyableTextField.stories.tsx deleted file mode 100644 index 2e12633f855..00000000000 --- a/packages/manager/src/components/CopyableTextField/CopyableTextField.stories.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; - -import { CopyableTextField } from './CopyableTextField'; - -import type { Meta, StoryFn, StoryObj } from '@storybook/react'; - -const meta: Meta = { - args: { - label: 'Label', - value: 'Text to copy', - }, - component: CopyableTextField, - decorators: [ - (Story: StoryFn) => ( -
- -
- ), - ], - title: 'Components/Input/CopyableTextField', -}; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = { - render: (args) => , -}; diff --git a/packages/manager/src/components/CopyableTextField/CopyableTextField.tsx b/packages/manager/src/components/CopyableTextField/CopyableTextField.tsx index 1f2da7ffcd7..cb86b4b93b0 100644 --- a/packages/manager/src/components/CopyableTextField/CopyableTextField.tsx +++ b/packages/manager/src/components/CopyableTextField/CopyableTextField.tsx @@ -1,14 +1,11 @@ import { styled } from '@mui/material/styles'; import * as React from 'react'; -import { Box } from 'src/components/Box'; -import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; -import { TextField } from 'src/components/TextField'; - -import { DownloadTooltip } from '../DownloadTooltip'; - -import type { CopyTooltipProps } from 'src/components/CopyTooltip/CopyTooltip'; -import type { TextFieldProps } from 'src/components/TextField'; +import { + CopyTooltip, + CopyTooltipProps, +} from 'src/components/CopyTooltip/CopyTooltip'; +import { TextField, TextFieldProps } from 'src/components/TextField'; interface CopyableTextFieldProps extends TextFieldProps { /** @@ -16,34 +13,23 @@ interface CopyableTextFieldProps extends TextFieldProps { */ CopyTooltipProps?: Partial; className?: string; - hideIcons?: boolean; - showDownloadIcon?: boolean; + hideIcon?: boolean; } export const CopyableTextField = (props: CopyableTextFieldProps) => { - const { - CopyTooltipProps, - className, - hideIcons, - showDownloadIcon, - value, - ...restProps - } = props; - - const fileName = showDownloadIcon ? snakeCase(props.label) : ''; + const { CopyTooltipProps, className, hideIcon, value, ...restProps } = props; return ( - {showDownloadIcon && ( - - )} - - + endAdornment: hideIcon ? undefined : ( + ), }} className={`${className} copy removeDisabledStyles`} @@ -58,6 +44,13 @@ const StyledTextField = styled(TextField)(({ theme }) => ({ backgroundColor: theme.name === 'dark' ? '#2f3236' : '#f4f4f4', opacity: 1, }, + '.copyIcon': { + '& svg': { + height: 14, + top: 1, + }, + marginRight: theme.spacing(0.5), + }, '.removeDisabledStyles': { '& .MuiInput-input': { WebkitTextFillColor: 'unset !important', @@ -74,27 +67,3 @@ const StyledTextField = styled(TextField)(({ theme }) => ({ }, }, })); - -const StyledIconBox = styled(Box)(({ theme }) => ({ - '& button svg': { - color: theme.color.grey1, - height: 14, - top: 1, - transition: theme.transitions.create(['color']), - }, - '& button svg:hover': { - color: theme.palette.primary.main, - }, - '&:last-child': { - marginRight: theme.spacing(0.5), - }, - display: 'flex', -})); - -const snakeCase = (str: string): string => { - return str - .replace(/\W+/g, ' ') - .split(/ |\B(?=[A-Z])/) - .map((word) => word.toLowerCase()) - .join('_'); -}; diff --git a/packages/manager/src/components/Currency/Currency.test.tsx b/packages/manager/src/components/Currency/Currency.test.tsx index b5cfbd02fe0..0b46d8a8410 100644 --- a/packages/manager/src/components/Currency/Currency.test.tsx +++ b/packages/manager/src/components/Currency/Currency.test.tsx @@ -8,79 +8,69 @@ import { Currency } from './Currency'; describe('Currency Component', () => { it('displays a given quantity in USD', () => { const { getByText, rerender } = renderWithTheme(); - expect(getByText('$5.00')).toBeVisible(); + getByText('$5.00'); rerender(); - expect(getByText('$99.99')).toBeVisible(); + getByText('$99.99'); rerender(); - expect(getByText('$0.00')).toBeVisible(); + getByText('$0.00'); }); it('handles negative quantities', () => { const { getByText, rerender } = renderWithTheme(); - expect(getByText('-$5.00')).toBeVisible(); + getByText('-$5.00'); rerender(); - expect(getByText('-$99.99')).toBeVisible(); + getByText('-$99.99'); rerender(); - expect(getByText('-$0.01')).toBeVisible(); + getByText('-$0.01'); }); it('wraps in parentheses', () => { const { getByText, rerender } = renderWithTheme( ); - expect(getByText('($5.00)')).toBeVisible(); + getByText('($5.00)'); rerender(); - expect(getByText('-($5.00)')).toBeVisible(); + getByText('-($5.00)'); rerender(); - expect(getByText('($0.00)')).toBeVisible(); + getByText('($0.00)'); }); it('handles custom number of decimal places', () => { const { getByText, rerender } = renderWithTheme( ); - expect(getByText('$5.000')).toBeVisible(); + getByText('$5.000'); rerender(); - expect(getByText('$99.999')).toBeVisible(); + getByText('$99.999'); rerender(); - expect(getByText('-$5.000')).toBeVisible(); + getByText('-$5.000'); }); it('handles custom default values', () => { const { getByText } = renderWithTheme( ); - expect(getByText(`$${UNKNOWN_PRICE}`)).toBeVisible(); + getByText(`$${UNKNOWN_PRICE}`); }); it('groups by comma', () => { const { getByText, rerender } = renderWithTheme( ); - expect(getByText('$1,000.00')).toBeVisible(); + getByText('$1,000.00'); rerender(); - expect(getByText('$100,000.00')).toBeVisible(); + getByText('$100,000.00'); }); it('displays --.-- when passed in as a quantity', () => { const { getByText } = renderWithTheme(); - expect(getByText('$--.--')).toBeVisible(); + getByText('$--.--'); }); it('applies the passed in data attributes', () => { const { getByTestId } = renderWithTheme( ); - expect(getByTestId('currency-test')).toBeInTheDocument(); - }); - - it('should display price with default 2 decimal places if decimalPlaces is negative or undefined', () => { - const { getByText, rerender } = renderWithTheme( - - ); - expect(getByText('$99.00')).toBeVisible(); - - rerender(); - expect(getByText('$99.00')).toBeVisible(); + getByTestId('currency-test'); }); }); diff --git a/packages/manager/src/components/Currency/Currency.tsx b/packages/manager/src/components/Currency/Currency.tsx index 5bf4cf71460..f1ad0259632 100644 --- a/packages/manager/src/components/Currency/Currency.tsx +++ b/packages/manager/src/components/Currency/Currency.tsx @@ -22,15 +22,11 @@ interface CurrencyFormatterProps { } export const Currency = (props: CurrencyFormatterProps) => { - const { dataAttrs, decimalPlaces, quantity, wrapInParentheses } = props; - - // Use the default value (2) when decimalPlaces is negative or undefined. - const minimumFractionDigits = - decimalPlaces !== undefined && decimalPlaces >= 0 ? decimalPlaces : 2; + const { dataAttrs, quantity, wrapInParentheses } = props; const formatter = new Intl.NumberFormat('en-US', { currency: 'USD', - minimumFractionDigits, + minimumFractionDigits: props.decimalPlaces ?? 2, style: 'currency', }); diff --git a/packages/manager/src/components/DisplayPrice/DisplayPrice.stories.tsx b/packages/manager/src/components/DisplayPrice/DisplayPrice.stories.tsx deleted file mode 100644 index 1c3aebab2b3..00000000000 --- a/packages/manager/src/components/DisplayPrice/DisplayPrice.stories.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import * as React from 'react'; - -import { DisplayPrice } from './DisplayPrice'; - -import type { Meta, StoryFn, StoryObj } from '@storybook/react'; - -type Story = StoryObj; - -const meta: Meta = { - component: DisplayPrice, - decorators: [ - (Story: StoryFn) => ( -
- -
- ), - ], - title: 'Components/DisplayPrice', -}; - -export default meta; - -export const Default: Story = { - args: { - price: 99, - }, - render: (args) => , -}; - -export const WithFontSize: Story = { - args: { - ...Default.args, - fontSize: '2rem', - }, -}; - -export const WithDecimalPlaces: Story = { - args: { - ...Default.args, - decimalPlaces: 1, - }, -}; - -export const WithInterval: Story = { - args: { - ...Default.args, - interval: 'mo', - }, -}; diff --git a/packages/manager/src/components/DisplayPrice/DisplayPrice.tsx b/packages/manager/src/components/DisplayPrice/DisplayPrice.tsx index 134d880c231..07bacae6467 100644 --- a/packages/manager/src/components/DisplayPrice/DisplayPrice.tsx +++ b/packages/manager/src/components/DisplayPrice/DisplayPrice.tsx @@ -5,24 +5,9 @@ import { Currency } from 'src/components/Currency'; import { Typography } from 'src/components/Typography'; export interface DisplayPriceProps { - /** - * The number of decimal places to display in the price. - */ decimalPlaces?: number; - /** - * The font size of the displayed price. - */ fontSize?: string; - /** - * The format interval to use for price formatting. - * @example 'mo' - * @example 'month' - * @example 'year' - */ interval?: string; - /** - * The price to display. - */ price: '--.--' | number; } diff --git a/packages/manager/src/components/DownloadTooltip.tsx b/packages/manager/src/components/DownloadTooltip.tsx index ec1a8abddf1..1b8a543f3ed 100644 --- a/packages/manager/src/components/DownloadTooltip.tsx +++ b/packages/manager/src/components/DownloadTooltip.tsx @@ -1,7 +1,8 @@ +import { Theme } from '@mui/material/styles'; +import { makeStyles } from 'tss-react/mui'; import * as React from 'react'; import FileDownload from 'src/assets/icons/download.svg'; -import { StyledIconButton } from 'src/components/CopyTooltip/CopyTooltip'; import { Tooltip } from 'src/components/Tooltip'; import { Typography } from 'src/components/Typography'; import { downloadFile } from 'src/utilities/downloadFile'; @@ -30,7 +31,41 @@ interface Props { text: string; } +const useStyles = makeStyles()((theme: Theme) => ({ + displayText: { + color: theme.textColors.linkActiveLight, + marginLeft: 6, + }, + flex: { + display: 'flex', + width: 'auto !important', + }, + root: { + '& svg': { + color: theme.color.grey1, + height: 20, + margin: 0, + position: 'relative', + transition: theme.transitions.create(['color']), + width: 20, + }, + '&:hover': { + backgroundColor: theme.color.white, + }, + backgroundColor: 'transparent', + border: 'none', + borderRadius: 4, + color: theme.color.grey1, + cursor: 'pointer', + padding: 4, + position: 'relative', + transition: theme.transitions.create(['background-color']), + }, +})); + export const DownloadTooltip = (props: Props) => { + const { classes, cx } = useStyles(); + const { className, displayText, fileName, onClickCallback, text } = props; const handleIconClick = () => { @@ -42,16 +77,24 @@ export const DownloadTooltip = (props: Props) => { return ( - - {displayText && {displayText}} - + {displayText && ( + {displayText} + )} + ); }; diff --git a/packages/manager/src/components/Encryption/constants.tsx b/packages/manager/src/components/Encryption/constants.tsx index 4f44f4fd100..0b49c80e40d 100644 --- a/packages/manager/src/components/Encryption/constants.tsx +++ b/packages/manager/src/components/Encryption/constants.tsx @@ -103,7 +103,7 @@ export const BLOCK_STORAGE_USER_SIDE_ENCRYPTION_CAVEAT = 'User-side encryption on top of encryption-enabled volumes is discouraged at this time, as it could severely impact your volume performance.'; export const BLOCK_STORAGE_ENCRYPTION_SETTING_IMMUTABLE_COPY = - 'The encryption setting cannot be modified after a volume has been created.'; + 'The encryption setting cannot be changed after creation.'; export const BLOCK_STORAGE_CLONING_INHERITANCE_CAVEAT = 'Encryption is inherited from the source volume and cannot be changed when cloning volumes.'; diff --git a/packages/manager/src/components/GravatarByEmail.tsx b/packages/manager/src/components/GravatarByEmail.tsx index 4ff64876d93..ea345f14cc2 100644 --- a/packages/manager/src/components/GravatarByEmail.tsx +++ b/packages/manager/src/components/GravatarByEmail.tsx @@ -6,14 +6,14 @@ import { getGravatarUrl } from 'src/utilities/gravatar'; export const DEFAULT_AVATAR_SIZE = 28; -export interface GravatarByEmailProps { +interface Props { className?: string; email: string; height?: number; width?: number; } -export const GravatarByEmail = (props: GravatarByEmailProps) => { +export const GravatarByEmail = (props: Props) => { const { className, email, diff --git a/packages/manager/src/components/GravatarByUsername.tsx b/packages/manager/src/components/GravatarByUsername.tsx index 3adb2262aa3..509a78eabc4 100644 --- a/packages/manager/src/components/GravatarByUsername.tsx +++ b/packages/manager/src/components/GravatarByUsername.tsx @@ -5,24 +5,18 @@ import UserIcon from 'src/assets/icons/account.svg'; import { useAccountUser } from 'src/queries/account/users'; import { getGravatarUrl } from 'src/utilities/gravatar'; -import { Box } from './Box'; import { DEFAULT_AVATAR_SIZE } from './GravatarByEmail'; -export interface GravatarByUsernameProps { +interface Props { className?: string; username: null | string; } -export const GravatarByUsername = (props: GravatarByUsernameProps) => { +export const GravatarByUsername = (props: Props) => { const { className, username } = props; - const { data: user, isLoading } = useAccountUser(username ?? ''); + const { data: user } = useAccountUser(username ?? ''); const url = user?.email ? getGravatarUrl(user.email) : undefined; - // Render placeholder instead of flashing default user icon briefly - if (isLoading) { - return ; - } - return ( { +export const GravatarForProxy = ({ height = 34, width = 34 }: Props) => { return ( ({ diff --git a/packages/manager/src/components/GravatarOrAvatar.tsx b/packages/manager/src/components/GravatarOrAvatar.tsx deleted file mode 100644 index 6a9ab209775..00000000000 --- a/packages/manager/src/components/GravatarOrAvatar.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; - -import { useGravatar } from 'src/hooks/useGravatar'; -import { useProfile } from 'src/queries/profile/profile'; - -import { DEFAULT_AVATAR_SIZE } from './Avatar/Avatar'; -import { Box } from './Box'; - -interface Props { - avatar: JSX.Element; - gravatar: JSX.Element; - height?: number; - width?: number; -} - -export const GravatarOrAvatar = (props: Props) => { - const { - avatar, - gravatar, - height = DEFAULT_AVATAR_SIZE, - width = DEFAULT_AVATAR_SIZE, - } = props; - const { data: profile } = useProfile(); - const { hasGravatar, isLoadingGravatar } = useGravatar(profile?.email); - - return isLoadingGravatar ? ( - - ) : hasGravatar ? ( - gravatar - ) : ( - avatar - ); -}; diff --git a/packages/manager/src/components/ImageSelectv2/ImageOptionv2.test.tsx b/packages/manager/src/components/ImageSelectv2/ImageOptionv2.test.tsx index c5e0726ddc4..757f966d63c 100644 --- a/packages/manager/src/components/ImageSelectv2/ImageOptionv2.test.tsx +++ b/packages/manager/src/components/ImageSelectv2/ImageOptionv2.test.tsx @@ -7,7 +7,7 @@ import { ImageOptionv2 } from './ImageOptionv2'; describe('ImageOptionv2', () => { it('renders the image label', () => { - const image = imageFactory.build({ eol: null }); + const image = imageFactory.build(); const { getByText } = renderWithTheme( @@ -49,27 +49,4 @@ describe('ImageOptionv2', () => { ) ).toBeVisible(); }); - - it('renders (deprecated) if the image is deprecated', () => { - const image = imageFactory.build({ deprecated: true }); - - const { getByText } = renderWithTheme( - - ); - - expect(getByText(`${image.label} (deprecated)`)).toBeVisible(); - }); - - it('renders (deprecated) if the image is past its end-of-life', () => { - const image = imageFactory.build({ - deprecated: false, - eol: '2015-01-01T00:00:00', - }); - - const { getByText } = renderWithTheme( - - ); - - expect(getByText(`${image.label} (deprecated)`)).toBeVisible(); - }); }); diff --git a/packages/manager/src/components/ImageSelectv2/ImageOptionv2.tsx b/packages/manager/src/components/ImageSelectv2/ImageOptionv2.tsx index f383832e6e2..4fb9f21ddf9 100644 --- a/packages/manager/src/components/ImageSelectv2/ImageOptionv2.tsx +++ b/packages/manager/src/components/ImageSelectv2/ImageOptionv2.tsx @@ -9,7 +9,6 @@ import { OSIcon } from '../OSIcon'; import { Stack } from '../Stack'; import { Tooltip } from '../Tooltip'; import { Typography } from '../Typography'; -import { isImageDeprecated } from './utilities'; import type { Image } from '@linode/api-v4'; @@ -33,9 +32,7 @@ export const ImageOptionv2 = ({ image, isSelected, listItemProps }: Props) => { > - - {image.label} {isImageDeprecated(image) && '(deprecated)'} - + {image.label} {image.capabilities.includes('distributed-sites') && ( diff --git a/packages/manager/src/components/ImageSelectv2/ImageSelectv2.test.tsx b/packages/manager/src/components/ImageSelectv2/ImageSelectv2.test.tsx index 31d6a76d46a..7555cb30123 100644 --- a/packages/manager/src/components/ImageSelectv2/ImageSelectv2.test.tsx +++ b/packages/manager/src/components/ImageSelectv2/ImageSelectv2.test.tsx @@ -1,5 +1,4 @@ import userEvent from '@testing-library/user-event'; -import { Settings } from 'luxon'; import React from 'react'; import { imageFactory } from 'src/factories'; @@ -25,7 +24,7 @@ describe('ImageSelectv2', () => { }); it('should render items returned by the API', async () => { - const images = imageFactory.buildList(5, { eol: null }); + const images = imageFactory.buildList(5); server.use( http.get('*/v4/images', () => { @@ -45,7 +44,7 @@ describe('ImageSelectv2', () => { }); it('should call onChange when a value is selected', async () => { - const image = imageFactory.build({ eol: null }); + const image = imageFactory.build(); const onChange = vi.fn(); server.use( @@ -100,45 +99,4 @@ describe('ImageSelectv2', () => { await findByTestId('os-icon'); }); - - it('does not render images that are more than 6 months past their eol', async () => { - // Mock the current date - Settings.now = () => new Date(2018, 1, 1).valueOf(); - - const images = [ - imageFactory.build({ - eol: '2018-04-01T00:00:00', // should show because this image is not EOL yet - label: 'linode/image-1', - }), - imageFactory.build({ - eol: '2017-01-01T00:00:00', // should not show because it is > 6 months past this EOL - label: 'linode/image-2', - }), - imageFactory.build({ - eol: null, // should show because this images does not have an EOL - label: 'linode/image-3', - }), - imageFactory.build({ - eol: '2017-11-01T00:00:00', // should show as deprecated because it is < 6 months past this EOL - label: 'linode/image-4', - }), - ]; - - server.use( - http.get('*/v4/images', () => { - return HttpResponse.json(makeResourcePage(images)); - }) - ); - - const { getByPlaceholderText, getByText, queryByText } = renderWithTheme( - - ); - - await userEvent.click(getByPlaceholderText('Choose an image')); - - expect(getByText('linode/image-1')).toBeVisible(); - expect(queryByText('linode/image-2')).toBeNull(); - expect(getByText('linode/image-3')).toBeVisible(); - expect(getByText('linode/image-4 (deprecated)')).toBeVisible(); - }); }); diff --git a/packages/manager/src/components/ImageSelectv2/utilities.test.ts b/packages/manager/src/components/ImageSelectv2/utilities.test.ts deleted file mode 100644 index 353a86944b8..00000000000 --- a/packages/manager/src/components/ImageSelectv2/utilities.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Settings } from 'luxon'; - -import { imageFactory } from 'src/factories'; - -import { isImageDeprecated, isImageTooFarPastEOL } from './utilities'; - -describe('isImageTooFarPastEOL', () => { - it('should return false if the image does not have an `eol`', () => { - const image = imageFactory.build({ eol: null }); - - expect(isImageTooFarPastEOL(image)).toBe(false); - }); - - it("should return true if it is more than 6 months past the Image's `eol`", () => { - // Mock the current date - Settings.now = () => new Date(2018, 1, 1).valueOf(); - - expect( - isImageTooFarPastEOL(imageFactory.build({ eol: '2015-01-01T00:00:00' })) - ).toBe(true); - - expect( - isImageTooFarPastEOL(imageFactory.build({ eol: '2017-07-01T00:00:00' })) - ).toBe(true); - }); - - it("should return false if it is not 6 months past the image's `eol`", () => { - // Mock the current date - Settings.now = () => new Date(2018, 1, 1).valueOf(); - - expect( - isImageTooFarPastEOL(imageFactory.build({ eol: '2018-04-01T00:00:00' })) - ).toBe(false); - - expect( - isImageTooFarPastEOL(imageFactory.build({ eol: '2018-08-01T00:00:00' })) - ).toBe(false); - - expect( - isImageTooFarPastEOL(imageFactory.build({ eol: '2032-01-01T00:00:00' })) - ).toBe(false); - }); -}); - -describe('isImageDeprecated', () => { - it('should return true image is `deprecated` according to the API', () => { - const image = imageFactory.build({ deprecated: true, eol: null }); - - expect(isImageDeprecated(image)).toBe(true); - }); - - it('should return false image is not `deprecated` and the image does not have an `eol`', () => { - const image = imageFactory.build({ deprecated: false, eol: null }); - - expect(isImageDeprecated(image)).toBe(false); - }); - - it("should return true if the current date is after the image's `eol` (the image is past its EOL)", () => { - // Mock the current date - Settings.now = () => new Date(2018, 5, 1).valueOf(); - - const image = imageFactory.build({ - deprecated: false, - eol: '2018-01-01T00:00:00', - }); - - expect(isImageDeprecated(image)).toBe(true); - }); - - it("should return false if the current date is before the image's `eol` (the image is not past its EOL)", () => { - // Mock the current date - Settings.now = () => new Date(2018, 1, 1).valueOf(); - - // Image with an end-of-life that is in the future compared to the mocked current date - const image = imageFactory.build({ - deprecated: false, - eol: '2019-05-02T00:00:00', - }); - - expect(isImageDeprecated(image)).toBe(false); - }); -}); diff --git a/packages/manager/src/components/ImageSelectv2/utilities.ts b/packages/manager/src/components/ImageSelectv2/utilities.ts index 7df5652a0b1..fadd4bf42cf 100644 --- a/packages/manager/src/components/ImageSelectv2/utilities.ts +++ b/packages/manager/src/components/ImageSelectv2/utilities.ts @@ -1,8 +1,5 @@ -import { DateTime } from 'luxon'; +import { ImageSelectVariant } from './ImageSelectv2'; -import { MAX_MONTHS_EOL_FILTER } from 'src/constants'; - -import type { ImageSelectVariant } from './ImageSelectv2'; import type { Image } from '@linode/api-v4'; /** @@ -24,11 +21,11 @@ export const getAPIFilterForImageSelect = ( }; /** - * Unfortunately, we can't use API filtering for all of our filtering needs. + * Using API filter, I can't think of a way to filter out + * LKE images that we don't want customers to see. * - * This function exists to... - * - filter out public Kubernetes images for the Images Select - * - filter out images that are too far past their end-of-life date + * This function exists to filter out public Kubernetes images + * for the Images Select. * * Please use API filtering (getAPIFilterForImageSelect) when possible! */ @@ -36,51 +33,7 @@ export const getFilteredImagesForImageSelect = ( images: Image[] | undefined, variant: ImageSelectVariant | undefined ) => { - if (variant === 'public') { - // For public images, we filter out LKE images and images that are > 6 months past their `eol` - return images?.filter( - (image) => !image.id.includes('kube') && !isImageTooFarPastEOL(image) - ); - } - - return images; -}; - -/** - * Returns whether or not an image is too far past its end-of-life based on MAX_MONTHS_EOL_FILTER - * - * This function is intended to be used to filter out end-of-life images based on - * an internal policy. See `M3-5753` for context. - * - * @param image an image from the API - * @returns true if the given image is more than 6 months past its end-of-life - */ -export const isImageTooFarPastEOL = (image: Image) => { - if (image.eol === null) { - return false; - } - - const imageEOL = DateTime.fromISO(image.eol); - const now = DateTime.now(); - - const differenceInMonths = now.diff(imageEOL, 'months').months; - - return differenceInMonths > MAX_MONTHS_EOL_FILTER; -}; - -/** - * Returns whether or not an image is deprecated - * - * Unfortunately, as per `M3-5753`, we can't just simply check `deprecated` on the `Image`, - * we must also consider the EOL date. - */ -export const isImageDeprecated = (image: Image) => { - if (image.eol === null) { - // If the image does not have an EOL, just use the `deprecated` field returned by the API - return image.deprecated; - } - - const isImageEOL = DateTime.fromISO(image.eol) < DateTime.now(); - - return image.deprecated || isImageEOL; + return variant === 'private' + ? images + : images?.filter((image) => !image.id.includes('kube')); }; diff --git a/packages/manager/src/dev-tools/FeatureFlagTool.tsx b/packages/manager/src/dev-tools/FeatureFlagTool.tsx index 5ee12154822..920a91ff689 100644 --- a/packages/manager/src/dev-tools/FeatureFlagTool.tsx +++ b/packages/manager/src/dev-tools/FeatureFlagTool.tsx @@ -24,6 +24,7 @@ const options: { flag: keyof Flags; label: string }[] = [ { flag: 'disableLargestGbPlans', label: 'Disable Largest GB Plans' }, { flag: 'gecko2', label: 'Gecko' }, { flag: 'imageServiceGen2', label: 'Image Service Gen2' }, + { flag: 'linodeCreateRefactor', label: 'Linode Create v2' }, { flag: 'linodeDiskEncryption', label: 'Linode Disk Encryption (LDE)' }, { flag: 'objMultiCluster', label: 'OBJ Multi-Cluster' }, { flag: 'objectStorageGen2', label: 'OBJ Gen2' }, diff --git a/packages/manager/src/featureFlags.ts b/packages/manager/src/featureFlags.ts index 09a3c167af7..35461f29ed5 100644 --- a/packages/manager/src/featureFlags.ts +++ b/packages/manager/src/featureFlags.ts @@ -99,6 +99,7 @@ export interface Flags { gpuv2: gpuV2; imageServiceGen2: boolean; ipv6Sharing: boolean; + linodeCreateRefactor: boolean; linodeCreateWithFirewall: boolean; linodeDiskEncryption: boolean; mainContentBanner: MainContentBanner; diff --git a/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/ContactInformation.tsx b/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/ContactInformation.tsx index e0b1aeb6cfa..3364e13861a 100644 --- a/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/ContactInformation.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/ContactInformation.tsx @@ -9,7 +9,6 @@ import { TooltipIcon } from 'src/components/TooltipIcon'; import { Typography } from 'src/components/Typography'; import { getRestrictedResourceText } from 'src/features/Account/utils'; import { EDIT_BILLING_CONTACT } from 'src/features/Billing/constants'; -import { StyledAutorenewIcon } from 'src/features/TopMenu/NotificationMenu/NotificationMenu'; import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; import { useNotificationsQuery } from 'src/queries/account/notifications'; @@ -240,8 +239,14 @@ const ContactInformation = (props: Props) => { {invalidTaxIdNotification && ( } - status="other" + sxTooltipIcon={{ + '& > svg': { + fontSize: '18px', + }, + paddingBottom: 0, + paddingTop: 0, + }} + status="warning" text={invalidTaxIdNotification.label} /> )} diff --git a/packages/manager/src/features/EntityTransfers/EntityTransfersLanding/CreateTransferSuccessDialog.tsx b/packages/manager/src/features/EntityTransfers/EntityTransfersLanding/CreateTransferSuccessDialog.tsx index 76ff9ae2422..e6b0ebb1156 100644 --- a/packages/manager/src/features/EntityTransfers/EntityTransfersLanding/CreateTransferSuccessDialog.tsx +++ b/packages/manager/src/features/EntityTransfers/EntityTransfersLanding/CreateTransferSuccessDialog.tsx @@ -92,7 +92,7 @@ This token will expire ${parseAPIDate(transfer.expiry).toLocaleString( @@ -115,7 +115,7 @@ This token will expire ${parseAPIDate(transfer.expiry).toLocaleString( { +describe('EventMessage', () => { it('renders null when message is null', () => { - const { queryByRole } = renderWithTheme( - - ); + const { queryByRole } = renderWithTheme(); expect(queryByRole('pre')).toBeNull(); }); it('renders message without ticks as plain text', () => { const { getByText, queryByRole } = renderWithTheme( - + ); expect(getByText('Hello, world!')).toBeInTheDocument(); @@ -24,22 +22,10 @@ describe('FormattedEventMessage', () => { it('renders message with ticks as inline code blocks', () => { const { container, getByText } = renderWithTheme( - + ); expect(getByText(/Hello,/)).toBeInTheDocument(); expect(container.querySelector('pre')).toHaveTextContent('world'); }); - - it('converts contact support links', () => { - const { container, getByText } = renderWithTheme( - - ); - - expect(getByText('contact Support')).toBeInTheDocument(); - expect(container.querySelector('a')).toHaveAttribute( - 'href', - '/support/tickets' - ); - }); }); diff --git a/packages/manager/src/features/Events/EventMessage.tsx b/packages/manager/src/features/Events/EventMessage.tsx new file mode 100644 index 00000000000..d555fb04eb1 --- /dev/null +++ b/packages/manager/src/features/Events/EventMessage.tsx @@ -0,0 +1,45 @@ +import { styled } from '@mui/material/styles'; +import * as React from 'react'; + +interface MessageLinkEntity { + message: null | string; +} + +/** + * Renders a message with inline code blocks. + * Meant to be used in the context of an event message. + * This component is only used to render {e.message} in the case of potential ticks we want to render as
.
+ */
+export const EventMessage = (props: MessageLinkEntity) => {
+  const { message } = props;
+
+  if (!message) {
+    return null;
+  }
+
+  return formatTicks(message);
+};
+
+const formatTicks = (message: string): JSX.Element => {
+  const parts = message.split(/(`[^`]*`)/g);
+
+  return (
+    <>
+      {parts.map((part, i) =>
+        part.startsWith('`') && part.endsWith('`') ? (
+          {part.slice(1, -1)}
+        ) : (
+          part
+        )
+      )}
+    
+  );
+};
+
+const StyledPre = styled('pre')(({ theme }) => ({
+  backgroundColor: theme.name === 'dark' ? '#222' : '#f4f4f4',
+  borderRadius: 4,
+  display: 'inline',
+  fontSize: '0.75rem',
+  padding: '0.15rem 0.25rem',
+}));
diff --git a/packages/manager/src/features/Events/EventRow.styles.ts b/packages/manager/src/features/Events/EventRow.styles.ts
index cf2743a97ed..08624554348 100644
--- a/packages/manager/src/features/Events/EventRow.styles.ts
+++ b/packages/manager/src/features/Events/EventRow.styles.ts
@@ -1,14 +1,10 @@
-// @TODO: delete file once Gravatar is sunset
 import { styled } from '@mui/material/styles';
 
-import { fadeIn } from 'src/styles/keyframes';
-
 import { GravatarByUsername } from '../../components/GravatarByUsername';
 
 export const StyledGravatar = styled(GravatarByUsername, {
   label: 'StyledGravatar',
 })(({ theme }) => ({
-  animation: `${fadeIn} .2s ease-in-out forwards`,
   height: theme.spacing(3),
   width: theme.spacing(3),
 }));
diff --git a/packages/manager/src/features/Events/EventRow.test.tsx b/packages/manager/src/features/Events/EventRow.test.tsx
index 311b12b1a74..825e4d7c09f 100644
--- a/packages/manager/src/features/Events/EventRow.test.tsx
+++ b/packages/manager/src/features/Events/EventRow.test.tsx
@@ -29,6 +29,6 @@ describe('EventRow', () => {
         name: /Two-factor authentication has been enabled./i,
       })
     ).toBeInTheDocument();
-    expect(getByRole('cell', { name: /test_user/i })).toBeInTheDocument();
+    expect(getByRole('cell', { name: 'test_user' })).toBeInTheDocument();
   });
 });
diff --git a/packages/manager/src/features/Events/EventRow.tsx b/packages/manager/src/features/Events/EventRow.tsx
index 40acfd1a063..9fab05311cb 100644
--- a/packages/manager/src/features/Events/EventRow.tsx
+++ b/packages/manager/src/features/Events/EventRow.tsx
@@ -1,15 +1,11 @@
-import { useTheme } from '@mui/material';
 import * as React from 'react';
 
-import { Avatar } from 'src/components/Avatar/Avatar';
 import { BarPercent } from 'src/components/BarPercent';
 import { Box } from 'src/components/Box';
 import { DateTimeDisplay } from 'src/components/DateTimeDisplay';
-import { GravatarOrAvatar } from 'src/components/GravatarOrAvatar';
 import { Hidden } from 'src/components/Hidden';
 import { TableCell } from 'src/components/TableCell';
 import { TableRow } from 'src/components/TableRow';
-import { useProfile } from 'src/queries/profile/profile';
 import { getEventTimestamp } from 'src/utilities/eventUtils';
 
 import { StyledGravatar } from './EventRow.styles';
@@ -28,14 +24,12 @@ interface EventRowProps {
 
 export const EventRow = (props: EventRowProps) => {
   const { event } = props;
-  const theme = useTheme();
   const timestamp = getEventTimestamp(event);
   const { action, message, username } = {
     action: event.action,
     message: getEventMessage(event),
     username: getEventUsername(event),
   };
-  const { data: profile } = useProfile();
 
   if (!message) {
     return null;
@@ -60,27 +54,7 @@ export const EventRow = (props: EventRowProps) => {
       
         
           
-            
-              }
-              gravatar={
-                
-              }
-              height={24}
-              width={24}
-            />
+            
             {username}
           
         
diff --git a/packages/manager/src/features/Events/EventsMessages.stories.tsx b/packages/manager/src/features/Events/EventsMessages.stories.tsx
index ae39354f47c..35ad2beb4cf 100644
--- a/packages/manager/src/features/Events/EventsMessages.stories.tsx
+++ b/packages/manager/src/features/Events/EventsMessages.stories.tsx
@@ -22,7 +22,7 @@ const event: Event = eventFactory.build({
     type: 'linode',
     url: 'https://google.com',
   },
-  message: 'message with a `ticked` word - please contact Support',
+  message: 'message with a `ticked` word',
   secondary_entity: {
     id: 1,
     label: '{secondary entity}',
diff --git a/packages/manager/src/features/Events/FormattedEventMessage.tsx b/packages/manager/src/features/Events/FormattedEventMessage.tsx
deleted file mode 100644
index 6a0677a7d75..00000000000
--- a/packages/manager/src/features/Events/FormattedEventMessage.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import { styled } from '@mui/material/styles';
-import * as React from 'react';
-
-import { SupportLink } from 'src/components/SupportLink';
-
-interface MessageLinkEntity {
-  message: null | string;
-}
-
-/**
- * Renders a message with inline code blocks.
- * Meant to be used in the context of an event message.
- * This component is only used to render {e.message} as JSX in order to:
- *  - render ticks as 
.
- *  - render "contact support" strings as .
- */
-export const FormattedEventMessage = (props: MessageLinkEntity) => {
-  const { message } = props;
-
-  if (!message) {
-    return null;
-  }
-
-  return formatMessage(message);
-};
-
-const formatMessage = (message: string): JSX.Element => {
-  const parts = message.split(/(`[^`]*`)/g);
-  const supportLinkMatch = /(contact support)/i;
-
-  return (
-    <>
-      {parts.map((part, i) => {
-        let formattedPart: JSX.Element | string = part;
-
-        if (part.startsWith('`') && part.endsWith('`')) {
-          formattedPart = (
-            {part.slice(1, -1)}
-          );
-        }
-
-        if (part.match(supportLinkMatch)) {
-          const [before, linkText, after] = part.split(supportLinkMatch);
-
-          formattedPart = (
-            
-              {before}
-              
-              {after}
-            
-          );
-        }
-
-        return formattedPart;
-      })}
-    
-  );
-};
-
-const StyledPre = styled('pre')(({ theme }) => ({
-  backgroundColor: theme.name === 'dark' ? '#222' : '#f4f4f4',
-  borderRadius: 4,
-  display: 'inline',
-  fontSize: '0.75rem',
-  padding: '0.15rem 0.25rem',
-}));
diff --git a/packages/manager/src/features/Events/factories/domain.tsx b/packages/manager/src/features/Events/factories/domain.tsx
index c29d2e95c27..c6b02034ca3 100644
--- a/packages/manager/src/features/Events/factories/domain.tsx
+++ b/packages/manager/src/features/Events/factories/domain.tsx
@@ -1,7 +1,7 @@
 import * as React from 'react';
 
 import { EventLink } from '../EventLink';
-import { FormattedEventMessage } from '../FormattedEventMessage';
+import { EventMessage } from '../EventMessage';
 
 import type { PartialEventMap } from '../types';
 
@@ -32,8 +32,8 @@ export const domain: PartialEventMap<'domain'> = {
   domain_record_create: {
     notification: (e) => (
       <>
-         has been{' '}
-        added to .
+         has been added to{' '}
+        .
       
     ),
   },
@@ -48,16 +48,16 @@ export const domain: PartialEventMap<'domain'> = {
   domain_record_update: {
     notification: (e) => (
       <>
-         has been{' '}
-        updated for .
+         has been updated{' '}
+        for .
       
     ),
   },
   domain_record_updated: {
     notification: (e) => (
       <>
-         has been{' '}
-        updated for .
+         has been updated{' '}
+        for .
       
     ),
   },
diff --git a/packages/manager/src/features/Events/factories/image.tsx b/packages/manager/src/features/Events/factories/image.tsx
index 0cc07e47362..a9912b6ab87 100644
--- a/packages/manager/src/features/Events/factories/image.tsx
+++ b/packages/manager/src/features/Events/factories/image.tsx
@@ -1,7 +1,6 @@
 import * as React from 'react';
 
 import { EventLink } from '../EventLink';
-import { FormattedEventMessage } from '../FormattedEventMessage';
 
 import type { PartialEventMap } from '../types';
 
@@ -45,17 +44,12 @@ export const image: PartialEventMap<'image'> = {
     ),
   },
   image_upload: {
-    failed: (e) => {
-      const message = e?.message?.replace(/(\d+)/g, '$1 MB') || '';
-
-      return (
-        <>
-          Image  could not{' '}
-          be uploaded:{' '}
-          .
-        
-      );
-    },
+    failed: (e) => (
+      <>
+        Image  could not be{' '}
+        uploaded: {e?.message?.replace(/(\d+)/g, '$1 MB')}.
+      
+    ),
 
     finished: (e) => (
       <>
diff --git a/packages/manager/src/features/GlobalNotifications/GlobalNotifications.tsx b/packages/manager/src/features/GlobalNotifications/GlobalNotifications.tsx
index 8654d0858ad..2d600740b1b 100644
--- a/packages/manager/src/features/GlobalNotifications/GlobalNotifications.tsx
+++ b/packages/manager/src/features/GlobalNotifications/GlobalNotifications.tsx
@@ -15,7 +15,6 @@ import { APIMaintenanceBanner } from './APIMaintenanceBanner';
 import { ComplianceBanner } from './ComplianceBanner';
 import { ComplianceUpdateModal } from './ComplianceUpdateModal';
 import { EmailBounceNotificationSection } from './EmailBounce';
-import { GravatarSunsetBanner } from './GravatarSunsetBanner';
 import { RegionStatusBanner } from './RegionStatusBanner';
 import { TaxCollectionBanner } from './TaxCollectionBanner';
 import { DesignUpdateBanner } from './TokensUpdateBanner';
@@ -87,7 +86,6 @@ export const GlobalNotifications = () => {
       Object.keys(flags.taxCollectionBanner).length > 0 ? (
         
       ) : null}
-      
     
   );
 };
diff --git a/packages/manager/src/features/GlobalNotifications/GravatarSunsetBanner.tsx b/packages/manager/src/features/GlobalNotifications/GravatarSunsetBanner.tsx
deleted file mode 100644
index efbc1d2ef71..00000000000
--- a/packages/manager/src/features/GlobalNotifications/GravatarSunsetBanner.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import React from 'react';
-
-import { DismissibleBanner } from 'src/components/DismissibleBanner/DismissibleBanner';
-import { Typography } from 'src/components/Typography';
-import { useGravatar } from 'src/hooks/useGravatar';
-
-interface Props {
-  email: string;
-}
-
-export const GravatarSunsetBanner = (props: Props) => {
-  const { email } = props;
-  const GRAVATAR_DEPRECATION_DATE = 'September 30th, 2024';
-
-  const { hasGravatar, isLoadingGravatar } = useGravatar(email);
-
-  if (isLoadingGravatar || !hasGravatar) {
-    return;
-  }
-  return (
-    
-      
-        {`Support for using Gravatar as your profile photo will be deprecated on ${GRAVATAR_DEPRECATION_DATE}. Your profile photo will automatically be changed to your username initial.`}
-      
-    
-  );
-};
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Addons/Addons.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Addons/Addons.tsx
index a8652f9eeec..de19fca6b22 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Addons/Addons.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Addons/Addons.tsx
@@ -28,7 +28,7 @@ export const Addons = () => {
     selectedRegion?.site_type === 'edge';
 
   return (
-    
+    
       
         Add-ons
         {isDistributedRegionSelected && (
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Security.test.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Security.test.tsx
index 1d837243fe9..acd014cd307 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Security.test.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Security.test.tsx
@@ -17,8 +17,7 @@ import { Security } from './Security';
 import type { LinodeCreateFormValues } from './utilities';
 
 describe('Security', () => {
-  // TODO: Unskip once M3-8559 is addressed.
-  it.skip(
+  it(
     'should render a root password input',
     async () => {
       const { findByLabelText } = renderWithThemeAndHookFormContext({
diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptImages.test.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptImages.test.tsx
index 346d4d1d4f3..c25ab318107 100644
--- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptImages.test.tsx
+++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/StackScriptImages.test.tsx
@@ -26,7 +26,7 @@ describe('Images', () => {
   });
 
   it('should only render images that are compatible with the selected StackScript', async () => {
-    const images = imageFactory.buildList(5, { eol: null });
+    const images = imageFactory.buildList(5);
 
     // For the sake of this test, we pretend this image is the only compatible image.
     const compatibleImage = images[2];
diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/EditIPRDNSDrawer.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/EditIPRDNSDrawer.test.tsx
deleted file mode 100644
index a2f6f2c25fb..00000000000
--- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/EditIPRDNSDrawer.test.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import { fireEvent } from '@testing-library/react';
-import * as React from 'react';
-
-import { ipAddressFactory } from 'src/factories';
-import { renderWithTheme } from 'src/utilities/testHelpers';
-
-import { EditIPRDNSDrawer } from './EditIPRDNSDrawer';
-
-const props = {
-  ip: ipAddressFactory.build({ rdns: '' }),
-  onClose: vi.fn(),
-  open: true,
-};
-
-describe('EditIPRDNSDrawer', () => {
-  it('renders the drawer correctly', () => {
-    const { getAllByRole, getByText } = renderWithTheme(
-      
-    );
-
-    // confirm drawer title and form fields render
-    expect(getByText('Edit Reverse DNS')).toBeVisible();
-    expect(getByText('Leave this field blank to reset RDNS')).toBeVisible();
-    expect(getByText('Enter a domain name')).toBeVisible();
-
-    // confirm buttons render
-    expect(getAllByRole('button')).toHaveLength(3);
-    expect(getByText('Cancel')).toBeVisible();
-    expect(getByText('Save')).toBeVisible();
-  });
-
-  it('closes the drawer', () => {
-    const { getByText } = renderWithTheme();
-
-    const cancelButton = getByText('Cancel');
-    fireEvent.click(cancelButton);
-    expect(props.onClose).toHaveBeenCalled();
-  });
-});
diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/EditIPRDNSDrawer.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/EditIPRDNSDrawer.tsx
index 80c94d6a5f7..61cefce6bf1 100644
--- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/EditIPRDNSDrawer.tsx
+++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/EditIPRDNSDrawer.tsx
@@ -1,3 +1,4 @@
+import { IPAddress } from '@linode/api-v4/lib/networking';
 import { useFormik } from 'formik';
 import { useSnackbar } from 'notistack';
 import * as React from 'react';
@@ -6,11 +7,10 @@ import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
 import { Drawer } from 'src/components/Drawer';
 import { Notice } from 'src/components/Notice/Notice';
 import { TextField } from 'src/components/TextField';
+import { Typography } from 'src/components/Typography';
 import { useLinodeIPMutation } from 'src/queries/linodes/networking';
 import { getErrorMap } from 'src/utilities/errorUtils';
 
-import type { IPAddress } from '@linode/api-v4/lib/networking';
-
 interface Props {
   ip: IPAddress | undefined;
   onClose: () => void;
@@ -38,27 +38,24 @@ export const EditIPRDNSDrawer = (props: Props) => {
         address: ip?.address ?? '',
         rdns: values.rdns === '' ? null : values.rdns,
       });
-      enqueueSnackbar(`Successfully updated RDNS for ${ip?.address}`, {
+      enqueueSnackbar(`Successfully updated RNS for ${ip?.address}`, {
         variant: 'success',
       });
       onClose();
     },
   });
 
-  const onExited = () => {
-    formik.resetForm();
-    reset();
-  };
+  React.useEffect(() => {
+    if (open) {
+      reset();
+      formik.resetForm();
+    }
+  }, [open]);
 
   const errorMap = getErrorMap(['rdns'], error);
 
   return (
-    
+    
       
{Boolean(errorMap.none) && ( {errorMap.none} @@ -66,7 +63,6 @@ export const EditIPRDNSDrawer = (props: Props) => { { placeholder="Enter a domain name" value={formik.values.rdns} /> + + Leave this field blank to reset RDNS + { loading: isPending, type: 'submit', }} - secondaryButtonProps={{ label: 'Cancel', onClick: onClose }} + secondaryButtonProps={{ label: 'cancel', onClick: onClose }} style={{ marginTop: 16 }} /> diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/EditRangeRDNSDrawer.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/EditRangeRDNSDrawer.test.tsx deleted file mode 100644 index 7fcab0a808d..00000000000 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/EditRangeRDNSDrawer.test.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { fireEvent } from '@testing-library/react'; -import * as React from 'react'; - -import { renderWithTheme } from 'src/utilities/testHelpers'; - -import { EditRangeRDNSDrawer } from './EditRangeRDNSDrawer'; - -const props = { - linodeId: 1, - onClose: vi.fn(), - open: true, - range: undefined, -}; - -describe('EditRangeRDNSDrawer', () => { - it('renders the drawer correctly', () => { - const { getAllByRole, getByText } = renderWithTheme( - - ); - - // confirm drawer title and fields render - expect(getByText('Edit Reverse DNS')).toBeVisible(); - expect(getByText('Enter an IPv6 address')).toBeVisible(); - expect(getByText('Enter a domain name')).toBeVisible(); - expect(getByText('Leave this field blank to reset RDNS')).toBeVisible(); - - // confirm buttons render - expect(getAllByRole('button')).toHaveLength(3); - expect(getByText('Close')).toBeVisible(); - expect(getByText('Save')).toBeVisible(); - }); - - it('closes the drawer', () => { - const { getByText } = renderWithTheme(); - - const cancelButton = getByText('Close'); - fireEvent.click(cancelButton); - expect(props.onClose).toHaveBeenCalled(); - }); -}); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/EditRangeRDNSDrawer.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/EditRangeRDNSDrawer.tsx index b2d2193f80f..dc8362c8e72 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/EditRangeRDNSDrawer.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/EditRangeRDNSDrawer.tsx @@ -61,7 +61,7 @@ export const EditRangeRDNSDrawer = (props: Props) => { address: values.address ?? '', rdns: values.rdns === '' ? null : values.rdns, }); - enqueueSnackbar(`Successfully updated RDNS for ${range?.range}`, { + enqueueSnackbar(`Successfully updated RNS for ${range?.range}`, { variant: 'success', }); onClose(); @@ -70,20 +70,17 @@ export const EditRangeRDNSDrawer = (props: Props) => { const theme = useTheme(); - const onExited = () => { - formik.resetForm(); - reset(); - }; + React.useEffect(() => { + if (open) { + formik.resetForm(); + reset(); + } + }, [open]); const errorMap = getErrorMap(['rdns'], error); return ( - +
{Boolean(errorMap.none) && ( @@ -101,13 +98,15 @@ export const EditRangeRDNSDrawer = (props: Props) => { + + Leave this field blank to reset RDNS + { Existing Records {ips.map((ip) => ( -
+
{ip.address} {ip.rdns || ''}
diff --git a/packages/manager/src/features/Linodes/index.tsx b/packages/manager/src/features/Linodes/index.tsx index 328b83f5951..5d5e707f8e7 100644 --- a/packages/manager/src/features/Linodes/index.tsx +++ b/packages/manager/src/features/Linodes/index.tsx @@ -1,7 +1,8 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Redirect, Route, Switch } from 'react-router-dom'; import { SuspenseLoader } from 'src/components/SuspenseLoader'; +import { useFlags } from 'src/hooks/useFlags'; import { useAllAccountMaintenanceQuery } from 'src/queries/account/maintenance'; import { useInProgressEvents } from 'src/queries/events/events'; import { useAllLinodesQuery } from 'src/queries/linodes/linodes'; @@ -17,6 +18,9 @@ const LinodesDetail = React.lazy(() => default: module.LinodeDetail, })) ); +const LinodesCreate = React.lazy( + () => import('./LinodesCreate/LinodeCreateContainer') +); const LinodesCreatev2 = React.lazy(() => import('./LinodeCreatev2').then((module) => ({ default: module.LinodeCreatev2, @@ -24,10 +28,23 @@ const LinodesCreatev2 = React.lazy(() => ); export 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 [isLinodeCreateV2EnabledStale] = useState(flags.linodeCreateRefactor); + + const isLinodeCreateV2Enabled = import.meta.env.DEV + ? flags.linodeCreateRefactor + : isLinodeCreateV2EnabledStale; + return ( }> - + diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.test.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.test.tsx deleted file mode 100644 index ade67435282..00000000000 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.test.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { fireEvent } from '@testing-library/react'; -import * as React from 'react'; - -import { renderWithTheme } from 'src/utilities/testHelpers'; - -import { NodeBalancerActionMenu } from './NodeBalancerActionMenu'; - -const props = { - label: 'nodebalancer-1', - nodeBalancerId: 1, - toggleDialog: vi.fn(), -}; - -describe('NodeBalancerActionMenu', () => { - afterEach(() => { - vi.resetAllMocks(); - }); - - it('renders the NodeBalancerActionMenu', () => { - const { getByText } = renderWithTheme( - - ); - - expect(getByText('Configurations')).toBeVisible(); - expect(getByText('Settings')).toBeVisible(); - expect(getByText('Delete')).toBeVisible(); - }); - - it('triggers the action to delete the NodeBalancer', () => { - const { getByText } = renderWithTheme( - - ); - - const deleteButton = getByText('Delete'); - fireEvent.click(deleteButton); - expect(props.toggleDialog).toHaveBeenCalled(); - }); -}); diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.tsx index f24b602548d..94d9de3fb16 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.tsx @@ -1,17 +1,14 @@ -import { useTheme } from '@mui/material/styles'; +import { Theme, useTheme } from '@mui/material/styles'; import useMediaQuery from '@mui/material/useMediaQuery'; import * as React from 'react'; import { useHistory } from 'react-router-dom'; -import { ActionMenu } from 'src/components/ActionMenu/ActionMenu'; +import { Action, ActionMenu } from 'src/components/ActionMenu/ActionMenu'; import { Hidden } from 'src/components/Hidden'; import { InlineMenuAction } from 'src/components/InlineMenuAction/InlineMenuAction'; import { getRestrictedResourceText } from 'src/features/Account/utils'; import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted'; -import type { Theme } from '@mui/material/styles'; -import type { Action } from 'src/components/ActionMenu/ActionMenu'; - interface Props { label: string; nodeBalancerId: number; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.test.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.test.tsx deleted file mode 100644 index 80df645c989..00000000000 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.test.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { fireEvent } from '@testing-library/react'; -import * as React from 'react'; - -import { nodeBalancerFactory } from 'src/factories'; -import { breakpoints } from 'src/foundations/breakpoints'; -import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted'; -import { renderWithTheme, resizeScreenSize } from 'src/utilities/testHelpers'; - -import { NodeBalancerTableRow } from './NodeBalancerTableRow'; - -vi.mock('src/hooks/useIsResourceRestricted'); - -const props = { - ...nodeBalancerFactory.build(), - onDelete: vi.fn(), -}; - -describe('NodeBalancerTableRow', () => { - beforeEach(() => { - vi.resetAllMocks(); - }); - - it('renders the NodeBalancer table row', () => { - const { getByText } = renderWithTheme(); - - expect(getByText('nodebalancer-id-1')).toBeVisible(); - expect(getByText('0.0.0.0')).toBeVisible(); - expect(getByText('Configurations')).toBeVisible(); - expect(getByText('Settings')).toBeVisible(); - expect(getByText('Delete')).toBeVisible(); - }); - - it('renders the hidden columns when the screen width is large enough', () => { - resizeScreenSize(breakpoints.values.lg); - const { getByText } = renderWithTheme(); - - expect(getByText('nodebalancer-id-1')).toBeVisible(); - expect(getByText('0 up')).toBeVisible(); - expect(getByText('0 down')).toBeVisible(); - expect(getByText('0 bytes')).toBeVisible(); - expect(getByText('0.0.0.0')).toBeVisible(); - expect(getByText('us-east')).toBeVisible(); - }); - - it('deletes the NodeBalancer', () => { - const { getByText } = renderWithTheme(); - - const deleteButton = getByText('Delete'); - fireEvent.click(deleteButton); - expect(props.onDelete).toHaveBeenCalled(); - }); - - it('does not delete the NodeBalancer if the delete button is disabled', () => { - vi.mocked(useIsResourceRestricted).mockReturnValue(true); - const { getByText } = renderWithTheme(); - - const deleteButton = getByText('Delete'); - fireEvent.click(deleteButton); - expect(props.onDelete).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.tsx index fc7fd795d91..e9a20f8274a 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.tsx @@ -1,3 +1,4 @@ +import { NodeBalancer } from '@linode/api-v4/lib/nodebalancers'; import * as React from 'react'; import { Link } from 'react-router-dom'; @@ -12,8 +13,6 @@ import { convertMegabytesTo } from 'src/utilities/unitConversions'; import { NodeBalancerActionMenu } from './NodeBalancerActionMenu'; -import type { NodeBalancer } from '@linode/api-v4/lib/nodebalancers'; - interface Props extends NodeBalancer { onDelete: () => void; } diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.test.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.test.tsx deleted file mode 100644 index e320fd915ab..00000000000 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.test.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { waitForElementToBeRemoved } from '@testing-library/react'; -import * as React from 'react'; - -import { nodeBalancerFactory } from 'src/factories'; -import { makeResourcePage } from 'src/mocks/serverHandlers'; -import { HttpResponse, http, server } from 'src/mocks/testServer'; -import { mockMatchMedia, renderWithTheme } from 'src/utilities/testHelpers'; - -import { NodeBalancersLanding } from './NodeBalancersLanding'; - -beforeAll(() => mockMatchMedia()); - -const loadingTestId = 'circle-progress'; - -describe('NodeBalancersLanding', () => { - it('renders the NodeBalancer empty state if there are no NodeBalancers', async () => { - server.use( - http.get('*/nodebalancers', () => { - return HttpResponse.json(makeResourcePage([])); - }) - ); - - const { getByTestId, getByText } = renderWithTheme( - - ); - - // expect loading state and wait for it to disappear - expect(getByTestId(loadingTestId)).toBeInTheDocument(); - await waitForElementToBeRemoved(getByTestId(loadingTestId)); - - expect(getByText('NodeBalancers')).toBeVisible(); - expect(getByText('Cloud-based load balancing service')).toBeVisible(); - expect( - getByText( - 'Add high availability and horizontal scaling to web applications hosted on Linode Compute Instances.' - ) - ).toBeVisible(); - }); - - it('renders the NodeBalancer table if there are NodeBalancers', async () => { - server.use( - http.get('*/nodebalancers', () => { - const nodebalancers = nodeBalancerFactory.buildList(1); - return HttpResponse.json(makeResourcePage(nodebalancers)); - }) - ); - - const { getByTestId, getByText } = renderWithTheme( - - ); - - // expect loading state and wait for it to disappear - expect(getByTestId(loadingTestId)).toBeInTheDocument(); - await waitForElementToBeRemoved(getByTestId(loadingTestId)); - - expect(getByText('NodeBalancers')).toBeVisible(); - expect(getByText('Create NodeBalancer')).toBeVisible(); - - // confirm table headers - expect(getByText('Label')).toBeVisible(); - expect(getByText('Backend Status')).toBeVisible(); - expect(getByText('Transferred')).toBeVisible(); - expect(getByText('Ports')).toBeVisible(); - expect(getByText('IP Address')).toBeVisible(); - expect(getByText('Region')).toBeVisible(); - }); -}); diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx index f094670b807..0626eca8bab 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx @@ -21,8 +21,8 @@ import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGran import { useNodeBalancersQuery } from 'src/queries/nodebalancers'; import { NodeBalancerDeleteDialog } from '../NodeBalancerDeleteDialog'; -import { NodeBalancerLandingEmptyState } from './NodeBalancersLandingEmptyState'; import { NodeBalancerTableRow } from './NodeBalancerTableRow'; +import { NodeBalancerLandingEmptyState } from './NodeBalancersLandingEmptyState'; const preferenceKey = 'nodebalancers'; export const NodeBalancersLanding = () => { diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLandingEmptyState.test.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLandingEmptyState.test.tsx deleted file mode 100644 index b3277aceb8b..00000000000 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLandingEmptyState.test.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { waitFor } from '@testing-library/react'; -import * as React from 'react'; - -import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; -import { renderWithTheme } from 'src/utilities/testHelpers'; - -import { NodeBalancerLandingEmptyState } from './NodeBalancersLandingEmptyState'; - -vi.mock('src/hooks/useRestrictedGlobalGrantCheck'); - -// Note: An integration test confirming the helper text and enabled Create NodeBalancer button already exists, so we're just checking for a disabled create button here -describe('NodeBalancersLandingEmptyState', () => { - afterEach(() => { - vi.resetAllMocks(); - }); - - it('disables the Create NodeBalancer button if user does not have permissions to create a NodeBalancer', async () => { - // disables the create button - vi.mocked(useRestrictedGlobalGrantCheck).mockReturnValue(true); - - const { getByText } = renderWithTheme(); - - await waitFor(() => { - const createNodeBalancerButton = getByText('Create NodeBalancer').closest( - 'button' - ); - - expect(createNodeBalancerButton).toBeDisabled(); - expect(createNodeBalancerButton).toHaveAttribute( - 'data-qa-tooltip', - "You don't have permissions to create NodeBalancers. Please contact your account administrator to request the necessary permissions." - ); - }); - }); -}); diff --git a/packages/manager/src/features/NotificationCenter/Events/NotificationCenterEvent.tsx b/packages/manager/src/features/NotificationCenter/Events/NotificationCenterEvent.tsx index fd0a62cdb70..446bffb1c48 100644 --- a/packages/manager/src/features/NotificationCenter/Events/NotificationCenterEvent.tsx +++ b/packages/manager/src/features/NotificationCenter/Events/NotificationCenterEvent.tsx @@ -1,19 +1,15 @@ -import { useTheme } from '@mui/material'; import * as React from 'react'; import { BarPercent } from 'src/components/BarPercent'; import { Box } from 'src/components/Box'; -import { GravatarOrAvatar } from 'src/components/GravatarOrAvatar'; import { Typography } from 'src/components/Typography'; import { formatProgressEvent, getEventMessage, getEventUsername, } from 'src/features/Events/utils'; -import { useProfile } from 'src/queries/profile/profile'; import { - NotificationEventAvatar, NotificationEventGravatar, NotificationEventStyledBox, notificationEventStyles, @@ -29,14 +25,11 @@ interface NotificationEventProps { export const NotificationCenterEvent = React.memo( (props: NotificationEventProps) => { const { event } = props; - const theme = useTheme(); const { classes, cx } = notificationEventStyles(); const unseenEventClass = cx({ [classes.unseenEvent]: !event.seen }); const message = getEventMessage(event); const username = getEventUsername(event); - const { data: profile } = useProfile(); - /** * Some event types may not be handled by our system (or new types or new ones may be added that we haven't caught yet). * Filter these out so we don't display blank messages to the user. @@ -51,25 +44,9 @@ export const NotificationCenterEvent = React.memo( return ( - - } - gravatar={} - height={32} - width={32} - /> + {message} {showProgress && ( diff --git a/packages/manager/src/features/NotificationCenter/NotificationCenter.styles.ts b/packages/manager/src/features/NotificationCenter/NotificationCenter.styles.ts index 620151a4769..2761e4d27f5 100644 --- a/packages/manager/src/features/NotificationCenter/NotificationCenter.styles.ts +++ b/packages/manager/src/features/NotificationCenter/NotificationCenter.styles.ts @@ -2,12 +2,10 @@ import KeyboardArrowDown from '@mui/icons-material/KeyboardArrowDown'; import { styled } from '@mui/material'; import { makeStyles } from 'tss-react/mui'; -import { Avatar } from 'src/components/Avatar/Avatar'; import { Box } from 'src/components/Box'; import { GravatarByUsername } from 'src/components/GravatarByUsername'; import { Link } from 'src/components/Link'; import { Typography } from 'src/components/Typography'; -import { fadeIn } from 'src/styles/keyframes'; import { omittedProps } from 'src/utilities/omittedProps'; import type { NotificationCenterNotificationMessageProps } from './types'; @@ -122,16 +120,6 @@ export const NotificationEventStyledBox = styled(Box, { export const NotificationEventGravatar = styled(GravatarByUsername, { label: 'StyledGravatarByUsername', -})(() => ({ - animation: `${fadeIn} .2s ease-in-out forwards`, - height: 32, - marginTop: 2, - minWidth: 32, - width: 32, -})); - -export const NotificationEventAvatar = styled(Avatar, { - label: 'StyledAvatar', })(() => ({ height: 32, marginTop: 2, diff --git a/packages/manager/src/features/ObjectStorage/BucketDetail/ObjectDetailsDrawer.test.tsx b/packages/manager/src/features/ObjectStorage/BucketDetail/ObjectDetailsDrawer.test.tsx index 44d9d7418a9..59b43fb96de 100644 --- a/packages/manager/src/features/ObjectStorage/BucketDetail/ObjectDetailsDrawer.test.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketDetail/ObjectDetailsDrawer.test.tsx @@ -2,7 +2,7 @@ import { act, waitFor } from '@testing-library/react'; import * as React from 'react'; import { profileFactory } from 'src/factories'; -import { HttpResponse, http, server } from 'src/mocks/testServer'; +import { http, HttpResponse, server } from 'src/mocks/testServer'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { ObjectDetailsDrawer } from './ObjectDetailsDrawer'; @@ -64,15 +64,4 @@ describe('ObjectDetailsDrawer', () => { expect(queryByTestId('lastModified')).not.toBeInTheDocument(); }); }); - - it("doesn't show the ACL Switch for E2 and E3 buckets", async () => { - const { queryByLabelText } = renderWithTheme( - - ); - await waitFor(() => { - expect( - queryByLabelText('Access Control List (ACL)') - ).not.toBeInTheDocument(); - }); - }); }); diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketDetailsDrawer.test.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketDetailsDrawer.test.tsx index 63b2a7929b3..12fa33d91f1 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketDetailsDrawer.test.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketDetailsDrawer.test.tsx @@ -4,7 +4,6 @@ import { vi } from 'vitest'; import { objectStorageBucketFactory, - objectStorageBucketFactoryGen2, profileFactory, regionFactory, } from 'src/factories'; @@ -201,100 +200,3 @@ describe('BucketDetailsDrawer: Legacy UI', () => { }); }); }); - -describe('BucketDetailDrawer: Gen2 UI', () => { - const e3Bucket = objectStorageBucketFactoryGen2.build(); - const e1Bucket = objectStorageBucketFactoryGen2.build({ - endpoint_type: 'E1', - }); - - const region = regionFactory.build({ - id: e3Bucket.region, - }); - - it('renders correctly when open', () => { - renderWithThemeAndHookFormContext({ - component: ( - - ), - options: { - flags: { objMultiCluster: false, objectStorageGen2: { enabled: true } }, - }, - }); - - expect(screen.getByText(e3Bucket.label)).toBeInTheDocument(); - expect(screen.getByTestId('createdTime')).toHaveTextContent( - 'Created: 2019-12-12' - ); - expect(screen.getByTestId('endpointType')).toHaveTextContent( - `Endpoint Type: E3` - ); - expect(screen.getByTestId('cluster')).toHaveTextContent(region.id); - expect(screen.getByText(e3Bucket.hostname)).toBeInTheDocument(); - expect(screen.getByText('1 MB')).toBeInTheDocument(); - expect(screen.getByText('103 objects')).toBeInTheDocument(); - }); - - it("doesn't show the CORS switch for E2 and E3 buckets", async () => { - const { getByText } = renderWithThemeAndHookFormContext({ - component: ( - - ), - options: { - flags: { objMultiCluster: false, objectStorageGen2: { enabled: true } }, - }, - }); - - expect( - getByText( - /CORS \(Cross Origin Sharing\) is not available for endpoint types E2 and E3./ - ) - ).toBeInTheDocument(); - }); - - it('renders the Bucket Rate Limit Table for E2 and E3 buckets', async () => { - const { findByTestId } = renderWithThemeAndHookFormContext({ - component: ( - - ), - options: { - flags: { objMultiCluster: false, objectStorageGen2: { enabled: true } }, - }, - }); - - const rateLimitTable = await findByTestId('bucket-rate-limit-table'); - expect(rateLimitTable).toBeVisible(); - }); - - it('renders the Bucket Rate Limit Text for E0 and E1 buckets', async () => { - const { findByText } = renderWithThemeAndHookFormContext({ - component: ( - - ), - options: { - flags: { objMultiCluster: false, objectStorageGen2: { enabled: true } }, - }, - }); - expect( - await findByText( - /This endpoint type supports up to 750 Requests Per Second \(RPS\)./ - ) - ).toBeInTheDocument(); - }); -}); diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketDetailsDrawer.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketDetailsDrawer.tsx index b4ebf39c075..b1ba1659944 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketDetailsDrawer.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketDetailsDrawer.tsx @@ -181,7 +181,6 @@ export const BucketDetailsDrawer = React.memo( payload ); }} - endpointType={endpoint_type} name={label} variant="bucket" /> diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketTable.test.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketTable.test.tsx index c1f86d77f06..bbce7fa8900 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketTable.test.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketTable.test.tsx @@ -1,10 +1,7 @@ import * as React from 'react'; -import { - objectStorageBucketFactory, - objectStorageBucketFactoryGen2, -} from 'src/factories'; -import { mockMatchMedia, renderWithTheme } from 'src/utilities/testHelpers'; +import { objectStorageBucketFactory } from 'src/factories'; +import { renderWithTheme, mockMatchMedia } from 'src/utilities/testHelpers'; import { BucketTable } from './BucketTable'; @@ -46,20 +43,4 @@ describe('BucketTable', () => { expect(getByText(bucket.label)).toBeVisible(); } }); - - it('renders "Endpoint Type" column when Gen 2 is enabled', () => { - const bucket = objectStorageBucketFactoryGen2.buildList(1); - const { getByText } = renderWithTheme( - - ); - expect(getByText('Endpoint Type')).toBeVisible(); - expect(getByText('Standard (E3)')).toBeVisible(); - }); }); diff --git a/packages/manager/src/features/OneClickApps/oneClickAppsv2.ts b/packages/manager/src/features/OneClickApps/oneClickAppsv2.ts index 4dbb5d87fc9..96f0e14d380 100644 --- a/packages/manager/src/features/OneClickApps/oneClickAppsv2.ts +++ b/packages/manager/src/features/OneClickApps/oneClickAppsv2.ts @@ -9,10 +9,11 @@ import type { OCA } from './types'; * for it to be visible to users. */ export const oneClickApps: Record = { - 0: oneClickAppFactory.build({ - isNew: true, - name: 'E2E Test App', - }), + 0: { + ...oneClickAppFactory.build({ + name: 'E2E Test App', + }), + }, 401697: { alt_description: 'Popular website content management system.', alt_name: 'CMS: content management system', diff --git a/packages/manager/src/features/Profile/APITokens/APITokenTable.styles.ts b/packages/manager/src/features/Profile/APITokens/APITokenTable.styles.ts new file mode 100644 index 00000000000..f232e89ce8c --- /dev/null +++ b/packages/manager/src/features/Profile/APITokens/APITokenTable.styles.ts @@ -0,0 +1,26 @@ +import Grid from '@mui/material/Unstable_Grid2'; +import { styled } from '@mui/material/styles'; + +import { Typography } from 'src/components/Typography'; + +export const StyledRootContainer = styled(Grid, { + label: 'StyledRootContainer', +})(({ theme }) => ({ + background: theme.color.white, + margin: 0, + width: '100%', +})); + +export const StyledHeadline = styled(Typography, { + label: 'StyledHeadline', +})(() => ({ + marginLeft: 7, +})); + +export const StyledAddNewWrapper = styled(Grid, { + label: 'StyledAddNewWrapper', +})(() => ({ + '&.MuiGrid-item': { + padding: 5, + }, +})); diff --git a/packages/manager/src/features/Profile/APITokens/APITokenTable.tsx b/packages/manager/src/features/Profile/APITokens/APITokenTable.tsx index e8f84e4f94d..84cb4fc7f28 100644 --- a/packages/manager/src/features/Profile/APITokens/APITokenTable.tsx +++ b/packages/manager/src/features/Profile/APITokens/APITokenTable.tsx @@ -1,10 +1,11 @@ +import { Token } from '@linode/api-v4/lib/profile'; +import Grid from '@mui/material/Unstable_Grid2'; import * as React from 'react'; import { Box } from 'src/components/Box'; import { Button } from 'src/components/Button/Button'; import { DateTimeDisplay } from 'src/components/DateTimeDisplay'; import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; -import { Paper } from 'src/components/Paper'; import { Table } from 'src/components/Table'; import { TableBody } from 'src/components/TableBody'; import { TableCell } from 'src/components/TableCell'; @@ -27,14 +28,17 @@ import { } from 'src/queries/profile/tokens'; import { APITokenMenu } from './APITokenMenu'; +import { + StyledAddNewWrapper, + StyledHeadline, + StyledRootContainer, +} from './APITokenTable.styles'; import { CreateAPITokenDrawer } from './CreateAPITokenDrawer'; import { EditAPITokenDrawer } from './EditAPITokenDrawer'; import { RevokeTokenDialog } from './RevokeTokenDialog'; import { isWayInTheFuture } from './utils'; import { ViewAPITokenDrawer } from './ViewAPITokenDrawer'; -import type { Token } from '@linode/api-v4'; - export type APITokenType = 'OAuth Client Token' | 'Personal Access Token'; export type APITokenTitle = @@ -181,34 +185,32 @@ export const APITokenTable = (props: Props) => { return ( - - - {title} - - {type === 'Personal Access Token' && ( - - )} - + + + {title} + + + + {type === 'Personal Access Token' && ( + + )} + + diff --git a/packages/manager/src/features/Profile/DisplaySettings/AvatarColorPickerDialog.test.tsx b/packages/manager/src/features/Profile/DisplaySettings/AvatarColorPickerDialog.test.tsx deleted file mode 100644 index 19b8557ac5d..00000000000 --- a/packages/manager/src/features/Profile/DisplaySettings/AvatarColorPickerDialog.test.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { fireEvent } from '@testing-library/react'; -import * as React from 'react'; - -import { renderWithTheme } from 'src/utilities/testHelpers'; - -import { AvatarColorPickerDialog } from './AvatarColorPickerDialog'; - -import type { AvatarColorPickerDialogProps } from './AvatarColorPickerDialog'; - -const mockProps: AvatarColorPickerDialogProps = { - handleClose: vi.fn(), - open: true, -}; - -describe('AvatarColorPicker', () => { - it('should render a dialog with a title, color picker, and avatar components', () => { - const { getByLabelText, getByTestId, getByTitle } = renderWithTheme( - - ); - - expect(getByTitle('Change Avatar Color')).toBeVisible(); - expect(getByLabelText('Avatar color picker')).toBeVisible(); - expect(getByTestId('avatar')).toBeVisible(); - }); - - it('calls onClose when Close button is clicked', async () => { - const { getByText } = renderWithTheme( - - ); - - await fireEvent.click(getByText('Close')); - expect(mockProps.handleClose).toHaveBeenCalled(); - }); - - it('closes when Save button is clicked', async () => { - const { getByText } = renderWithTheme( - - ); - - await fireEvent.click(getByText('Save')); - expect(mockProps.handleClose).toHaveBeenCalled(); - }); -}); diff --git a/packages/manager/src/features/Profile/DisplaySettings/AvatarColorPickerDialog.tsx b/packages/manager/src/features/Profile/DisplaySettings/AvatarColorPickerDialog.tsx deleted file mode 100644 index dd0cca24ccf..00000000000 --- a/packages/manager/src/features/Profile/DisplaySettings/AvatarColorPickerDialog.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { Typography } from '@mui/material'; -import React from 'react'; -import { useState } from 'react'; - -import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; -import { Avatar } from 'src/components/Avatar/Avatar'; -import { ColorPicker } from 'src/components/ColorPicker/ColorPicker'; -import { Dialog } from 'src/components/Dialog/Dialog'; -import { Stack } from 'src/components/Stack'; -import { - useMutatePreferences, - usePreferences, -} from 'src/queries/profile/preferences'; - -export interface AvatarColorPickerDialogProps { - handleClose: () => void; - open: boolean; -} - -export const AvatarColorPickerDialog = ( - props: AvatarColorPickerDialogProps -) => { - const { handleClose, open } = props; - - const [avatarColor, setAvatarColor] = useState(); - - const { data: preferences } = usePreferences(); - const { mutateAsync: updatePreferences } = useMutatePreferences(); - - return ( - - - - Select a background color for your avatar: - - setAvatarColor(color)} - /> - - - - - - { - if (avatarColor) { - updatePreferences({ - avatarColor, - }).catch(() => {}); - } - handleClose(); - }, - }} - secondaryButtonProps={{ - 'data-testid': 'close-button', - label: 'Close', - onClick: handleClose, - }} - sx={{ - display: 'flex', - }} - /> - - ); -}; diff --git a/packages/manager/src/features/Profile/DisplaySettings/DisplaySettings.tsx b/packages/manager/src/features/Profile/DisplaySettings/DisplaySettings.tsx index 6c003f183c2..1a1ba9ae96b 100644 --- a/packages/manager/src/features/Profile/DisplaySettings/DisplaySettings.tsx +++ b/packages/manager/src/features/Profile/DisplaySettings/DisplaySettings.tsx @@ -5,26 +5,21 @@ import { useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; import { v4 } from 'uuid'; -import { Avatar } from 'src/components/Avatar/Avatar'; import { Box } from 'src/components/Box'; -import { Button } from 'src/components/Button/Button'; import { Divider } from 'src/components/Divider'; -import { GravatarOrAvatar } from 'src/components/GravatarOrAvatar'; +import { GravatarByEmail } from 'src/components/GravatarByEmail'; import { Link } from 'src/components/Link'; import { Paper } from 'src/components/Paper'; import { SingleTextFieldForm } from 'src/components/SingleTextFieldForm/SingleTextFieldForm'; import { TooltipIcon } from 'src/components/TooltipIcon'; import { Typography } from 'src/components/Typography'; import { RESTRICTED_FIELD_TOOLTIP } from 'src/features/Account/constants'; -import { useGravatar } from 'src/hooks/useGravatar'; import { useNotificationsQuery } from 'src/queries/account/notifications'; import { useMutateProfile, useProfile } from 'src/queries/profile/profile'; -import { AvatarColorPickerDialog } from './AvatarColorPickerDialog'; import { TimezoneForm } from './TimezoneForm'; import type { ApplicationState } from 'src/store'; -import { GravatarByEmail } from 'src/components/GravatarByEmail'; export const DisplaySettings = () => { const theme = useTheme(); @@ -39,13 +34,6 @@ export const DisplaySettings = () => { const isProxyUser = profile?.user_type === 'proxy'; - const { hasGravatar } = useGravatar(profile?.email); - - const [ - isColorPickerDialogOpen, - setAvatarColorPickerDialogOpen, - ] = React.useState(false); - React.useEffect(() => { if (location.state?.focusEmail && emailRef.current) { emailRef.current.focus(); @@ -101,50 +89,31 @@ export const DisplaySettings = () => { }} display="flex" > - - } - avatar={} +
- {hasGravatar ? 'Profile photo' : 'Avatar'} - {hasGravatar && ( - - )} + Profile photo + - {hasGravatar - ? 'Create, upload, and manage your globally recognized avatar from a single place with Gravatar.' - : 'Your avatar is automatically generated using the first character of your username.'} + Create, upload, and manage your globally recognized avatar from + a single place with Gravatar. - {hasGravatar ? ( - - Manage photo - - ) : ( - - )} + + Manage photo +
@@ -186,10 +155,6 @@ export const DisplaySettings = () => { /> - setAvatarColorPickerDialogOpen(false)} - open={isColorPickerDialogOpen} - /> ); }; diff --git a/packages/manager/src/features/Profile/SecretTokenDialog/SecretTokenDialog.tsx b/packages/manager/src/features/Profile/SecretTokenDialog/SecretTokenDialog.tsx index b5f225153c4..d4468be8382 100644 --- a/packages/manager/src/features/Profile/SecretTokenDialog/SecretTokenDialog.tsx +++ b/packages/manager/src/features/Profile/SecretTokenDialog/SecretTokenDialog.tsx @@ -4,10 +4,10 @@ import * as React from 'react'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { Box } from 'src/components/Box'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; -import { CopyableTextField } from 'src/components/CopyableTextField/CopyableTextField'; +import { CopyableAndDownloadableTextField } from 'src/components/CopyableAndDownloadableTextField'; import { Notice } from 'src/components/Notice/Notice'; -import { CopyAllHostnames } from 'src/features/ObjectStorage/AccessKeyLanding/CopyAllHostnames'; import { HostNamesList } from 'src/features/ObjectStorage/AccessKeyLanding/HostNamesList'; +import { CopyAllHostnames } from 'src/features/ObjectStorage/AccessKeyLanding/CopyAllHostnames'; import { useAccountManagement } from 'src/hooks/useAccountManagement'; import { useFlags } from 'src/hooks/useFlags'; import { useRegionsQuery } from 'src/queries/regions/regions'; @@ -118,19 +118,17 @@ export const SecretTokenDialog = (props: Props) => { {objectStorageKey ? ( <> - - @@ -138,10 +136,9 @@ export const SecretTokenDialog = (props: Props) => { ) : value ? ( - diff --git a/packages/manager/src/features/Support/ExpandableTicketPanel.tsx b/packages/manager/src/features/Support/ExpandableTicketPanel.tsx index 80d5ab5cd5a..5cbdf6960ff 100644 --- a/packages/manager/src/features/Support/ExpandableTicketPanel.tsx +++ b/packages/manager/src/features/Support/ExpandableTicketPanel.tsx @@ -1,23 +1,18 @@ +import { SupportReply, SupportTicket } from '@linode/api-v4'; import Avatar from '@mui/material/Avatar'; -import { useTheme } from '@mui/material/styles'; +import { Theme } from '@mui/material/styles'; import Grid from '@mui/material/Unstable_Grid2'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; import UserIcon from 'src/assets/icons/account.svg'; -import { Avatar as NewAvatar } from 'src/components/Avatar/Avatar'; import { DateTimeDisplay } from 'src/components/DateTimeDisplay'; -import { GravatarOrAvatar } from 'src/components/GravatarOrAvatar'; import { Typography } from 'src/components/Typography'; -import { useProfile } from 'src/queries/profile/profile'; import { Hively, shouldRenderHively } from './Hively'; import { TicketDetailText } from './TicketDetailText'; import { OFFICIAL_USERNAMES } from './ticketUtils'; -import type { SupportReply, SupportTicket } from '@linode/api-v4'; -import type { Theme } from '@mui/material/styles'; - const useStyles = makeStyles()((theme: Theme) => ({ '@keyframes fadeIn': { from: { @@ -104,14 +99,10 @@ interface Data { export const ExpandableTicketPanel = React.memo((props: Props) => { const { classes } = useStyles(); - const theme = useTheme(); - const { open, parentTicket, reply, ticket, ticketUpdated } = props; const [data, setData] = React.useState(undefined); - const { data: profile } = useProfile(); - React.useEffect(() => { if (!ticket && !reply) { return; @@ -146,28 +137,13 @@ export const ExpandableTicketPanel = React.memo((props: Props) => { const renderAvatar = (id: string) => { return (
- - } - gravatar={ - - - - } - /> + + +
); }; diff --git a/packages/manager/src/features/TopMenu/NotificationMenu/NotificationMenu.tsx b/packages/manager/src/features/TopMenu/NotificationMenu/NotificationMenu.tsx index f3f8f2dfe22..65d78b3cdcb 100644 --- a/packages/manager/src/features/TopMenu/NotificationMenu/NotificationMenu.tsx +++ b/packages/manager/src/features/TopMenu/NotificationMenu/NotificationMenu.tsx @@ -23,7 +23,7 @@ import { usePrevious } from 'src/hooks/usePrevious'; import { useNotificationsQuery } from 'src/queries/account/notifications'; import { isInProgressEvent } from 'src/queries/events/event.helpers'; import { - useInitialEventsQuery, + useEventsInfiniteQuery, useMarkEventsAsSeen, } from 'src/queries/events/events'; import { rotate360 } from 'src/styles/keyframes'; @@ -37,8 +37,7 @@ export const NotificationMenu = () => { const formattedNotifications = useFormattedNotifications(); const notificationContext = React.useContext(_notificationContext); - const { data, events } = useInitialEventsQuery(); - const eventsData = data?.data ?? []; + const { data, events } = useEventsInfiniteQuery(); const { mutateAsync: markEventsAsSeen } = useMarkEventsAsSeen(); const numNotifications = @@ -133,7 +132,6 @@ export const NotificationMenu = () => { }, }} anchorEl={anchorRef.current} - data-qa-notification-menu id={id} onClose={handleClose} open={notificationContext.menuOpen} @@ -152,22 +150,13 @@ export const NotificationMenu = () => {
- - {eventsData.length > 0 ? ( - eventsData - .slice(0, 20) - .map((event) => ( - - )) - ) : ( - - No recent events to display - - )} + {data?.pages[0].data.slice(0, 20).map((event) => ( + + ))}
@@ -194,7 +183,7 @@ const StyledChip = styled(Chip, { width: props.showPlus ? 22 : 18, })); -export const StyledAutorenewIcon = styled(AutorenewIcon)(({ theme }) => ({ +const StyledAutorenewIcon = styled(AutorenewIcon)(({ theme }) => ({ animation: `${rotate360} 2s linear infinite`, bottom: 4, color: theme.palette.primary.main, diff --git a/packages/manager/src/features/TopMenu/TopMenu.tsx b/packages/manager/src/features/TopMenu/TopMenu.tsx index 4276ca7695f..c9f8c5bdc91 100644 --- a/packages/manager/src/features/TopMenu/TopMenu.tsx +++ b/packages/manager/src/features/TopMenu/TopMenu.tsx @@ -46,7 +46,7 @@ export const TopMenu = React.memo((props: TopMenuProps) => { )} - + ({ '&.MuiToolbar-root': { diff --git a/packages/manager/src/features/TopMenu/UserMenu/UserMenu.tsx b/packages/manager/src/features/TopMenu/UserMenu/UserMenu.tsx index c1470ebbaaf..f6d9ee4c6b2 100644 --- a/packages/manager/src/features/TopMenu/UserMenu/UserMenu.tsx +++ b/packages/manager/src/features/TopMenu/UserMenu/UserMenu.tsx @@ -1,18 +1,17 @@ +import { GlobalGrantTypes } from '@linode/api-v4/lib/account'; import KeyboardArrowDown from '@mui/icons-material/KeyboardArrowDown'; import KeyboardArrowUp from '@mui/icons-material/KeyboardArrowUp'; -import { styled, useMediaQuery } from '@mui/material'; +import { Theme, styled, useMediaQuery } from '@mui/material'; import Popover from '@mui/material/Popover'; import Grid from '@mui/material/Unstable_Grid2'; import { useSnackbar } from 'notistack'; import * as React from 'react'; -import { Avatar } from 'src/components/Avatar/Avatar'; -import { AvatarForProxy } from 'src/components/AvatarForProxy'; import { Box } from 'src/components/Box'; import { Button } from 'src/components/Button/Button'; import { Divider } from 'src/components/Divider'; import { GravatarByEmail } from 'src/components/GravatarByEmail'; -import { GravatarOrAvatar } from 'src/components/GravatarOrAvatar'; +import { GravatarForProxy } from 'src/components/GravatarForProxy'; import { Hidden } from 'src/components/Hidden'; import { Link } from 'src/components/Link'; import { Stack } from 'src/components/Stack'; @@ -30,9 +29,6 @@ import { getStorage, setStorage } from 'src/utilities/storage'; import { getCompanyNameOrEmail } from './utils'; -import type { GlobalGrantTypes } from '@linode/api-v4/lib/account'; -import type { Theme } from '@mui/material'; - interface MenuLink { display: string; hide?: boolean; @@ -211,12 +207,9 @@ export const UserMenu = React.memo(() => {