diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e2590d5c19..018fb457ed6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,11 +16,9 @@ jobs: [ "linode-manager", "@linode/api-v4", - "@linode/queries", - "@linode/shared", + "@linode/validation", "@linode/ui", "@linode/utilities", - "@linode/validation", ] runs-on: ubuntu-latest steps: @@ -195,7 +193,6 @@ jobs: - run: pnpm run --filter @linode/ui test test-utilities: - needs: build-sdk runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -207,57 +204,9 @@ jobs: with: node-version: "20.17" cache: "pnpm" - - uses: actions/download-artifact@v4 - with: - name: packages-api-v4-lib - path: packages/api-v4/lib - run: pnpm install --frozen-lockfile - run: pnpm run --filter @linode/utilities test - test-queries: - runs-on: ubuntu-latest - needs: build-sdk - steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - with: - run_install: false - version: 10 - - uses: actions/setup-node@v4 - with: - node-version: "20.17" - cache: "pnpm" - - uses: actions/download-artifact@v4 - with: - name: packages-api-v4-lib - path: packages/api-v4/lib - - run: pnpm install --frozen-lockfile - - run: pnpm run --filter @linode/queries test - - test-shared: - runs-on: ubuntu-latest - needs: build-sdk - steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - with: - run_install: false - version: 10 - - uses: actions/setup-node@v4 - with: - node-version: "20.17" - cache: "pnpm" - - uses: actions/download-artifact@v4 - with: - name: packages-api-v4-lib - path: packages/api-v4/lib - - uses: actions/download-artifact@v4 - with: - name: packages-validation-lib - path: packages/validation/lib - - run: pnpm install --frozen-lockfile - - run: pnpm run --filter @linode/shared test - typecheck-ui: runs-on: ubuntu-latest needs: build-sdk @@ -287,57 +236,9 @@ jobs: with: node-version: "20.17" cache: "pnpm" - - uses: actions/download-artifact@v4 - with: - name: packages-api-v4-lib - path: packages/api-v4/lib - run: pnpm install --frozen-lockfile - run: pnpm run --filter @linode/utilities typecheck - typecheck-queries: - runs-on: ubuntu-latest - needs: build-sdk - steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - with: - run_install: false - version: 10 - - uses: actions/setup-node@v4 - with: - node-version: "20.17" - cache: "pnpm" - - uses: actions/download-artifact@v4 - with: - name: packages-api-v4-lib - path: packages/api-v4/lib - - run: pnpm install --frozen-lockfile - - run: pnpm run --filter @linode/queries typecheck - - typecheck-shared: - runs-on: ubuntu-latest - needs: build-sdk - steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - with: - run_install: false - version: 10 - - uses: actions/setup-node@v4 - with: - node-version: "20.17" - cache: "pnpm" - - uses: actions/download-artifact@v4 - with: - name: packages-api-v4-lib - path: packages/api-v4/lib - - uses: actions/download-artifact@v4 - with: - name: packages-validation-lib - path: packages/validation/lib - - run: pnpm install --frozen-lockfile - - run: pnpm run --filter @linode/shared typecheck - typecheck-manager: runs-on: ubuntu-latest needs: build-sdk @@ -391,7 +292,7 @@ jobs: - run: pnpm install --frozen-lockfile - run: npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN} env: - NPM_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - run: pnpm publish -r --filter @linode/api-v4 --filter @linode/validation --no-git-checks --access public - name: slack-notify uses: rtCamp/action-slack-notify@master diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 00000000000..efe2cd70c4b --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,102 @@ +name: Code Coverage + +on: [pull_request] + +jobs: + base_branch: + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.base_ref }} # The base branch of the PR (develop) + + - uses: pnpm/action-setup@v4 + with: + run_install: false + version: 10 + + - name: Use Node.js v20.17 LTS + uses: actions/setup-node@v4 + with: + node-version: "20.17" + cache: "pnpm" + + - name: Install Dependencies + run: pnpm install --frozen-lockfile + + - name: Build @linode/validation + run: pnpm build:validation + + - name: Build @linode/api-v4 + run: pnpm build:sdk + + - name: Run Base Branch Coverage + run: pnpm coverage:summary + + - name: Write Base Coverage to an Artifact + run: | + coverage_json=$(cat ./packages/manager/coverage/coverage-summary.json) + pct=$(echo "$coverage_json" | jq -r '.total.statements.pct') + echo "$pct" > ref_code_coverage.txt + + - name: Upload Base Coverage Artifact + uses: actions/upload-artifact@v4 + with: + name: ref_code_coverage + path: ref_code_coverage.txt + + current_branch: + # We want to make sure we only run on open PRs (not drafts), but also should run even if the base branch coverage job fails. + # If the base branch coverage job fails to create a report, the current branch coverage job will fail as well, but this may help us debug the CI on the current branch. + if: ${{ always() && github.event.pull_request.draft == false }} + runs-on: ubuntu-latest + needs: base_branch + + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + run_install: false + version: 10 + + - name: Use Node.js v20.17 LTS + uses: actions/setup-node@v4 + with: + node-version: "20.17" + cache: "pnpm" + + - name: Install Dependencies + run: pnpm install --frozen-lockfile + + - name: Build @linode/validation + run: pnpm build:validation + + - name: Build @linode/api-v4 + run: pnpm build:sdk + + - name: Run Current Branch Coverage + run: pnpm coverage:summary + + - name: Write PR Number to an Artifact + run: | + echo "${{ github.event.number }}" > pr_number.txt + + - name: Write Current Coverage to an Artifact + run: | + coverage_json=$(cat ./packages/manager/coverage/coverage-summary.json) + pct=$(echo "$coverage_json" | jq -r '.total.statements.pct') + echo "$pct" > current_code_coverage.txt + + - name: Upload PR Number Artifact + uses: actions/upload-artifact@v4 + with: + name: pr_number + path: pr_number.txt + + - name: Upload Current Coverage Artifact + uses: actions/upload-artifact@v4 + with: + name: current_code_coverage + path: current_code_coverage.txt diff --git a/.github/workflows/coverage_comment.yml b/.github/workflows/coverage_comment.yml new file mode 100644 index 00000000000..9c395dd244d --- /dev/null +++ b/.github/workflows/coverage_comment.yml @@ -0,0 +1,68 @@ +name: Coverage Comment + +on: + workflow_run: + workflows: ["Code Coverage"] + types: + - completed + +permissions: + pull-requests: write + +jobs: + comment: + if: ${{ github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Use Node.js v20.17 LTS + uses: actions/setup-node@v4 + with: + node-version: "20.17" + + - name: Download PR Number Artifact + uses: dawidd6/action-download-artifact@268677152d06ba59fcec7a7f0b5d961b6ccd7e1e #v2.28.0 + with: + workflow: "coverage.yml" + run_id: ${{ github.event.workflow_run.id }} + name: pr_number + + - name: Download Base Coverage Artifact + uses: dawidd6/action-download-artifact@268677152d06ba59fcec7a7f0b5d961b6ccd7e1e #v2.28.0 + with: + workflow: "coverage.yml" + run_id: ${{ github.event.workflow_run.id }} + name: ref_code_coverage + + - name: Download Current Coverage Artifact + uses: dawidd6/action-download-artifact@268677152d06ba59fcec7a7f0b5d961b6ccd7e1e #v2.28.0 + with: + workflow: "coverage.yml" + run_id: ${{ github.event.workflow_run.id }} + name: current_code_coverage + + - name: Set PR Number Environment Variables + run: | + echo "PR_NUMBER=$(cat pr_number.txt)" >> $GITHUB_ENV + + - name: Generate Coverage Comment + run: | + base_coverage=$(cat ref_code_coverage.txt) + current_coverage=$(cat current_code_coverage.txt) + if (( $(echo "$current_coverage < $base_coverage" | bc -l) )); then + icon="❌" # Error icon + else + icon="✅" # Check mark icon + fi + comment_message="**Coverage Report:** $icon
Base Coverage: $base_coverage%
Current Coverage: $current_coverage%" + echo "Coverage: $comment_message" + echo "$comment_message" > updated_comment.txt + + - name: Post Comment + uses: mshick/add-pr-comment@7c0890544fb33b0bdd2e59467fbacb62e028a096 #v2.8.1 + with: + issue: ${{ env.PR_NUMBER }} + message-path: updated_comment.txt diff --git a/.github/workflows/e2e_schedule_and_push.yml b/.github/workflows/e2e_schedule_and_push.yml index 6def67c0a41..6afacc93369 100644 --- a/.github/workflows/e2e_schedule_and_push.yml +++ b/.github/workflows/e2e_schedule_and_push.yml @@ -8,8 +8,6 @@ env: CLIENT_ID: ${{ secrets.REACT_APP_CLIENT_ID }} CY_TEST_FAIL_ON_MANAGED: 1 CY_TEST_RESET_PREFERENCES: 1 - CY_TEST_SPLIT_RUN: 1 - CY_TEST_SPLIT_RUN_TOTAL: 4 on: schedule: - cron: "0 13 * * 1-5" @@ -21,17 +19,14 @@ on: jobs: run-cypress-e2e: - name: Cypress Tests (User ${{ matrix.user.index }}) runs-on: ubuntu-latest strategy: fail-fast: false matrix: - user: - - { index: 1, name: 'USER_1' } - - { index: 2, name: 'USER_2' } - - { index: 3, name: 'USER_3' } - - { index: 4, name: 'USER_4' } + user: ["USER_1", "USER_2", "USER_3", "USER_4"] steps: + - name: install command line utilities + run: sudo apt-get install -y expect - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 with: @@ -41,7 +36,16 @@ jobs: with: node-version: "20.17" - run: | + echo "CYPRESS_RECORD_KEY=${{ secrets.CYPRESS_RECORD_KEY }}" >> $GITHUB_ENV echo "GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV + - run: echo "MANAGER_OAUTH=$USER_1" >> ./packages/manager/.env + if: matrix['user'] == 'USER_1' + - run: echo "MANAGER_OAUTH=$USER_2" >> ./packages/manager/.env + if: matrix['user'] == 'USER_2' + - run: echo "MANAGER_OAUTH=$USER_3" >> ./packages/manager/.env + if: matrix['user'] == 'USER_3' + - run: echo "MANAGER_OAUTH=$USER_4" >> ./packages/manager/.env + if: matrix['user'] == 'USER_4' - run: | echo "REACT_APP_LAUNCH_DARKLY_ID=${{ secrets.REACT_APP_LAUNCH_DARKLY_ID }}" >> ./packages/manager/.env echo "REACT_APP_CLIENT_ID=$CLIENT_ID" >> ./packages/manager/.env @@ -49,8 +53,6 @@ jobs: echo "REACT_APP_API_ROOT=${{ secrets.REACT_APP_API_ROOT }}" >> ./packages/manager/.env echo "REACT_APP_APP_ROOT=${{ secrets.REACT_APP_APP_ROOT }}" >> ./packages/manager/.env echo "REACT_APP_DISABLE_NEW_RELIC=1" >> ./packages/manager/.env - echo "MANAGER_OAUTH=${{ secrets[matrix.user.name] }}" >> ./packages/manager/.env - echo "CY_TEST_SPLIT_RUN_INDEX=${{ matrix.user.index }}" >> ./packages/manager/.env - run: pnpm install --frozen-lockfile - run: pnpm run --filter @linode/validation build - run: pnpm run --filter @linode/api-v4 build @@ -64,5 +66,5 @@ jobs: build: pnpm run build start: pnpm start:ci browser: chrome - record: false - parallel: false + record: true + parallel: true diff --git a/.github/workflows/eslint_review.yml b/.github/workflows/eslint_review.yml deleted file mode 100644 index 5fc14caf77e..00000000000 --- a/.github/workflows/eslint_review.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: ESLint Review -on: [pull_request] -jobs: - eslint: - name: ESLint Review - runs-on: ubuntu-latest - permissions: - contents: read - checks: write - strategy: - matrix: - package: [manager, api-v4, queries, shared, ui, utilities, validation] - steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 - with: - run_install: false - version: 10 - - uses: actions/setup-node@v4 - with: - node-version: "20.17" - cache: "pnpm" - - run: pnpm install - - uses: abailly-akamai/action-eslint@8ad68ba04fa60924ef7607b07deb5989f38f5ed6 # v1.0.2 - with: - workdir: packages/${{ matrix.package }} - github_token: ${{ secrets.GITHUB_TOKEN }} - reporter: github-pr-check - level: warning # This will report both warnings and errors - filter_mode: added # Only comment on new/modified lines \ No newline at end of file diff --git a/.gitignore b/.gitignore index fe569e46f54..7e58fb3e68e 100644 --- a/.gitignore +++ b/.gitignore @@ -128,7 +128,6 @@ packages/manager/test-report.xml **/manager/cypress/downloads/ **/manager/cypress/results/ **/manager/cypress/screenshots/ -**/manager/cypress/reports/ packages/manager/cypress/fixtures/example.json diff --git a/README.md b/README.md index 991b48cfd68..6e7a3525d15 100644 --- a/README.md +++ b/README.md @@ -41,14 +41,7 @@ ## Overview -This repository is home to the Akamai Connected **[Cloud Manager](https://cloud.linode.com)** and related Typescript packages: -- [`@linode/api-v4`](packages/api-v4/) -- [`@linode/queries`](packages/queries/) -- [`@linode/search`](packages/search/) -- [`@linode/shared`](packages/shared/) -- [`@linode/ui`](packages/ui/) -- [`@linode/utilities`](packages/utilities/) -- [`@linode/validation`](packages/validation/) +This repository is home to the Akamai Connected **[Cloud Manager](https://cloud.linode.com)** and related [`@linode/api-v4`](packages/api-v4/), [`@linode/validation`](packages/validation/), [`@linode/ui`](packages/ui/), and [`@linode/utilities`](packages/utilities/) Typescript packages. ## Developing Locally diff --git a/docker-compose.yml b/docker-compose.yml index f37da3eaccd..d7ccac0bb01 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -49,7 +49,6 @@ x-e2e-env: # Cypress reporting. CY_TEST_JUNIT_REPORT: ${CY_TEST_JUNIT_REPORT} - CY_TEST_HTML_REPORT: ${CY_TEST_HTML_REPORT} CY_TEST_USER_REPORT: ${CY_TEST_USER_REPORT} # Cloud Manager build environment. diff --git a/docs/development-guide/05-fetching-data.md b/docs/development-guide/05-fetching-data.md index 7b37b724ead..e6fcd59fc95 100644 --- a/docs/development-guide/05-fetching-data.md +++ b/docs/development-guide/05-fetching-data.md @@ -113,7 +113,7 @@ const profileQueries = createQueryKeys('profile', { queryFn: getProfile, queryKey: null, }, -}); +}) export const useProfile = () => useQuery({ @@ -146,7 +146,7 @@ const UsernameDisplay = () => { ## When to use React Query or an api-v4 method directly Because **api-v4** methods don't commit data to a cache, it is acceptable to use **api-v4** methods directly -when performing **_one-time actions_** that do not require any immediate state change in Cloud Manager's UI. +when performing ***one-time actions*** that do not require any immediate state change in Cloud Manager's UI. While use of **api-v4** methods directly are acceptable, use of **React Query** Queries or Mutations are **still prefered** for the benefits described above. @@ -246,7 +246,7 @@ console.log(errorMap); #### Scrolling to errors -For deep forms, we provide a utility that will scroll to the first error encountered within a defined container. We do this to improve error visibility, because the user can be unaware of an error that isn't in the viewport. +For deep forms, we provide a utility that will scroll to the first error encountered within a defined container. We do this to improve error visibility, because the user can be unaware of an error that isn't in the viewport. An error can be a notice (API error) or a Formik field error. In order to implement this often needed functionality, we must declare a form or form container via ref, then pass it to the `scrollErrorIntoViewV2` util (works both for class & functional components). Note: the legacy `scrollErrorIntoView` is deprecated in favor of `scrollErrorIntoViewV2`. @@ -254,11 +254,10 @@ Note: the legacy `scrollErrorIntoView` is deprecated in favor of `scrollErrorInt Since Cloud Manager uses different ways of handling forms and validation, the `scrollErrorIntoViewV2` util should be implemented using the following patterns to ensure consistency. ##### Formik (deprecated) - ```Typescript import * as React from 'react'; -import { scrollErrorIntoViewV2 } from '@linode/utilities'; +import { scrollErrorIntoViewV2 } from 'src/utilities/scrollErrorIntoViewV2'; export const MyComponent = () => { const formContainerRef = React.useRef(null); @@ -286,11 +285,10 @@ export const MyComponent = () => { ``` ##### React Hook Forms - ```Typescript import * as React from 'react'; -import { scrollErrorIntoViewV2 } from '@linode/utilities'; +import { scrollErrorIntoViewV2 } from 'src/utilities/scrollErrorIntoViewV2'; export const MyComponent = () => { const formContainerRef = React.useRef(null); @@ -318,11 +316,10 @@ export const MyComponent = () => { ``` ##### Uncontrolled forms - ```Typescript import * as React from 'react'; -import { scrollErrorIntoViewV2 } from '@linode/utilities'; +import { scrollErrorIntoViewV2 } from 'src/utilities/scrollErrorIntoViewV2'; export const MyComponent = () => { const formContainerRef = React.useRef(null); @@ -346,8 +343,6 @@ export const MyComponent = () => { ``` ### Toast / Event Message Punctuation - **Best practice:** - - If a message is a sentence or a sentence fragment with a subject and a verb, add punctuation. Otherwise, leave punctuation off. -- If a developer notices inconsistencies within files they are already working in, they can progressively fix them. In this case, be prepared to fix any Cypress test failures. +- If a developer notices inconsistencies within files they are already working in, they can progressively fix them. In this case, be prepared to fix any Cypress test failures. \ No newline at end of file diff --git a/docs/development-guide/08-testing.md b/docs/development-guide/08-testing.md index ac2e8fe9db4..8d51f0c4208 100644 --- a/docs/development-guide/08-testing.md +++ b/docs/development-guide/08-testing.md @@ -210,7 +210,6 @@ Environment variables related to Cypress logging and reporting, as well as repor |---------------------------------|----------------------------------------------------|------------------|----------------------------| | `CY_TEST_USER_REPORT` | Log test account information when tests begin | `1` | Unset; disabled by default | | `CY_TEST_JUNIT_REPORT` | Enable JUnit reporting | `1` | Unset; disabled by default | -| `CY_TEST_HTML_REPORT` | Generate html report containing E2E test results | `1` | Unset; disabled by default | | `CY_TEST_DISABLE_FILE_WATCHING` | Disable file watching in Cypress UI | `1` | Unset; disabled by default | | `CY_TEST_DISABLE_RETRIES` | Disable test retries on failure in CI | `1` | Unset; disabled by default | | `CY_TEST_FAIL_ON_MANAGED` | Fail affected tests when Managed is enabled | `1` | Unset; disabled by default | diff --git a/docs/development-guide/16-design-tokens.md b/docs/development-guide/16-design-tokens.md deleted file mode 100644 index 71225d5bc75..00000000000 --- a/docs/development-guide/16-design-tokens.md +++ /dev/null @@ -1,114 +0,0 @@ - -# Design Tokens -## Token Usage - -Our design system provides tokens via the `theme.tokens` object, which contains various token categories like spacing, color, typography, etc. These tokens are the building blocks of our design system and should be used consistently throughout the application. - -```tsx -// Accessing spacing tokens directly -theme.tokens.spacing.S16 // "1rem" - -// Accessing typography tokens -theme.tokens.alias.Typography.Heading.Xxl // "800 1.625rem/2rem 'Nunito Sans'" -``` - -### ⚠️ Warning: Global vs. Theme-Sensitive Tokens - -**Do not use `theme.tokens.color` directly in application code.** These are global tokens which are not theme-sensitive and will not respond to theme changes (light/dark mode). - -```tsx -// ❌ Incorrect: Using global color tokens directly - ({ backgroundColor: theme.tokens.color.Neutral[5] })}> - -// ✅ Correct: Using alias (semantic) color tokens - ({ backgroundColor: theme.tokens.alias.Content.Background.Normal })}> - - ({ backgroundColor: theme.tokens.alias.Content.Text.Primary })}> -``` - -### Best Practices for Token Usage - -- Use the most specific token available for your use case. -- Prefer alias or component tokens (which describe purpose) over global tokens (which describe appearance). -- For spacing, use `theme.spacingFunction()` instead of accessing tokens directly when building layouts. - -## Spacing - -### The spacingFunction - -We are transitioning from using MUI's default `theme.spacing` to our own custom `theme.spacingFunction` to ensure consistency with our design token system. - -```tsx -// ❌ Deprecated: Using MUI's default spacing - ({ padding: theme.spacing(2) })}> // 16px (2 × 8px base unit) - -// ✅ Preferred: Using our custom spacingFunction - ({ padding: theme.spacingFunction(16) })}> // "1rem" (S16 token = 16px) -``` - -#### Key Differences - -- **Direct Token Mapping**: Values map directly to our design tokens (S4 = 4px, S8 = 8px, etc.) -- **No Multiplication**: Unlike MUI's spacing that multiplies by a base unit (typically 8px), our spacingFunction maps the value directly to the closest token -- **Rounding Behavior**: For values that don't exactly match a token, the function rounds to the nearest available token - -#### Examples - -```tsx -// Direct token mapping -theme.spacingFunction(4) // "0.25rem" (S4 token = 4px) -theme.spacingFunction(8) // "0.5rem" (S8 token = 8px) -theme.spacingFunction(16) // "1rem" (S16 token = 16px) - -// Rounding behavior -theme.spacingFunction(3) // "0.25rem" (S4 token = 4px, closest to 3) -theme.spacingFunction(5) // "0.25rem" (S4 token = 4px, closest to 5) -theme.spacingFunction(7) // "0.5rem" (S8 token = 8px, closest to 7) - -// Multiple values (CSS shorthand) -theme.spacingFunction(4, 8) // "0.25rem 0.5rem" -theme.spacingFunction(4, 8, 16) // "0.25rem 0.5rem 1rem" -theme.spacingFunction(4, 8, 16, 24) // "0.25rem 0.5rem 1rem 1.5rem" -``` - -#### Migration Guide - -When migrating from `theme.spacing` to `theme.spacingFunction`, use this mapping as a reference: - -| MUI spacing | spacingFunction equivalent | -|-------------|----------------------------| -| `theme.spacing(0.5)` | `theme.spacingFunction(4)` | -| `theme.spacing(1)` | `theme.spacingFunction(8)` | -| `theme.spacing(1.5)` | `theme.spacingFunction(12)` | -| `theme.spacing(2)` | `theme.spacingFunction(16)` | -| `theme.spacing(3)` | `theme.spacingFunction(24)` | -| `theme.spacing(4)` | `theme.spacingFunction(32)` | -| `theme.spacing(5)` | `theme.spacingFunction(40)` | -| `theme.spacing(6)` | `theme.spacingFunction(48)` | -| `theme.spacing(8)` | `theme.spacingFunction(64)` | -| `theme.spacing(9)` | `theme.spacingFunction(72)` | -| `theme.spacing(12)` | `theme.spacingFunction(96)` | - -#### Available Tokens - -Our spacingFunction works with the following design tokens: - -``` -S0: "0" -S2: "0.125rem" (2px) -S4: "0.25rem" (4px) -S6: "0.375rem" (6px) -S8: "0.5rem" (8px) -S12: "0.75rem" (12px) -S16: "1rem" (16px) -S20: "1.25rem" (20px) -S24: "1.5rem" (24px) -S28: "1.75rem" (28px) -S32: "2rem" (32px) -S36: "2.25rem" (36px) -S40: "2.5rem" (40px) -S48: "3rem" (48px) -S64: "4rem" (64px) -S72: "4.5rem" (72px) -S96: "6rem" (96px) -``` diff --git a/docs/tooling/analytics.md b/docs/tooling/analytics.md index 3cd5f883d7b..e9d91cb86e2 100644 --- a/docs/tooling/analytics.md +++ b/docs/tooling/analytics.md @@ -14,6 +14,7 @@ Important notes: - Pendo is only loaded if the user has enabled Performance Cookies via OneTrust *and* if a valid `PENDO_API_KEY` is configured as an environment variable. In our development, staging, and production environments, `PENDO_API_KEY` is available at build time. See **Locally Testing Page Views & Custom Events and/or Troubleshooting Pendo** for set up with local environments. - We load the Pendo agent from the CDN, rather than [self-hosting](https://support.pendo.io/hc/en-us/articles/360038969692-Self-hosting-the-Pendo-agent), and we have configured a [CNAME](https://support.pendo.io/hc/en-us/articles/360043539891-CNAME-for-Pendo). +- We are hashing account and visitor IDs in a way that is consistent with Akamai's standards. - At initialization, we do string transformation on select URL patterns to **remove sensitive data**. When new URL patterns are added to Cloud Manager, verify that existing transforms remove sensitive data; if not, update the transforms. - Pendo will respect OneTrust cookie preferences in development, staging, and production environments and does not check cookie preferences in the local environment. - Pendo makes use of the existing `data-testid` properties, used in our automated testing, for tagging elements. They are more persistent and reliable than CSS properties, which are liable to change. @@ -23,7 +24,7 @@ Important notes: 1. Set the `REACT_APP_PENDO_API_KEY` environment variable in `.env`. 2. Use the browser tools Network tab, filter requests by "psp.cloud", and check that successful network requests have been made to load Pendo scripts (also visible in the browser tools Sources tab). 3. In the browser console, type `pendo.validateEnvironment()`. -4. You should see command output in the console, and it should include an `accountId` and a `visitorId` that correspond with your APIv4 account `euuid` and profile `uid`, respectively. Each page view change or custom event that fires should be visible as a request in the Network tab. +4. You should see command output in the console, and it should include a hashed `accountId` and hashed `visitorId`. Each page view change or custom event that fires should be visible as a request in the Network tab. 5. If the console does not output the expected ids and instead outputs something like `Cookies are disabled in Pendo config. Is this expected?` in response to the above command, clear app storage with the browser tools. Once redirected back to Login, update the OneTrust cookie settings to enable cookies via "Manage Preferences" in the banner at the bottom of the screen. Log back into Cloud Manager and Pendo should load. ## Adobe Analytics diff --git a/package.json b/package.json index 29a279af2a1..3b82c8380ea 100644 --- a/package.json +++ b/package.json @@ -2,35 +2,16 @@ "name": "root", "private": true, "license": "Apache-2.0", - "type": "module", "devDependencies": { - "@eslint/js": "^9.23.0", "concurrently": "9.1.0", "husky": "^9.1.6", "typescript": "^5.7.3", "vitest": "^3.0.7", "@vitest/ui": "^3.0.7", - "lint-staged": "^15.4.3", - "eslint": "^9.23.0", - "eslint-config-prettier": "^10.1.1", - "eslint-plugin-cypress": "^4.2.0", - "eslint-plugin-jsx-a11y": "^6.10.2", - "eslint-plugin-perfectionist": "^4.10.1", - "eslint-plugin-prettier": "~5.2.6", - "eslint-plugin-react": "^7.37.4", - "eslint-plugin-react-hooks": "^5.2.0", - "eslint-plugin-sonarjs": "^3.0.2", - "eslint-plugin-testing-library": "^7.1.1", - "eslint-plugin-xss": "^0.1.12", - "prettier": "~3.5.3", - "typescript-eslint": "^8.29.0", - "@typescript-eslint/eslint-plugin": "^8.29.0", - "@typescript-eslint/parser": "^8.29.0", - "@linode/eslint-plugin-cloud-manager": "^0.0.10", - "jiti": "^2.4.2" + "lint-staged": "^15.4.3" }, "scripts": { - "lint:all": "pnpm -r --parallel lint", + "lint": "eslint . --quiet --ext .js,.ts,.tsx", "install:all": "pnpm install --frozen-lockfile", "build:sdk": "pnpm run --filter @linode/api-v4 build", "build:validation": "pnpm run --filter @linode/validation build", @@ -38,8 +19,8 @@ "build:analyze": "pnpm run --filter linode-manager build:analyze", "bootstrap": "pnpm install:all && pnpm build:validation && pnpm build:sdk", "up:expose": "npm_config_package_import_method=clone-or-copy pnpm install:all && pnpm build:validation && pnpm build:sdk && pnpm start:all:expose", - "dev": "concurrently -n api-v4,validation,ui,utilities,queries,shared,manager -c blue,yellow,magenta,cyan,gray,blue,green \"pnpm run --filter @linode/api-v4 start\" \"pnpm run --filter @linode/validation start\" \"pnpm run --filter @linode/ui start\" \"pnpm run --filter @linode/utilities start\" \"pnpm run --filter @linode/queries start\" \"pnpm run --filter @linode/shared start\" \"pnpm run --filter linode-manager start\"", - "start:all:expose": "concurrently -n api-v4,validation,ui,utilities,queries,shared,manager -c blue,yellow,magenta,cyan,gray,blue,green \"pnpm run --filter @linode/api-v4 start\" \"pnpm run --filter @linode/validation start\" \"pnpm run --filter @linode/ui start\" \"pnpm run --filter @linode/utilities start\" \"pnpm run --filter @linode/queries start\" \"pnpm run --filter @linode/shared start\" \"pnpm run --filter linode-manager start:expose\"", + "dev": "concurrently -n api-v4,validation,ui,utilities,manager -c blue,yellow,magenta,cyan,green \"pnpm run --filter @linode/api-v4 start\" \"pnpm run --filter @linode/validation start\" \"pnpm run --filter @linode/ui start\" \"pnpm run --filter @linode/utilities start\" \"pnpm run --filter linode-manager start\"", + "start:all:expose": "concurrently -n api-v4,validation,ui,utilities,manager -c blue,yellow,magenta,cyan,green \"pnpm run --filter @linode/api-v4 start\" \"pnpm run --filter @linode/validation start\" \"pnpm run --filter @linode/ui start\" \"pnpm run --filter @linode/utilities start\" \"pnpm run --filter linode-manager start:expose\"", "start:manager": "pnpm --filter linode-manager start", "start:manager:ci": "pnpm run --filter linode-manager start:ci", "docs": "bunx vitepress@1.0.0-rc.44 dev docs", @@ -65,7 +46,7 @@ "package-versions": "pnpm run --filter @linode/scripts package-versions", "junit:summary": "pnpm run --filter @linode/scripts --silent junit:summary", "generate-tod": "pnpm run --filter @linode/scripts --silent generate-tod", - "clean": "concurrently \"rm -rf node_modules\" \"pnpm -r exec rm -rf node_modules lib dist\" \"pnpm store prune\"", + "clean": "rm -rf node_modules && rm -rf packages/manager/node_modules && rm -rf packages/api-v4/node_modules && rm -rf packages/validation/node_modules && rm -rf packages/api-v4/lib && rm -rf packages/validation/lib && rm -rf packages/ui/node_modules && rm -rf packages/utilities/node_modules", "prepare": "husky" }, "resolutions": { diff --git a/packages/api-v4/.changeset/pr-11621-upcoming-features-1738790605953.md b/packages/api-v4/.changeset/pr-11621-upcoming-features-1738790605953.md new file mode 100644 index 00000000000..774a76d0aa8 --- /dev/null +++ b/packages/api-v4/.changeset/pr-11621-upcoming-features-1738790605953.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Upcoming Features +--- + +Update region capability and Public Interface object for Linode Interfaces ([#11621](https://github.com/linode/manager/pull/11621)) diff --git a/packages/api-v4/.changeset/pr-11653-changed-1739986135472.md b/packages/api-v4/.changeset/pr-11653-changed-1739986135472.md new file mode 100644 index 00000000000..291c60cf5ac --- /dev/null +++ b/packages/api-v4/.changeset/pr-11653-changed-1739986135472.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Changed +--- + +Added `type` and `lke_cluster` to Nodebalancer interface and `getNodeBalancerBeta` function ([#11653](https://github.com/linode/manager/pull/11653)) diff --git a/packages/api-v4/.changeset/pr-11655-changed-1740493497447.md b/packages/api-v4/.changeset/pr-11655-changed-1740493497447.md new file mode 100644 index 00000000000..f6fd66e78b6 --- /dev/null +++ b/packages/api-v4/.changeset/pr-11655-changed-1740493497447.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Changed +--- + +Made `interface_generation` on `Linode` optional ([#11655](https://github.com/linode/manager/pull/11655)) diff --git a/packages/api-v4/.changeset/pr-11669-upcoming-features-1739899408035.md b/packages/api-v4/.changeset/pr-11669-upcoming-features-1739899408035.md new file mode 100644 index 00000000000..fa7f77ea7a1 --- /dev/null +++ b/packages/api-v4/.changeset/pr-11669-upcoming-features-1739899408035.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Upcoming Features +--- + +Added the payload type for EditAlertDefinition, API request changes for the user edit functionality ([#11669](https://github.com/linode/manager/pull/11669)) diff --git a/packages/api-v4/.changeset/pr-11677-changed-1739995208483.md b/packages/api-v4/.changeset/pr-11677-changed-1739995208483.md new file mode 100644 index 00000000000..add94420352 --- /dev/null +++ b/packages/api-v4/.changeset/pr-11677-changed-1739995208483.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Changed +--- + +Make `label` field in `CreateFirewallPayload` required ([#11677](https://github.com/linode/manager/pull/11677)) diff --git a/packages/api-v4/.changeset/pr-11685-upcoming-features-1739973458559.md b/packages/api-v4/.changeset/pr-11685-upcoming-features-1739973458559.md new file mode 100644 index 00000000000..6dcd9761b3d --- /dev/null +++ b/packages/api-v4/.changeset/pr-11685-upcoming-features-1739973458559.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Upcoming Features +--- + +add `getAlertDefinitionByServiceType` in alerts.ts ([#11685](https://github.com/linode/manager/pull/11685)) diff --git a/packages/api-v4/.changeset/pr-11727-upcoming-features-1740523371376.md b/packages/api-v4/.changeset/pr-11727-upcoming-features-1740523371376.md new file mode 100644 index 00000000000..acab0df136d --- /dev/null +++ b/packages/api-v4/.changeset/pr-11727-upcoming-features-1740523371376.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Upcoming Features +--- + +Update Linode Config types for Linode Interfaces ([#11727](https://github.com/linode/manager/pull/11727)) diff --git a/packages/api-v4/.changeset/pr-11735-upcoming-features-1740581707757.md b/packages/api-v4/.changeset/pr-11735-upcoming-features-1740581707757.md new file mode 100644 index 00000000000..1f29a2ddbfd --- /dev/null +++ b/packages/api-v4/.changeset/pr-11735-upcoming-features-1740581707757.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Upcoming Features +--- + +DBaaS Advanced Configurations: added `engine_config` to the Database Instance ([#11735](https://github.com/linode/manager/pull/11735)) diff --git a/packages/api-v4/.changeset/pr-11746-upcoming-features-1740774717280.md b/packages/api-v4/.changeset/pr-11746-upcoming-features-1740774717280.md new file mode 100644 index 00000000000..379dad9f117 --- /dev/null +++ b/packages/api-v4/.changeset/pr-11746-upcoming-features-1740774717280.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Upcoming Features +--- + +Use different validation schema for creating enterprise LKE cluster ([#11746](https://github.com/linode/manager/pull/11746)) diff --git a/packages/api-v4/.changeset/pr-11783-changed-1741210351721.md b/packages/api-v4/.changeset/pr-11783-changed-1741210351721.md new file mode 100644 index 00000000000..101deb73a84 --- /dev/null +++ b/packages/api-v4/.changeset/pr-11783-changed-1741210351721.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Changed +--- + +Region `Capabilities` type to temporarily include LA Disk Encryption ([#11783](https://github.com/linode/manager/pull/11783)) diff --git a/packages/api-v4/.changeset/pr-11941-tech-stories-1743701271429.md b/packages/api-v4/.changeset/pr-11941-tech-stories-1743701271429.md deleted file mode 100644 index e4690c79a77..00000000000 --- a/packages/api-v4/.changeset/pr-11941-tech-stories-1743701271429.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/api-v4": Tech Stories ---- - -Eslint Overhaul ([#11941](https://github.com/linode/manager/pull/11941)) diff --git a/packages/api-v4/.eslintrc.json b/packages/api-v4/.eslintrc.json new file mode 100644 index 00000000000..f551347ff3f --- /dev/null +++ b/packages/api-v4/.eslintrc.json @@ -0,0 +1,112 @@ +{ + "ignorePatterns": ["node_modules", "lib", "index.js", "!.eslintrc.js"], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2020, + "warnOnUnsupportedTypeScriptVersion": true + }, + "plugins": ["@typescript-eslint", "sonarjs", "prettier"], + "extends": [ + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "plugin:sonarjs/recommended", + "plugin:prettier/recommended" + ], + "rules": { + "@typescript-eslint/naming-convention": [ + "warn", + { + "format": ["camelCase", "UPPER_CASE", "PascalCase"], + "leadingUnderscore": "allow", + "selector": "variable", + "trailingUnderscore": "allow" + }, + { + "format": null, + "modifiers": ["destructured"], + "selector": "variable" + }, + { + "format": ["camelCase", "PascalCase"], + "selector": "function" + }, + { + "format": ["camelCase"], + "leadingUnderscore": "allow", + "selector": "parameter" + }, + { + "format": ["PascalCase"], + "selector": "typeLike" + } + ], + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/no-inferrable-types": "off", + "@typescript-eslint/no-namespace": "warn", + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-empty-interface": "warn", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-use-before-define": "off", + "@typescript-eslint/interface-name-prefix": "off", + "no-unused-vars": [ + "warn", + { + "argsIgnorePattern": "^_" + } + ], + "no-unused-expressions": "warn", + "no-bitwise": "error", + "no-caller": "error", + "no-eval": "error", + "no-throw-literal": "warn", + "no-loop-func": "error", + "no-await-in-loop": "error", + "array-callback-return": "error", + "no-invalid-this": "off", + "no-new-wrappers": "error", + "no-restricted-imports": ["error", "rxjs"], + "no-console": "error", + "no-undef-init": "off", + "radix": "error", + "sonarjs/cognitive-complexity": "warn", + "sonarjs/no-duplicate-string": "warn", + "sonarjs/prefer-immediate-return": "warn", + "sonarjs/no-identical-functions": "warn", + "sonarjs/no-redundant-jump": "warn", + "sonarjs/no-small-switch": "warn", + "no-multiple-empty-lines": "error", + "curly": "warn", + "sort-keys": "off", + "comma-dangle": "off", + "no-trailing-spaces": "warn", + "no-mixed-requires": "warn", + "spaced-comment": "warn", + "object-shorthand": "warn", + "prettier/prettier": "warn", + "@typescript-eslint/explicit-module-boundary-types": "off" + }, + "overrides": [ + { + "files": ["*ts"], + "rules": { + "@typescript-eslint/ban-types": [ + "warn", + { + "types": { + "String": true, + "Boolean": true, + "Number": true, + "Symbol": true, + "{}": false, + "Object": false, + "object": false, + "Function": false + }, + "extendDefaults": true + } + ] + } + } + ] +} diff --git a/packages/api-v4/CHANGELOG.md b/packages/api-v4/CHANGELOG.md index 8514e69fac1..b03c3100b3d 100644 --- a/packages/api-v4/CHANGELOG.md +++ b/packages/api-v4/CHANGELOG.md @@ -1,62 +1,3 @@ -## [2025-04-08] - v0.137.0 - - -### Added: - -- DBaaS Advanced Configurations: Add `getDatabaseEngineConfig` request to fetch all advanced configurations and updated types for advanced configs ([#11812](https://github.com/linode/manager/pull/11812)) - -### Changed: - -- DBaaS Advanced Configurations: remove `engine_config` from the DatabaseEngineConfig type ([#11885](https://github.com/linode/manager/pull/11885)) -- DBaaS Advanced Configurations: rename `restart_cluster` to `requires_restart` to align with the API response ([#11979](https://github.com/linode/manager/pull/11979)) - -### Fixed: - -- Remove trailing slash from outgoing Linode API GET request ([#11939](https://github.com/linode/manager/pull/11939)) - -### Removed: - -- DBaaS: unused functions getDatabaseType, getEngineDatabases, getDatabaseBackup ([#11909](https://github.com/linode/manager/pull/11909)) - -### Upcoming Features: - -- Add `/v4beta/nodebalancers` and `/v4/nodebalancers` endpoints for NB-VPC Integration ([#11832](https://github.com/linode/manager/pull/11832)) -- Update `ipv6` type in `CreateSubnetPayload` and rename `createSubnetSchema` to `createSubnetSchemaIPv4` ([#11896](https://github.com/linode/manager/pull/11896)) -- Update iam apis ([#11919](https://github.com/linode/manager/pull/11919)) -- Add support for IPv6 to `VPCIP` ([#11938](https://github.com/linode/manager/pull/11938)) - -## [2025-03-25] - v0.136.0 - - -### Added: - -- Add and update `/v4beta/nodebalancers` endpoints for NB-VPC Integration ([#11811](https://github.com/linode/manager/pull/11811)) - -### Changed: - -- Add `type` and `lke_cluster` to Nodebalancer interface and `getNodeBalancerBeta` function ([#11653](https://github.com/linode/manager/pull/11653)) -- Make `interface_generation` on `Linode` optional ([#11655](https://github.com/linode/manager/pull/11655)) -- Make `label` field in `CreateFirewallPayload` required ([#11677](https://github.com/linode/manager/pull/11677)) -- Region `Capabilities` type to temporarily include LA Disk Encryption ([#11783](https://github.com/linode/manager/pull/11783)) - -### Tech Stories: - -- Upgrade tsup to 8.4.0 ([#11866](https://github.com/linode/manager/pull/11866)) - -### Upcoming Features: - -- Update region capability and Public Interface object for Linode Interfaces ([#11621](https://github.com/linode/manager/pull/11621)) -- Add the payload type for EditAlertDefinition, API request changes for the user edit functionality ([#11669](https://github.com/linode/manager/pull/11669)) -- Add `getAlertDefinitionByServiceType` in alerts.ts ([#11685](https://github.com/linode/manager/pull/11685)) -- Update Linode Config types for Linode Interfaces ([#11727](https://github.com/linode/manager/pull/11727)) -- DBaaS Advanced Configurations: add `engine_config` to the Database Instance ([#11735](https://github.com/linode/manager/pull/11735)) -- Use different validation schema for creating enterprise LKE cluster ([#11746](https://github.com/linode/manager/pull/11746)) -- Add the 'account_viewer' type to the IAM types. ([#11762](https://github.com/linode/manager/pull/11762)) -- Add `EntityAlertUpdatePayload` cloudpulse types.ts ([#11785](https://github.com/linode/manager/pull/11785)) -- Switch Quota endpoints to use beta API ([#11818](https://github.com/linode/manager/pull/11818)) -- Fix the type of parameter in api in IAM ([#11840](https://github.com/linode/manager/pull/11840)) -- Add optional ipv6 property to VPC entity ([#11852](https://github.com/linode/manager/pull/11852)) - ## [2025-02-25] - v0.135.0 ### Changed: diff --git a/packages/api-v4/eslint.config.js b/packages/api-v4/eslint.config.js deleted file mode 100644 index 8818da48db2..00000000000 --- a/packages/api-v4/eslint.config.js +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from 'eslint/config'; - -import { baseConfig } from '../manager/eslint.config.js'; - -export default defineConfig({ - extends: baseConfig, -}); diff --git a/packages/api-v4/package.json b/packages/api-v4/package.json index e497a194fcc..0b2fc97ae2e 100644 --- a/packages/api-v4/package.json +++ b/packages/api-v4/package.json @@ -1,6 +1,6 @@ { "name": "@linode/api-v4", - "version": "0.137.0", + "version": "0.135.0", "homepage": "https://github.com/linode/manager/tree/develop/packages/api-v4", "bugs": { "url": "https://github.com/linode/manager/issues" @@ -41,7 +41,7 @@ "unpkg": "./lib/index.global.js", "dependencies": { "@linode/validation": "workspace:*", - "axios": "~1.8.3", + "axios": "~1.7.4", "ipaddr.js": "^2.0.0", "yup": "^1.4.0" }, @@ -58,7 +58,10 @@ "devDependencies": { "axios-mock-adapter": "^1.22.0", "concurrently": "^9.0.1", - "tsup": "^8.4.0" + "eslint": "^6.8.0", + "eslint-plugin-sonarjs": "^0.5.0", + "prettier": "~2.2.1", + "tsup": "^8.2.4" }, "lint-staged": { "*.{ts,tsx,js}": [ diff --git a/packages/api-v4/src/cloudpulse/alerts.ts b/packages/api-v4/src/cloudpulse/alerts.ts index e62401bc99c..f79104daf03 100644 --- a/packages/api-v4/src/cloudpulse/alerts.ts +++ b/packages/api-v4/src/cloudpulse/alerts.ts @@ -82,34 +82,3 @@ export const getAlertDefinitionByServiceType = (serviceType: string) => ), setMethod('GET') ); - -export const addEntityToAlert = ( - serviceType: string, - entityId: string, - data: { 'alert-definition-id': number } -) => - Request<{}>( - setURL( - `${API_ROOT}/monitor/service/${encodeURIComponent( - serviceType - )}/entity/${encodeURIComponent(entityId)}/alert-definition` - ), - setMethod('POST'), - setData(data) - ); - -export const deleteEntityFromAlert = ( - serviceType: string, - entityId: string, - alertId: number -) => - Request<{}>( - setURL( - `${API_ROOT}/monitor/service/${encodeURIComponent( - serviceType - )}/entity/${encodeURIComponent( - entityId - )}/alert-definition/${encodeURIComponent(alertId)}` - ), - setMethod('DELETE') - ); diff --git a/packages/api-v4/src/cloudpulse/types.ts b/packages/api-v4/src/cloudpulse/types.ts index ec6abdaa067..a903db83ed6 100644 --- a/packages/api-v4/src/cloudpulse/types.ts +++ b/packages/api-v4/src/cloudpulse/types.ts @@ -9,7 +9,7 @@ export type DimensionFilterOperatorType = | 'startswith' | 'endswith'; export type AlertDefinitionType = 'system' | 'user'; -export type AlertStatusType = 'enabled' | 'disabled' | 'in progress' | 'failed'; +export type AlertStatusType = 'enabled' | 'disabled'; export type CriteriaConditionType = 'ALL'; export type MetricUnitType = | 'number' @@ -328,8 +328,3 @@ export interface EditAlertPayloadWithService } export type AlertStatusUpdateType = 'Enable' | 'Disable'; - -export interface EntityAlertUpdatePayload { - entityId: string; - alert: Alert; -} diff --git a/packages/api-v4/src/databases/databases.ts b/packages/api-v4/src/databases/databases.ts index 08edf17bf50..374ed3c6519 100644 --- a/packages/api-v4/src/databases/databases.ts +++ b/packages/api-v4/src/databases/databases.ts @@ -23,7 +23,6 @@ import { SSLFields, UpdateDatabasePayload, DatabaseFork, - DatabaseEngineConfig, } from './types'; /** @@ -347,15 +346,3 @@ export const resumeDatabase = (engine: Engine, databaseID: number) => ), setMethod('POST') ); - -/** - * getConfig - * - * Return detailed list of all the configuration options - * - */ -export const getDatabaseEngineConfig = (engine: Engine) => - Request( - setURL(`${API_ROOT}/databases/${encodeURIComponent(engine)}/config`), - setMethod('GET') - ); diff --git a/packages/api-v4/src/databases/types.ts b/packages/api-v4/src/databases/types.ts index 444b391a18b..180cf098dc6 100644 --- a/packages/api-v4/src/databases/types.ts +++ b/packages/api-v4/src/databases/types.ts @@ -41,38 +41,16 @@ export type DatabaseStatus = | 'resuming' | 'suspended' | 'suspending'; -/** @deprecated TODO (UIE-8214) remove after migration */ + export type DatabaseBackupType = 'snapshot' | 'auto'; -/** @deprecated TODO (UIE-8214) remove after migration */ + export interface DatabaseBackup { id: number; type: DatabaseBackupType; label: string; created: string; } -export interface ConfigurationItem { - description?: string; - example?: string | number | boolean; - minimum?: number; // min value for the number input - maximum?: number; // max value for the number input - maxLength?: number; // max length for the text input - minLength?: number; // min length for the text input - pattern?: string; - type?: string | [string, null] | string[]; - enum?: string[]; - requires_restart?: boolean; -} - -export type ConfigValue = number | string | boolean; - -export type ConfigCategoryValues = Record; -export type DatabaseEngineConfig = Record< - string, - Record | ConfigurationItem ->; -export interface DatabaseInstanceAdvancedConfig { - [category: string]: ConfigCategoryValues | ConfigValue; -} + export interface DatabaseFork { source: number; restore_time?: string; @@ -92,7 +70,26 @@ interface DatabaseHosts { export interface SSLFields { ca_certificate: string; } - +// TODO: This will be changed in the next PR +export interface MySQLAdvancedConfig { + binlog_retention_period?: number; + advanced?: { + connect_timeout?: number; + default_time_zone?: string; + group_concat_max_len?: number; + information_schema_stats_expiry?: number; + innodb_print_all_deadlocks?: boolean; + sql_mode?: string; + }; +} +// TODO: This will be changed in the next PR +export interface PostgresAdvancedConfig { + advanced?: { + max_files_per_process?: number; + timezone?: string; + pg_stat_monitor_enable?: boolean; + }; +} type MemberType = 'primary' | 'failover'; // DatabaseInstance is the interface for the shape of data returned by the /databases/instances endpoint. @@ -121,7 +118,7 @@ export interface DatabaseInstance { updated: string; updates: UpdatesSchedule; version: string; - engine_config: DatabaseInstanceAdvancedConfig; + engine_config?: MySQLAdvancedConfig | PostgresAdvancedConfig; } export type ClusterSize = 1 | 2 | 3; @@ -234,5 +231,4 @@ export interface UpdateDatabasePayload { updates?: UpdatesSchedule; type?: string; version?: string; - engine_config?: DatabaseInstanceAdvancedConfig; } diff --git a/packages/api-v4/src/entities/entities.ts b/packages/api-v4/src/entities/entities.ts deleted file mode 100644 index 56ea45db5a6..00000000000 --- a/packages/api-v4/src/entities/entities.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ResourcePage } from 'src/types'; -import { BETA_API_ROOT } from '../constants'; -import Request, { setMethod, setURL } from '../request'; -import { AccountEntity } from './types'; - -/** - * getAccountEntities - * - * Return all entities for account. - * - */ -export const getAccountEntities = () => { - return Request>( - setURL(`${BETA_API_ROOT}/entities`), - setMethod('GET') - ); -}; diff --git a/packages/api-v4/src/entities/index.ts b/packages/api-v4/src/entities/index.ts deleted file mode 100644 index de6289aa660..00000000000 --- a/packages/api-v4/src/entities/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './entities'; - -export * from './types'; diff --git a/packages/api-v4/src/entities/types.ts b/packages/api-v4/src/entities/types.ts deleted file mode 100644 index bad27588967..00000000000 --- a/packages/api-v4/src/entities/types.ts +++ /dev/null @@ -1,17 +0,0 @@ -export type EntityType = - | 'database' - | 'domain' - | 'firewall' - | 'image' - | 'linode' - | 'longview' - | 'nodebalancer' - | 'stackscript' - | 'volume' - | 'vpc'; - -export interface AccountEntity { - label: string; - type: EntityType; - id: number; -} diff --git a/packages/api-v4/src/firewalls/types.ts b/packages/api-v4/src/firewalls/types.ts index ec3ce8dea40..4f5eca2dbce 100644 --- a/packages/api-v4/src/firewalls/types.ts +++ b/packages/api-v4/src/firewalls/types.ts @@ -48,7 +48,7 @@ export interface FirewallRuleType { export interface FirewallDeviceEntity { id: number; type: FirewallDeviceEntityType; - label: string | null; + label: string; url: string; } @@ -89,10 +89,10 @@ export interface FirewallDevicePayload { } export interface DefaultFirewallIDs { - public_interface: number | null; - vpc_interface: number | null; - linode: number | null; - nodebalancer: number | null; + public_interface: number; + vpc_interface: number; + linode: number; + nodebalancer: number; } export interface FirewallSettings { diff --git a/packages/api-v4/src/iam/iam.ts b/packages/api-v4/src/iam/iam.ts index 27940e4ba5a..2a0e70071a0 100644 --- a/packages/api-v4/src/iam/iam.ts +++ b/packages/api-v4/src/iam/iam.ts @@ -14,9 +14,9 @@ import { IamUserPermissions, IamAccountPermissions } from './types'; export const getUserPermissions = (username: string) => Request( setURL( - `${BETA_API_ROOT}/iam/users/${encodeURIComponent( + `${BETA_API_ROOT}/iam/role-permissions/users/${encodeURIComponent( username - )}/role-permissions` + )}` ), setMethod('GET') ); @@ -31,7 +31,7 @@ export const getUserPermissions = (username: string) => */ export const updateUserPermissions = ( username: string, - data: IamUserPermissions + data: Partial ) => Request( setURL( diff --git a/packages/api-v4/src/iam/types.ts b/packages/api-v4/src/iam/types.ts index 6d821d0f352..0c1ed28bbfd 100644 --- a/packages/api-v4/src/iam/types.ts +++ b/packages/api-v4/src/iam/types.ts @@ -1,39 +1,38 @@ -export type EntityTypePermissions = - | 'account' - | 'database' - | 'domain' - | 'firewall' - | 'image' +export type ResourceTypePermissions = | 'linode' - | 'longview' + | 'firewall' | 'nodebalancer' + | 'longview' + | 'domain' | 'stackscript' + | 'image' | 'volume' + | 'database' + | 'account' | 'vpc'; export type AccountAccessType = - | 'account_admin' | 'account_linode_admin' - | 'account_viewer' - | 'firewall_creator' + | 'linode_creator' | 'linode_contributor' - | 'linode_creator'; + | 'account_admin' + | 'firewall_creator'; export type RoleType = - | 'firewall_admin' - | 'firewall_creator' | 'linode_contributor' - | 'linode_creator' | 'linode_viewer' - | 'update_firewall'; + | 'firewall_admin' + | 'linode_creator' + | 'update_firewall' + | 'firewall_creator'; export interface IamUserPermissions { account_access: AccountAccessType[]; - entity_access: EntityAccess[]; + resource_access: ResourceAccess[]; } -export interface EntityAccess { - id: number; - type: EntityTypePermissions; +export interface ResourceAccess { + resource_id: number; + resource_type: ResourceTypePermissions; roles: RoleType[]; } @@ -196,11 +195,11 @@ export type PermissionType = export interface IamAccountPermissions { account_access: IamAccess[]; - entity_access: IamAccess[]; + resource_access: IamAccess[]; } export interface IamAccess { - type: EntityTypePermissions; + resource_type: ResourceTypePermissions; roles: Roles[]; } diff --git a/packages/api-v4/src/index.ts b/packages/api-v4/src/index.ts index a1a181f55cf..c8eea9ea812 100644 --- a/packages/api-v4/src/index.ts +++ b/packages/api-v4/src/index.ts @@ -56,7 +56,7 @@ export * from './betas'; export * from './iam'; -export * from './entities'; +export * from './resources'; export { baseRequest, diff --git a/packages/api-v4/src/kubernetes/kubernetes.ts b/packages/api-v4/src/kubernetes/kubernetes.ts index faba1e5ba66..029bd0df976 100644 --- a/packages/api-v4/src/kubernetes/kubernetes.ts +++ b/packages/api-v4/src/kubernetes/kubernetes.ts @@ -1,4 +1,7 @@ -import { createKubeClusterSchema } from '@linode/validation/lib/kubernetes.schema'; +import { + createKubeClusterSchema, + createKubeEnterpriseClusterSchema, +} from '@linode/validation/lib/kubernetes.schema'; import { API_ROOT, BETA_API_ROOT } from '../constants'; import Request, { setData, @@ -94,7 +97,12 @@ export const createKubernetesClusterBeta = (data: CreateKubeClusterPayload) => { return Request( setMethod('POST'), setURL(`${BETA_API_ROOT}/lke/clusters`), - setData(data, createKubeClusterSchema) + setData( + data, + data.tier === 'enterprise' + ? createKubeEnterpriseClusterSchema + : createKubeClusterSchema + ) ); }; diff --git a/packages/api-v4/src/linodes/linodes.ts b/packages/api-v4/src/linodes/linodes.ts index c69795ffad7..57cf449c934 100644 --- a/packages/api-v4/src/linodes/linodes.ts +++ b/packages/api-v4/src/linodes/linodes.ts @@ -72,7 +72,7 @@ export const getLinodeVolumes = ( */ export const getLinodes = (params?: Params, filter?: Filter) => Request>( - setURL(`${API_ROOT}/linode/instances`), + setURL(`${API_ROOT}/linode/instances/`), setMethod('GET'), setXFilter(filter), setParams(params) diff --git a/packages/api-v4/src/linodes/types.ts b/packages/api-v4/src/linodes/types.ts index bf83834138a..557504d6341 100644 --- a/packages/api-v4/src/linodes/types.ts +++ b/packages/api-v4/src/linodes/types.ts @@ -201,23 +201,7 @@ export interface Interface { ip_ranges?: string[]; } -export interface InterfacePayload { - /** - * Required to specify a VLAN - */ - label?: string | null; - purpose: InterfacePurpose; - /** - * Used for VLAN, but is optional - */ - ipam_address?: string | null; - primary?: boolean; - subnet_id?: number | null; - vpc_id?: number | null; - ipv4?: ConfigInterfaceIPv4; - ipv6?: ConfigInterfaceIPv6; - ip_ranges?: string[] | null; -} +export type InterfacePayload = Omit; export interface ConfigInterfaceOrderPayload { ids: number[]; @@ -543,7 +527,7 @@ export interface CreateLinodeRequest { * This is used to set the swap disk size for the newly-created Linode. * @default 512 */ - swap_size?: number | null; + swap_size?: number; /** * An Image ID to deploy the Linode Disk from. */ @@ -556,7 +540,7 @@ export interface CreateLinodeRequest { * A list of public SSH keys that will be automatically appended to the root user’s * `~/.ssh/authorized_keys`file when deploying from an Image. */ - authorized_keys?: string[] | null; + authorized_keys?: string[]; /** * If this field is set to true, the created Linode will automatically be enrolled in the Linode Backup service. * This will incur an additional charge. The cost for the Backup service is dependent on the Type of Linode deployed. @@ -565,7 +549,7 @@ export interface CreateLinodeRequest { * * @default false */ - backups_enabled?: boolean | null; + backups_enabled?: boolean; /** * This field is required only if the StackScript being deployed requires input data from the User for successful completion */ @@ -576,29 +560,29 @@ export interface CreateLinodeRequest { * @default true if the Linode is created with an Image or from a Backup. * @default false if using new Linode Interfaces and no interfaces are defined */ - booted?: boolean | null; + booted?: boolean; /** * The Linode’s label is for display purposes only. * If no label is provided for a Linode, a default will be assigned. */ - label?: string | null; + label?: string; /** * An array of tags applied to this object. * * Tags are for organizational purposes only. */ - tags?: string[] | null; + tags?: string[]; /** * If true, the created Linode will have private networking enabled and assigned a private IPv4 address. * @default false */ - private_ip?: boolean | null; + private_ip?: boolean; /** * A list of usernames. If the usernames have associated SSH keys, * the keys will be appended to the root users `~/.ssh/authorized_keys` * file automatically when deploying from an Image. */ - authorized_users?: string[] | null; + authorized_users?: string[]; /** * An array of Network Interfaces to add to this Linode’s Configuration Profile. */ @@ -614,7 +598,7 @@ export interface CreateLinodeRequest { * * Default value on depends on interfaces_for_new_linodes field in AccountSettings object. */ - interface_generation?: InterfaceGenerationType | null; + interface_generation?: InterfaceGenerationType; /** * Default value mirrors network_helper in AccountSettings object. Should only be * present when using Linode Interfaces. @@ -628,7 +612,7 @@ export interface CreateLinodeRequest { /** * An object containing user-defined data relevant to the creation of Linodes. */ - metadata?: UserData | null; + metadata?: UserData; /** * The `id` of the Firewall to attach this Linode to upon creation. */ @@ -636,12 +620,12 @@ export interface CreateLinodeRequest { /** * An object that assigns this the Linode to a placement group upon creation. */ - placement_group?: CreateLinodePlacementGroupPayload | null; + placement_group?: CreateLinodePlacementGroupPayload; /** * A property with a string literal type indicating whether the Linode is encrypted or unencrypted. * @default 'enabled' (if the region supports LDE) */ - disk_encryption?: EncryptionStatus | null; + disk_encryption?: EncryptionStatus; } export interface MigrateLinodeRequest { diff --git a/packages/api-v4/src/nodebalancers/nodebalancer-config-nodes.ts b/packages/api-v4/src/nodebalancers/nodebalancer-config-nodes.ts index ccff24807b7..20b9d7df86a 100644 --- a/packages/api-v4/src/nodebalancers/nodebalancer-config-nodes.ts +++ b/packages/api-v4/src/nodebalancers/nodebalancer-config-nodes.ts @@ -1,5 +1,5 @@ import { nodeBalancerConfigNodeSchema } from '@linode/validation/lib/nodebalancers.schema'; -import { API_ROOT, BETA_API_ROOT } from '../constants'; +import { API_ROOT } from '../constants'; import Request, { setData, setMethod, setURL } from '../request'; import { ResourcePage as Page } from '../types'; import { @@ -31,28 +31,6 @@ export const getNodeBalancerConfigNodes = ( ) ); -/** - * getNodeBalancerConfigNodesBeta - * - * Returns a paginated list of nodes for the specified NodeBalancer configuration profile from the beta API. - * Note: Returns the vpc_config_id in case of a VPC backend - * - * @param nodeBalancerId { number } The ID of the NodeBalancer the config belongs to. - * @param configId { number } The configuration profile to retrieve nodes for. - */ -export const getNodeBalancerConfigNodesBeta = ( - nodeBalancerId: number, - configId: number -) => - Request>( - setMethod('GET'), - setURL( - `${BETA_API_ROOT}/nodebalancers/${encodeURIComponent( - nodeBalancerId - )}/configs/${encodeURIComponent(configId)}/nodes` - ) - ); - /** * getNodeBalancerConfigNode * @@ -77,32 +55,6 @@ export const getNodeBalancerConfigNode = ( )}` ) ); -/** - * getNodeBalancerConfigNodeBeta - * - * Returns details about a specific node for the given NodeBalancer configuration profile from the beta API. - * - * Note: Returns the vpc_config_id in case of a VPC backend - * - * @param nodeBalancerId { number } The ID of the NodeBalancer the config belongs to. - * @param configId { number } The configuration profile to retrieve nodes for. - * @param nodeId { number } The Node to be retrieved. - */ -export const getNodeBalancerConfigNodeBeta = ( - nodeBalancerId: number, - configId: number, - nodeId: number -) => - Request>( - setMethod('GET'), - setURL( - `${BETA_API_ROOT}/nodebalancers/${encodeURIComponent( - nodeBalancerId - )}/configs/${encodeURIComponent(configId)}/nodes/${encodeURIComponent( - nodeId - )}` - ) - ); /** * createNodeBalancerConfigNode * @@ -143,34 +95,7 @@ export const createNodeBalancerConfigNode = ( ); /** - * createNodeBalancerConfigNodeBeta - * - * Creates a NodeBalancer Node, a backend that can accept traffic for - * this NodeBalancer Config. Nodes are routed requests on the configured port based on their status. - * - * Note: The BETA version accepts a Node's VPC IP address and subnet-id - * - * @param nodeBalancerId { number } The ID of the NodeBalancer the config belongs to. - * @param configId { number } The configuration profile to add a node to. - * @param data - */ -export const createNodeBalancerConfigNodeBeta = ( - nodeBalancerId: number, - configId: number, - data: CreateNodeBalancerConfigNode -) => - Request( - setMethod('POST'), - setURL( - `${BETA_API_ROOT}/nodebalancers/${encodeURIComponent( - nodeBalancerId - )}/configs/${encodeURIComponent(configId)}/nodes` - ), - setData(data, nodeBalancerConfigNodeSchema, mergeAddressAndPort) - ); - -/** - * UpdateNodeBalancerConfigNode + * createNodeBalancerConfigNode * * Updates a backend node for the specified NodeBalancer configuration profile. * @@ -210,34 +135,6 @@ export const updateNodeBalancerConfigNode = ( setData(data, nodeBalancerConfigNodeSchema, mergeAddressAndPort) ); -/** - * UpdateNodeBalancerConfigNodeBeta - * - * Updates a backend node for the specified NodeBalancer configuration profile. - * - * Note: The BETA version accepts a Node's VPC IP address and subnet-id - * - * @param nodeBalancerId { number } The ID of the NodeBalancer the config belongs to. - * @param configId { number } The configuration profile to add a node to. - * @param data - */ -export const updateNodeBalancerConfigNodeBeta = ( - nodeBalancerId: number, - configId: number, - nodeId: number, - data: UpdateNodeBalancerConfigNode -) => - Request( - setMethod('PUT'), - setURL( - `${BETA_API_ROOT}/nodebalancers/${encodeURIComponent( - nodeBalancerId - )}/configs/${encodeURIComponent(configId)}/nodes/${encodeURIComponent( - nodeId - )}` - ), - setData(data, nodeBalancerConfigNodeSchema, mergeAddressAndPort) - ); /** * deleteNodeBalancerConfigNode * diff --git a/packages/api-v4/src/nodebalancers/nodebalancer-configs.ts b/packages/api-v4/src/nodebalancers/nodebalancer-configs.ts index 2a2806e0712..ac81afa6c28 100644 --- a/packages/api-v4/src/nodebalancers/nodebalancer-configs.ts +++ b/packages/api-v4/src/nodebalancers/nodebalancer-configs.ts @@ -2,19 +2,15 @@ import { createNodeBalancerConfigSchema, UpdateNodeBalancerConfigSchema, } from '@linode/validation/lib/nodebalancers.schema'; -import { API_ROOT, BETA_API_ROOT } from '../constants'; +import { API_ROOT } from '../constants'; import Request, { setData, setMethod, setParams, setURL } from '../request'; import { ResourcePage as Page, Params } from '../types'; import { CreateNodeBalancerConfig, NodeBalancerConfig, - RebuildNodeBalancerConfig, UpdateNodeBalancerConfig, } from './types'; -import { - combineConfigNodeAddressAndPort, - combineConfigNodeAddressAndPortBeta, -} from './utils'; +import { combineConfigNodeAddressAndPort } from './utils'; /** * getNodeBalancerConfigs @@ -79,88 +75,6 @@ export const createNodeBalancerConfig = ( ) ); -/** - * createNodeBalancerConfigBeta - * - * Creates a NodeBalancer Config, which allows the NodeBalancer to accept traffic on a new port. - * You will need to add NodeBalancer Nodes to the new Config before it can actually serve requests. - * - * Note: The BETA version accepts a Node's VPC IP address and subnet-id - * - * @param nodeBalancerId { number } The NodeBalancer to receive the new config. - */ -export const createNodeBalancerConfigBeta = ( - nodeBalancerId: number, - data: CreateNodeBalancerConfig -) => - Request( - setMethod('POST'), - setURL( - `${BETA_API_ROOT}/nodebalancers/${encodeURIComponent( - nodeBalancerId - )}/configs` - ), - setData( - data, - createNodeBalancerConfigSchema, - combineConfigNodeAddressAndPortBeta - ) - ); - -/** - * rebuildNodeBalancerConfig - * - * Rebuilds a NodeBalancer Config and its Nodes that you have permission to modify. - * - * @param nodeBalancerId { number } The NodeBalancer to receive the new config. - * @param configId { number } The ID of the configuration profile to be updated - */ -export const rebuildNodeBalancerConfig = ( - nodeBalancerId: number, - configId: number, - data: RebuildNodeBalancerConfig -) => - Request( - setMethod('POST'), - setURL( - `${API_ROOT}/nodebalancers/${encodeURIComponent( - nodeBalancerId - )}/configs/${encodeURIComponent(configId)}/rebuild` - ), - setData( - data, - createNodeBalancerConfigSchema, - combineConfigNodeAddressAndPort - ) - ); - -/** - * rebuildNodeBalancerConfigBeta - * - * Rebuilds a NodeBalancer Config and its Nodes that you have permission to modify. - * - * @param nodeBalancerId { number } The NodeBalancer to receive the new config. - * @param configId { number } The ID of the configuration profile to be updated - */ -export const rebuildNodeBalancerConfigBeta = ( - nodeBalancerId: number, - configId: number, - data: RebuildNodeBalancerConfig -) => - Request( - setMethod('POST'), - setURL( - `${BETA_API_ROOT}/nodebalancers/${encodeURIComponent( - nodeBalancerId - )}/configs/${encodeURIComponent(configId)}/rebuild` - ), - setData( - data, - createNodeBalancerConfigSchema, - combineConfigNodeAddressAndPortBeta - ) - ); - /** * updateNodeBalancerConfig * diff --git a/packages/api-v4/src/nodebalancers/nodebalancers.ts b/packages/api-v4/src/nodebalancers/nodebalancers.ts index 8560eafe0eb..35f378bdea4 100644 --- a/packages/api-v4/src/nodebalancers/nodebalancers.ts +++ b/packages/api-v4/src/nodebalancers/nodebalancers.ts @@ -15,12 +15,8 @@ import type { CreateNodeBalancerPayload, NodeBalancer, NodeBalancerStats, - NodebalancerVpcConfig, } from './types'; -import { - combineNodeBalancerConfigNodeAddressAndPort, - combineNodeBalancerConfigNodeAddressAndPortBeta, -} from './utils'; +import { combineNodeBalancerConfigNodeAddressAndPort } from './utils'; import type { Firewall } from '../firewalls/types'; /** @@ -99,22 +95,6 @@ export const createNodeBalancer = (data: CreateNodeBalancerPayload) => ) ); -/** - * createNodeBalancerBeta - * - * Add a NodeBalancer to your account using the beta API - */ -export const createNodeBalancerBeta = (data: CreateNodeBalancerPayload) => - Request( - setMethod('POST'), - setURL(`${BETA_API_ROOT}/nodebalancers`), - setData( - data, - NodeBalancerSchema, - combineNodeBalancerConfigNodeAddressAndPortBeta - ) - ); - /** * deleteNodeBalancer * @@ -178,45 +158,3 @@ export const getNodeBalancerTypes = (params?: Params) => setMethod('GET'), setParams(params) ); - -/** - * getNodeBalancerVPCConfigsBeta - * - * View all VPC Config information for this NodeBalancer - * - * @param nodeBalancerId { number } The ID of the NodeBalancer to view vpc config info for. - */ -export const getNodeBalancerVPCConfigsBeta = ( - nodeBalancerId: number, - params?: Params, - filter?: Filter -) => - Request>( - setURL( - `${BETA_API_ROOT}/nodebalancers/${encodeURIComponent( - nodeBalancerId - )}/vpcs` - ), - setMethod('GET'), - setXFilter(filter), - setParams(params) - ); -/** - * getNodeBalancerVPCConfigBeta - * - * View VPC Config information for this NodeBalancer and VPC Config id - * - * @param nodeBalancerId { number } The ID of the NodeBalancer to view vpc config info for. - */ -export const getNodeBalancerVPCConfigBeta = ( - nodeBalancerId: number, - nbVpcConfigId: number -) => - Request( - setURL( - `${BETA_API_ROOT}/nodebalancers/${encodeURIComponent( - nodeBalancerId - )}/vpcs/${encodeURIComponent(nbVpcConfigId)}` - ), - setMethod('GET') - ); diff --git a/packages/api-v4/src/nodebalancers/types.ts b/packages/api-v4/src/nodebalancers/types.ts index 0ce208c81fa..f969daa2c4c 100644 --- a/packages/api-v4/src/nodebalancers/types.ts +++ b/packages/api-v4/src/nodebalancers/types.ts @@ -134,15 +134,6 @@ export interface NodeBalancerStats { }; } -export interface NodebalancerVpcConfig { - id: number; - nodebalancer_id: number; - vpc_id: number; - subnet_id: number; - ipv4_range: string | null; - ipv6_range: string | null; -} - export interface CreateNodeBalancerConfig { port?: number; /** @@ -190,13 +181,10 @@ export interface CreateNodeBalancerConfig { cipher_suite?: 'recommended' | 'legacy' | 'none'; ssl_cert?: string; ssl_key?: string; - nodes?: CreateNodeBalancerConfigNode[]; } export type UpdateNodeBalancerConfig = CreateNodeBalancerConfig; -export type RebuildNodeBalancerConfig = CreateNodeBalancerConfig; - export interface CreateNodeBalancerConfigNode { address: string; label: string; @@ -205,7 +193,6 @@ export interface CreateNodeBalancerConfigNode { */ mode?: NodeBalancerConfigNodeMode; weight?: number; - subnet_id?: number; } export type UpdateNodeBalancerConfigNode = Partial; @@ -219,7 +206,6 @@ export interface NodeBalancerConfigNode { nodebalancer_id: number; status: 'unknown' | 'UP' | 'DOWN'; weight: number; - vpc_config_id?: number | null; } export interface NodeBalancerConfigNodeWithPort extends NodeBalancerConfigNode { @@ -246,9 +232,4 @@ export interface CreateNodeBalancerPayload { configs: CreateNodeBalancerConfig[]; firewall_id?: number; tags?: string[]; - vpcs?: { - subnet_id: number; - ipv4_range: string; - ipv6_range?: string; - }[]; } diff --git a/packages/api-v4/src/nodebalancers/utils.ts b/packages/api-v4/src/nodebalancers/utils.ts index 17d4fdab160..42f81e54700 100644 --- a/packages/api-v4/src/nodebalancers/utils.ts +++ b/packages/api-v4/src/nodebalancers/utils.ts @@ -10,17 +10,6 @@ export const combineConfigNodeAddressAndPort = (data: any) => ({ })), }); -export const combineConfigNodeAddressAndPortBeta = (data: any) => ({ - ...data, - nodes: data.nodes.map((n: any) => ({ - address: `${n.address}:${n.port}`, - label: n.label, - mode: n.mode, - weight: n.weight, - subnet_id: n.subnet_id, - })), -}); - export const combineNodeBalancerConfigNodeAddressAndPort = (data: any) => ({ ...data, configs: data.configs.map((c: any) => ({ @@ -34,20 +23,6 @@ export const combineNodeBalancerConfigNodeAddressAndPort = (data: any) => ({ })), }); -export const combineNodeBalancerConfigNodeAddressAndPortBeta = (data: any) => ({ - ...data, - configs: data.configs.map((c: any) => ({ - ...c, - nodes: c.nodes.map((n: any) => ({ - address: `${n.address}:${n.port}`, - label: n.label, - mode: n.mode, - weight: n.weight, - subnet_id: n.subnet_id, - })), - })), -}); - export const mergeAddressAndPort = (node: NodeBalancerConfigNodeWithPort) => ({ ...node, address: `${node.address}:${node.port}`, diff --git a/packages/api-v4/src/quotas/quotas.ts b/packages/api-v4/src/quotas/quotas.ts index 6e790425bb5..dfdd5e8eed1 100644 --- a/packages/api-v4/src/quotas/quotas.ts +++ b/packages/api-v4/src/quotas/quotas.ts @@ -1,5 +1,5 @@ import { Filter, Params, ResourcePage as Page } from 'src/types'; -import { BETA_API_ROOT } from '../constants'; +import { API_ROOT } from '../constants'; import Request, { setMethod, setParams, setURL, setXFilter } from '../request'; import { Quota, QuotaType, QuotaUsage } from './types'; @@ -12,10 +12,7 @@ import { Quota, QuotaType, QuotaUsage } from './types'; * @param id { number } the quota ID to look up. */ export const getQuota = (type: QuotaType, id: number) => - Request( - setURL(`${BETA_API_ROOT}/${type}/quotas/${id}`), - setMethod('GET') - ); + Request(setURL(`${API_ROOT}/${type}/quotas/${id}`), setMethod('GET')); /** * getQuotas @@ -32,7 +29,7 @@ export const getQuotas = ( filter: Filter = {} ) => Request>( - setURL(`${BETA_API_ROOT}/${type}/quotas`), + setURL(`${API_ROOT}/${type}/quotas`), setMethod('GET'), setXFilter(filter), setParams(params) @@ -48,6 +45,6 @@ export const getQuotas = ( */ export const getQuotaUsage = (type: QuotaType, id: number) => Request( - setURL(`${BETA_API_ROOT}/${type}/quotas/${id}/usage`), + setURL(`${API_ROOT}/${type}/quotas/${id}/usage`), setMethod('GET') ); diff --git a/packages/api-v4/src/request.ts b/packages/api-v4/src/request.ts index c63a5198048..926e6a5bd57 100644 --- a/packages/api-v4/src/request.ts +++ b/packages/api-v4/src/request.ts @@ -11,8 +11,6 @@ interface RequestConfig extends AxiosRequestConfig { validationErrors?: APIError[]; } -type RequestConfigFn = (config: RequestConfig) => RequestConfig; - type ConfigField = 'headers' | 'data' | 'params' | 'method' | 'url'; export const baseRequest = Axios.create({ @@ -73,13 +71,11 @@ export const setMethod = (method: 'GET' | 'POST' | 'PUT' | 'DELETE') => /** Param */ export const setParams = (params: Params | undefined) => set('params', params); -export const setHeaders = - (newHeaders: any = {}) => - (object: any) => { - return !isEmpty(newHeaders) - ? { ...object, headers: { ...object.headers, ...newHeaders } } - : object; - }; +export const setHeaders = (newHeaders: any = {}) => (object: any) => { + return !isEmpty(newHeaders) + ? { ...object, headers: { ...object.headers, ...newHeaders } } + : object; +}; /** * Validate and set data in the request configuration object. @@ -97,7 +93,7 @@ export const setData = ( * object, after the validation has happened. Use with caution: It was created as a trap door for * merging IPv4 addresses and ports in the NodeBalancer creation flow. */ - postValidationTransform?: (_: any) => any, + postValidationTransform?: (_: any) => any ): any => { if (!schema) { return set('data', data); @@ -125,7 +121,7 @@ export const setData = ( * to itself since we have nested structures (think NodeBalancers). */ export const convertYupToLinodeErrors = ( - validationError: ValidationError, + validationError: ValidationError ): APIError[] => { const { inner } = validationError; @@ -173,18 +169,18 @@ export const setXFilter = (xFilter: Filter | undefined) => { * is an error. * @param fns An array of functions to be applied to the config object. */ -const reduceRequestConfig = (...fns: RequestConfigFn[]): RequestConfig => - fns.reduceRight((result, fn) => fn(result), { +const reduceRequestConfig = (...fns: Function[]): RequestConfig => + fns.reduceRight((result, fn) => fn(result), { url: 'https://api.linode.com/v4', headers: {}, }); /** Generator */ -export const requestGenerator = (...fns: RequestConfigFn[]): Promise => { +export const requestGenerator = (...fns: Function[]): Promise => { const config = reduceRequestConfig(...fns); if (config.validationErrors) { return Promise.reject( - config.validationErrors, // All failed requests, client or server errors, should be APIError[] + config.validationErrors // All failed requests, client or server errors, should be APIError[] ); } return baseRequest(config).then((response) => response.data); @@ -203,7 +199,7 @@ export const requestGenerator = (...fns: RequestConfigFn[]): Promise => { export const mockAPIError = ( status: number = 400, statusText: string = 'Internal Server Error', - data: any = {}, + data: any = {} ): Promise => new Promise((resolve, reject) => setTimeout( @@ -217,10 +213,10 @@ export const mockAPIError = ( config: { headers: new AxiosHeaders(), }, - }), + }) ), - process.env.NODE_ENV === 'test' ? 0 : 250, - ), + process.env.NODE_ENV === 'test' ? 0 : 250 + ) ); const createError = (message: string, response: AxiosResponse) => { @@ -235,7 +231,7 @@ export interface CancellableRequest { } export const CancellableRequest = ( - ...fns: RequestConfigFn[] + ...fns: Function[] ): CancellableRequest => { const config = reduceRequestConfig(...fns); const source = Axios.CancelToken.source(); @@ -255,7 +251,7 @@ export const CancellableRequest = ( cancel: source.cancel, request: () => baseRequest({ ...config, cancelToken: source.token }).then( - (response) => response.data, + (response) => response.data ), }; }; diff --git a/packages/api-v4/src/resources/index.ts b/packages/api-v4/src/resources/index.ts new file mode 100644 index 00000000000..b8a322debd8 --- /dev/null +++ b/packages/api-v4/src/resources/index.ts @@ -0,0 +1,3 @@ +export * from './resources'; + +export * from './types'; diff --git a/packages/api-v4/src/resources/resources.ts b/packages/api-v4/src/resources/resources.ts new file mode 100644 index 00000000000..55a576694bd --- /dev/null +++ b/packages/api-v4/src/resources/resources.ts @@ -0,0 +1,16 @@ +import { BETA_API_ROOT } from '../constants'; +import Request, { setMethod, setURL } from '../request'; +import { IamAccountResource } from './types'; + +/** + * getAccountResources + * + * Return all resources for account. + * + */ +export const getAccountResources = () => { + return Request( + setURL(`${BETA_API_ROOT}/resources`), + setMethod('GET') + ); +}; diff --git a/packages/api-v4/src/resources/types.ts b/packages/api-v4/src/resources/types.ts new file mode 100644 index 00000000000..680aa4c03a5 --- /dev/null +++ b/packages/api-v4/src/resources/types.ts @@ -0,0 +1,21 @@ +export type ResourceType = + | 'linode' + | 'firewall' + | 'nodebalancer' + | 'longview' + | 'domain' + | 'stackscript' + | 'image' + | 'volume' + | 'database' + | 'vpc'; + +export interface IamAccountResource { + resource_type: ResourceType; + resources: Resource[]; +} + +export interface Resource { + name: string; + id: number; +} diff --git a/packages/api-v4/src/vpcs/types.ts b/packages/api-v4/src/vpcs/types.ts index 2626c8de526..cf15d1112dd 100644 --- a/packages/api-v4/src/vpcs/types.ts +++ b/packages/api-v4/src/vpcs/types.ts @@ -1,11 +1,3 @@ -interface VPCIPv6 { - range?: string; -} - -interface CreateVPCIPv6 extends VPCIPv6 { - allocation_class?: string; -} - export interface VPC { id: number; label: string; @@ -14,14 +6,12 @@ export interface VPC { subnets: Subnet[]; created: string; updated: string; - ipv6?: VPCIPv6[]; } export interface CreateVPCPayload { label: string; description?: string; region: string; - ipv6?: CreateVPCIPv6[]; subnets?: CreateSubnetPayload[]; } @@ -30,14 +20,10 @@ export interface UpdateVPCPayload { description?: string; } -interface VPCIPv6Subnet { - range: string; -} - export interface CreateSubnetPayload { label: string; ipv4?: string; - ipv6?: VPCIPv6Subnet[]; + ipv6?: string; } export interface Subnet extends CreateSubnetPayload { @@ -66,11 +52,6 @@ export interface VPCIP { active: boolean; address: string | null; address_range: string | null; - ipv6_range: string | null; - ipv6_is_public: boolean | null; - ipv6_addresses: { - slaac_address: string; - }[]; config_id: number | null; gateway: string | null; interface_id: number; diff --git a/packages/api-v4/src/vpcs/vpcs.ts b/packages/api-v4/src/vpcs/vpcs.ts index 88daa6015bf..30254700f6e 100644 --- a/packages/api-v4/src/vpcs/vpcs.ts +++ b/packages/api-v4/src/vpcs/vpcs.ts @@ -1,5 +1,5 @@ import { - createSubnetSchemaIPv4, + createSubnetSchema, createVPCSchema, modifySubnetSchema, updateVPCSchema, @@ -129,7 +129,7 @@ export const createSubnet = (vpcID: number, data: CreateSubnetPayload) => Request( setURL(`${API_ROOT}/vpcs/${encodeURIComponent(vpcID)}/subnets`), setMethod('POST'), - setData(data, createSubnetSchemaIPv4) + setData(data, createSubnetSchema) ); /** diff --git a/packages/manager/.changeset/pr-10806-tests-1725030349963.md b/packages/manager/.changeset/pr-10806-tests-1725030349963.md new file mode 100644 index 00000000000..6bc32665a7d --- /dev/null +++ b/packages/manager/.changeset/pr-10806-tests-1725030349963.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Add Cypress integration test to enable Linode Managed ([#10806](https://github.com/linode/manager/pull/10806)) diff --git a/packages/manager/.changeset/pr-11362-tests-1740410848045.md b/packages/manager/.changeset/pr-11362-tests-1740410848045.md new file mode 100644 index 00000000000..99a0e353749 --- /dev/null +++ b/packages/manager/.changeset/pr-11362-tests-1740410848045.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Improve Cypress test VLAN handling ([#11362](https://github.com/linode/manager/pull/11362)) diff --git a/packages/manager/.changeset/pr-11581-tech-stories-1738841023177.md b/packages/manager/.changeset/pr-11581-tech-stories-1738841023177.md new file mode 100644 index 00000000000..4ea6dd03114 --- /dev/null +++ b/packages/manager/.changeset/pr-11581-tech-stories-1738841023177.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Optimize CreateCluster component: Use React Hook Form ([#11581](https://github.com/linode/manager/pull/11581)) diff --git a/packages/manager/.changeset/pr-11607-tests-1738694493702.md b/packages/manager/.changeset/pr-11607-tests-1738694493702.md new file mode 100644 index 00000000000..21643289c99 --- /dev/null +++ b/packages/manager/.changeset/pr-11607-tests-1738694493702.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Add Cypress test for Service Transfers fetch error ([#11607](https://github.com/linode/manager/pull/11607)) diff --git a/packages/manager/.changeset/pr-11629-tech-stories-1738888746194.md b/packages/manager/.changeset/pr-11629-tech-stories-1738888746194.md new file mode 100644 index 00000000000..f8a8d3231a3 --- /dev/null +++ b/packages/manager/.changeset/pr-11629-tech-stories-1738888746194.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Refactor the Linode Rebuild dialog ([#11629](https://github.com/linode/manager/pull/11629)) diff --git a/packages/manager/.changeset/pr-11647-upcoming-features-1739465882408.md b/packages/manager/.changeset/pr-11647-upcoming-features-1739465882408.md new file mode 100644 index 00000000000..e934362a18f --- /dev/null +++ b/packages/manager/.changeset/pr-11647-upcoming-features-1739465882408.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Build new Quotas Controls ([#11647](https://github.com/linode/manager/pull/11647)) diff --git a/packages/manager/.changeset/pr-11653-added-1739985989958.md b/packages/manager/.changeset/pr-11653-added-1739985989958.md new file mode 100644 index 00000000000..d4d78d51040 --- /dev/null +++ b/packages/manager/.changeset/pr-11653-added-1739985989958.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Added +--- + +LKE UI updates for checkout bar & Nodebalancer Details summary ([#11653](https://github.com/linode/manager/pull/11653)) diff --git a/packages/manager/.changeset/pr-11655-upcoming-features-1739459039200.md b/packages/manager/.changeset/pr-11655-upcoming-features-1739459039200.md new file mode 100644 index 00000000000..7a97e405ec5 --- /dev/null +++ b/packages/manager/.changeset/pr-11655-upcoming-features-1739459039200.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add Linode Interfaces Table to the Linode Details page ([#11655](https://github.com/linode/manager/pull/11655)) diff --git a/packages/manager/.changeset/pr-11662-fixed-1739467765713.md b/packages/manager/.changeset/pr-11662-fixed-1739467765713.md new file mode 100644 index 00000000000..55c42293fc6 --- /dev/null +++ b/packages/manager/.changeset/pr-11662-fixed-1739467765713.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Document titles of ACPL with appropriate keyword ([#11662](https://github.com/linode/manager/pull/11662)) diff --git a/packages/manager/.changeset/pr-11663-tests-1740588226694.md b/packages/manager/.changeset/pr-11663-tests-1740588226694.md new file mode 100644 index 00000000000..d95f0db5e54 --- /dev/null +++ b/packages/manager/.changeset/pr-11663-tests-1740588226694.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Add Cypress tests for restricted user Linode create flow ([#11663](https://github.com/linode/manager/pull/11663)) diff --git a/packages/manager/.changeset/pr-11664-changed-1739979366424.md b/packages/manager/.changeset/pr-11664-changed-1739979366424.md new file mode 100644 index 00000000000..6bd40d0fc99 --- /dev/null +++ b/packages/manager/.changeset/pr-11664-changed-1739979366424.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +Update copy in Node Pool resize, autoscale, and recycle CTAs ([#11664](https://github.com/linode/manager/pull/11664)) diff --git a/packages/manager/.changeset/pr-11664-upcoming-features-1739979306430.md b/packages/manager/.changeset/pr-11664-upcoming-features-1739979306430.md new file mode 100644 index 00000000000..3425af6a526 --- /dev/null +++ b/packages/manager/.changeset/pr-11664-upcoming-features-1739979306430.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add final copy and docs links for LKE-E ([#11664](https://github.com/linode/manager/pull/11664)) diff --git a/packages/manager/.changeset/pr-11666-removed-1739780145710.md b/packages/manager/.changeset/pr-11666-removed-1739780145710.md new file mode 100644 index 00000000000..25f7df1932a --- /dev/null +++ b/packages/manager/.changeset/pr-11666-removed-1739780145710.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Removed +--- + +Move `capitalize` utility and `useInterval` hook to `@linode/utilities` package ([#11666](https://github.com/linode/manager/pull/11666)) diff --git a/packages/manager/.changeset/pr-11668-upcoming-features-1739789880185.md b/packages/manager/.changeset/pr-11668-upcoming-features-1739789880185.md new file mode 100644 index 00000000000..798f05147b4 --- /dev/null +++ b/packages/manager/.changeset/pr-11668-upcoming-features-1739789880185.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +fix bugs with long username and email for users table and details page, fix filter ([#11668](https://github.com/linode/manager/pull/11668)) diff --git a/packages/manager/.changeset/pr-11669-upcoming-features-1739899370024.md b/packages/manager/.changeset/pr-11669-upcoming-features-1739899370024.md new file mode 100644 index 00000000000..f6617ec1ed7 --- /dev/null +++ b/packages/manager/.changeset/pr-11669-upcoming-features-1739899370024.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Added EditAlertDefinition, EditAlertLanding components with Unit Tests ([#11669](https://github.com/linode/manager/pull/11669)) diff --git a/packages/manager/.changeset/pr-11670-tests-1739857190104.md b/packages/manager/.changeset/pr-11670-tests-1739857190104.md new file mode 100644 index 00000000000..d45f99e92af --- /dev/null +++ b/packages/manager/.changeset/pr-11670-tests-1739857190104.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Add test for ACLP Create Alerts. Improve test coverage and ensure validations are in place ([#11670](https://github.com/linode/manager/pull/11670)) diff --git a/packages/manager/.changeset/pr-11672-fixed-1739863532671.md b/packages/manager/.changeset/pr-11672-fixed-1739863532671.md new file mode 100644 index 00000000000..4cb90fe4701 --- /dev/null +++ b/packages/manager/.changeset/pr-11672-fixed-1739863532671.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +OBJ Create flow for Restricted user ([#11672](https://github.com/linode/manager/pull/11672)) diff --git a/packages/manager/.changeset/pr-11674-fixed-1739881665819.md b/packages/manager/.changeset/pr-11674-fixed-1739881665819.md new file mode 100644 index 00000000000..e44a1550a4e --- /dev/null +++ b/packages/manager/.changeset/pr-11674-fixed-1739881665819.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Added Permission Notice on create pages for restricted user ([#11674](https://github.com/linode/manager/pull/11674)) diff --git a/packages/manager/.changeset/pr-11677-tech-stories-1739897652667.md b/packages/manager/.changeset/pr-11677-tech-stories-1739897652667.md new file mode 100644 index 00000000000..a2a30278ccc --- /dev/null +++ b/packages/manager/.changeset/pr-11677-tech-stories-1739897652667.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Refactor CreateFirewallDrawer to use `react-hook-form` ([#11677](https://github.com/linode/manager/pull/11677)) diff --git a/packages/manager/.changeset/pr-11678-upcoming-features-1739907724334.md b/packages/manager/.changeset/pr-11678-upcoming-features-1739907724334.md new file mode 100644 index 00000000000..5c4da65c5cf --- /dev/null +++ b/packages/manager/.changeset/pr-11678-upcoming-features-1739907724334.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add ability to create firewalls from templates ([#11678](https://github.com/linode/manager/pull/11678)) diff --git a/packages/manager/.changeset/pr-11681-changed-1739908630049.md b/packages/manager/.changeset/pr-11681-changed-1739908630049.md new file mode 100644 index 00000000000..95db0b790c6 --- /dev/null +++ b/packages/manager/.changeset/pr-11681-changed-1739908630049.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +Make "Public" checkbox default-checked in OAuth App creation form ([#11681](https://github.com/linode/manager/pull/11681)) diff --git a/packages/manager/.changeset/pr-11683-changed-1739919225511.md b/packages/manager/.changeset/pr-11683-changed-1739919225511.md new file mode 100644 index 00000000000..c789094f794 --- /dev/null +++ b/packages/manager/.changeset/pr-11683-changed-1739919225511.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +Improve error handling for kubeconfig download during cluster provisioning ([#11683](https://github.com/linode/manager/pull/11683)) diff --git a/packages/manager/.changeset/pr-11685-upcoming-features-1739973428216.md b/packages/manager/.changeset/pr-11685-upcoming-features-1739973428216.md new file mode 100644 index 00000000000..b4f25c12a69 --- /dev/null +++ b/packages/manager/.changeset/pr-11685-upcoming-features-1739973428216.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +add `AlertReusableComponent`, add `convertAlertsToTypeSet` and `filterAlertsByStatusAndType` methods in utils.ts, add `useAlertDefinitionByServiceTypeQuery` in alerts.ts ([#11685](https://github.com/linode/manager/pull/11685)) diff --git a/packages/manager/.changeset/pr-11687-fixed-1739979963091.md b/packages/manager/.changeset/pr-11687-fixed-1739979963091.md new file mode 100644 index 00000000000..8e9fd5a9a80 --- /dev/null +++ b/packages/manager/.changeset/pr-11687-fixed-1739979963091.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +LKE create page error for restricted user ([#11687](https://github.com/linode/manager/pull/11687)) diff --git a/packages/manager/.changeset/pr-11688-tech-stories-1739984410717.md b/packages/manager/.changeset/pr-11688-tech-stories-1739984410717.md new file mode 100644 index 00000000000..e7c1ab7c86e --- /dev/null +++ b/packages/manager/.changeset/pr-11688-tech-stories-1739984410717.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Upgrade to MUI v6 ([#11688](https://github.com/linode/manager/pull/11688)) diff --git a/packages/manager/.changeset/pr-11689-tests-1739986270581.md b/packages/manager/.changeset/pr-11689-tests-1739986270581.md new file mode 100644 index 00000000000..f4063941cea --- /dev/null +++ b/packages/manager/.changeset/pr-11689-tests-1739986270581.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Apply new custom eslint rule ([#11689](https://github.com/linode/manager/pull/11689)) diff --git a/packages/manager/.changeset/pr-11692-upcoming-features-1740111658194.md b/packages/manager/.changeset/pr-11692-upcoming-features-1740111658194.md new file mode 100644 index 00000000000..b44e3c72864 --- /dev/null +++ b/packages/manager/.changeset/pr-11692-upcoming-features-1740111658194.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Filter regions by supported region ids - `getSupportedRegionIds` in CloudPulse alerts ([#11692](https://github.com/linode/manager/pull/11692)) diff --git a/packages/manager/.changeset/pr-11693-upcoming-features-1740051990439.md b/packages/manager/.changeset/pr-11693-upcoming-features-1740051990439.md new file mode 100644 index 00000000000..399856b6347 --- /dev/null +++ b/packages/manager/.changeset/pr-11693-upcoming-features-1740051990439.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add new tags filter in the resources section of CloudPulse Alerts ([#11693](https://github.com/linode/manager/pull/11693)) diff --git a/packages/manager/.changeset/pr-11696-changed-1740156201213.md b/packages/manager/.changeset/pr-11696-changed-1740156201213.md new file mode 100644 index 00000000000..049773696af --- /dev/null +++ b/packages/manager/.changeset/pr-11696-changed-1740156201213.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +Allow Tags in Volume Create Flow ([#11696](https://github.com/linode/manager/pull/11696)) diff --git a/packages/manager/.changeset/pr-11700-fixed-1740116651657.md b/packages/manager/.changeset/pr-11700-fixed-1740116651657.md new file mode 100644 index 00000000000..482ef010f21 --- /dev/null +++ b/packages/manager/.changeset/pr-11700-fixed-1740116651657.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Error message for restricted user when navigating to create Access Drawer ([#11700](https://github.com/linode/manager/pull/11700)) diff --git a/packages/manager/.changeset/pr-11701-fixed-1740128402516.md b/packages/manager/.changeset/pr-11701-fixed-1740128402516.md new file mode 100644 index 00000000000..7e9de7c72de --- /dev/null +++ b/packages/manager/.changeset/pr-11701-fixed-1740128402516.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Node Pools CTA buttons on small screens ([#11701](https://github.com/linode/manager/pull/11701)) diff --git a/packages/manager/.changeset/pr-11704-tech-stories-1740340564875.md b/packages/manager/.changeset/pr-11704-tech-stories-1740340564875.md new file mode 100644 index 00000000000..d8e44456431 --- /dev/null +++ b/packages/manager/.changeset/pr-11704-tech-stories-1740340564875.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Migrate Firewalls feature to Tanstack routing ([#11704](https://github.com/linode/manager/pull/11704)) diff --git a/packages/manager/.changeset/pr-11705-tests-1740407261971.md b/packages/manager/.changeset/pr-11705-tests-1740407261971.md new file mode 100644 index 00000000000..26d242db706 --- /dev/null +++ b/packages/manager/.changeset/pr-11705-tests-1740407261971.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Cypress test for Image create page for restricted users ([#11705](https://github.com/linode/manager/pull/11705)) diff --git a/packages/manager/.changeset/pr-11706-tests-1740406319898.md b/packages/manager/.changeset/pr-11706-tests-1740406319898.md new file mode 100644 index 00000000000..8d7ae1b2fee --- /dev/null +++ b/packages/manager/.changeset/pr-11706-tests-1740406319898.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Configure caddy to ignore test output ([#11706](https://github.com/linode/manager/pull/11706)) diff --git a/packages/manager/.changeset/pr-11711-removed-1740406956736.md b/packages/manager/.changeset/pr-11711-removed-1740406956736.md new file mode 100644 index 00000000000..69b67cae828 --- /dev/null +++ b/packages/manager/.changeset/pr-11711-removed-1740406956736.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Removed +--- + +Migrate utilities from `manager` to `utilities` package ([#11711](https://github.com/linode/manager/pull/11711)) diff --git a/packages/manager/.changeset/pr-11714-fixed-1740421546666.md b/packages/manager/.changeset/pr-11714-fixed-1740421546666.md new file mode 100644 index 00000000000..8834bb2d7fc --- /dev/null +++ b/packages/manager/.changeset/pr-11714-fixed-1740421546666.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +404 cluster endpoint errors on Linode details page for non-LKE Linodes ([#11714](https://github.com/linode/manager/pull/11714)) diff --git a/packages/manager/.changeset/pr-11714-upcoming-features-1740421599114.md b/packages/manager/.changeset/pr-11714-upcoming-features-1740421599114.md new file mode 100644 index 00000000000..4934115e76e --- /dev/null +++ b/packages/manager/.changeset/pr-11714-upcoming-features-1740421599114.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Fix LKE cluster table sorting when LKE-E beta endpoint is used ([#11714](https://github.com/linode/manager/pull/11714)) diff --git a/packages/manager/.changeset/pr-11718-removed-1741099193168.md b/packages/manager/.changeset/pr-11718-removed-1741099193168.md new file mode 100644 index 00000000000..3c5bb8e8ab4 --- /dev/null +++ b/packages/manager/.changeset/pr-11718-removed-1741099193168.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Removed +--- + +Migrate ErrorState to ui package ([#11718](https://github.com/linode/manager/pull/11718)) diff --git a/packages/manager/.changeset/pr-11719-tests-1740478883806.md b/packages/manager/.changeset/pr-11719-tests-1740478883806.md new file mode 100644 index 00000000000..401954a7229 --- /dev/null +++ b/packages/manager/.changeset/pr-11719-tests-1740478883806.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Add Automation for edit functionality of user defined alert ([#11719](https://github.com/linode/manager/pull/11719)) diff --git a/packages/manager/.changeset/pr-11722-tests-1740498549527.md b/packages/manager/.changeset/pr-11722-tests-1740498549527.md new file mode 100644 index 00000000000..50c1e23f7c9 --- /dev/null +++ b/packages/manager/.changeset/pr-11722-tests-1740498549527.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Linting cypress files ([#11722](https://github.com/linode/manager/pull/11722)) diff --git a/packages/manager/.changeset/pr-11723-fixed-1740501014063.md b/packages/manager/.changeset/pr-11723-fixed-1740501014063.md new file mode 100644 index 00000000000..7670382f47d --- /dev/null +++ b/packages/manager/.changeset/pr-11723-fixed-1740501014063.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Mobile primary nav height ([#11723](https://github.com/linode/manager/pull/11723)) diff --git a/packages/manager/.changeset/pr-11726-upcoming-features-1740510652938.md b/packages/manager/.changeset/pr-11726-upcoming-features-1740510652938.md new file mode 100644 index 00000000000..b14c9db0096 --- /dev/null +++ b/packages/manager/.changeset/pr-11726-upcoming-features-1740510652938.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Hide GPU plans tab for LKE-E ([#11726](https://github.com/linode/manager/pull/11726)) diff --git a/packages/manager/.changeset/pr-11727-upcoming-features-1740523339141.md b/packages/manager/.changeset/pr-11727-upcoming-features-1740523339141.md new file mode 100644 index 00000000000..309aae00843 --- /dev/null +++ b/packages/manager/.changeset/pr-11727-upcoming-features-1740523339141.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Hide Networking sections from Linode Configurations page for Linodes with new interfaces ([#11727](https://github.com/linode/manager/pull/11727)) diff --git a/packages/manager/.changeset/pr-11728-tests-1740514341603.md b/packages/manager/.changeset/pr-11728-tests-1740514341603.md new file mode 100644 index 00000000000..73ac4330ba2 --- /dev/null +++ b/packages/manager/.changeset/pr-11728-tests-1740514341603.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Fixed CloudPulse test failures triggered by new notice ([#11728](https://github.com/linode/manager/pull/11728)) diff --git a/packages/manager/.changeset/pr-11730-tests-1740520042307.md b/packages/manager/.changeset/pr-11730-tests-1740520042307.md new file mode 100644 index 00000000000..fdecf92eae0 --- /dev/null +++ b/packages/manager/.changeset/pr-11730-tests-1740520042307.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Apply linting to additional directories ([#11730](https://github.com/linode/manager/pull/11730)) diff --git a/packages/manager/.changeset/pr-11731-fixed-1740521257689.md b/packages/manager/.changeset/pr-11731-fixed-1740521257689.md new file mode 100644 index 00000000000..5483cf3cedd --- /dev/null +++ b/packages/manager/.changeset/pr-11731-fixed-1740521257689.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +RTX 6000 plans showing up in LKE UI ([#11731](https://github.com/linode/manager/pull/11731)) diff --git a/packages/manager/.changeset/pr-11732-fixed-1740533390539.md b/packages/manager/.changeset/pr-11732-fixed-1740533390539.md new file mode 100644 index 00000000000..c4f2289f3c7 --- /dev/null +++ b/packages/manager/.changeset/pr-11732-fixed-1740533390539.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Authentication Provider Selection Card UI regression ([#11732](https://github.com/linode/manager/pull/11732)) diff --git a/packages/manager/.changeset/pr-11733-removed-1740656248435.md b/packages/manager/.changeset/pr-11733-removed-1740656248435.md new file mode 100644 index 00000000000..64fa897644c --- /dev/null +++ b/packages/manager/.changeset/pr-11733-removed-1740656248435.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Removed +--- + +Migrate utilities from `manager` to `utilities` package - pt2 ([#11733](https://github.com/linode/manager/pull/11733)) diff --git a/packages/manager/.changeset/pr-11734-upcoming-features-1740564048829.md b/packages/manager/.changeset/pr-11734-upcoming-features-1740564048829.md new file mode 100644 index 00000000000..01e031ee246 --- /dev/null +++ b/packages/manager/.changeset/pr-11734-upcoming-features-1740564048829.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Update `AlertReusableComponent` to add table, Add `AlertInformationActionTable` and `AlertInformationActionRow` compoenent ([#11734](https://github.com/linode/manager/pull/11734)) diff --git a/packages/manager/.changeset/pr-11735-upcoming-features-1740581501727.md b/packages/manager/.changeset/pr-11735-upcoming-features-1740581501727.md new file mode 100644 index 00000000000..370e28b24fb --- /dev/null +++ b/packages/manager/.changeset/pr-11735-upcoming-features-1740581501727.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +DBaaS: Advanced Configurations initial set up (new tab, drawer) ([#11735](https://github.com/linode/manager/pull/11735)) diff --git a/packages/manager/.changeset/pr-11736-added-1740581762216.md b/packages/manager/.changeset/pr-11736-added-1740581762216.md new file mode 100644 index 00000000000..ae20eb5662f --- /dev/null +++ b/packages/manager/.changeset/pr-11736-added-1740581762216.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Added +--- + +Link to Linode's Firewall in Linode Entity Details ([#11736](https://github.com/linode/manager/pull/11736)) diff --git a/packages/manager/.changeset/pr-11736-upcoming-features-1740581729228.md b/packages/manager/.changeset/pr-11736-upcoming-features-1740581729228.md new file mode 100644 index 00000000000..6326ffc1b1f --- /dev/null +++ b/packages/manager/.changeset/pr-11736-upcoming-features-1740581729228.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add Interface type to Linode Entity Detail ([#11736](https://github.com/linode/manager/pull/11736)) diff --git a/packages/manager/.changeset/pr-11737-tests-1740586626716.md b/packages/manager/.changeset/pr-11737-tests-1740586626716.md new file mode 100644 index 00000000000..592aff7bb84 --- /dev/null +++ b/packages/manager/.changeset/pr-11737-tests-1740586626716.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Remove Cypress test assertion involving Login app text ([#11737](https://github.com/linode/manager/pull/11737)) diff --git a/packages/manager/.changeset/pr-11738-upcoming-features-1740600904078.md b/packages/manager/.changeset/pr-11738-upcoming-features-1740600904078.md new file mode 100644 index 00000000000..7e46621c772 --- /dev/null +++ b/packages/manager/.changeset/pr-11738-upcoming-features-1740600904078.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add support for `nodebalancerVPC` feature flag for NodeBalancer-VPC integration ([#11738](https://github.com/linode/manager/pull/11738)) diff --git a/packages/manager/.changeset/pr-11745-upcoming-features-1740672674751.md b/packages/manager/.changeset/pr-11745-upcoming-features-1740672674751.md new file mode 100644 index 00000000000..cea39d1c65d --- /dev/null +++ b/packages/manager/.changeset/pr-11745-upcoming-features-1740672674751.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Fix LKE-E provisioning placeholder when filtering by status ([#11745](https://github.com/linode/manager/pull/11745)) diff --git a/packages/manager/.changeset/pr-11746-changed-1740774516193.md b/packages/manager/.changeset/pr-11746-changed-1740774516193.md new file mode 100644 index 00000000000..5839a44b21e --- /dev/null +++ b/packages/manager/.changeset/pr-11746-changed-1740774516193.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +Update copy for LKE ACL section ([#11746](https://github.com/linode/manager/pull/11746)) diff --git a/packages/manager/.changeset/pr-11746-upcoming-features-1740774643778.md b/packages/manager/.changeset/pr-11746-upcoming-features-1740774643778.md new file mode 100644 index 00000000000..8785b2567f2 --- /dev/null +++ b/packages/manager/.changeset/pr-11746-upcoming-features-1740774643778.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Enable ACL by default for LKE-E clusters ([#11746](https://github.com/linode/manager/pull/11746)) diff --git a/packages/manager/.changeset/pr-11748-upcoming-features-1740653230064.md b/packages/manager/.changeset/pr-11748-upcoming-features-1740653230064.md new file mode 100644 index 00000000000..08e55492167 --- /dev/null +++ b/packages/manager/.changeset/pr-11748-upcoming-features-1740653230064.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +CloudPulse alerting UX improvements in `create flow` and `resources section` ([#11748](https://github.com/linode/manager/pull/11748)) diff --git a/packages/manager/.changeset/pr-11755-changed-1740752072171.md b/packages/manager/.changeset/pr-11755-changed-1740752072171.md new file mode 100644 index 00000000000..09ec7125b29 --- /dev/null +++ b/packages/manager/.changeset/pr-11755-changed-1740752072171.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +Move @vitest/ui to monorepo root dependency ([#11755](https://github.com/linode/manager/pull/11755)) diff --git a/packages/manager/.changeset/pr-11755-changed-1740752328523.md b/packages/manager/.changeset/pr-11755-changed-1740752328523.md new file mode 100644 index 00000000000..995b466c860 --- /dev/null +++ b/packages/manager/.changeset/pr-11755-changed-1740752328523.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +Upgrade vitest and @vitest/ui to 3.0.7 ([#11755](https://github.com/linode/manager/pull/11755)) diff --git a/packages/manager/.changeset/pr-11756-tests-1741019349059.md b/packages/manager/.changeset/pr-11756-tests-1741019349059.md new file mode 100644 index 00000000000..d15aad5eb88 --- /dev/null +++ b/packages/manager/.changeset/pr-11756-tests-1741019349059.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Fix linting errors in additional directories in packages/manager/cypress/e2e ([#11756](https://github.com/linode/manager/pull/11756)) diff --git a/packages/manager/.changeset/pr-11757-tech-stories-1740683464911.md b/packages/manager/.changeset/pr-11757-tech-stories-1740683464911.md new file mode 100644 index 00000000000..4aaf5affe96 --- /dev/null +++ b/packages/manager/.changeset/pr-11757-tech-stories-1740683464911.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +4.0.0 Design Tokens - New Spacing & Badge Tokens ([#11757](https://github.com/linode/manager/pull/11757)) diff --git a/packages/manager/.changeset/pr-11758-changed-1740751964030.md b/packages/manager/.changeset/pr-11758-changed-1740751964030.md new file mode 100644 index 00000000000..cc1f3bda0e2 --- /dev/null +++ b/packages/manager/.changeset/pr-11758-changed-1740751964030.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +Update react-vnc to 3.0.7 ([#11758](https://github.com/linode/manager/pull/11758)) diff --git a/packages/manager/.changeset/pr-11765-fixed-1740766037419.md b/packages/manager/.changeset/pr-11765-fixed-1740766037419.md new file mode 100644 index 00000000000..eab1fa0a115 --- /dev/null +++ b/packages/manager/.changeset/pr-11765-fixed-1740766037419.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Show details button for selected Stackscript ([#11765](https://github.com/linode/manager/pull/11765)) diff --git a/packages/manager/.changeset/pr-11767-fixed-1740775625877.md b/packages/manager/.changeset/pr-11767-fixed-1740775625877.md new file mode 100644 index 00000000000..46dab168c42 --- /dev/null +++ b/packages/manager/.changeset/pr-11767-fixed-1740775625877.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Filter out Linodes in distributed regions from Create flow Backups & Clone tab ([#11767](https://github.com/linode/manager/pull/11767)) diff --git a/packages/manager/.changeset/pr-11768-tech-stories-1741017663608.md b/packages/manager/.changeset/pr-11768-tech-stories-1741017663608.md new file mode 100644 index 00000000000..65e6f66e435 --- /dev/null +++ b/packages/manager/.changeset/pr-11768-tech-stories-1741017663608.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Update jspdf dependencies to resolve DOMPurify dependabot alert ([#11768](https://github.com/linode/manager/pull/11768)) diff --git a/packages/manager/.changeset/pr-11769-fixed-1740985683618.md b/packages/manager/.changeset/pr-11769-fixed-1740985683618.md new file mode 100644 index 00000000000..540dd93f121 --- /dev/null +++ b/packages/manager/.changeset/pr-11769-fixed-1740985683618.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Confusing wording on DBaaS suspend dialog ([#11769](https://github.com/linode/manager/pull/11769)) diff --git a/packages/manager/.changeset/pr-11771-fixed-1740992423294.md b/packages/manager/.changeset/pr-11771-fixed-1740992423294.md new file mode 100644 index 00000000000..2b9fec92a8b --- /dev/null +++ b/packages/manager/.changeset/pr-11771-fixed-1740992423294.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Incorrect helper text in `Add an SSH Key` Drawer ([#11771](https://github.com/linode/manager/pull/11771)) diff --git a/packages/manager/.changeset/pr-11772-tech-stories-1741017357763.md b/packages/manager/.changeset/pr-11772-tech-stories-1741017357763.md new file mode 100644 index 00000000000..eae46700ae1 --- /dev/null +++ b/packages/manager/.changeset/pr-11772-tech-stories-1741017357763.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Update Shiki to 3.1.0 ([#11772](https://github.com/linode/manager/pull/11772)) diff --git a/packages/manager/.changeset/pr-11775-changed-1741032651842.md b/packages/manager/.changeset/pr-11775-changed-1741032651842.md new file mode 100644 index 00000000000..7874293cadc --- /dev/null +++ b/packages/manager/.changeset/pr-11775-changed-1741032651842.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +Update copy for LKE Recycle, Upgrade Version, and Delete Pool modals ([#11775](https://github.com/linode/manager/pull/11775)) diff --git a/packages/manager/.changeset/pr-11776-fixed-1741039493782.md b/packages/manager/.changeset/pr-11776-fixed-1741039493782.md new file mode 100644 index 00000000000..c864ca9aed9 --- /dev/null +++ b/packages/manager/.changeset/pr-11776-fixed-1741039493782.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Linode Backups Drawer style regressions ([#11776](https://github.com/linode/manager/pull/11776)) diff --git a/packages/manager/.changeset/pr-11779-fixed-1741096736423.md b/packages/manager/.changeset/pr-11779-fixed-1741096736423.md new file mode 100644 index 00000000000..bfa60f12722 --- /dev/null +++ b/packages/manager/.changeset/pr-11779-fixed-1741096736423.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +NodeBalancer Create Summary broken dividers and spacing ([#11779](https://github.com/linode/manager/pull/11779)) diff --git a/packages/manager/.changeset/pr-11780-tests-1741102827420.md b/packages/manager/.changeset/pr-11780-tests-1741102827420.md new file mode 100644 index 00000000000..aae538082f3 --- /dev/null +++ b/packages/manager/.changeset/pr-11780-tests-1741102827420.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Delete region test suite ([#11780](https://github.com/linode/manager/pull/11780)) diff --git a/packages/manager/.changeset/pr-11783-tests-1741210181065.md b/packages/manager/.changeset/pr-11783-tests-1741210181065.md new file mode 100644 index 00000000000..7c3df96b66e --- /dev/null +++ b/packages/manager/.changeset/pr-11783-tests-1741210181065.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Revise LDE-related logic in unit tests for EnableBackupsDialog and NodeTable and in E2E test for Linode Create ([#11783](https://github.com/linode/manager/pull/11783)) diff --git a/packages/manager/.changeset/pr-11783-upcoming-features-1741210015259.md b/packages/manager/.changeset/pr-11783-upcoming-features-1741210015259.md new file mode 100644 index 00000000000..e566fad83d3 --- /dev/null +++ b/packages/manager/.changeset/pr-11783-upcoming-features-1741210015259.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Revise logic governing display of LDE sections and data ([#11783](https://github.com/linode/manager/pull/11783)) diff --git a/packages/manager/.changeset/pr-11787-fixed-1741204647647.md b/packages/manager/.changeset/pr-11787-fixed-1741204647647.md new file mode 100644 index 00000000000..39a2000c123 --- /dev/null +++ b/packages/manager/.changeset/pr-11787-fixed-1741204647647.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Incorrect default color shown in Avatar color picker ([#11787](https://github.com/linode/manager/pull/11787)) diff --git a/packages/manager/.changeset/pr-11791-changed-1741262873635.md b/packages/manager/.changeset/pr-11791-changed-1741262873635.md new file mode 100644 index 00000000000..1b5e1f62fce --- /dev/null +++ b/packages/manager/.changeset/pr-11791-changed-1741262873635.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +Update RegionSelect placement group tooltiptext copy ([#11791](https://github.com/linode/manager/pull/11791)) diff --git a/packages/manager/.changeset/pr-11816-fixed-1742349641632.md b/packages/manager/.changeset/pr-11816-fixed-1742349641632.md deleted file mode 100644 index 0589e79781f..00000000000 --- a/packages/manager/.changeset/pr-11816-fixed-1742349641632.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Fixed ---- - -Visual UI bug with Payment Amount adornment ([#11816](https://github.com/linode/manager/pull/11816)) diff --git a/packages/manager/.changeset/pr-11906-fixed-1742567649683.md b/packages/manager/.changeset/pr-11906-fixed-1742567649683.md deleted file mode 100644 index 015d42aa964..00000000000 --- a/packages/manager/.changeset/pr-11906-fixed-1742567649683.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Fixed ---- - -Pagination for subnets in VPC Subnet table ([#11906](https://github.com/linode/manager/pull/11906)) diff --git a/packages/manager/.changeset/pr-11906-fixed-1742567667084.md b/packages/manager/.changeset/pr-11906-fixed-1742567667084.md deleted file mode 100644 index 9b1877fc086..00000000000 --- a/packages/manager/.changeset/pr-11906-fixed-1742567667084.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Fixed ---- - -IP incrementation in Subnet Create drawer ([#11906](https://github.com/linode/manager/pull/11906)) diff --git a/packages/manager/.changeset/pr-11906-tech-stories-1742567613345.md b/packages/manager/.changeset/pr-11906-tech-stories-1742567613345.md deleted file mode 100644 index 589b14a18c9..00000000000 --- a/packages/manager/.changeset/pr-11906-tech-stories-1742567613345.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tech Stories ---- - -VPC rerouting (TanStack) ([#11906](https://github.com/linode/manager/pull/11906)) diff --git a/packages/manager/.changeset/pr-11924-tech-stories-1743082255119.md b/packages/manager/.changeset/pr-11924-tech-stories-1743082255119.md deleted file mode 100644 index d2c9b40cba3..00000000000 --- a/packages/manager/.changeset/pr-11924-tech-stories-1743082255119.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tech Stories ---- - -Migrate Object Storage to Tanstack Router ([#11924](https://github.com/linode/manager/pull/11924)) diff --git a/packages/manager/.changeset/pr-11934-upcoming-features-1743443080180.md b/packages/manager/.changeset/pr-11934-upcoming-features-1743443080180.md deleted file mode 100644 index 191e72b6ed1..00000000000 --- a/packages/manager/.changeset/pr-11934-upcoming-features-1743443080180.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Disable Upgrade Interfaces feature for LKE Linodes and other conditions ([#11934](https://github.com/linode/manager/pull/11934)) diff --git a/packages/manager/.changeset/pr-11941-tech-stories-1743701285887.md b/packages/manager/.changeset/pr-11941-tech-stories-1743701285887.md deleted file mode 100644 index 7beac16a8d3..00000000000 --- a/packages/manager/.changeset/pr-11941-tech-stories-1743701285887.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tech Stories ---- - -Eslint Overhaul ([#11941](https://github.com/linode/manager/pull/11941)) diff --git a/packages/manager/.changeset/pr-11943-upcoming-features-1743494415175.md b/packages/manager/.changeset/pr-11943-upcoming-features-1743494415175.md deleted file mode 100644 index 40d3cef7c49..00000000000 --- a/packages/manager/.changeset/pr-11943-upcoming-features-1743494415175.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Enhance CloudPulse alerting resource selection section with maximum selection limitations ([#11943](https://github.com/linode/manager/pull/11943)) diff --git a/packages/manager/.changeset/pr-11944-changed-1743506054914.md b/packages/manager/.changeset/pr-11944-changed-1743506054914.md deleted file mode 100644 index 1b523d5be17..00000000000 --- a/packages/manager/.changeset/pr-11944-changed-1743506054914.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Changed ---- - -Remove min length validation for tag and added validation for empty string ([#11944](https://github.com/linode/manager/pull/11944)) diff --git a/packages/manager/.changeset/pr-11953-upcoming-features-1743614906461.md b/packages/manager/.changeset/pr-11953-upcoming-features-1743614906461.md deleted file mode 100644 index 26f9bab4a56..00000000000 --- a/packages/manager/.changeset/pr-11953-upcoming-features-1743614906461.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Fix SubnetLinodeRow for Linodes using new interfaces ([#11953](https://github.com/linode/manager/pull/11953)) diff --git a/packages/manager/.changeset/pr-11955-removed-1743596163534.md b/packages/manager/.changeset/pr-11955-removed-1743596163534.md deleted file mode 100644 index b004250bd76..00000000000 --- a/packages/manager/.changeset/pr-11955-removed-1743596163534.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Removed ---- - -Move `getUserTimeZone` and its associated profile factories to `@linode/utilities` ([#11955](https://github.com/linode/manager/pull/11955)) diff --git a/packages/manager/.changeset/pr-11958-tests-1743610427083.md b/packages/manager/.changeset/pr-11958-tests-1743610427083.md deleted file mode 100644 index f1805971d36..00000000000 --- a/packages/manager/.changeset/pr-11958-tests-1743610427083.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tests ---- - -Add `env:marketplaceApps`, `env:multipleRegions`, and `env:stackScripts` tags for Cypress tests ([#11958](https://github.com/linode/manager/pull/11958)) diff --git a/packages/manager/.changeset/pr-11961-tests-1743634308900.md b/packages/manager/.changeset/pr-11961-tests-1743634308900.md deleted file mode 100644 index d165a7b28fa..00000000000 --- a/packages/manager/.changeset/pr-11961-tests-1743634308900.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tests ---- - -Avoid selecting regions that do not support Machine Images in Image upload tests ([#11961](https://github.com/linode/manager/pull/11961)) diff --git a/packages/manager/.changeset/pr-11962-changed-1743652792312.md b/packages/manager/.changeset/pr-11962-changed-1743652792312.md deleted file mode 100644 index 2116ae7e2b7..00000000000 --- a/packages/manager/.changeset/pr-11962-changed-1743652792312.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Changed ---- - -Update toast styling to Akamai Design System specs ([#11962](https://github.com/linode/manager/pull/11962)) diff --git a/packages/manager/.changeset/pr-11963-upcoming-features-1743680039001.md b/packages/manager/.changeset/pr-11963-upcoming-features-1743680039001.md deleted file mode 100644 index 95f920cae93..00000000000 --- a/packages/manager/.changeset/pr-11963-upcoming-features-1743680039001.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -UI bugfixes: Resetting Trigger Occurences, Resources values when service type is cleared, Disabling Trigger Occurences, Threshold values unless Service Type is selected. Added Max value for Trigger Occurences and Threshold TextField components ([#11963](https://github.com/linode/manager/pull/11963)) diff --git a/packages/manager/.changeset/pr-11966-fixed-1743718029581.md b/packages/manager/.changeset/pr-11966-fixed-1743718029581.md deleted file mode 100644 index 5ae611d9e0f..00000000000 --- a/packages/manager/.changeset/pr-11966-fixed-1743718029581.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Fixed ---- - -LKE-E related network requests on the NodeBalancer details page ([#11966](https://github.com/linode/manager/pull/11966)) diff --git a/packages/manager/.changeset/pr-11967-upcoming-features-1743747392300.md b/packages/manager/.changeset/pr-11967-upcoming-features-1743747392300.md deleted file mode 100644 index 4c6efebfce9..00000000000 --- a/packages/manager/.changeset/pr-11967-upcoming-features-1743747392300.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Remove `or` condition in filtering of `/instances` call at CloudPulse Metrics ([#11967](https://github.com/linode/manager/pull/11967)) diff --git a/packages/manager/.changeset/pr-11968-fixed-1743780227804.md b/packages/manager/.changeset/pr-11968-fixed-1743780227804.md deleted file mode 100644 index dcc7fbf49f1..00000000000 --- a/packages/manager/.changeset/pr-11968-fixed-1743780227804.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Fixed ---- - -Update grid width in CloudPulseDashboardLanding.tsx, Change time range preference key in GlobalFilter.tsx, Change maxHeight of applied filter box to 78px in CloudPulseAppliedFilter.tsx ([#11968](https://github.com/linode/manager/pull/11968)) diff --git a/packages/manager/.changeset/pr-11969-added-1743779981445.md b/packages/manager/.changeset/pr-11969-added-1743779981445.md deleted file mode 100644 index 5ec79486e71..00000000000 --- a/packages/manager/.changeset/pr-11969-added-1743779981445.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Added ---- - -Add `cache update` logic in alerts.ts query file ([#11969](https://github.com/linode/manager/pull/11969)) diff --git a/packages/manager/.changeset/pr-11974-upcoming-features-1743784910488.md b/packages/manager/.changeset/pr-11974-upcoming-features-1743784910488.md deleted file mode 100644 index 65a1f280cd4..00000000000 --- a/packages/manager/.changeset/pr-11974-upcoming-features-1743784910488.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Feature flag for VM Host Maintenance policy ([#11974](https://github.com/linode/manager/pull/11974)) diff --git a/packages/manager/.changeset/pr-11981-upcoming-features-1744104834195.md b/packages/manager/.changeset/pr-11981-upcoming-features-1744104834195.md deleted file mode 100644 index 016235f78df..00000000000 --- a/packages/manager/.changeset/pr-11981-upcoming-features-1744104834195.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Add `Confirmation Dialog` in `AlertListTable.tsx`, add `message` prop in `AlertConfirmationDialog.tsx` ([#11981](https://github.com/linode/manager/pull/11981)) diff --git a/packages/manager/.changeset/pr-11984-upcoming-features-1744052007574.md b/packages/manager/.changeset/pr-11984-upcoming-features-1744052007574.md deleted file mode 100644 index c5082d44b47..00000000000 --- a/packages/manager/.changeset/pr-11984-upcoming-features-1744052007574.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Fix displaying empty state when user doesn't have the assigned roles in iam ([#11984](https://github.com/linode/manager/pull/11984)) diff --git a/packages/manager/.changeset/pr-11987-changed-1744123144788.md b/packages/manager/.changeset/pr-11987-changed-1744123144788.md deleted file mode 100644 index 0a8b802052b..00000000000 --- a/packages/manager/.changeset/pr-11987-changed-1744123144788.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Changed ---- - -Update config label to follow the category.label format, rename Monitor tab ([#11987](https://github.com/linode/manager/pull/11987)) diff --git a/packages/manager/.changeset/pr-11989-changed-1744126754414.md b/packages/manager/.changeset/pr-11989-changed-1744126754414.md deleted file mode 100644 index a98ead02e29..00000000000 --- a/packages/manager/.changeset/pr-11989-changed-1744126754414.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Changed ---- - -Image service Gen2 - Copy updates ([#11989](https://github.com/linode/manager/pull/11989)) diff --git a/packages/manager/.changeset/pr-11993-added-1744134925847.md b/packages/manager/.changeset/pr-11993-added-1744134925847.md deleted file mode 100644 index 98b06ec1784..00000000000 --- a/packages/manager/.changeset/pr-11993-added-1744134925847.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Added ---- - -Display encryption status with lock icon in Image Edit Drawer ([#11993](https://github.com/linode/manager/pull/11993)) diff --git a/packages/manager/.eslintrc.cjs b/packages/manager/.eslintrc.cjs new file mode 100644 index 00000000000..1febffd7198 --- /dev/null +++ b/packages/manager/.eslintrc.cjs @@ -0,0 +1,404 @@ +/* eslint-disable sonarjs/no-duplicate-string */ +module.exports = { + env: { + browser: true, + }, + extends: [ + // disables a few of the recommended rules from the previous set that we know are already covered by TypeScript's typechecker + 'plugin:@typescript-eslint/eslint-recommended', + // like eslint:recommended, except it only turns on rules from our TypeScript-specific plugin. + 'plugin:@typescript-eslint/recommended', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + 'plugin:jsx-a11y/recommended', + 'plugin:sonarjs/recommended', + 'plugin:ramda/recommended', + 'plugin:cypress/recommended', + 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. + 'plugin:testing-library/react', + 'plugin:perfectionist/recommended-natural', + ], + ignorePatterns: [ + 'node_modules', + 'build', + 'storybook-static', + '.storybook', + 'e2e/core/placementGroups', + 'e2e/core/stackscripts', + 'e2e/core/volumes', + 'e2e/core/vpc', + 'public', + '!.eslintrc.js', + ], + overrides: [ + { + files: ['*.ts', '*.tsx'], + rules: { + '@typescript-eslint/ban-types': [ + 'error', + { + extendDefaults: true, + types: { + '{}': false, + }, + }, + ], + '@typescript-eslint/no-unused-vars': [ + 'warn', + { argsIgnorePattern: '^_' }, + ], + // eslint not typescript does a bad job with type aliases, we let typescript eslint do it + 'no-unused-vars': 'off', + }, + }, + { + files: ['*js'], + rules: { + '@typescript-eslint/explicit-function-return-type': 'off', + }, + }, + { + env: { node: true }, + // node files + files: [ + '**/*.test.*', + '**/*.spec.js', + '**/*.stories.js', + 'scripts/**', + 'config/**', + 'cypress/**', + ], + rules: { + '@typescript-eslint/no-empty-function': 'warn', // possible for tests + '@typescript-eslint/no-var-requires': 'off', + 'array-callback-return': 'off', + 'no-unused-expressions': 'off', + }, + }, + { + env: { + 'cypress/globals': true, + node: true, + }, + // scrips, config and cypress files can use console + files: ['scripts/**', 'config/**', 'cypress/**'], + rules: { + 'no-console': 'off', + // here we get false positives as cypress self handles async/await + 'testing-library/await-async-query': 'off', + }, + }, + // restrict usage of react-router-dom during migration to tanstack/react-router + // TODO: TanStack Router - remove this override when migration is complete + { + files: [ + // for each new features added to the migration router, add its directory here + 'src/features/Betas/**/*', + 'src/features/Domains/**/*', + 'src/features/Firewalls/**/*', + 'src/features/Images/**/*', + 'src/features/Longview/**/*', + 'src/features/PlacementGroups/**/*', + 'src/features/Volumes/**/*', + ], + rules: { + 'no-restricted-imports': [ + // This needs to remain an error however trying to link to a feature that is not yet migrated will break the router + // For those cases react-router-dom history.push is still needed + // using `eslint-disable-next-line no-restricted-imports` can help bypass those imports + 'error', + { + paths: [ + { + importNames: [ + // intentionally not including in this list as this will be updated last globally + 'useNavigate', + 'useParams', + 'useLocation', + 'useHistory', + 'useRouteMatch', + 'matchPath', + 'MemoryRouter', + 'Route', + 'RouteProps', + 'Switch', + 'Redirect', + 'RouteComponentProps', + 'withRouter', + ], + message: + 'Please use routing utilities intended for @tanstack/react-router.', + name: 'react-router-dom', + }, + { + importNames: ['TabLinkList'], + message: + 'Please use the TanStackTabLinkList component for components being migrated to TanStack Router.', + name: 'src/components/Tabs/TabLinkList', + }, + { + importNames: ['OrderBy', 'default'], + message: + 'Please use useOrderV2 hook for components being migrated to TanStack Router.', + name: 'src/components/OrderBy', + }, + { + importNames: ['Prompt'], + message: + 'Please use the TanStack useBlocker hook for components/features being migrated to TanStack Router.', + name: 'src/components/Prompt/Prompt', + }, + ], + }, + ], + }, + }, + // Apply `no-createLinode` rule to `cypress` related files only. + { + files: ['cypress/**'], + rules: { + '@linode/cloud-manager/no-createLinode': 'error', + }, + }, + ], + parser: '@typescript-eslint/parser', // Specifies the ESLint parser + parserOptions: { + // Warning if you want to set tsconfig.json, you ll need laso to set `tsconfigRootDir:__dirname` + // BUT we decided not to use this feature due to a very important performance impact + // project: 'tsconfig.json', + ecmaFeatures: { + jsx: true, + }, + // Only ESLint 6.2.0 and later support ES2020. + ecmaVersion: 2020, + warnOnUnsupportedTypeScriptVersion: true, + }, + plugins: [ + '@typescript-eslint', + 'react', + 'react-hooks', + 'jsx-a11y', + 'sonarjs', + 'ramda', + 'cypress', + 'prettier', + 'testing-library', + 'scanjs-rules', + 'xss', + 'perfectionist', + '@linode/eslint-plugin-cloud-manager', + 'react-refresh', + ], + rules: { + '@linode/cloud-manager/deprecate-formik': 'warn', + '@linode/cloud-manager/no-createLinode': 'off', + '@linode/cloud-manager/no-custom-fontWeight': 'error', + '@typescript-eslint/consistent-type-imports': 'warn', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/naming-convention': [ + 'warn', + { + format: ['camelCase', 'UPPER_CASE', 'PascalCase'], + leadingUnderscore: 'allow', + selector: 'variable', + trailingUnderscore: 'allow', + }, + { + format: null, + modifiers: ['destructured'], + selector: 'variable', + }, + { + format: ['camelCase', 'PascalCase'], + selector: 'function', + }, + { + format: ['camelCase'], + leadingUnderscore: 'allow', + selector: 'parameter', + }, + { + format: ['PascalCase'], + selector: 'typeLike', + }, + ], + '@typescript-eslint/no-empty-interface': 'warn', + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-inferrable-types': 'off', + '@typescript-eslint/no-namespace': 'warn', + // this would disallow usage of ! postfix operator on non null types + '@typescript-eslint/no-non-null-assertion': 'off', + // This rules is disabled to avoid duplicates errors as no-unused-vars is set + '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/no-use-before-define': 'off', + 'array-callback-return': 'error', + 'comma-dangle': 'off', // Prettier and TS both handle and check for this one + // radix: Codacy considers it as an error, i put it here to fix it before push + curly: 'warn', + eqeqeq: 'warn', + // See: https://www.w3.org/TR/graphics-aria-1.0/ + 'jsx-a11y/aria-role': [ + 'error', + { + allowedInvalidRoles: [ + 'graphics-document', + 'graphics-object', + 'graphics-symbol', + ], + }, + ], + // typescript-eslint specific rules + 'no-await-in-loop': 'error', + 'no-bitwise': 'error', + 'no-caller': 'error', + 'no-console': 'error', + 'no-eval': 'error', + // turned off to allow arrow functions in React Class Component + 'no-invalid-this': 'off', + // loop rules + 'no-loop-func': 'error', + 'no-mixed-requires': 'warn', + // style errors + 'no-multiple-empty-lines': 'error', + 'no-new-wrappers': 'error', + 'no-restricted-imports': [ + 'error', + 'rxjs', + '@mui/core', + '@mui/system', + '@mui/icons-material', + { + importNames: ['Typography'], + message: + 'Please use Typography component from @linode/ui instead of @mui/material', + name: '@mui/material', + }, + ], + 'no-restricted-syntax': [ + 'error', + { + message: + "The 'data-test-id' attribute is not allowed; use 'data-testid' instead.", + selector: "JSXAttribute[name.name='data-test-id']", + }, + ], + 'no-throw-literal': 'warn', + 'no-trailing-spaces': 'warn', + // allowing to init vars to undefined + 'no-undef-init': 'off', + 'no-unused-expressions': 'warn', + // prepend `_` to an arg you accept to ignore + 'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], + 'object-shorthand': 'warn', + // Perfectionist + 'perfectionist/sort-array-includes': 'warn', + 'perfectionist/sort-classes': 'warn', + 'perfectionist/sort-enums': 'warn', + 'perfectionist/sort-exports': 'warn', + 'perfectionist/sort-imports': [ + 'warn', + { + 'custom-groups': { + type: { + react: ['react', 'react-*'], + src: ['src*'], + }, + value: { + src: ['src/**/*'], + }, + }, + groups: [ + ['builtin', 'libraries', 'external'], + ['src', 'internal'], + ['parent', 'sibling', 'index'], + 'object', + 'unknown', + [ + 'type', + 'internal-type', + 'parent-type', + 'sibling-type', + 'index-type', + ], + ], + 'newlines-between': 'always', + }, + ], + 'perfectionist/sort-interfaces': 'warn', + 'perfectionist/sort-jsx-props': 'warn', + 'perfectionist/sort-map-elements': 'warn', + 'perfectionist/sort-named-exports': 'warn', + 'perfectionist/sort-named-imports': 'warn', + 'perfectionist/sort-object-types': 'warn', + 'perfectionist/sort-objects': 'warn', + 'perfectionist/sort-union-types': 'warn', + // make prettier issues warnings + 'prettier/prettier': 'warn', + // radix requires to give the base in parseInt https://eslint.org/docs/rules/radix + radix: 'error', + // ramda + 'ramda/prefer-ramda-boolean': 'off', + // react and jsx specific rules + 'react/display-name': 'off', + 'react/jsx-no-bind': 'off', + 'react/jsx-no-script-url': 'error', + 'react/jsx-no-useless-fragment': 'warn', + 'react/no-unescaped-entities': 'warn', + // requires the definition of proptypes for react components + 'react/prop-types': 'off', + 'react/self-closing-comp': 'warn', + 'react-hooks/exhaustive-deps': 'warn', + 'react-hooks/rules-of-hooks': 'error', + 'react-refresh/only-export-components': 'warn', + 'scanjs-rules/assign_to_hostname': 'warn', + 'scanjs-rules/assign_to_href': 'warn', + 'scanjs-rules/assign_to_location': 'warn', + 'scanjs-rules/assign_to_onmessage': 'warn', + 'scanjs-rules/assign_to_pathname': 'warn', + 'scanjs-rules/assign_to_protocol': 'error', + 'scanjs-rules/assign_to_search': 'warn', + 'scanjs-rules/assign_to_src': 'warn', + // Allow roles from WAI-ARIA graphics module proposal. + 'scanjs-rules/call_Function': 'error', + // Prevent patterns susceptible to XSS, like '
' + userInput + '
'. + 'scanjs-rules/call_addEventListener': 'warn', + 'scanjs-rules/call_parseFromString': 'error', + 'scanjs-rules/new_Function': 'error', + 'scanjs-rules/property_geolocation': 'error', + // sonar + 'sonarjs/cognitive-complexity': 'off', + 'sonarjs/no-duplicate-string': 'warn', + 'sonarjs/no-identical-functions': 'warn', + 'sonarjs/no-redundant-jump': 'warn', + 'sonarjs/no-small-switch': 'warn', + 'sonarjs/prefer-immediate-return': 'warn', + 'sort-keys': 'off', + 'spaced-comment': 'warn', + // https://github.com/Rantanen/eslint-plugin-xss/blob/master/docs/rules/no-mixed-html.md + 'xss/no-mixed-html': [ + 'error', + { + // It's only valid to assign HTML to variables/attributes named "_html" (for React's + functions: { + // Declare "sanitizeHTML" as a function that accepts HTML as input and output, and that + // it's "safe", meaning callers can trust the output (but the output still can only be + // assigned to a variable with the naming convention above). + sanitizeHTML: { + htmlInput: true, + htmlOutput: true, + safe: true, + }, + }, + // dangerouslySetInnerHTML) and /sanitize/i (regex matching). + htmlVariableRules: ['__html', 'sanitize/i'], + }, + ], + }, + settings: { + react: { + version: 'detect', // Tells eslint-plugin-react to automatically detect the version of React to use + }, + }, +}; diff --git a/packages/manager/.storybook/main.ts b/packages/manager/.storybook/main.ts index b1270f5ba05..09b512e1a99 100644 --- a/packages/manager/.storybook/main.ts +++ b/packages/manager/.storybook/main.ts @@ -5,7 +5,6 @@ const config: StorybookConfig = { stories: [ '../src/components/**/*.@(mdx|stories.@(js|ts|jsx|tsx))', '../src/features/**/*.@(mdx|stories.@(js|ts|jsx|tsx))', - '../../shared/src/**/*.@(mdx|stories.@(js|ts|jsx|tsx))', '../../ui/src/components/**/*.@(mdx|stories.@(js|ts|jsx|tsx))', ], addons: [ diff --git a/packages/manager/CHANGELOG.md b/packages/manager/CHANGELOG.md index 601edf78d3e..c422e9236ae 100644 --- a/packages/manager/CHANGELOG.md +++ b/packages/manager/CHANGELOG.md @@ -4,273 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## [2025-04-08] - v1.139.0 - - -### Added: - -- Add cache update logic on edit alert query ([#11917](https://github.com/linode/manager/pull/11917)) - -### Changed: - -- Update Breadcrumb component to conform to Akamai Design System specs ([#11841](https://github.com/linode/manager/pull/11841)) -- Display interface type first in Linode Network IP Addresses table ([#11865](https://github.com/linode/manager/pull/11865)) -- Update Radio Button component to conform to Akamai Design System specs ([#11878](https://github.com/linode/manager/pull/11878)) -- Change `GlobalFilters.tsx` and `Zoomer.tsx` to add color on hover of icon ([#11883](https://github.com/linode/manager/pull/11883)) -- Update styles to CDS for profile menu ([#11884](https://github.com/linode/manager/pull/11884)) -- Update BetaChip styles, its usage and updated BetaChip component tests ([#11965](https://github.com/linode/manager/pull/11965)) -- Disable form fields on Firewall Create page for restricted users ([#11954](https://github.com/linode/manager/pull/11954)) -- Update 'Learn more' docs link for Accelerated Compute plans ([#11970](https://github.com/linode/manager/pull/11970)) - -### Fixed: - -- Database action menu incorrectly enabled with `read-only` grant and `Delete Cluster` button incorrectly disabled with `read/write` grant ([#11890](https://github.com/linode/manager/pull/11890)) -- Tabs keyboard navigation on some Tanstack rerouted features ([#11894](https://github.com/linode/manager/pull/11894)) -- Console errors on create menu & Linode create flow ([#11933](https://github.com/linode/manager/pull/11933)) -- PAT Token drawer logic when Child Account Access is hidden ([#11935](https://github.com/linode/manager/pull/11935)) -- Profile Menu Icon Size Inconsistency ([#11946](https://github.com/linode/manager/pull/11946)) -- Unclearable ACL IP addresses for LKE clusters ([#11947](https://github.com/linode/manager/pull/11947)) -- DBaaS Advanced Configuration: drawer shows outdated config values after save and reopen ([#11979](https://github.com/linode/manager/pull/11979)) - -### Removed: - -- Ramda from `Utilities` package ([#11861](https://github.com/linode/manager/pull/11861)) -- Move `ListItemOption` from `manager` to `ui` package ([#11790](https://github.com/linode/manager/pull/11790)) -- Move `regionsData` from `manager` to `utilities` package ([#11790](https://github.com/linode/manager/pull/11790)) -- Move `LinodeCreateType` to `utilities` package ([#11790](https://github.com/linode/manager/pull/11790)) -- Move `LinodeSelect` to new `shared` package ([#11844](https://github.com/linode/manager/pull/11844)) -- Legacy BetaChip component ([#11872](https://github.com/linode/manager/pull/11872)) -- Move `doesRegionSupportFeature` from `manager` to `utilities` package ([#11891](https://github.com/linode/manager/pull/11891)) -- Move Tags-related queries and dependencies to shares `queries` package ([#11897](https://github.com/linode/manager/pull/11897)) -- Move Support-related queries and dependencies to shared `queries` package ([#11904](https://github.com/linode/manager/pull/11904)) -- Move `luxon` dependent utils from `manager` to `utilities` package ([#11905](https://github.com/linode/manager/pull/11905)) -- Move ramda dependent utils ([#11913](https://github.com/linode/manager/pull/11913)) -- Move `useIsGeckoEnabled` hook out of `RegionSelect` to `@linode/shared` package ([#11918](https://github.com/linode/manager/pull/11918)) -- Remove region selector from Edit VPC drawer since data center assignment cannot be changed. ([#11929](https://github.com/linode/manager/pull/11929)) -- DBaaS: deprecated types, outdated and unused code in DatabaseCreate and DatabaseSummary ([#11909](https://github.com/linode/manager/pull/11909)) -- Move `useFormattedDate` from `manager` to `utilities` package ([#11931](https://github.com/linode/manager/pull/11931)) -- Move stackscripts-related queries and dependencies to shared `queries` package ([#11949](https://github.com/linode/manager/pull/11949)) - -### Tech Stories: - -- Make `RegionSelect` and `RegionMultiSelect` pure ([#11790](https://github.com/linode/manager/pull/11790)) -- Nodebalancer routing (Tanstack) ([#11858](https://github.com/linode/manager/pull/11858)) -- Add `FirewallSelect` component ([#11887](https://github.com/linode/manager/pull/11887)) -- Add eslint rule for deprecating mui theme.spacing ([#11889](https://github.com/linode/manager/pull/11889)) -- Resolve Path Traversal Vulnerabilities detected from semgrep ([#11914](https://github.com/linode/manager/pull/11914)) -- Move feature flag code out of Kubernetes queries file ([#11922](https://github.com/linode/manager/pull/11922)) -- Fix incorrect secret in `publish-packages` Github Action ([#11923](https://github.com/linode/manager/pull/11923)) -- Remove hashing on Pendo account and visitor ids ([#11950](https://github.com/linode/manager/pull/11950)) - -### Tests: - -- Add HTML report generation for Cypress test results ([#11795](https://github.com/linode/manager/pull/11795)) -- Add `env:premiumPlans` test tag for tests which require premium plan availability ([#11886](https://github.com/linode/manager/pull/11886)) -- Fix Linode create end-to-end test failures against alternative environments ([#11886](https://github.com/linode/manager/pull/11886)) -- Delete redundant Linode create SSH key test ([#11886](https://github.com/linode/manager/pull/11886)) -- Add test for Add Linode Interface drawer ([#11887](https://github.com/linode/manager/pull/11887)) -- Prevent legacy regions from being used by Cypress tests ([#11892](https://github.com/linode/manager/pull/11892)) -- Temporarily skip Firewall end-to-end tests ([#11898](https://github.com/linode/manager/pull/11898)) -- Add tests for restricted user on database page ([#11912](https://github.com/linode/manager/pull/11912)) -- Allow Cypress Volume tests to pass against alternative environments ([#11939](https://github.com/linode/manager/pull/11939)) -- Fix create-linode-view-code-snippet.spec.ts test broken in devcloud ([#11948](https://github.com/linode/manager/pull/11948)) -- Improve stability of Linode config Cypress tests ([#11951](https://github.com/linode/manager/pull/11951)) - -### Upcoming Features: - -- DBaaS Advanced Configurations: Add UI for existing engine options in the drawer ([#11812](https://github.com/linode/manager/pull/11812)) -- Add Default Firewalls paper to Account Settings ([#11828](https://github.com/linode/manager/pull/11828)) -- Add functionality to support the 'Assign New Roles' drawer for a single user in IAM ([#11834](https://github.com/linode/manager/pull/11834)) -- Update Firewall Devices Linode landing table to account for new interface devices ([#11842](https://github.com/linode/manager/pull/11842)) -- Add Quotas Tab Beta Chip ([#11872](https://github.com/linode/manager/pull/11872)) -- Add AlertListNoticeMessages component for handling multiple API error messages, update AddChannelListing and MetricCriteria components to display these errors, add handleMultipleError util method for aggregating, mapping the errors to fields ([#11874](https://github.com/linode/manager/pull/11874)) -- Disable query to get Linode Interface when Interface Delete dialog is closed ([#11881](https://github.com/linode/manager/pull/11881)) -- Update title for Delete Interface dialog ([#11881](https://github.com/linode/manager/pull/11881)) -- Add VPC support to the Add Network Interface Drawer ([#11887](https://github.com/linode/manager/pull/11887)) -- Add Interface Details drawer for Linode Interfaces ([#11888](https://github.com/linode/manager/pull/11888)) -- Add a new confirmation dialog for the unassigning role flow in IAM ([#11893](https://github.com/linode/manager/pull/11893)) -- Add VPC & Firewall section to LKE-E create flow ([#11901](https://github.com/linode/manager/pull/11901)) -- Update success message for create/edit/enable/disable alert at `CreateAlertDefinition.tsx`, `EditAlertDefinition.tsx`, and `AlertListTable.tsx` ([#11903](https://github.com/linode/manager/pull/11903)) -- Update Firewall Landing table to account for Linode Interface devices and Default Firewalls ([#11920](https://github.com/linode/manager/pull/11920)) -- Add Default Firewall chips to Firewall Detail page ([#11920](https://github.com/linode/manager/pull/11920)) -- Remove preselected role from Change Role drawer ([#11926](https://github.com/linode/manager/pull/11926)) -- Adjust logic for displaying encryption status on Linode Details page and encryption copy on LKE Create page ([#11930](https://github.com/linode/manager/pull/11930)) -- DBaaS Advanced Configurations: set up Autocomplete to display categorized options, add/remove configs, and implement a dynamic validation schema for all field types ([#11885](https://github.com/linode/manager/pull/11885)) -- Support more VPC features when using Linode Interfaces on the Linode Create page ([#11915](https://github.com/linode/manager/pull/11915)) -- Pre-select default firewalls on the Linode Create flow ([#11915](https://github.com/linode/manager/pull/11915)) -- Update mock data and tests according to IAM backend response updates ([#11919](https://github.com/linode/manager/pull/11919)) -- Update `vpcIPFactory` to support IPv6 ([#11938](https://github.com/linode/manager/pull/11938)) -- Add a 2-minute refetch interval in alerts.ts, add isLoading and remove isFetching in AlertDetail.tsx. ([#11945](https://github.com/linode/manager/pull/11945)) - -## [2025-03-26] - v1.138.1 - -### Fixed: - -- Authentication redirect issue ([#11925](https://github.com/linode/manager/pull/11925)) - -## [2025-03-25] - v1.138.0 - - -### Added: - -- LKE UI updates for checkout bar & NodeBalancer Details summary ([#11653](https://github.com/linode/manager/pull/11653)) -- Link to Linode's Firewall in Linode Entity Details ([#11736](https://github.com/linode/manager/pull/11736)) -- Logic to redirect invalid paths to home page of `/metrics` & `/alerts/definitions` url ([#11837](https://github.com/linode/manager/pull/11837)) -- Tags in Volume Create Flow ([#11696](https://github.com/linode/manager/pull/11696)) - -### Changed: - -- Copy in Node Pool resize, autoscale, and recycle CTAs ([#11664](https://github.com/linode/manager/pull/11664)) -- Make "Public" checkbox default-checked in OAuth App creation form ([#11681](https://github.com/linode/manager/pull/11681)) -- Improve error handling for KubeConfig download during cluster provisioning ([#11683](https://github.com/linode/manager/pull/11683)) -- Update copy for LKE ACL section ([#11746](https://github.com/linode/manager/pull/11746)) -- Update copy for LKE Recycle, Upgrade Version, and Delete Pool modals ([#11775](https://github.com/linode/manager/pull/11775)) -- Update RegionSelect placement group tooltiptext copy ([#11791](https://github.com/linode/manager/pull/11791)) -- Enhance MUI Adornments: Unify Theme for Autocomplete and TextField Components via InputBase Styling ([#11807](https://github.com/linode/manager/pull/11807)) -- Update main search to use new API search implementation for large accounts ([#11819](https://github.com/linode/manager/pull/11819)) -- Update styles to CSD for create menu ([#11821](https://github.com/linode/manager/pull/11821)) -- Bucket create `Label` to `Bucket Name` ([#11877](https://github.com/linode/manager/pull/11877)) -- Account for `LA Disk Encryption` region capability when checking if region supports Disk Encryption ([#11833](https://github.com/linode/manager/pull/11833)) -- Account for whether region supports LDE when determining tooltip display for unencrypted linodes & node pools ([#11833](https://github.com/linode/manager/pull/11833)) - -### Fixed: - -- Document titles of ACPL with appropriate keyword ([#11662](https://github.com/linode/manager/pull/11662)) -- Missing disabled treatment and notices on several create flows for restricted users (#11674, #11687, #11672, #11700) -- Node Pools CTA buttons on small screens ([#11701](https://github.com/linode/manager/pull/11701)) -- 404 cluster endpoint errors on Linode details page for non-LKE Linodes ([#11714](https://github.com/linode/manager/pull/11714)) -- Mobile primary nav height ([#11723](https://github.com/linode/manager/pull/11723)) -- RTX 6000 plans showing up in LKE UI ([#11731](https://github.com/linode/manager/pull/11731)) -- Authentication Provider Selection Card UI regression ([#11732](https://github.com/linode/manager/pull/11732)) -- Unresponsive show details button for selected Stackscript ([#11765](https://github.com/linode/manager/pull/11765)) -- Linodes from distributed regions appearing in Create flow Backups & Clone tab ([#11767](https://github.com/linode/manager/pull/11767)) -- Confusing wording on DBaaS suspend dialog ([#11769](https://github.com/linode/manager/pull/11769)) -- Incorrect helper text in `Add an SSH Key` Drawer ([#11771](https://github.com/linode/manager/pull/11771)) -- Linode Backups Drawer style regressions ([#11776](https://github.com/linode/manager/pull/11776)) -- NodeBalancer Create Summary broken dividers and spacing ([#11779](https://github.com/linode/manager/pull/11779)) -- Disable Firewall Selection in Linode Clone ([#11784](https://github.com/linode/manager/pull/11784)) -- Incorrect default color shown in Avatar color picker ([#11787](https://github.com/linode/manager/pull/11787)) -- PaginationFooter page size select ([#11798](https://github.com/linode/manager/pull/11798)) -- `Add an SSH Key` button spacing ([#11800](https://github.com/linode/manager/pull/11800)) -- Hide VPC Section from Linode Create Clone Tab ([#11805](https://github.com/linode/manager/pull/11805)) -- Minor spacing inconsistencies throughout LKE ([#11827](https://github.com/linode/manager/pull/11827)) -- Storybook not rendering due to crypto.randomUUID not being available in Storybook context ([#11835](https://github.com/linode/manager/pull/11835)) -- Show details button misalignment for selected StackScript ([#11838](https://github.com/linode/manager/pull/11838)) -- Navigation for metrics and alerts under Monitor at `PrimaryNav.tsx` ([#11869](https://github.com/linode/manager/pull/11869)) - -### Removed: - -- Rate limits table from Object Storage details drawer ([#11848](https://github.com/linode/manager/pull/11848)) -- Move `capitalize` utility and `useInterval` hook to `@linode/utilities` package ([#11666](https://github.com/linode/manager/pull/11666)) -- Migrate utilities from `manager` to `utilities` package ([#11711](https://github.com/linode/manager/pull/11711)) -- Migrate ErrorState to ui package ([#11718](https://github.com/linode/manager/pull/11718)) -- Migrate utilities from `manager` to `utilities` package - pt2 ([#11733](https://github.com/linode/manager/pull/11733)) -- Migrate hooks from `manager` to `utilities` package ([#11770](https://github.com/linode/manager/pull/11770)) -- Move linodes-related queries and dependencies to shared packages ([#11774](https://github.com/linode/manager/pull/11774)) -- Migrate utilities from `manager` to `utilities` package - pt3 ([#11778](https://github.com/linode/manager/pull/11778)) -- Migrate Drawer to ui package ([#11789](https://github.com/linode/manager/pull/11789)) -- Migrate ActionsPanel to ui package ([#11810](https://github.com/linode/manager/pull/11810)) -- Unnecessary styled component from Linode Detail summary ([#11820](https://github.com/linode/manager/pull/11820)) -- Move volumes-related queries and dependencies to shared `queries` package ([#11843](https://github.com/linode/manager/pull/11843)) -- Move the entire `sort-by.ts` (excluding sortByUTFDate) to `utilities` package ([#11846](https://github.com/linode/manager/pull/11846)) -- Migrate hooks from `manager` to `utilities` package ([#11850](https://github.com/linode/manager/pull/11850)) -- Migrate utilities from `manager` to `utilities` package - pt4 ([#11859](https://github.com/linode/manager/pull/11859)) -- Code coverage comparison jobs ([#11879](https://github.com/linode/manager/pull/11879)) - -### Tech Stories: - -- Refactor the Linode Rebuild dialog ([#11629](https://github.com/linode/manager/pull/11629)) -- Refactor CreateFirewallDrawer to use `react-hook-form` ([#11677](https://github.com/linode/manager/pull/11677)) -- Upgrade to MUI v6 ([#11688](https://github.com/linode/manager/pull/11688)) -- Migrate Firewalls feature to Tanstack routing ([#11704](https://github.com/linode/manager/pull/11704)) -- Upgrade to 4.0.0 Design Tokens - New Spacing & Badge Tokens ([#11757](https://github.com/linode/manager/pull/11757)) -- Update jspdf dependencies to resolve DOMPurify dependabot alert ([#11768](https://github.com/linode/manager/pull/11768)) -- Upgrade Shiki to 3.1.0 ([#11772](https://github.com/linode/manager/pull/11772)) -- Move @vitest/ui to monorepo root dependency ([#11755](https://github.com/linode/manager/pull/11755)) -- Upgrade vitest and @vitest/ui to 3.0.7 ([#11755](https://github.com/linode/manager/pull/11755)) -- Update react-vnc to 3.0.7 ([#11758](https://github.com/linode/manager/pull/11758)) -- Restrict direct imports of Link from `react-router-dom` ([#11801](https://github.com/linode/manager/pull/11801)) -- Refactor Stackscripts routing (Tanstack) ([#11806](https://github.com/linode/manager/pull/11806)) -- Update main search to not depend on `recompose` library ([#11819](https://github.com/linode/manager/pull/11819)) -- Remedy canvg dependency vulnerability ([#11839](https://github.com/linode/manager/pull/11839)) -- Improve type-safety of Linode Create flow form ([#11847](https://github.com/linode/manager/pull/11847)) -- Upgrade Vite to 6.2.2 ([#11866](https://github.com/linode/manager/pull/11866)) -- Upgrade tsx to 4.19.3 ([#11866](https://github.com/linode/manager/pull/11866)) -- Add MSW crud support for new Linode Interface endpoints ([#11875](https://github.com/linode/manager/pull/11875)) -- Upgrade Storybook to 8.6.7 ([#11876](https://github.com/linode/manager/pull/11876)) - -### Tests: - -- Add Cypress integration test to enable Linode Managed ([#10806](https://github.com/linode/manager/pull/10806)) -- Improve Cypress test VLAN handling ([#11362](https://github.com/linode/manager/pull/11362)) -- Add Cypress test for Service Transfers fetch error ([#11607](https://github.com/linode/manager/pull/11607)) -- Add Cypress tests for restricted user Linode create flow ([#11663](https://github.com/linode/manager/pull/11663)) -- Add test for ACLP Create Alerts ([#11670](https://github.com/linode/manager/pull/11670)) -- Add Cypress test for Image create page for restricted users ([#11705](https://github.com/linode/manager/pull/11705)) -- Configure caddy to ignore test output ([#11706](https://github.com/linode/manager/pull/11706)) -- Add Cypress test for ACLP edit functionality of user defined alert ([#11719](https://github.com/linode/manager/pull/11719)) -- Fix CloudPulse test failures triggered by new notice ([#11728](https://github.com/linode/manager/pull/11728)) -- Remove Cypress test assertion involving Login app text ([#11737](https://github.com/linode/manager/pull/11737)) -- Add Cypress test for Volume create page for restricted users ([#11743](https://github.com/linode/manager/pull/11743)) -- Delete region test suite ([#11780](https://github.com/linode/manager/pull/11780)) -- Add Cypress test for LKE create page for restricted users ([#11793](https://github.com/linode/manager/pull/11793)) -- Fix bug in Edit User alert ([#11822](https://github.com/linode/manager/pull/11822)) -- Fix VPC test failures when factory default region does not exist ([#11862](https://github.com/linode/manager/pull/11862)) -- Add unit tests for `sortByUTFDate` utility ([#11846](https://github.com/linode/manager/pull/11846)) -- Fix Google Pay test failures when using Braintree sandbox environment (#11863) -- Apply new custom eslint rule and lint files (#11689, #11722, #11730, #11756, #11766, #11814) - - -### Upcoming Features: - -- Build new Quotas Controls ([#11647](https://github.com/linode/manager/pull/11647)) -- Add Linode Interfaces Table to the Linode Details page ([#11655](https://github.com/linode/manager/pull/11655)) -- Add final copy and docs links for LKE-E ([#11664](https://github.com/linode/manager/pull/11664)) -- Truncate long usernames and emails in IAM users table and details page ([#11668](https://github.com/linode/manager/pull/11668)) -- Fix filtering in IAM users table ([#11668](https://github.com/linode/manager/pull/11668)) -- Add ability to edit alerts for CloudPulse User Alerts ([#11669](https://github.com/linode/manager/pull/11669)) -- Add ability to create Firewalls from templates ([#11678](https://github.com/linode/manager/pull/11678)) -- Add CloudPulse AlertReusableComponent, utils, and queries for contextual view ([#11685](https://github.com/linode/manager/pull/11685)) -- Filter regions by supported region ids - `getSupportedRegionIds` in CloudPulse alerts ([#11692](https://github.com/linode/manager/pull/11692)) -- Add new tags filter in the resources section of CloudPulse Alerts ([#11693](https://github.com/linode/manager/pull/11693)) -- Fix LKE cluster table sorting when LKE-E beta endpoint is used ([#11714](https://github.com/linode/manager/pull/11714)) -- Hide GPU plans tab for LKE-E ([#11726](https://github.com/linode/manager/pull/11726)) -- Hide Networking sections from Linode Configurations page for Linodes with new interfaces ([#11727](https://github.com/linode/manager/pull/11727)) -- Add table components to CloudPulse Alert Information contextual view ([#11734](https://github.com/linode/manager/pull/11734)) -- Add DBaaS Advanced Configurations initial set up (new tab, drawer) ([#11735](https://github.com/linode/manager/pull/11735)) -- Add Interface type to Linode Entity Detail ([#11736](https://github.com/linode/manager/pull/11736)) -- Add support for `nodebalancerVPC` feature flag for NodeBalancer-VPC integration ([#11738](https://github.com/linode/manager/pull/11738)) -- Fix LKE-E provisioning placeholder when filtering by status ([#11745](https://github.com/linode/manager/pull/11745)) -- Enable ACL by default for LKE-E clusters ([#11746](https://github.com/linode/manager/pull/11746)) -- Improve UX of CloudPulse Alerts create flow and resources section ([#11748](https://github.com/linode/manager/pull/11748)) -- Update IAM assigned roles and entities table and refine styles for IAM permissions component. ([#11762](https://github.com/linode/manager/pull/11762)) -- Enhance UI for Cloudpulse Alerting: Notifications, Metric Limits, and Dimensions ([#11773](https://github.com/linode/manager/pull/11773)) -- Ability to add and remove Linode interfaces ([#11782](https://github.com/linode/manager/pull/11782)) -- Add Confirmation Dialog when toggling an entity’s alert for CloudPulse Alerting ([#11785](https://github.com/linode/manager/pull/11785)) -- Update warnings and actions for LKE-E VPCs ([#11786](https://github.com/linode/manager/pull/11786)) -- Support Linode Interface Account Setting on Linode Create Flow ([#11788](https://github.com/linode/manager/pull/11788)) -- Request for Quota increase modal ([#11792](https://github.com/linode/manager/pull/11792)) -- Disable query to get Linode's firewalls for Linodes using new interfaces in LinodeEntityDetail ([#11796](https://github.com/linode/manager/pull/11796)) -- Update navigation for CloudPulse Metrics to `/metrics` and CloudPulse Alerts to `/alerts` ([#11803](https://github.com/linode/manager/pull/11803)) -- Add Upgrade Interfaces dialog for Linodes using legacy Configuration Profile Interfaces ([#11808](https://github.com/linode/manager/pull/11808)) -- Disable Akamai App Platform beta for LKE-E clusters on create flow ([#11809](https://github.com/linode/manager/pull/11809)) -- Handle errors while enabling and disabling alerts in Monitor at `AlertListTable.tsx` ([#11813](https://github.com/linode/manager/pull/11813)) -- Set `refetchInterval` for 2 mins in CloudPulse alert queries ([#11815](https://github.com/linode/manager/pull/11815)) -- Add resources selection limitation in CloudPulse Alerting resources section for create and edit flows ([#11823](https://github.com/linode/manager/pull/11823)) -- Remove `sxEndIcon` prop from Add Metric, Dimension Filter and Notification Channel buttons ([#11825](https://github.com/linode/manager/pull/11825)) -- Add query to update roles in IAM ([#11840](https://github.com/linode/manager/pull/11840)) -- Add a new drawer for changing role flow in IAM ([#11840](https://github.com/linode/manager/pull/11840)) -- Initial support for VPCs using Linode Interfaces on the Linode create flow ([#11847](https://github.com/linode/manager/pull/11847)) -- Restrict enable/disable actions in CloudPulse Alerts action menu based on alert status ([#11860](https://github.com/linode/manager/pull/11860)) -- Remove toggle in the 'Add A User' drawer and default to limited access for users for IAM ([#11870](https://github.com/linode/manager/pull/11870)) -- Update LKE-E flows to account for LDE status at LA launch ([#11880](https://github.com/linode/manager/pull/11880)) - - ## [2025-02-27] - v1.137.2 ### Fixed: -- Disk Encryption logic preventing Linode deployment in distributed regions ([#11760](https://github.com/linode/manager/pull/11760)) +- Disk Encryption logic preventing Linode deployment in distributed regions ([#11760](https://github.com/linode/manager/pull/11760) ## [2025-02-25] - v1.137.1 diff --git a/packages/manager/cypress.config.ts b/packages/manager/cypress.config.ts index d3c582dff3b..b8596bed4d9 100644 --- a/packages/manager/cypress.config.ts +++ b/packages/manager/cypress.config.ts @@ -3,10 +3,7 @@ import { defineConfig } from 'cypress'; import { setupPlugins } from './cypress/support/plugins'; import { configureBrowser } from './cypress/support/plugins/configure-browser'; import { configureFileWatching } from './cypress/support/plugins/configure-file-watching'; -import { - enableJunitE2eReport, - enableJunitComponentReport, -} from './cypress/support/plugins/junit-report'; +import { configureTestSuite } from './cypress/support/plugins/configure-test-suite'; import { discardPassedTestRecordings } from './cypress/support/plugins/discard-passed-test-recordings'; import { loadEnvironmentConfig } from './cypress/support/plugins/load-env-config'; import { nodeVersionCheck } from './cypress/support/plugins/node-version-check'; @@ -16,15 +13,14 @@ import { configureApi } from './cypress/support/plugins/configure-api'; import { fetchAccount } from './cypress/support/plugins/fetch-account'; import { fetchLinodeRegions } from './cypress/support/plugins/fetch-linode-regions'; import { splitCypressRun } from './cypress/support/plugins/split-run'; +import { enableJunitReport } from './cypress/support/plugins/junit-report'; import { generateTestWeights } from './cypress/support/plugins/generate-weights'; import { logTestTagInfo } from './cypress/support/plugins/test-tagging-info'; import cypressViteConfig from './cypress/vite.config'; import { featureFlagOverrides } from './cypress/support/plugins/feature-flag-override'; import { postRunCleanup } from './cypress/support/plugins/post-run-cleanup'; import { resetUserPreferences } from './cypress/support/plugins/reset-user-preferences'; -import { enableHtmlReport } from './cypress/support/plugins/html-report'; -import { configureMultiReporters } from './cypress/support/plugins/configure-multi-reporters'; -import cypressOnFix from 'cypress-on-fix'; + /** * Exports a Cypress configuration object. * @@ -66,14 +62,11 @@ export default defineConfig({ viewportWidth: 500, viewportHeight: 500, - setupNodeEvents(cypressOn, config) { - const on = cypressOnFix(cypressOn); + setupNodeEvents(on, config) { return setupPlugins(on, config, [ loadEnvironmentConfig, discardPassedTestRecordings, - enableJunitComponentReport, - enableHtmlReport, - configureMultiReporters, + enableJunitReport('Component', true), ]); }, }, @@ -83,15 +76,18 @@ export default defineConfig({ // This can be overridden using `CYPRESS_BASE_URL`. baseUrl: 'http://localhost:3000', + + // This is overridden when `CY_TEST_SUITE` is defined. + // See `cypress/support/plugins/configure-test-suite.ts`. specPattern: 'cypress/e2e/core/**/*.spec.{ts,tsx}', - setupNodeEvents(cypressOn, config) { - const on = cypressOnFix(cypressOn); + setupNodeEvents(on, config) { return setupPlugins(on, config, [ loadEnvironmentConfig, nodeVersionCheck, configureApi, configureFileWatching, + configureTestSuite, configureBrowser, vitePreprocess, discardPassedTestRecordings, @@ -102,10 +98,8 @@ export default defineConfig({ featureFlagOverrides, logTestTagInfo, splitCypressRun, + enableJunitReport(), generateTestWeights, - enableJunitE2eReport, - enableHtmlReport, - configureMultiReporters, postRunCleanup, ]); }, diff --git a/packages/manager/cypress/component/components/beta-chip.spec.tsx b/packages/manager/cypress/component/components/beta-chip.spec.tsx index dff3a4aa0f7..58641a6e63b 100644 --- a/packages/manager/cypress/component/components/beta-chip.spec.tsx +++ b/packages/manager/cypress/component/components/beta-chip.spec.tsx @@ -1,17 +1,28 @@ -import { BetaChip } from '@linode/ui'; import * as React from 'react'; import { checkComponentA11y } from 'support/util/accessibility'; import { componentTests, visualTests } from 'support/util/components'; +import { BetaChip } from 'src/components/BetaChip/BetaChip'; + componentTests('BetaChip', () => { visualTests((mount) => { - it('renders "BETA" text indicator', () => { - mount(); + it('renders "BETA" text indicator with primary color', () => { + mount(); + cy.findByText('beta').should('be.visible'); + }); + + it('renders "BETA" text indicator with default color', () => { + mount(); cy.findByText('beta').should('be.visible'); }); - it('passes aXe accessibility', () => { - mount(); + it('passes aXe check with primary color', () => { + mount(); + checkComponentA11y(); + }); + + it('passes aXe check with default color', () => { + mount(); checkComponentA11y(); }); }); diff --git a/packages/manager/cypress/component/components/region-select.spec.tsx b/packages/manager/cypress/component/components/region-select.spec.tsx index 91d5200a39c..656d5dacaf3 100644 --- a/packages/manager/cypress/component/components/region-select.spec.tsx +++ b/packages/manager/cypress/component/components/region-select.spec.tsx @@ -1,4 +1,3 @@ -import { accountAvailabilityFactory, regionFactory } from '@linode/utilities'; import * as React from 'react'; import { mockGetAccountAvailability } from 'support/intercepts/account'; import { ui } from 'support/ui'; @@ -7,6 +6,7 @@ import { createSpy } from 'support/util/components'; import { componentTests, visualTests } from 'support/util/components'; import { RegionSelect } from 'src/components/RegionSelect/RegionSelect'; +import { accountAvailabilityFactory, regionFactory } from 'src/factories'; componentTests('RegionSelect', (mount) => { beforeEach(() => { @@ -26,7 +26,6 @@ componentTests('RegionSelect', (mount) => { mount( {}} regions={[region]} value={undefined} @@ -55,7 +54,6 @@ componentTests('RegionSelect', (mount) => { mount( {}} regions={[region]} value={undefined} @@ -85,7 +83,6 @@ componentTests('RegionSelect', (mount) => { mount( {}} regions={[region]} value={undefined} @@ -115,7 +112,6 @@ componentTests('RegionSelect', (mount) => { Other Element {}} regions={[region]} value={undefined} @@ -149,7 +145,6 @@ componentTests('RegionSelect', (mount) => { mount( {}} regions={regions} value={undefined} @@ -184,7 +179,6 @@ componentTests('RegionSelect', (mount) => { mount( {}} regions={regions} value={regionToPreselect.id} @@ -220,7 +214,6 @@ componentTests('RegionSelect', (mount) => { mount( {}} regions={regions} value={regionToSelect.id} @@ -248,7 +241,6 @@ componentTests('RegionSelect', (mount) => { {}} regions={regions} value={regionToSelect.id} @@ -268,7 +260,6 @@ componentTests('RegionSelect', (mount) => { mount( {}} regions={regions} value={undefined} @@ -286,7 +277,6 @@ componentTests('RegionSelect', (mount) => { mount( { mount( { mount( {}} regions={regions} value={undefined} @@ -391,7 +379,6 @@ componentTests('RegionSelect', (mount) => { mount( {}} regions={regions} value={undefined} @@ -421,7 +408,6 @@ componentTests('RegionSelect', (mount) => { mount( {}} regions={regions} value={undefined} @@ -452,7 +438,6 @@ componentTests('RegionSelect', (mount) => { mount( {}} regions={regions} value={undefined} @@ -465,7 +450,6 @@ componentTests('RegionSelect', (mount) => { mount( {}} regions={regions} value={selectedRegion.id} @@ -478,7 +462,6 @@ componentTests('RegionSelect', (mount) => { mount( {}} regions={regions} value={selectedRegion.id} diff --git a/packages/manager/cypress/component/components/tabs.spec.tsx b/packages/manager/cypress/component/components/tabs.spec.tsx deleted file mode 100644 index 130b8072555..00000000000 --- a/packages/manager/cypress/component/components/tabs.spec.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { createRoute } from '@tanstack/react-router'; -import * as React from 'react'; -import { ui } from 'support/ui'; -import { checkComponentA11y } from 'support/util/accessibility'; -import { componentTests, visualTests } from 'support/util/components'; - -import { SuspenseLoader } from 'src/components/SuspenseLoader'; -import { SafeTabPanel } from 'src/components/Tabs/SafeTabPanel'; -import { TabPanels } from 'src/components/Tabs/TabPanels'; -import { Tabs } from 'src/components/Tabs/Tabs'; -import { TanStackTabLinkList } from 'src/components/Tabs/TanStackTabLinkList'; -import { useTabs } from 'src/hooks/useTabs'; - -const CustomTabs = () => { - const { handleTabChange, tabIndex, tabs } = useTabs([ - { - title: 'Tab 1', - to: '/tab-1', - }, - { - title: 'Tab 2', - to: '/tab-2', - }, - { - title: 'Tab 3', - to: '/tab-3', - }, - ]); - - return ( - - - }> - - -
Tab 1 content
-
- -
Tab 2 content
-
- -
Tab 3 content
-
-
-
-
- ); -}; - -componentTests( - 'Tabs', - (mount) => { - describe('Tabs', () => { - it('should render all tabs and default to the first tab', () => { - mount(); - ui.tabList - .findTabByTitle('Tab 1') - .should('exist') - .should('have.attr', 'aria-selected', 'true'); - ui.tabList - .findTabByTitle('Tab 2') - .should('exist') - .should('have.attr', 'aria-selected', 'false'); - ui.tabList - .findTabByTitle('Tab 3') - .should('exist') - .should('have.attr', 'aria-selected', 'false'); - - cy.get('[data-reach-tab-panels]').should('have.text', 'Tab 1 content'); - }); - - it('should render the correct tab content when a tab is clicked', () => { - mount(); - - ui.tabList.findTabByTitle('Tab 2').click(); - ui.tabList - .findTabByTitle('Tab 2') - .should('exist') - .should('have.attr', 'aria-selected', 'true'); - cy.get('[data-reach-tab-panels]').should('have.text', 'Tab 2 content'); - - ui.tabList.findTabByTitle('Tab 3').click(); - ui.tabList - .findTabByTitle('Tab 3') - .should('exist') - .should('have.attr', 'aria-selected', 'true'); - cy.get('[data-reach-tab-panels]').should('have.text', 'Tab 3 content'); - }); - - it('should handle keyboard navigation', () => { - mount(); - - ui.tabList.findTabByTitle('Tab 1').focus(); - cy.get('body').type('{rightArrow}'); - ui.tabList - .findTabByTitle('Tab 2') - .should('exist') - .should('have.attr', 'aria-selected', 'true'); - cy.get('[data-reach-tab-panels]').should('have.text', 'Tab 2 content'); - - cy.get('body').type('{rightArrow}'); - ui.tabList - .findTabByTitle('Tab 3') - .should('exist') - .should('have.attr', 'aria-selected', 'true'); - cy.get('[data-reach-tab-panels]').should('have.text', 'Tab 3 content'); - - cy.get('body').type('{leftArrow}'); - ui.tabList - .findTabByTitle('Tab 2') - .should('exist') - .should('have.attr', 'aria-selected', 'true'); - cy.get('[data-reach-tab-panels]').should('have.text', 'Tab 2 content'); - - cy.get('body').type('{leftArrow}'); - ui.tabList - .findTabByTitle('Tab 1') - .should('exist') - .should('have.attr', 'aria-selected', 'true'); - cy.get('[data-reach-tab-panels]').should('have.text', 'Tab 1 content'); - }); - }); - }, - { - routeTree: (parentRoute) => [ - createRoute({ - getParentRoute: () => parentRoute, - path: '/tab-1', - }), - createRoute({ - getParentRoute: () => parentRoute, - path: '/tab-2', - }), - createRoute({ - getParentRoute: () => parentRoute, - path: '/tab-3', - }), - ], - useTanstackRouter: true, - } -); - -visualTests( - (mount) => { - describe('Accessibility checks', () => { - it('passes aXe check when menu is closed without an item selected', () => { - mount(); - checkComponentA11y(); - }); - }); - }, - { - useTanstackRouter: true, - } -); diff --git a/packages/manager/cypress/e2e/core/account/account-cancellation.spec.ts b/packages/manager/cypress/e2e/core/account/account-cancellation.spec.ts index 78797cd18bb..728ea5695d7 100644 --- a/packages/manager/cypress/e2e/core/account/account-cancellation.spec.ts +++ b/packages/manager/cypress/e2e/core/account/account-cancellation.spec.ts @@ -2,7 +2,6 @@ * @file Integration tests for Cloud Manager account cancellation flows. */ -import { profileFactory } from '@linode/utilities'; import { cancellationDataLossWarning, cancellationDialogTitle, @@ -23,6 +22,7 @@ import { } from 'support/util/random'; import { accountFactory } from 'src/factories/account'; +import { profileFactory } from 'src/factories/profile'; import { CHILD_USER_CLOSE_ACCOUNT_TOOLTIP_TEXT, PARENT_USER_CLOSE_ACCOUNT_TOOLTIP_TEXT, diff --git a/packages/manager/cypress/e2e/core/account/account-linode-managed.spec.ts b/packages/manager/cypress/e2e/core/account/account-linode-managed.spec.ts index 163164c7ee3..3410dd7ad46 100644 --- a/packages/manager/cypress/e2e/core/account/account-linode-managed.spec.ts +++ b/packages/manager/cypress/e2e/core/account/account-linode-managed.spec.ts @@ -2,28 +2,27 @@ * @file Integration tests for Cloud Manager account enable Linode Managed flows. */ -import { linodeFactory, profileFactory } from '@linode/utilities'; -import { - visitUrlWithManagedDisabled, - visitUrlWithManagedEnabled, -} from 'support/api/managed'; -import { - linodeEnabledMessageText, - linodeManagedStateMessageText, -} from 'support/constants/account'; +import { profileFactory } from 'src/factories/profile'; +import { accountFactory } from 'src/factories/account'; +import { linodeFactory } from 'src/factories/linodes'; +import { chooseRegion } from 'support/util/regions'; +import { Linode } from '@linode/api-v4'; import { + mockGetAccount, mockEnableLinodeManaged, mockEnableLinodeManagedError, - mockGetAccount, } from 'support/intercepts/account'; import { mockGetLinodes } from 'support/intercepts/linodes'; +import { + linodeEnabledMessageText, + linodeManagedStateMessageText, +} from 'support/constants/account'; import { mockGetProfile } from 'support/intercepts/profile'; import { ui } from 'support/ui'; -import { chooseRegion } from 'support/util/regions'; - -import { accountFactory } from 'src/factories/account'; - -import type { Linode } from '@linode/api-v4'; +import { + visitUrlWithManagedDisabled, + visitUrlWithManagedEnabled, +} from 'support/api/managed'; describe('Account Linode Managed', () => { /* @@ -34,17 +33,17 @@ describe('Account Linode Managed', () => { it('users can enable Linode Managed', () => { const mockAccount = accountFactory.build(); const mockProfile = profileFactory.build({ - restricted: false, username: 'mock-user', + restricted: false, }); - const mockLinodes = new Array(5) - .fill(null) - .map((item: null, index: number): Linode => { + const mockLinodes = new Array(5).fill(null).map( + (item: null, index: number): Linode => { return linodeFactory.build({ label: `Linode ${index}`, region: chooseRegion().id, }); - }); + } + ); mockGetLinodes(mockLinodes).as('getLinodes'); mockGetAccount(mockAccount).as('getAccount'); @@ -97,8 +96,8 @@ describe('Account Linode Managed', () => { it('restricted users cannot enable Managed', () => { const mockAccount = accountFactory.build(); const mockProfile = profileFactory.build({ - restricted: true, username: 'mock-restricted-user', + restricted: true, }); const errorMessage = 'Unauthorized'; @@ -145,8 +144,8 @@ describe('Account Linode Managed', () => { it('users can only open a support ticket to cancel Linode Managed', () => { const mockAccount = accountFactory.build(); const mockProfile = profileFactory.build({ - restricted: false, username: 'mock-user', + restricted: false, }); mockGetAccount(mockAccount).as('getAccount'); diff --git a/packages/manager/cypress/e2e/core/account/account-login-history.spec.ts b/packages/manager/cypress/e2e/core/account/account-login-history.spec.ts index e18840149e9..b53a18ef7d0 100644 --- a/packages/manager/cypress/e2e/core/account/account-login-history.spec.ts +++ b/packages/manager/cypress/e2e/core/account/account-login-history.spec.ts @@ -2,7 +2,6 @@ * @file Integration tests for Cloud Manager account login history flows. */ -import { profileFactory } from '@linode/utilities'; import { loginEmptyStateMessageText, loginHelperText, @@ -10,6 +9,7 @@ import { import { mockGetAccountLogins } from 'support/intercepts/account'; import { mockGetProfile } from 'support/intercepts/profile'; +import { profileFactory } from 'src/factories'; import { accountLoginFactory } from 'src/factories/accountLogin'; import { PARENT_USER } from 'src/features/Account/constants'; import { formatDate } from 'src/utilities/formatDate'; diff --git a/packages/manager/cypress/e2e/core/account/display-settings.spec.ts b/packages/manager/cypress/e2e/core/account/display-settings.spec.ts index e5b0e1655c6..64f950dd149 100644 --- a/packages/manager/cypress/e2e/core/account/display-settings.spec.ts +++ b/packages/manager/cypress/e2e/core/account/display-settings.spec.ts @@ -1,4 +1,4 @@ -import { profileFactory } from '@linode/utilities'; +import { profileFactory } from '@src/factories'; import { getProfile } from 'support/api/account'; import { mockUpdateUsername } from 'support/intercepts/account'; import { interceptGetProfile } from 'support/intercepts/profile'; 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 9e5554532c1..81d1f357097 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 @@ -2,7 +2,6 @@ * @file Integration tests for personal access token CRUD operations. */ -import { profileFactory } from '@linode/utilities'; import { mockCreatePersonalAccessToken, mockGetAppTokens, @@ -15,6 +14,7 @@ import { ui } from 'support/ui'; import { randomLabel, randomString } from 'support/util/random'; import { appTokenFactory } from 'src/factories/oauth'; +import { profileFactory } from 'src/factories/profile'; import { PROXY_USER_RESTRICTED_TOOLTIP_TEXT } from 'src/features/Account/constants'; import type { Token } from '@linode/api-v4'; @@ -179,72 +179,6 @@ describe('Personal access tokens', () => { }); }); - it('sends scope as "*" when all permissions are set to read/write', () => { - const token = appTokenFactory.build({ - label: randomLabel(), - token: randomString(64), - }); - - mockCreatePersonalAccessToken(token).as('createToken'); - - cy.visitWithLogin('/profile/tokens'); - - // 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(); - - 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'); - - // Confirm submit button is disabled without specifying scopes. - ui.buttonGroup.findButtonByTitle('Create Token').scrollIntoView(); - ui.buttonGroup.findButtonByTitle('Create Token').should('be.disabled'); - - // Select "Read/Write" for all scopes. - cy.get( - '[aria-label="Personal Access Token Permissions"] tr:gt(1)' - ).each((row) => - cy.wrap(row).within(() => { - cy.get('[type="radio"]').eq(2).click(); - }) - ); - - // Verify "Select All" radio for "Read/Write" is active - cy.get('[data-qa-perm-rw-radio]').should( - 'have.attr', - 'data-qa-radio', - 'true' - ); - - // Specify a label and submit. - cy.findByLabelText('Label').scrollIntoView(); - cy.findByLabelText('Label') - .should('be.visible') - .should('be.enabled') - .click(); - cy.findByLabelText('Label').type(token.label); - - ui.buttonGroup.findButtonByTitle('Create Token').scrollIntoView(); - ui.buttonGroup - .findButtonByTitle('Create Token') - .should('be.visible') - .should('be.enabled') - .click(); - }); - - // Confirm that new PAT's scopes are '*' - cy.wait('@createToken').then((xhr) => { - expect(xhr.request.body.scopes).to.equal('*'); - }); - }); - /* * - Uses mocked API requests to confirm UI flow when renaming and revoking tokens * - Confirms that list shows the correct label after renaming a token diff --git a/packages/manager/cypress/e2e/core/account/security-questions.spec.ts b/packages/manager/cypress/e2e/core/account/security-questions.spec.ts index f2746a5377e..3d161c672b4 100644 --- a/packages/manager/cypress/e2e/core/account/security-questions.spec.ts +++ b/packages/manager/cypress/e2e/core/account/security-questions.spec.ts @@ -2,7 +2,6 @@ * @file Integration tests for account security questions. */ -import { profileFactory, securityQuestionsFactory } from '@linode/utilities'; import { mockGetProfile, mockGetSecurityQuestions, @@ -10,6 +9,9 @@ import { } from 'support/intercepts/profile'; import { ui } from 'support/ui'; +import { securityQuestionsFactory } from 'src/factories/profile'; +import { profileFactory } from 'src/factories/profile'; + /** * Finds the "Security Questions" section on the profile auth page. * 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 be0c71fc660..5f6e29425bf 100644 --- a/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts +++ b/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts @@ -3,16 +3,15 @@ */ import { getProfile } from '@linode/api-v4/lib/profile'; -import { createLinodeRequestFactory, linodeFactory } from '@linode/utilities'; import { authenticate } from 'support/api/authentication'; import { visitUrlWithManagedEnabled } from 'support/api/managed'; import { interceptInitiateEntityTransfer, mockAcceptEntityTransfer, mockGetEntityTransfers, - mockGetEntityTransfersError, mockInitiateEntityTransferError, mockReceiveEntityTransfer, + mockGetEntityTransfersError, } from 'support/intercepts/account'; import { mockGetLinodes } from 'support/intercepts/linodes'; import { ui } from 'support/ui'; @@ -22,15 +21,13 @@ import { pollLinodeStatus } from 'support/util/polling'; import { randomLabel, randomUuid } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; +import { linodeFactory } from 'src/factories'; import { entityTransferFactory } from 'src/factories/entityTransfers'; +import { createLinodeRequestFactory } from 'src/factories/linodes'; import { formatDate } from 'src/utilities/formatDate'; -import type { - EntityTransfer, - EntityTransferStatus, - Linode, - Profile, -} from '@linode/api-v4'; +import type { EntityTransferStatus } from '@linode/api-v4'; +import type { EntityTransfer, Linode, Profile } from '@linode/api-v4'; // Service transfer empty state message. const serviceTransferEmptyState = 'No data to display.'; diff --git a/packages/manager/cypress/e2e/core/account/sms-verification.spec.ts b/packages/manager/cypress/e2e/core/account/sms-verification.spec.ts index 74f59ac48a7..6a5ed1aed43 100644 --- a/packages/manager/cypress/e2e/core/account/sms-verification.spec.ts +++ b/packages/manager/cypress/e2e/core/account/sms-verification.spec.ts @@ -2,7 +2,6 @@ * @file Integration tests for SMS phone verification. */ -import { profileFactory } from '@linode/utilities'; import { mockGetProfile, mockSendVerificationCode, @@ -16,6 +15,7 @@ import { randomPhoneNumber, } from 'support/util/random'; +import { profileFactory } from 'src/factories/profile'; import { getFormattedNumber } from 'src/features/Profile/AuthenticationSettings/PhoneVerification/helpers'; describe('SMS phone verification', () => { diff --git a/packages/manager/cypress/e2e/core/account/ssh-keys.spec.ts b/packages/manager/cypress/e2e/core/account/ssh-keys.spec.ts index cf24e9644d9..5afeac8936c 100644 --- a/packages/manager/cypress/e2e/core/account/ssh-keys.spec.ts +++ b/packages/manager/cypress/e2e/core/account/ssh-keys.spec.ts @@ -1,4 +1,3 @@ -import { sshKeyFactory } from '@linode/utilities'; import { sshFormatErrorMessage } from 'support/constants/account'; import { mockCreateSSHKey, @@ -10,6 +9,8 @@ import { import { ui } from 'support/ui'; import { randomLabel, randomString } from 'support/util/random'; +import { sshKeyFactory } from 'src/factories'; + describe('SSH keys', () => { /* * - Vaildates SSH key creation flow using mock data. diff --git a/packages/manager/cypress/e2e/core/account/two-factor-auth.spec.ts b/packages/manager/cypress/e2e/core/account/two-factor-auth.spec.ts index 75001e64ccf..9f1e945cb5a 100644 --- a/packages/manager/cypress/e2e/core/account/two-factor-auth.spec.ts +++ b/packages/manager/cypress/e2e/core/account/two-factor-auth.spec.ts @@ -2,7 +2,6 @@ * @file Integration tests for account two-factor authentication functionality. */ -import { profileFactory, securityQuestionsFactory } from '@linode/utilities'; import { mockConfirmTwoFactorAuth, mockDisableTwoFactorAuth, @@ -18,6 +17,11 @@ import { randomString, } from 'support/util/random'; +import { + profileFactory, + securityQuestionsFactory, +} from 'src/factories/profile'; + import type { SecurityQuestionsData } from '@linode/api-v4'; /** diff --git a/packages/manager/cypress/e2e/core/account/user-permissions.spec.ts b/packages/manager/cypress/e2e/core/account/user-permissions.spec.ts index 6dcb0232917..ccaa315dfa9 100644 --- a/packages/manager/cypress/e2e/core/account/user-permissions.spec.ts +++ b/packages/manager/cypress/e2e/core/account/user-permissions.spec.ts @@ -1,4 +1,4 @@ -import { profileFactory } from '@linode/utilities'; +import { profileFactory } from '@src/factories'; import { accountUserFactory } from '@src/factories/accountUsers'; import { grantsFactory } from '@src/factories/grants'; import { userPermissionsGrants } from 'support/constants/user-permissions'; @@ -581,7 +581,7 @@ describe('User permission management', () => { ); // Confirm that no "Profile" tab is present on the proxy user's User Permissions page. - cy.findByText('User Profile').should('not.exist'); + expect(cy.findByText('User Profile').should('not.exist')); cy.get('[data-qa-global-section]') .should('be.visible') diff --git a/packages/manager/cypress/e2e/core/account/user-verification-banner.spec.ts b/packages/manager/cypress/e2e/core/account/user-verification-banner.spec.ts index d3b5cc0a771..d2547970203 100644 --- a/packages/manager/cypress/e2e/core/account/user-verification-banner.spec.ts +++ b/packages/manager/cypress/e2e/core/account/user-verification-banner.spec.ts @@ -1,4 +1,4 @@ -import { profileFactory, securityQuestionsFactory } from '@linode/utilities'; +import { profileFactory, securityQuestionsFactory } from '@src/factories'; import { accountUserFactory } from '@src/factories/accountUsers'; import { grantsFactory } from '@src/factories/grants'; import { verificationBannerNotice } from 'support/constants/user'; diff --git a/packages/manager/cypress/e2e/core/account/users-landing-page.spec.ts b/packages/manager/cypress/e2e/core/account/users-landing-page.spec.ts index ae8960a5f8a..af4179b74c5 100644 --- a/packages/manager/cypress/e2e/core/account/users-landing-page.spec.ts +++ b/packages/manager/cypress/e2e/core/account/users-landing-page.spec.ts @@ -1,4 +1,4 @@ -import { profileFactory } from '@linode/utilities'; +import { profileFactory } from '@src/factories'; import { accountUserFactory } from '@src/factories/accountUsers'; import { grantsFactory } from '@src/factories/grants'; import { @@ -31,12 +31,13 @@ import type { Profile } from '@linode/api-v4'; const initTestUsers = (profile: Profile, enableChildAccountAccess: boolean) => { const mockProfile = profile; - const mockRestrictedParentWithoutChildAccountAccess = - accountUserFactory.build({ + const mockRestrictedParentWithoutChildAccountAccess = accountUserFactory.build( + { restricted: true, user_type: 'parent', username: 'restricted-parent-user-without-child-account-access', - }); + } + ); const mockRestrictedParentWithChildAccountAccess = accountUserFactory.build({ restricted: true, diff --git a/packages/manager/cypress/e2e/core/billing/billing-invoices.spec.ts b/packages/manager/cypress/e2e/core/billing/billing-invoices.spec.ts index 1c419543396..b00ce254e9e 100644 --- a/packages/manager/cypress/e2e/core/billing/billing-invoices.spec.ts +++ b/packages/manager/cypress/e2e/core/billing/billing-invoices.spec.ts @@ -168,15 +168,14 @@ describe('Account invoices', () => { // If the invoice item has a region, confirm that it is displayed // in the table row. Otherwise, confirm that the table cell which // would normally show the region is empty. - if (invoiceItem.region) { - cy.findByText(getRegionLabel(invoiceItem.region)).should( - 'be.visible' - ); - } else { - cy.get('[data-qa-region]') - .should('be.visible') - .should('be.empty'); - } + !!invoiceItem.region + ? cy + .findByText(getRegionLabel(invoiceItem.region)) + .should('be.visible') + : cy + .get('[data-qa-region]') + .should('be.visible') + .should('be.empty'); }); } ); @@ -199,13 +198,11 @@ describe('Account invoices', () => { // If the invoice item has a region, confirm that it is displayed // in the table row. Otherwise, confirm that "Global" is displayed // in the region column. - if (invoiceItem.region) { - cy.findByText(getRegionLabel(invoiceItem.region)).should( - 'be.visible' - ); - } else { - cy.findByText('Global').should('be.visible'); - } + !!invoiceItem.region + ? cy + .findByText(getRegionLabel(invoiceItem.region)) + .should('be.visible') + : cy.findByText('Global').should('be.visible'); }); } ); diff --git a/packages/manager/cypress/e2e/core/billing/google-pay.spec.ts b/packages/manager/cypress/e2e/core/billing/google-pay.spec.ts index 3c484ee5154..f4d90998420 100644 --- a/packages/manager/cypress/e2e/core/billing/google-pay.spec.ts +++ b/packages/manager/cypress/e2e/core/billing/google-pay.spec.ts @@ -52,8 +52,7 @@ const mockPaymentMethodsExpired: PaymentMethod[] = [ ]; const pastDueExpiry = 'Expired 07/20'; -const braintreeURL = - 'https://+(payments.braintree-api.com|payments.sandbox.braintree-api.com)/*'; +const braintreeURL = 'https://client-analytics.braintreegateway.com/*'; describe('Google Pay', () => { it('adds google pay method', () => { diff --git a/packages/manager/cypress/e2e/core/billing/restricted-user-billing.spec.ts b/packages/manager/cypress/e2e/core/billing/restricted-user-billing.spec.ts index e47db4f1307..4c55cefadfa 100644 --- a/packages/manager/cypress/e2e/core/billing/restricted-user-billing.spec.ts +++ b/packages/manager/cypress/e2e/core/billing/restricted-user-billing.spec.ts @@ -2,8 +2,7 @@ * @file Integration tests for restricted user billing flows. */ -import { profileFactory } from '@linode/utilities'; -import { paymentMethodFactory } from '@src/factories'; +import { paymentMethodFactory, profileFactory } from '@src/factories'; import { accountUserFactory } from '@src/factories/accountUsers'; import { grantsFactory } from '@src/factories/grants'; import { mockGetPaymentMethods, mockGetUser } from 'support/intercepts/account'; diff --git a/packages/manager/cypress/e2e/core/billing/smoke-billing-activity.spec.ts b/packages/manager/cypress/e2e/core/billing/smoke-billing-activity.spec.ts index 73fb4893f57..9189a234d17 100644 --- a/packages/manager/cypress/e2e/core/billing/smoke-billing-activity.spec.ts +++ b/packages/manager/cypress/e2e/core/billing/smoke-billing-activity.spec.ts @@ -1,5 +1,5 @@ import { getProfile } from '@linode/api-v4'; -import { profileFactory } from '@linode/utilities'; +import { profileFactory } from '@src/factories'; import { formatDate } from '@src/utilities/formatDate'; import { DateTime } from 'luxon'; import { authenticate } from 'support/api/authentication'; @@ -125,21 +125,24 @@ describe('Billing Activity Feed', () => { * - Confirms that clicking on an invoice's label directs the user to the invoice details page. */ it('lists invoices and payments', () => { - const invoiceMocks = buildArray(10, (i: number): Invoice => { - const id = randomNumber(1, 999999); - const date = DateTime.now().minus({ days: 2, months: i }).toISO(); - const subtotal = randomNumber(25, 949); - const tax = randomNumber(5, 50); - - return invoiceFactory.build({ - date, - id, - label: `Invoice #${id}`, - subtotal, - tax, - total: subtotal + tax, - }); - }); + const invoiceMocks = buildArray( + 10, + (i: number): Invoice => { + const id = randomNumber(1, 999999); + const date = DateTime.now().minus({ days: 2, months: i }).toISO(); + const subtotal = randomNumber(25, 949); + const tax = randomNumber(5, 50); + + return invoiceFactory.build({ + date, + id, + label: `Invoice #${id}`, + subtotal, + tax, + total: subtotal + tax, + }); + } + ); const paymentMocks = invoiceMocks.map( (invoice: Invoice, i: number): Payment => { diff --git a/packages/manager/cypress/e2e/core/cloudpulse/alert-errors.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/alert-errors.spec.ts deleted file mode 100644 index 328620c8cf5..00000000000 --- a/packages/manager/cypress/e2e/core/cloudpulse/alert-errors.spec.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { mockGetAccount } from 'support/intercepts/account'; -import { - mockGetAllAlertDefinitions, - mockGetCloudPulseServices, - mockUpdateAlertDefinitionsError, -} from 'support/intercepts/cloudpulse'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; -import { ui } from 'support/ui'; - -import { accountFactory, alertFactory } from 'src/factories'; - -import type { Flags } from 'src/featureFlags'; - -const flags: Partial = { aclp: { beta: true, enabled: true } }; -const mockAccount = accountFactory.build(); -const mockAlerts = [ - alertFactory.build({ - label: 'Alert-1', - service_type: 'dbaas', - status: 'enabled', - type: 'user', - }), - alertFactory.build({ - label: 'Alert-2', - service_type: 'dbaas', - status: 'disabled', - type: 'user', - }), -]; - -describe('Alerts Listing Page - Error Handling', () => { - /** - * - * - Confirms that users can attempt to enable or disable alerts from the Alerts Listing page. - * - Confirms that API failures when updating an alert are handled correctly. - * - Confirms that an error message is displayed in the UI when an alert update fails. - * - Confirms that the error message appears in a toast notification. - * - Confirms that users remain on the Alerts Listing page even after an update failure. - * - Confirms that the UI does not reflect a successful state change if the request fails. - */ - beforeEach(() => { - mockAppendFeatureFlags(flags); - mockGetAccount(mockAccount); - mockGetCloudPulseServices(['linode', 'dbaas']); - mockGetAllAlertDefinitions(mockAlerts).as('getAlertDefinitionsList'); - mockUpdateAlertDefinitionsError( - 'dbaas', - 1, - 'An error occurred while disabling the alert' - ).as('getFirstAlertDefinitions'); - mockUpdateAlertDefinitionsError( - 'dbaas', - 2, - 'An error occurred while enabling the alert' - ).as('getSecondAlertDefinitions'); - cy.visitWithLogin('/alerts/definitions'); - cy.wait('@getAlertDefinitionsList'); - }); - - it('should display correct error messages when disabling or enabling alerts fails', () => { - // Function to search for an alert - const searchAlert = (alertName: string) => { - cy.findByPlaceholderText('Search for Alerts') - .should('be.visible') - .and('not.be.disabled') - .clear(); - - cy.findByPlaceholderText('Search for Alerts').type(alertName); - }; - - // Function to toggle an alert's status - const toggleAlertStatus = ( - alertName: string, - action: 'Disable' | 'Enable', - alias: string - ) => { - cy.findByText(alertName) - .should('be.visible') - .closest('tr') - .within(() => { - ui.actionMenu - .findByTitle(`Action menu for Alert ${alertName}`) - .should('be.visible') - .click(); - }); - - ui.actionMenuItem.findByTitle(action).should('be.visible').click(); - ui.button.findByTitle(action).should('be.visible').click(); - cy.wait(alias).then(({ response }) => { - ui.toast.assertMessage(response?.body.errors[0].reason); - }); - }; - // Disable "Alert-1" - searchAlert('Alert-1'); - toggleAlertStatus('Alert-1', 'Disable', '@getFirstAlertDefinitions'); - - // Enable "Alert-2" - searchAlert('Alert-2'); - toggleAlertStatus('Alert-2', 'Enable', '@getSecondAlertDefinitions'); - }); -}); diff --git a/packages/manager/cypress/e2e/core/cloudpulse/alert-show-details.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/alert-show-details.spec.ts index 5dfa6cbc1c5..5e921b0a002 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/alert-show-details.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/alert-show-details.spec.ts @@ -4,7 +4,7 @@ * This file contains Cypress tests that validate the display and content of the Alerts Show Detail Page in the CloudPulse application. * It ensures that all alert details, criteria, and resource information are displayed correctly. */ -import { capitalize, regionFactory } from '@linode/utilities'; +import { capitalize } from '@linode/utilities'; import { aggregationTypeMap, dimensionOperatorTypeMap, @@ -28,6 +28,7 @@ import { alertRulesFactory, databaseFactory, notificationChannelFactory, + regionFactory, } from 'src/factories'; import { formatDate } from 'src/utilities/formatDate'; @@ -108,7 +109,7 @@ describe('Integration Tests for Alert Show Detail Page', () => { it('navigates to the Show Details page from the list page', () => { // Navigate to the alert definitions list page with login - cy.visitWithLogin('/alerts/definitions'); + cy.visitWithLogin('/monitor/alerts/definitions'); // Wait for the alert definitions list API call to complete cy.wait('@getAlertDefinitionsList'); @@ -132,7 +133,9 @@ describe('Integration Tests for Alert Show Detail Page', () => { }); it('should correctly display the details of the DBaaS alert in the alert details view', () => { - cy.visitWithLogin(`/alerts/definitions/detail/${service_type}/${id}`); + cy.visitWithLogin( + `/monitor/alerts/definitions/detail/${service_type}/${id}` + ); cy.wait(['@getDBaaSAlertDefinitions', '@getMockedDbaasDatabases']); // Validating contents of Overview Section diff --git a/packages/manager/cypress/e2e/core/cloudpulse/alerts-listing-page.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/alerts-listing-page.spec.ts index 3b754ca5e5b..ceb7c10d904 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/alerts-listing-page.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/alerts-listing-page.spec.ts @@ -13,10 +13,7 @@ import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { ui } from 'support/ui'; import { accountFactory, alertFactory } from 'src/factories'; -import { - UPDATE_ALERT_SUCCESS_MESSAGE, - alertStatuses, -} from 'src/features/CloudPulse/Alerts/constants'; +import { alertStatuses } from 'src/features/CloudPulse/Alerts/constants'; import { formatDate } from 'src/utilities/formatDate'; import type { Alert } from '@linode/api-v4'; @@ -64,17 +61,6 @@ const mockAlerts = [ }), ]; -interface AlertActionOptions { - action: 'Disable' | 'Enable'; - alertName: string; - alias: string; -} - -interface AlertToggleOptions extends AlertActionOptions { - confirmationText: string; - successMessage: string; -} - /** * @description * This code validates the presence and correct text of the table headers @@ -146,7 +132,7 @@ const validateAlertDetails = (alert: Alert) => { .and( 'have.attr', 'href', - `/alerts/definitions/detail/${service_type}/${id}` + `/monitor/alerts/definitions/detail/${service_type}/${id}` ); cy.findByText(formatDate(updated, { format: 'MMM dd, yyyy, h:mm a' })) .should('be.visible') @@ -178,7 +164,7 @@ describe('Integration Tests for CloudPulse Alerts Listing Page', () => { mockUpdateAlertDefinitions('dbaas', 2, mockAlerts[1]).as( 'getSecondAlertDefinitions' ); - cy.visitWithLogin('/alerts/definitions'); + cy.visitWithLogin('/monitor/alerts/definitions'); cy.wait('@getAlertDefinitionsList'); }); @@ -210,11 +196,12 @@ describe('Integration Tests for CloudPulse Alerts Listing Page', () => { it('should validate UI elements and alert details', () => { // Validate navigation links and buttons - cy.findByText('Alerts').should('be.visible'); - + cy.findByText('Alerts') + .should('be.visible') + .and('have.attr', 'href', '/monitor/alerts'); cy.findByText('Definitions') .should('be.visible') - .and('have.attr', 'href', '/alerts/definitions'); + .and('have.attr', 'href', '/monitor/alerts/definitions'); ui.buttonGroup.findButtonByTitle('Create Alert').should('be.visible'); // Validate table headers @@ -316,13 +303,12 @@ describe('Integration Tests for CloudPulse Alerts Listing Page', () => { }; // Function to toggle an alert's status - const toggleAlertStatus = ({ - action, - alertName, - alias, - confirmationText, - successMessage, - }: AlertToggleOptions) => { + const toggleAlertStatus = ( + alertName: string, + action: 'Disable' | 'Enable', + alias: string, + successMessage: string + ) => { cy.findByText(alertName) .should('be.visible') .closest('tr') @@ -332,48 +318,29 @@ describe('Integration Tests for CloudPulse Alerts Listing Page', () => { .should('be.visible') .click(); }); - ui.actionMenuItem.findByTitle(action).should('be.visible').click(); - // verify dialog title - ui.dialog - .findByTitle(`${action} ${alertName} Alert?`) - .should('be.visible') - .within(() => { - cy.findByText(confirmationText).should('be.visible'); - ui.button - .findByTitle(action) - .should('be.visible') - .should('be.enabled') - .click(); - }); + ui.actionMenuItem.findByTitle(action).should('be.visible').click(); - cy.wait(alias).then(() => { + cy.wait(alias).then(({ response }) => { ui.toast.assertMessage(successMessage); }); }; // Disable "Alert-1" - const actions: Array = [ - { - action: 'Disable', - alertName: 'Alert-1', - alias: '@getFirstAlertDefinitions', - }, - { - action: 'Enable', - alertName: 'Alert-2', - alias: '@getSecondAlertDefinitions', - }, - ]; + searchAlert('Alert-1'); + toggleAlertStatus( + 'Alert-1', + 'Disable', + '@getFirstAlertDefinitions', + 'Alert disabled' + ); - actions.forEach(({ action, alertName, alias }) => { - searchAlert(alertName); - toggleAlertStatus({ - action, - alertName, - alias, - confirmationText: `Are you sure you want to ${action.toLowerCase()} this alert definition?`, - successMessage: UPDATE_ALERT_SUCCESS_MESSAGE, - }); - }); + // Enable "Alert-2" + searchAlert('Alert-2'); + toggleAlertStatus( + 'Alert-2', + 'Enable', + '@getSecondAlertDefinitions', + 'Alert enabled' + ); }); }); diff --git a/packages/manager/cypress/e2e/core/cloudpulse/cloudpulse-dashboard-errors.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/cloudpulse-dashboard-errors.spec.ts index 24571e6df3d..66aeae2d68b 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/cloudpulse-dashboard-errors.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/cloudpulse-dashboard-errors.spec.ts @@ -1,7 +1,6 @@ /** * @file Error Handling Tests for CloudPulse Dashboard. */ -import { regionFactory } from '@linode/utilities'; import { widgetDetails } from 'support/constants/widgets'; import { mockGetAccount } from 'support/intercepts/account'; import { @@ -33,6 +32,7 @@ import { dashboardFactory, dashboardMetricFactory, databaseFactory, + regionFactory, widgetFactory, } from 'src/factories'; @@ -134,7 +134,7 @@ describe('Tests for API error handling', () => { 'Internal Server Error' ).as('getMetricDefinitions'); - cy.visitWithLogin('/metrics'); + cy.visitWithLogin('monitor/cloudpulse'); // Wait for the API calls . cy.wait(['@fetchServices', '@fetchDashboard']); @@ -196,7 +196,7 @@ describe('Tests for API error handling', () => { // Mocking an error response for the 'fetchServices' API request. mockGetCloudPulseServicesError('Internal Server Error').as('fetchServices'); - cy.visitWithLogin('/metrics'); + cy.visitWithLogin('monitor/cloudpulse'); // Wait for the API calls . cy.wait('@fetchServices'); @@ -211,7 +211,7 @@ describe('Tests for API error handling', () => { 'getCloudPulseTokenError' ); - cy.visitWithLogin('/metrics'); + cy.visitWithLogin('monitor/cloudpulse'); // Wait for the API calls . cy.wait(['@fetchServices', '@fetchDashboard']); @@ -277,7 +277,7 @@ describe('Tests for API error handling', () => { 'fetchDashboard' ); - cy.visitWithLogin('/metrics'); + cy.visitWithLogin('monitor/cloudpulse'); // Wait for the API calls . cy.wait(['@fetchServices', '@fetchDashboard']); @@ -294,7 +294,7 @@ describe('Tests for API error handling', () => { 'getCloudPulseDashboardError' ); - cy.visitWithLogin('/metrics'); + cy.visitWithLogin('monitor/cloudpulse'); // Wait for the API calls . cy.wait(['@fetchServices', '@fetchDashboard']); @@ -358,7 +358,7 @@ describe('Tests for API error handling', () => { 'getCloudPulseRegionsError' ); - cy.visitWithLogin('/metrics'); + cy.visitWithLogin('monitor/cloudpulse'); // Wait for the API calls . cy.wait(['@fetchServices', '@fetchDashboard']); @@ -388,7 +388,7 @@ describe('Tests for API error handling', () => { 'getDatabaseInstancesError' ); - cy.visitWithLogin('/metrics'); + cy.visitWithLogin('monitor/cloudpulse'); // Wait for the API calls . cy.wait(['@fetchServices', '@fetchDashboard']); diff --git a/packages/manager/cypress/e2e/core/cloudpulse/cloudpulse-navigation.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/cloudpulse-navigation.spec.ts index e86622b72bf..89d0c23ca31 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/cloudpulse-navigation.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/cloudpulse-navigation.spec.ts @@ -1,5 +1,5 @@ /** - * @file Integration tests for Moniter navigation. + * @file Integration tests for CloudPulse navigation. */ import { mockGetAccount } from 'support/intercepts/account'; @@ -10,16 +10,16 @@ import { accountFactory } from 'src/factories'; const mockAccount = accountFactory.build(); -describe('Moniter navigation', () => { +describe('CloudPulse navigation', () => { beforeEach(() => { mockGetAccount(mockAccount).as('getAccount'); }); /* - * - Confirms that Metrics navigation item is shown when feature flag is enabled. - * - Confirms that clicking Metrics navigation item directs user to Metrics landing page. + * - Confirms that Cloudpulse navigation item is shown when feature flag is enabled. + * - Confirms that clicking Cloudpulse navigation item directs user to Cloudpulse landing page. */ - it('can navigate to metrics landing page', () => { + it('can navigate to Cloudpulse landing page', () => { mockAppendFeatureFlags({ aclp: { beta: true, @@ -30,14 +30,14 @@ describe('Moniter navigation', () => { cy.visitWithLogin('/linodes'); cy.wait('@getFeatureFlags'); - cy.get('[data-testid="menu-item-Metrics"]').should('be.visible').click(); - cy.url().should('endWith', '/metrics'); + cy.get('[data-testid="menu-item-Monitor"]').should('be.visible').click(); + cy.url().should('endWith', '/monitor'); }); /* - * - Confirms that metrics navigation item is not shown when feature flag is disabled. + * - Confirms that Cloudpulse navigation item is not shown when feature flag is disabled. */ - it('does not show metrics navigation item when feature is disabled', () => { + it('does not show Cloudpulse navigation item when feature is disabled', () => { mockAppendFeatureFlags({ aclp: { beta: true, @@ -49,14 +49,14 @@ describe('Moniter navigation', () => { cy.wait('@getFeatureFlags'); ui.nav.find().within(() => { - cy.get('[data-testid="menu-item-Metrics"]').should('not.exist'); + cy.get('[data-testid="menu-item-Monitor"]').should('not.exist'); }); }); /* - * - Confirms that manual navigation to metrics landing page with feature is disabled displays Not Found to user. + * - Confirms that manual navigation to Cloudpulse landing page with feature is disabled displays Not Found to user. */ - it('displays Not Found when manually navigating to /metrics with feature flag disabled', () => { + it('displays Not Found when manually navigating to /cloudpulse with feature flag disabled', () => { mockAppendFeatureFlags({ aclp: { beta: true, @@ -64,14 +64,14 @@ describe('Moniter navigation', () => { }, }).as('getFeatureFlags'); - cy.visitWithLogin('/metrics'); + cy.visitWithLogin('monitor'); cy.wait('@getFeatureFlags'); cy.findByText('Not Found').should('be.visible'); }); /* - * - Confirms that manual navigation to the 'Alert' landing page is disabled, and users are shown a 'Not Found' message.. + * - Confirms that manual navigation to the 'Alert' page on the Cloudpulse landing page is disabled, and users are shown a 'Not Found' message.. */ it('should display "Not Found" when navigating to alert definitions with feature flag disabled', () => { mockAppendFeatureFlags({ @@ -79,7 +79,7 @@ describe('Moniter navigation', () => { }).as('getFeatureFlags'); // Attempt to visit the alert definitions page for a specific alert using a manual URL - cy.visitWithLogin('/alerts'); + cy.visitWithLogin('monitor/alerts/definitions'); // Wait for the feature flag to be fetched and applied cy.wait('@getFeatureFlags'); @@ -89,7 +89,7 @@ describe('Moniter navigation', () => { }); /* - * - Confirms that manual navigation to the 'Alert Definitions Detail' page on the Alert landing page is disabled, and users are shown a 'Not Found' message.. + * - Confirms that manual navigation to the 'Alert Definitions Detail' page on the Cloudpulse landing page is disabled, and users are shown a 'Not Found' message.. */ it('should display "Not Found" when manually navigating to alert details with feature flag disabled', () => { mockAppendFeatureFlags({ @@ -97,7 +97,7 @@ describe('Moniter navigation', () => { }).as('getFeatureFlags'); // Attempt to visit the alert detail page for a specific alert using a manual URL - cy.visitWithLogin('/alerts/definitions/detail/dbaas/20000'); + cy.visitWithLogin('monitor/alerts/definitions/detail/dbaas/20000'); // Wait for the feature flag to be fetched and applied cy.wait('@getFeatureFlags'); diff --git a/packages/manager/cypress/e2e/core/cloudpulse/create-user-alert.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/create-user-alert.spec.ts index 97d48c5a7a5..281e69dd407 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/create-user-alert.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/create-user-alert.spec.ts @@ -2,7 +2,6 @@ * @fileoverview Cypress test suite for the "Create Alert" functionality. */ -import { regionFactory } from '@linode/utilities'; import { statusMap } from 'support/constants/alert'; import { widgetDetails } from 'support/constants/widgets'; import { mockGetAccount } from 'support/intercepts/account'; @@ -27,9 +26,9 @@ import { databaseFactory, memoryRulesFactory, notificationChannelFactory, + regionFactory, triggerConditionFactory, } from 'src/factories'; -import { CREATE_ALERT_SUCCESS_MESSAGE } from 'src/features/CloudPulse/Alerts/constants'; import { formatDate } from 'src/utilities/formatDate'; import type { Flags } from 'src/featureFlags'; @@ -165,14 +164,14 @@ describe('Create Alert', () => { mockGetDatabases(databaseMock); mockGetAllAlertDefinitions([mockAlerts]).as('getAlertDefinitionsList'); mockGetAlertChannels([notificationChannels]); - mockCreateAlertDefinition(serviceType, mockAlerts).as( + mockCreateAlertDefinition(serviceType, customAlertDefinition).as( 'createAlertDefinition' ); }); it('should navigate to the Create Alert page from the Alert Listings page', () => { // Navigate to the alert definitions list page with login - cy.visitWithLogin('/alerts/definitions'); + cy.visitWithLogin('/monitor/alerts/definitions'); // Wait for the alert definitions list API call to complete cy.wait('@getAlertDefinitionsList'); @@ -184,18 +183,18 @@ describe('Create Alert', () => { .click(); // Verify the URL ends with the expected details page path - cy.url().should('endWith', '/alerts/definitions/create'); + cy.url().should('endWith', 'monitor/alerts/definitions/create'); }); it('should successfully create a new alert', () => { - cy.visitWithLogin('/alerts/definitions/create'); + cy.visitWithLogin('monitor/alerts/definitions/create'); // Enter Name and Description - cy.findByPlaceholderText('Enter a Name') + cy.findByPlaceholderText('Enter Name') .should('be.visible') .type(customAlertDefinition.label); - cy.findByPlaceholderText('Enter a Description') + cy.findByPlaceholderText('Enter Description') .should('be.visible') .type(customAlertDefinition.description ?? ''); @@ -228,7 +227,7 @@ describe('Create Alert', () => { const cpuUsageMetricDetails = { aggregationType: 'Average', dataField: 'CPU Utilization', - operator: '=', + operator: '==', ruleIndex: 0, threshold: '1000', }; @@ -274,7 +273,7 @@ describe('Create Alert', () => { const memoryUsageMetricDetails = { aggregationType: 'Average', dataField: 'Memory Usage', - operator: '=', + operator: '==', ruleIndex: 1, threshold: '1000', }; @@ -405,8 +404,8 @@ describe('Create Alert', () => { expect(request.body.channel_ids).to.include(1); // Verify URL redirection and toast notification - cy.url().should('endWith', '/alerts/definitions'); - ui.toast.assertMessage(CREATE_ALERT_SUCCESS_MESSAGE); + cy.url().should('endWith', 'monitor/alerts/definitions'); + ui.toast.assertMessage('Alert successfully created'); // Confirm that Alert is listed on landing page with expected configuration. cy.findByText(label) diff --git a/packages/manager/cypress/e2e/core/cloudpulse/dbaas-widgets-verification.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/dbaas-widgets-verification.spec.ts index 1b877d9f04b..7e2b870bace 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/dbaas-widgets-verification.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/dbaas-widgets-verification.spec.ts @@ -1,7 +1,6 @@ /** * @file Integration Tests for CloudPulse Dbass Dashboard. */ -import { linodeFactory, regionFactory } from '@linode/utilities'; import { widgetDetails } from 'support/constants/widgets'; import { mockGetAccount } from 'support/intercepts/account'; import { @@ -27,6 +26,8 @@ import { dashboardMetricFactory, databaseFactory, kubeLinodeFactory, + linodeFactory, + regionFactory, widgetFactory, } from 'src/factories'; import { generateGraphData } from 'src/features/CloudPulse/Utils/CloudPulseWidgetUtils'; @@ -198,8 +199,8 @@ describe('Integration Tests for DBaaS Dashboard ', () => { mockGetUserPreferences({}); mockGetDatabases([databaseMock]).as('getDatabases'); - // navigate to the metrics page - cy.visitWithLogin('/metrics'); + // navigate to the cloudpulse page + cy.visitWithLogin('monitor'); // Wait for the services and dashboard API calls to complete before proceeding cy.wait(['@fetchServices', '@fetchDashboard']); diff --git a/packages/manager/cypress/e2e/core/cloudpulse/edit-system-alert.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/edit-system-alert.spec.ts index de0def5b4b4..d78e9ce7631 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/edit-system-alert.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/edit-system-alert.spec.ts @@ -4,7 +4,6 @@ * This file contains Cypress tests for the Edit Alert page of the CloudPulse application. * It ensures that users can navigate to the Edit Alert Page and that alerts are correctly displayed and interactive on the Edit page. */ -import { regionFactory } from '@linode/utilities'; import { mockGetAccount } from 'support/intercepts/account'; import { mockGetAlertDefinitions, @@ -16,7 +15,12 @@ import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { mockGetRegions } from 'support/intercepts/regions'; import { ui } from 'support/ui'; -import { accountFactory, alertFactory, databaseFactory } from 'src/factories'; +import { + accountFactory, + alertFactory, + databaseFactory, + regionFactory, +} from 'src/factories'; import type { Alert, Database } from '@linode/api-v4'; import type { Flags } from 'src/featureFlags'; @@ -84,7 +88,7 @@ describe('Integration Tests for Edit Alert', () => { it('should navigate from the Alert Definitions List page to the Edit Alert page', () => { // Navigate to the alert definitions list page with login - cy.visitWithLogin('/alerts/definitions'); + cy.visitWithLogin('/monitor/alerts/definitions'); // Wait for the alert definitions list API call to complete cy.wait('@getAlertDefinitionsList'); @@ -109,7 +113,7 @@ describe('Integration Tests for Edit Alert', () => { it('should correctly display and update the details of the alert in the edit alert page', () => { // Navigate to the Edit Alert page - cy.visitWithLogin(`/alerts/definitions/edit/${service_type}/${id}`); + cy.visitWithLogin(`/monitor/alerts/definitions/edit/${service_type}/${id}`); cy.wait(['@getAlertDefinitions', '@getDatabases']); @@ -127,7 +131,7 @@ describe('Integration Tests for Edit Alert', () => { ui.button.findByTitle('Select All').should('be.visible').click(); ui.button - .findByTitle('Deselect All') + .findByTitle('Unselect All') .should('be.visible') .should('be.enabled'); }); @@ -268,7 +272,7 @@ describe('Integration Tests for Edit Alert', () => { expect(tags).to.include('tag2'); // Validate navigation - cy.url().should('endWith', '/alerts/definitions'); + cy.url().should('endWith', '/monitor/alerts/definitions'); // Confirm toast notification appears ui.toast.assertMessage('Alert resources successfully updated.'); diff --git a/packages/manager/cypress/e2e/core/cloudpulse/edit-user-alert.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/edit-user-alert.spec.ts index ccb7427af30..cbb6451d06f 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/edit-user-alert.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/edit-user-alert.spec.ts @@ -5,7 +5,6 @@ * It verifies that alert details are correctly displayed, interactive, and editable. */ -import { regionFactory } from '@linode/utilities'; import { EVALUATION_PERIOD_DESCRIPTION, METRIC_DESCRIPTION_DATA_FIELD, @@ -30,15 +29,16 @@ import { ui } from 'support/ui'; import { accountFactory, + alertDefinitionFactory, alertFactory, cpuRulesFactory, dashboardMetricFactory, databaseFactory, memoryRulesFactory, notificationChannelFactory, + regionFactory, triggerConditionFactory, } from 'src/factories'; -import { UPDATE_ALERT_SUCCESS_MESSAGE } from 'src/features/CloudPulse/Alerts/constants'; import { formatDate } from 'src/utilities/formatDate'; import type { Database } from '@linode/api-v4'; @@ -48,6 +48,20 @@ import type { Flags } from 'src/featureFlags'; const flags: Partial = { aclp: { beta: true, enabled: true } }; const mockAccount = accountFactory.build(); +// Mock alert definition +const customAlertDefinition = alertDefinitionFactory.build({ + channel_ids: [1], + description: 'update-description', + entity_ids: ['1', '2', '3', '4', '5'], + label: 'Alert-1', + rule_criteria: { + rules: [cpuRulesFactory.build(), memoryRulesFactory.build()], + }, + severity: 0, + tags: [''], + trigger_conditions: triggerConditionFactory.build(), +}); + // Mock alert details const alertDetails = alertFactory.build({ alert_channels: [{ id: 1 }], @@ -135,7 +149,7 @@ describe('Integration Tests for Edit Alert', () => { mockUpdateAlertDefinitions(service_type, id, alertDetails).as( 'updateDefinitions' ); - mockCreateAlertDefinition(service_type, alertDetails).as( + mockCreateAlertDefinition(service_type, customAlertDefinition).as( 'createAlertDefinition' ); mockGetCloudPulseMetricDefinitions(service_type, metricDefinitions); @@ -173,7 +187,7 @@ describe('Integration Tests for Edit Alert', () => { }; it('should correctly display the details of the alert in the Edit Alert page', () => { - cy.visitWithLogin(`/alerts/definitions/edit/${service_type}/${id}`); + cy.visitWithLogin(`/monitor/alerts/definitions/edit/${service_type}/${id}`); cy.wait('@getAlertDefinitions'); // Verify form fields @@ -204,7 +218,7 @@ describe('Integration Tests for Edit Alert', () => { assertRuleValues(0, { aggregationType: 'Average', dataField: 'CPU Utilization', - operator: '=', + operator: '==', threshold: '1000', }); @@ -212,7 +226,7 @@ describe('Integration Tests for Edit Alert', () => { assertRuleValues(1, { aggregationType: 'Average', dataField: 'Memory Usage', - operator: '=', + operator: '==', threshold: '1000', }); @@ -262,7 +276,7 @@ describe('Integration Tests for Edit Alert', () => { }); it('successfully updated alert details and verified that the API request matches the expected test data.', () => { - cy.visitWithLogin(`/alerts/definitions/edit/${service_type}/${id}`); + cy.visitWithLogin(`/monitor/alerts/definitions/edit/${service_type}/${id}`); cy.wait('@getAlertDefinitions'); // Make changes to alert form @@ -274,10 +288,6 @@ describe('Integration Tests for Edit Alert', () => { ui.autocomplete.findByLabel('Severity').clear(); ui.autocomplete.findByLabel('Severity').type('Info'); ui.autocompletePopper.findByTitle('Info').should('be.visible').click(); - cy.get('[data-qa-notice="true"]') - .find('button') - .contains('Deselect All') - .click(); cy.get('[data-qa-notice="true"]') .find('button') .contains('Select All') @@ -351,8 +361,8 @@ describe('Integration Tests for Edit Alert', () => { expect(request.body.rule_criteria.rules[1].threshold).to.equal(1000); // Verify URL redirection and toast notification - cy.url().should('endWith', 'alerts/definitions'); - ui.toast.assertMessage(UPDATE_ALERT_SUCCESS_MESSAGE); + cy.url().should('endWith', 'monitor/alerts/definitions'); + ui.toast.assertMessage('Alert successfully updated.'); // Confirm that Alert is listed on landing page with expected configuration. cy.findByText('Alert-2') diff --git a/packages/manager/cypress/e2e/core/cloudpulse/linode-widget-verification.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/linode-widget-verification.spec.ts index 1b76d04fc66..f3735d6413a 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/linode-widget-verification.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/linode-widget-verification.spec.ts @@ -1,7 +1,6 @@ /** * @file Integration Tests for CloudPulse Linode Dashboard. */ -import { linodeFactory, regionFactory } from '@linode/utilities'; import { widgetDetails } from 'support/constants/widgets'; import { mockGetAccount } from 'support/intercepts/account'; import { @@ -25,6 +24,8 @@ import { dashboardFactory, dashboardMetricFactory, kubeLinodeFactory, + linodeFactory, + regionFactory, widgetFactory, } from 'src/factories'; import { generateGraphData } from 'src/features/CloudPulse/Utils/CloudPulseWidgetUtils'; @@ -178,8 +179,8 @@ describe('Integration Tests for Linode Dashboard ', () => { mockGetRegions([mockRegion]); mockGetUserPreferences({}); - // navigate to the metrics page - cy.visitWithLogin('/metrics'); + // navigate to the cloudpulse page + cy.visitWithLogin('monitor'); // Wait for the services and dashboard API calls to complete before proceeding cy.wait(['@fetchServices', '@fetchDashboard']); diff --git a/packages/manager/cypress/e2e/core/cloudpulse/timerange-verification.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/timerange-verification.spec.ts index af475bfb926..dd6638e8fa9 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/timerange-verification.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/timerange-verification.spec.ts @@ -1,7 +1,6 @@ /** * @file Integration Tests for CloudPulse Custom and Preset Verification */ -import { profileFactory, regionFactory } from '@linode/utilities'; import { DateTime } from 'luxon'; import { widgetDetails } from 'support/constants/widgets'; import { mockGetAccount } from 'support/intercepts/account'; @@ -29,6 +28,8 @@ import { dashboardFactory, dashboardMetricFactory, databaseFactory, + profileFactory, + regionFactory, widgetFactory, } from 'src/factories'; import { convertToGmt } from 'src/features/CloudPulse/Utils/CloudPulseDateTimePickerUtils'; @@ -239,7 +240,7 @@ describe('Integration tests for verifying Cloudpulse custom and preset configura }).as('fetchPreferences'); mockGetDatabases([databaseMock]); - cy.visitWithLogin('/metrics'); + cy.visitWithLogin('monitor'); cy.wait(['@fetchServices', '@fetchDashboard', '@fetchPreferences']); }); diff --git a/packages/manager/cypress/e2e/core/databases/create-database.spec.ts b/packages/manager/cypress/e2e/core/databases/create-database.spec.ts index 3c8718fd66a..db6a3dee064 100644 --- a/packages/manager/cypress/e2e/core/databases/create-database.spec.ts +++ b/packages/manager/cypress/e2e/core/databases/create-database.spec.ts @@ -1,10 +1,9 @@ -import { profileFactory } from '@linode/utilities'; import { databaseConfigurations, mockDatabaseEngineTypes, mockDatabaseNodeTypes, } from 'support/constants/databases'; -import { mockGetAccount, mockGetUser } from 'support/intercepts/account'; +import { mockGetAccount } from 'support/intercepts/account'; import { mockCreateDatabase, mockGetDatabaseEngines, @@ -12,28 +11,17 @@ import { mockGetDatabases, } from 'support/intercepts/databases'; import { mockGetEvents } from 'support/intercepts/events'; -import { - mockGetProfile, - mockGetProfileGrants, -} from 'support/intercepts/profile'; import { ui } from 'support/ui'; -import { randomLabel } from 'support/util/random'; import { getRegionById } from 'support/util/regions'; -import { - accountFactory, - accountUserFactory, - databaseFactory, - eventFactory, - grantsFactory, -} from 'src/factories'; +import { accountFactory, databaseFactory, eventFactory } from 'src/factories'; import type { Database } from '@linode/api-v4'; -import type { DatabaseClusterConfiguration } from 'support/constants/databases'; +import type { databaseClusterConfiguration } from 'support/constants/databases'; describe('create a database cluster, mocked data', () => { databaseConfigurations.forEach( - (configuration: DatabaseClusterConfiguration) => { + (configuration: databaseClusterConfiguration) => { // @TODO Add assertions for DBaaS pricing. it(`creates a ${configuration.linodeType} ${configuration.engine} v${configuration.version}.x ${configuration.clusterSize}-node cluster`, () => { // Database mock immediately after instance has been created. @@ -170,80 +158,3 @@ describe('create a database cluster, mocked data', () => { } ); }); - -describe('restricted user cannot create database', () => { - beforeEach(() => { - // Mock setup for user profile, account user, and user grants with restricted permissions, - const mockProfile = profileFactory.build({ - restricted: true, - username: randomLabel(), - }); - - const mockUser = accountUserFactory.build({ - restricted: true, - user_type: 'default', - username: mockProfile.username, - }); - - const mockGrants = grantsFactory.build({ - global: { - add_databases: false, - }, - }); - - mockGetProfile(mockProfile); - mockGetProfileGrants(mockGrants); - mockGetUser(mockUser); - mockGetDatabases([]).as('getDatabases'); - }); - it('cannot create database on landing page', () => { - // Login and wait for application to load - cy.visitWithLogin('/databases'); - cy.wait('@getDatabases'); - // Assert that Create Database button is visible and disabled - ui.button - .findByTitle('Create Database Cluster') - .should('be.visible') - .and('be.disabled') - .trigger('mouseover'); - - // Assert that tooltip is visible with message - ui.tooltip - .findByText( - "You don't have permissions to create Databases. Please contact your account administrator to request the necessary permissions." - ) - .should('be.visible'); - - // table not present for restricted user - cy.get('table[aria-label="Database Clusters"]').should('not.exist'); - // link to Docs should exist - cy.findByText('Getting Started Guides').should('be.visible'); - cy.findByText('Video Playlist').should('be.visible'); - }); - - it('cannot create database from Create menu', () => { - // Login and wait for application to load - cy.visitWithLogin('/databases/create'); - - // table present for restricted user but its inputs will be disabled - cy.get('table[aria-label="List of Linode Plans"]').should('exist'); - // Assert that Create Database button is visible and disabled - ui.button - .findByTitle('Create Database Cluster') - .should('be.visible') - .and('be.disabled') - .trigger('mouseover'); - - // Info message is visible - cy.findByText( - "You don't have permissions to create this Database. Please contact your account administrator to request the necessary permissions." - ); - - // all form inputs are disabled - cy.get('[data-testid="db-create-form"]').within(() => { - cy.get('input').each((input) => { - cy.wrap(input).should('be.disabled'); - }); - }); - }); -}); diff --git a/packages/manager/cypress/e2e/core/databases/delete-database.spec.ts b/packages/manager/cypress/e2e/core/databases/delete-database.spec.ts index a80b2d93b4e..9d6565873c4 100644 --- a/packages/manager/cypress/e2e/core/databases/delete-database.spec.ts +++ b/packages/manager/cypress/e2e/core/databases/delete-database.spec.ts @@ -18,11 +18,11 @@ import { randomIp, randomNumber } from 'support/util/random'; import { accountFactory, databaseFactory } from 'src/factories'; -import type { DatabaseClusterConfiguration } from 'support/constants/databases'; +import type { databaseClusterConfiguration } from 'support/constants/databases'; describe('Delete database clusters', () => { databaseConfigurations.forEach( - (configuration: DatabaseClusterConfiguration) => { + (configuration: databaseClusterConfiguration) => { describe(`Deletes a ${configuration.linodeType} ${configuration.engine} v${configuration.version}.x ${configuration.clusterSize}-node cluster`, () => { /* * - Tests database deletion UI flow using mocked data. diff --git a/packages/manager/cypress/e2e/core/databases/resize-database.spec.ts b/packages/manager/cypress/e2e/core/databases/resize-database.spec.ts index 2680ac03fa9..d3222977fb0 100644 --- a/packages/manager/cypress/e2e/core/databases/resize-database.spec.ts +++ b/packages/manager/cypress/e2e/core/databases/resize-database.spec.ts @@ -20,7 +20,7 @@ import { randomIp, randomNumber, randomString } from 'support/util/random'; import { databaseFactory, possibleStatuses } from 'src/factories/databases'; -import type { DatabaseClusterConfiguration } from 'support/constants/databases'; +import type { databaseClusterConfiguration } from 'support/constants/databases'; /** * Resizes a current database cluster to a larger plan size. @@ -52,7 +52,7 @@ const resizeDatabase = (initialLabel: string) => { describe('Resizing existing clusters', () => { databaseConfigurationsResize.forEach( - (configuration: DatabaseClusterConfiguration) => { + (configuration: databaseClusterConfiguration) => { describe(`Resizes a ${configuration.linodeType} ${configuration.engine} v${configuration.version}.x ${configuration.clusterSize}-node cluster (legacy DBaaS)`, () => { /* * - Tests active database resize UI flows using mocked data. diff --git a/packages/manager/cypress/e2e/core/databases/update-database.spec.ts b/packages/manager/cypress/e2e/core/databases/update-database.spec.ts index 6a14ee4dc9d..fd998f69319 100644 --- a/packages/manager/cypress/e2e/core/databases/update-database.spec.ts +++ b/packages/manager/cypress/e2e/core/databases/update-database.spec.ts @@ -17,6 +17,7 @@ import { mockUpdateDatabase, mockUpdateProvisioningDatabase, } from 'support/intercepts/databases'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { ui } from 'support/ui'; import { randomIp, @@ -27,7 +28,7 @@ import { import { databaseFactory } from 'src/factories/databases'; -import type { DatabaseClusterConfiguration } from 'support/constants/databases'; +import type { databaseClusterConfiguration } from 'support/constants/databases'; /** * Updates a database cluster's label. @@ -143,47 +144,35 @@ const resetRootPassword = () => { }); }; -/** - * Updates engine version if applicable and maintenance window for a given day and time. - * - * This requires that the 'Summary' or 'Settings' tab is currently active. - * - * @param engine - database engine for version upgrade. - * @param version - current database engine version to be upgraded. - */ -const upgradeEngineVersion = (engine: string, version: string) => { - const dbEngine = engine == 'mysql' ? 'MySQL' : 'PostgreSQL'; - cy.get('[data-qa-settings-section="Maintenance"]') - .should('be.visible') - .within(() => { - cy.findByText('Maintenance'); - cy.findByText('Version'); - cy.findByText(`${dbEngine} v${version}`); - ui.button.findByTitle('Upgrade Version').should('be.visible'); +describe('Update database clusters', () => { + beforeEach(() => { + const mockAccount = accountFactory.build({ + capabilities: [ + 'Akamai Cloud Pulse', + 'Block Storage', + 'Cloud Firewall', + 'Disk Encryption', + 'Kubernetes', + 'Linodes', + 'LKE HA Control Planes', + 'Machine Images', + 'Managed Databases', + 'NodeBalancers', + 'Object Storage Access Key Regions', + 'Object Storage Endpoint Types', + 'Object Storage', + 'Placement Group', + 'Vlans', + ], }); -}; - -/** - * Updates maintenance window for a given day and time. - * - * This requires that the 'Summary' or 'Settings' tab is currently active. - * Assertion is made on the toast thrown while updating maintenance window. - * - * @param label - type of window (day/time) to update - * @param windowValue - maintenance window value to update - */ -const modifyMaintenanceWindow = (label: string, windowValue: string) => { - cy.findByText('Set a Weekly Maintenance Window'); - cy.findByTitle('Save Changes').should('be.visible').should('be.disabled'); - - ui.autocomplete.findByLabel(label).should('be.visible').type(windowValue); - cy.contains(windowValue).should('be.visible').click(); - ui.button.findByTitle('Save Changes').should('be.visible').click(); -}; + mockAppendFeatureFlags({ + dbaasV2: { beta: false, enabled: false }, + }); + mockGetAccount(mockAccount); + }); -describe('Update database clusters', () => { databaseConfigurations.forEach( - (configuration: DatabaseClusterConfiguration) => { + (configuration: databaseClusterConfiguration) => { describe(`updates a ${configuration.linodeType} ${configuration.engine} v${configuration.version}.x ${configuration.clusterSize}-node cluster`, () => { /* * - Tests active database update UI flows using mocked data. @@ -203,14 +192,12 @@ describe('Update database clusters', () => { engine: configuration.dbType, id: randomNumber(1, 1000), label: initialLabel, - platform: 'rdbms-default', + platform: 'rdbms-legacy', region: configuration.region.id, status: 'active', type: configuration.linodeType, - version: configuration.version, }); - mockGetAccount(accountFactory.build()).as('getAccount'); mockGetDatabase(database).as('getDatabase'); mockGetDatabaseTypes(mockDatabaseNodeTypes).as('getDatabaseTypes'); mockResetPassword(database.id, database.engine).as( @@ -225,29 +212,27 @@ describe('Update database clusters', () => { cy.visitWithLogin(`/databases/${database.engine}/${database.id}`); cy.wait(['@getDatabase', '@getDatabaseTypes']); - cy.findByText('Cluster Configuration'); - cy.findByText(configuration.region.label).should('be.visible'); - cy.findByText(database.total_disk_size_gb + ' GB').should( - 'be.visible' - ); - - cy.findByText('Connection Details'); - // "Show" button should be enabled to reveal password when DB is active. - ui.button - .findByTitle('Show') - .should('be.visible') - .should('be.enabled') - .click(); + cy.get('[data-qa-cluster-config]').within(() => { + cy.findByText(configuration.region.label).should('be.visible'); + cy.findByText(database.used_disk_size_gb + ' GB').should( + 'be.visible' + ); + cy.findByText(database.total_disk_size_gb + ' GB').should( + 'be.visible' + ); + }); - cy.wait('@getCredentials'); - cy.findByText(`${initialPassword}`); + cy.get('[data-qa-connection-details]').within(() => { + // "Show" button should be enabled to reveal password when DB is active. + cy.findByText('Show') + .closest('button') + .should('be.visible') + .should('be.enabled') + .click(); - // "Hide" button should be enabled to hide password when password is revealed. - ui.button - .findByTitle('Hide') - .should('be.visible') - .should('be.enabled') - .click(); + cy.wait('@getCredentials'); + cy.findByText(`= ${initialPassword}`); + }); mockUpdateDatabase(database.id, database.engine, { ...database, @@ -259,13 +244,6 @@ describe('Update database clusters', () => { .should('be.visible') .should('have.text', updatedLabel); - // Navigate to "Settings" tab. - ui.tabList.findTabByTitle('Settings').click(); - - // Reset root password. - resetRootPassword(); - cy.wait('@resetRootPassword'); - // Remove allowed IP, manage IP access control. mockUpdateDatabase(database.id, database.engine, { ...database, @@ -284,19 +262,25 @@ describe('Update database clusters', () => { cy.findByText(newAllowedIp).should('be.visible'); }); - // Change maintenance window and databe version upgrade. + // Navigate to "Settings" tab. + ui.tabList.findTabByTitle('Settings').click(); + + // Reset root password. + resetRootPassword(); + cy.wait('@resetRootPassword'); + + // Change maintenance. mockUpdateDatabase(database.id, database.engine, database).as( 'updateDatabaseMaintenance' ); - upgradeEngineVersion(database.engine, database.version); + cy.findByText('Monthly').should('be.visible').click(); - modifyMaintenanceWindow('Day of Week', 'Wednesday'); - cy.wait('@updateDatabaseMaintenance'); - ui.toast.assertMessage( - 'Maintenance Window settings saved successfully.' - ); + ui.button + .findByTitle('Save Changes') + .should('be.visible') + .should('be.enabled') + .click(); - modifyMaintenanceWindow('Time', '12:00'); cy.wait('@updateDatabaseMaintenance'); ui.toast.assertMessage( 'Maintenance Window settings saved successfully.' @@ -324,7 +308,7 @@ describe('Update database clusters', () => { }, id: randomNumber(1, 1000), label: initialLabel, - platform: 'rdbms-default', + platform: 'rdbms-legacy', region: configuration.region.id, status: 'provisioning', type: configuration.linodeType, @@ -332,8 +316,7 @@ describe('Update database clusters', () => { const errorMessage = 'Your database is provisioning; please wait until provisioning is complete to perform this operation.'; - const hostnameRegex = - /your hostnames? will appear here once (it is|they are) available./i; + const hostnameRegex = /your hostnames? will appear here once (it is|they are) available./i; mockGetAccount(accountFactory.build()).as('getAccount'); mockGetDatabase(database).as('getDatabase'); @@ -363,34 +346,16 @@ describe('Update database clusters', () => { .should('be.enabled') .click(); - cy.findByText('Connection Details'); - // DBaaS hostnames are not available until database/cluster has provisioned. - cy.findByText(hostnameRegex).should('be.visible'); + cy.get('[data-qa-connection-details]').within(() => { + // DBaaS hostnames are not available until database/cluster has provisioned. + cy.findByText(hostnameRegex).should('be.visible'); - // DBaaS passwords cannot be revealed until database/cluster has provisioned. - ui.button - .findByTitle('Show') - .should('be.visible') - .should('be.disabled'); - - // Navigate to "Settings" tab. - ui.tabList.findTabByTitle('Settings').click(); - - // Cannot reset root password before database/cluster has provisioned. - resetRootPassword(); - cy.wait('@resetRootPassword'); - ui.dialog - .findByTitle('Reset Root Password') - .should('be.visible') - .within(() => { - cy.findByText(errorMessage).should('be.visible'); - - ui.buttonGroup - .findButtonByTitle('Cancel') - .should('be.visible') - .should('be.enabled') - .click(); - }); + // DBaaS passwords cannot be revealed until database/cluster has provisioned. + cy.findByText('Show') + .closest('button') + .should('be.visible') + .should('be.disabled'); + }); // Cannot add or remove allowed IPs before database/cluster has provisioned. removeAllowedIp(allowedIp); @@ -413,8 +378,34 @@ describe('Update database clusters', () => { ui.drawerCloseButton.find().click(); }); + // Navigate to "Settings" tab. + ui.tabList.findTabByTitle('Settings').click(); + + // Cannot reset root password before database/cluster has provisioned. + resetRootPassword(); + cy.wait('@resetRootPassword'); + ui.dialog + .findByTitle('Reset Root Password') + .should('be.visible') + .within(() => { + cy.findByText(errorMessage).should('be.visible'); + + ui.buttonGroup + .findButtonByTitle('Cancel') + .should('be.visible') + .should('be.enabled') + .click(); + }); + // Cannot change maintenance schedule before database/cluster has provisioned. - modifyMaintenanceWindow('Day of Week', 'Wednesday'); + cy.findByText('Monthly').should('be.visible').click(); + + ui.button + .findByTitle('Save Changes') + .should('be.visible') + .should('be.enabled') + .click(); + cy.wait('@updateDatabase'); cy.findByText(errorMessage).should('be.visible'); }); 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 43822b48fa3..314bc91aebf 100644 --- a/packages/manager/cypress/e2e/core/firewalls/create-firewall.spec.ts +++ b/packages/manager/cypress/e2e/core/firewalls/create-firewall.spec.ts @@ -1,4 +1,3 @@ -import { createLinodeRequestFactory } from '@linode/utilities'; import { authenticate } from 'support/api/authentication'; import { interceptCreateFirewall } from 'support/intercepts/firewalls'; import { ui } from 'support/ui'; @@ -6,10 +5,11 @@ import { cleanUp } from 'support/util/cleanup'; import { createTestLinode } from 'support/util/linodes'; import { randomLabel, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; + +import { createLinodeRequestFactory } from 'src/factories/linodes'; + authenticate(); -// Firewall GET API request performance issues need to be addressed in order to unskip this test -// See M3-9619 -describe.skip('create firewall', () => { +describe('create firewall', () => { before(() => { cleanUp(['lke-clusters', 'linodes', 'firewalls']); }); 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 b8ede2742eb..99bbabeb1a2 100644 --- a/packages/manager/cypress/e2e/core/firewalls/delete-firewall.spec.ts +++ b/packages/manager/cypress/e2e/core/firewalls/delete-firewall.spec.ts @@ -9,9 +9,7 @@ import { firewallFactory } from 'src/factories/firewalls'; import type { Firewall } from '@linode/api-v4'; authenticate(); -// Firewall GET API request performance issues need to be addressed in order to unskip this test -// See M3-9619 -describe.skip('delete firewall', () => { +describe('delete firewall', () => { before(() => { cleanUp('firewalls'); }); 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 4ad04ce9844..7ad3575f76e 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 @@ -1,10 +1,10 @@ /* eslint-disable sonarjs/no-duplicate-string */ import { createLinodeRequestFactory, + firewallFactory, linodeFactory, regionFactory, -} from '@linode/utilities'; -import { firewallFactory } from '@src/factories'; +} from '@src/factories'; import { authenticate } from 'support/api/authentication'; import { interceptCreateFirewall, @@ -21,7 +21,8 @@ import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; import { createTestLinode } from 'support/util/linodes'; import { randomLabel, randomNumber } from 'support/util/random'; -import { chooseRegions, extendRegion } from 'support/util/regions'; +import { extendRegion } from 'support/util/regions'; +import { chooseRegions } from 'support/util/regions'; import type { Linode, Region } from '@linode/api-v4'; @@ -143,104 +144,99 @@ describe('Migrate Linode With Firewall', () => { /* * - Uses real API data to create a Firewall, attach a Linode to it, then migrate the Linode. */ - it.skip('migrates linode with firewall - real data', () => { - cy.tag('method:e2e', 'purpose:dcTesting', 'env:multipleRegions'); - - // Execute the body of the test inside Cypress's command queue to ensure - // that logic that requires multiple regions only executes after tags are evaluated. - cy.defer(async () => {}).then(() => { - const [migrationRegionStart, migrationRegionEnd] = chooseRegions(2); - const firewallLabel = randomLabel(); - const linodePayload = createLinodeRequestFactory.build({ - label: randomLabel(), - region: migrationRegionStart.id, - }); - - interceptCreateFirewall().as('createFirewall'); - interceptGetFirewalls().as('getFirewalls'); - - // Create a Linode, then navigate to the Firewalls landing page. - cy.defer(() => - createTestLinode(linodePayload, { securityMethod: 'powered_off' }) - ).then((linode: Linode) => { - interceptMigrateLinode(linode.id).as('migrateLinode'); - cy.visitWithLogin('/firewalls'); - cy.wait('@getFirewalls'); - - ui.button - .findByTitle('Create Firewall') - .should('be.visible') - .should('be.enabled') - .click(); + 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({ + label: randomLabel(), + region: migrationRegionStart.id, + }); - ui.drawer - .findByTitle('Create Firewall') - .should('be.visible') - .within(() => { - cy.findByText('Label').should('be.visible').click(); - cy.focused().type(firewallLabel); - - cy.findByText('Linodes').should('be.visible').click(); - cy.focused().type(linode.label); - - ui.autocompletePopper - .findByTitle(linode.label) - .should('be.visible') - .click(); - - // Click on the Select again to dismiss the autocomplete popper. - cy.findByLabelText('Linodes').should('be.visible').click(); - - ui.buttonGroup - .findButtonByTitle('Create Firewall') - .should('be.visible') - .should('be.enabled') - .click(); - }); - - cy.wait('@createFirewall'); - cy.visitWithLogin(`/linodes/${linode.id}`); - cy.get('[data-qa-link-text="true"]') - .should('be.visible') - .within(() => { - cy.findByText('linodes').should('be.visible'); - }); + interceptCreateFirewall().as('createFirewall'); + interceptGetFirewalls().as('getFirewalls'); + + // Create a Linode, then navigate to the Firewalls landing page. + cy.defer(() => + createTestLinode(linodePayload, { securityMethod: 'powered_off' }) + ).then((linode: Linode) => { + interceptMigrateLinode(linode.id).as('migrateLinode'); + cy.visitWithLogin('/firewalls'); + cy.wait('@getFirewalls'); + + ui.button + .findByTitle('Create Firewall') + .should('be.visible') + .should('be.enabled') + .click(); + + ui.drawer + .findByTitle('Create Firewall') + .should('be.visible') + .within(() => { + cy.findByText('Label').should('be.visible').click(); + cy.focused().type(firewallLabel); + + cy.findByText('Linodes').should('be.visible').click(); + cy.focused().type(linode.label); + + ui.autocompletePopper + .findByTitle(linode.label) + .should('be.visible') + .click(); + + // Click on the Select again to dismiss the autocomplete popper. + cy.findByLabelText('Linodes').should('be.visible').click(); + + ui.buttonGroup + .findButtonByTitle('Create Firewall') + .should('be.visible') + .should('be.enabled') + .click(); + }); - // Make sure Linode is running before attempting to migrate. - cy.get('[data-qa-linode-status]').within(() => { - cy.findByText('OFFLINE'); + cy.wait('@createFirewall'); + cy.visitWithLogin(`/linodes/${linode.id}`); + cy.get('[data-qa-link-text="true"]') + .should('be.visible') + .within(() => { + cy.findByText('linodes').should('be.visible'); }); - ui.actionMenu - .findByTitle(`Action menu for Linode ${linode.label}`) - .should('be.visible') - .click(); + // Make sure Linode is running before attempting to migrate. + cy.get('[data-qa-linode-status]').within(() => { + cy.findByText('OFFLINE'); + }); - ui.actionMenuItem.findByTitle('Migrate').should('be.visible').click(); + ui.actionMenu + .findByTitle(`Action menu for Linode ${linode.label}`) + .should('be.visible') + .click(); + + ui.actionMenuItem.findByTitle('Migrate').should('be.visible').click(); + + ui.dialog + .findByTitle(`Migrate Linode ${linode.label} to another region`) + .should('be.visible') + .within(() => { + // Click "Accept" check box. + cy.findByText('Accept').should('be.visible').click(); + + // Select region for migration. + ui.regionSelect.find().click(); + ui.regionSelect + .findItemByRegionLabel(migrationRegionEnd.label) + .click(); + + // Initiate migration. + ui.button + .findByTitle('Enter Migration Queue') + .should('be.visible') + .should('be.enabled') + .click(); + }); - ui.dialog - .findByTitle(`Migrate Linode ${linode.label} to another region`) - .should('be.visible') - .within(() => { - // Click "Accept" check box. - cy.findByText('Accept').should('be.visible').click(); - - // Select region for migration. - ui.regionSelect.find().click(); - ui.regionSelect - .findItemByRegionLabel(migrationRegionEnd.label) - .click(); - - // Initiate migration. - ui.button - .findByTitle('Enter Migration Queue') - .should('be.visible') - .should('be.enabled') - .click(); - }); - - cy.wait('@migrateLinode').its('response.statusCode').should('eq', 200); - }); + cy.wait('@migrateLinode').its('response.statusCode').should('eq', 200); }); }); }); 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 e90d64d3c02..930a90bf216 100644 --- a/packages/manager/cypress/e2e/core/firewalls/update-firewall.spec.ts +++ b/packages/manager/cypress/e2e/core/firewalls/update-firewall.spec.ts @@ -1,5 +1,4 @@ import { createFirewall, createLinode } from '@linode/api-v4'; -import { createLinodeRequestFactory } from '@linode/utilities'; import { authenticate } from 'support/api/authentication'; import { interceptUpdateFirewallLinodes, @@ -11,6 +10,7 @@ import { randomItem, randomLabel, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; import { + createLinodeRequestFactory, firewallFactory, firewallRuleFactory, firewallRulesFactory, @@ -167,9 +167,7 @@ const createLinodeAndFirewall = async ( }; authenticate(); -// Firewall GET API request performance issues need to be addressed in order to unskip this test -// See M3-9619 -describe.skip('update firewall', () => { +describe('update firewall', () => { before(() => { cleanUp('firewalls'); }); 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 18755ff73db..69f9ff92b38 100644 --- a/packages/manager/cypress/e2e/core/general/gdpr-agreement.spec.ts +++ b/packages/manager/cypress/e2e/core/general/gdpr-agreement.spec.ts @@ -1,4 +1,4 @@ -import { linodeFactory, regionFactory } from '@linode/utilities'; +import { linodeFactory, regionFactory } from '@src/factories'; import { mockGetAccountAgreements } from 'support/intercepts/account'; import { mockCreateLinode } from 'support/intercepts/linodes'; import { mockGetRegions } from 'support/intercepts/regions'; 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 821ea4a8103..8cbe786110b 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 @@ -15,7 +15,7 @@ describe('smoke - deep links', () => { cy.log(`Go to ${page.name}`); page.goWithUI?.forEach((uiPath) => { cy.log(`by ${uiPath.name}`); - cy.findByText(uiPath.name).should('not.be.empty'); + expect(uiPath.name).not.to.be.empty; uiPath.go(); cy.url().should('be.eq', `${Cypress.config('baseUrl')}${page.url}`); }); 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 5d81c61c9d8..2efb32ab3d2 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 @@ -1,6 +1,5 @@ // must turn off sort-objects rule in this file bc mockTicket.description is set by formatDescription fn in which attribute order is nonalphabetical and affects test result /* eslint-disable perfectionist/sort-objects */ -import { linodeFactory } from '@linode/utilities'; /* eslint-disable sonarjs/no-duplicate-string */ import 'cypress-file-upload'; import { mockGetAccount } from 'support/intercepts/account'; @@ -34,6 +33,7 @@ import { chooseRegion } from 'support/util/regions'; import { accountFactory, domainFactory, + linodeFactory, supportTicketFactory, } from 'src/factories'; import { 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 48f2f3c4250..a25682e6111 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,4 +1,3 @@ -import { linodeConfigInterfaceFactory, linodeFactory } from '@linode/utilities'; import { mockGetLinodeConfigs } from 'support/intercepts/configs'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { @@ -20,8 +19,10 @@ import { } from 'support/util/random'; import { + LinodeConfigInterfaceFactory, entityFactory, linodeConfigFactory, + linodeFactory, supportTicketFactory, volumeFactory, } from 'src/factories'; @@ -187,7 +188,7 @@ describe('support tickets landing page', () => { label: `${randomLabel()}-linode`, }); const mockVolume = volumeFactory.build(); - const mockPublicConfigInterface = linodeConfigInterfaceFactory.build({ + const mockPublicConfigInterface = LinodeConfigInterfaceFactory.build({ ipam_address: null, purpose: 'public', }); 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 b71bf8fd0ba..070c484fc37 100644 --- a/packages/manager/cypress/e2e/core/images/create-image.spec.ts +++ b/packages/manager/cypress/e2e/core/images/create-image.spec.ts @@ -1,11 +1,10 @@ +import type { Linode } from '@linode/api-v4'; import { authenticate } from 'support/api/authentication'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; import { createTestLinode } from 'support/util/linodes'; import { randomLabel, randomPhrase } from 'support/util/random'; -import type { Linode } from '@linode/api-v4'; - authenticate(); describe('create image (e2e)', () => { before(() => { diff --git a/packages/manager/cypress/e2e/core/images/create-linode-from-image.spec.ts b/packages/manager/cypress/e2e/core/images/create-linode-from-image.spec.ts index 22e776ed49e..da4f61f4efb 100644 --- a/packages/manager/cypress/e2e/core/images/create-linode-from-image.spec.ts +++ b/packages/manager/cypress/e2e/core/images/create-linode-from-image.spec.ts @@ -1,23 +1,22 @@ -import { linodeFactory } from '@linode/utilities'; -import { imageFactory } from '@src/factories'; -import { mockGetAllImages } from 'support/intercepts/images'; -import { ui } from 'support/ui'; import { apiMatcher } from 'support/util/intercepts'; import { randomLabel, randomNumber, randomString } from 'support/util/random'; +import { mockGetAllImages } from 'support/intercepts/images'; +import { imageFactory, linodeFactory } from '@src/factories'; import { chooseRegion } from 'support/util/regions'; +import { ui } from 'support/ui'; const region = chooseRegion(); const mockLinode = linodeFactory.build({ - id: 123456, region: region.id, + id: 123456, }); const mockImage = imageFactory.build({ + label: randomLabel(), + is_public: false, eol: null, id: `private/${randomNumber()}`, - is_public: false, - label: randomLabel(), }); const createLinodeWithImageMock = (url: string, preselectedImage: boolean) => { diff --git a/packages/manager/cypress/e2e/core/images/images-empty-landing-page.spec.ts b/packages/manager/cypress/e2e/core/images/images-empty-landing-page.spec.ts index 139652ad412..3f7eb49d61d 100644 --- a/packages/manager/cypress/e2e/core/images/images-empty-landing-page.spec.ts +++ b/packages/manager/cypress/e2e/core/images/images-empty-landing-page.spec.ts @@ -1,13 +1,13 @@ -import { profileFactory } from '@linode/utilities'; +import { ui } from 'support/ui'; +import { mockGetAllImages } from 'support/intercepts/images'; +import { profileFactory } from '@src/factories'; import { accountUserFactory } from '@src/factories/accountUsers'; import { grantsFactory } from '@src/factories/grants'; import { mockGetUser } from 'support/intercepts/account'; -import { mockGetAllImages } from 'support/intercepts/images'; import { mockGetProfile, mockGetProfileGrants, } from 'support/intercepts/profile'; -import { ui } from 'support/ui'; import { randomLabel } from 'support/util/random'; describe('Images empty landing page', () => { @@ -67,15 +67,15 @@ describe('Images empty landing page', () => { it('checks restricted user has no access to create Image on Image landing page', () => { // object to create a mockProfile for non-restricted user const mockProfile = profileFactory.build({ - restricted: true, username: randomLabel(), + restricted: true, }); // object to create a mockUser for non-restricted user const mockUser = accountUserFactory.build({ + username: mockProfile.username, restricted: true, user_type: 'default', - username: mockProfile.username, }); // object to create a mockGrants for non-restricted user diff --git a/packages/manager/cypress/e2e/core/images/images-non-empty-landing-page.spec.ts b/packages/manager/cypress/e2e/core/images/images-non-empty-landing-page.spec.ts index d8c0a835697..5154138d6f8 100644 --- a/packages/manager/cypress/e2e/core/images/images-non-empty-landing-page.spec.ts +++ b/packages/manager/cypress/e2e/core/images/images-non-empty-landing-page.spec.ts @@ -1,18 +1,16 @@ -import { profileFactory } from '@linode/utilities'; -import { mockGetUser } from 'support/intercepts/account'; +import { imageFactory } from 'src/factories'; +import { ui } from 'support/ui'; import { mockGetAllImages } from 'support/intercepts/images'; +import { profileFactory } from 'src/factories'; +import { randomLabel } from 'support/util/random'; +import { grantsFactory } from 'src/factories'; +import { accountUserFactory } from 'src/factories'; +import { mockGetUser } from 'support/intercepts/account'; import { mockGetProfile, mockGetProfileGrants, } from 'support/intercepts/profile'; -import { ui } from 'support/ui'; -import { randomLabel } from 'support/util/random'; - -import { imageFactory } from 'src/factories'; -import { grantsFactory } from 'src/factories'; -import { accountUserFactory } from 'src/factories'; - -import type { Image } from '@linode/api-v4'; +import { Image } from '@linode/api-v4'; function checkActionMenu(tableAlias: string, mockImages: any[]) { mockImages.forEach((image) => { @@ -50,14 +48,14 @@ function checkActionMenu(tableAlias: string, mockImages: any[]) { describe('image landing checks for non-empty state with restricted user', () => { beforeEach(() => { - const mockImages: Image[] = new Array(3) - .fill(null) - .map((_item: null, index: number): Image => { + const mockImages: Image[] = new Array(3).fill(null).map( + (_item: null, index: number): Image => { return imageFactory.build({ label: `Image ${index}`, tags: [index % 2 == 0 ? 'even' : 'odd', 'nums'], }); - }); + } + ); // Mock setup to display the Image landing page in an non-empty state mockGetAllImages(mockImages).as('getImages'); @@ -69,14 +67,14 @@ describe('image landing checks for non-empty state with restricted user', () => it('checks restricted user with read access has no access to create image and can see existing images', () => { // Mock setup for user profile, account user, and user grants with restricted permissions, const mockProfile = profileFactory.build({ - restricted: true, username: randomLabel(), + restricted: true, }); const mockUser = accountUserFactory.build({ + username: mockProfile.username, restricted: true, user_type: 'default', - username: mockProfile.username, }); const mockGrants = grantsFactory.build({ diff --git a/packages/manager/cypress/e2e/core/images/machine-image-upload.spec.ts b/packages/manager/cypress/e2e/core/images/machine-image-upload.spec.ts index 715d7885f45..4f968e849ef 100644 --- a/packages/manager/cypress/e2e/core/images/machine-image-upload.spec.ts +++ b/packages/manager/cypress/e2e/core/images/machine-image-upload.spec.ts @@ -1,13 +1,15 @@ +import { EventStatus } from '@linode/api-v4'; import { eventFactory, imageFactory } from '@src/factories'; import { makeResourcePage } from '@src/mocks/serverHandlers'; import 'cypress-file-upload'; +import { RecPartial } from 'factory.ts'; import { DateTime } from 'luxon'; import { authenticate } from 'support/api/authentication'; import { mockDeleteImage, mockGetCustomImages, - mockGetImage, mockUpdateImage, + mockGetImage, } from 'support/intercepts/images'; import { ui } from 'support/ui'; import { interceptOnce } from 'support/ui/common'; @@ -16,9 +18,6 @@ import { apiMatcher } from 'support/util/intercepts'; import { randomLabel, randomPhrase } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import type { EventStatus } from '@linode/api-v4'; -import type { RecPartial } from 'factory.ts'; - /** * Returns a numeric image ID from a string-based image ID. * @@ -58,17 +57,17 @@ const eventIntercept = ( apiMatcher('account/events*'), makeResourcePage( eventFactory.buildList(1, { - action: 'image_upload', created: created ? created : DateTime.local().toISO(), + action: 'image_upload', entity: { + label: label, id: numericId, - label, type: 'image', url: `/v4/images/private/${numericId}`, }, - message: message ? message : '', - secondary_entity: null, status, + secondary_entity: null, + message: message ? message : '', }) ) ).as('getEvent'); @@ -113,16 +112,7 @@ const assertProcessing = (label: string, id: string) => { * @param label - Label to apply to uploaded image. */ const uploadImage = (label: string) => { - // Disallow these regions from being returned by `chooseRegion` because they do not support Machine Images: - // - au-mel - // - gb-lon - // - sg-sin-2 - // - // See also BAC-862. - const region = chooseRegion({ - capabilities: ['Object Storage'], - exclude: ['au-mel', 'gb-lon', 'sg-sin-2'], - }); + const region = chooseRegion({ capabilities: ['Object Storage'] }); const upload = 'machine-images/test-image.gz'; cy.visitWithLogin('/images/create/upload'); @@ -176,8 +166,8 @@ describe('machine image', () => { const mockImageUpdated = { ...mockImage, - description: updatedDescription, label: updatedLabel, + description: updatedDescription, }; mockGetCustomImages([mockImage]).as('getImages'); @@ -266,7 +256,7 @@ describe('machine image', () => { const imageId = xhr.response?.body.image.id; assertProcessing(label, imageId); mockGetCustomImages([ - imageFactory.build({ id: imageId, label, status: 'available' }), + imageFactory.build({ label, id: imageId, status: 'available' }), ]).as('getImages'); eventIntercept(label, imageId, status); ui.toast.assertMessage(uploadMessage); diff --git a/packages/manager/cypress/e2e/core/images/manage-image-regions.spec.ts b/packages/manager/cypress/e2e/core/images/manage-image-regions.spec.ts index c4139b628ad..72d984d6c4b 100644 --- a/packages/manager/cypress/e2e/core/images/manage-image-regions.spec.ts +++ b/packages/manager/cypress/e2e/core/images/manage-image-regions.spec.ts @@ -1,17 +1,14 @@ -import { regionFactory } from '@linode/utilities'; +import { imageFactory, regionFactory } from 'src/factories'; import { mockGetCustomImages, - mockGetImage, mockGetRecoveryImages, mockUpdateImageRegions, + mockGetImage, } from 'support/intercepts/images'; import { mockGetRegions } from 'support/intercepts/regions'; import { ui } from 'support/ui'; -import { extendRegion } from 'support/util/regions'; - -import { imageFactory } from 'src/factories'; - import type { Image, Region } from '@linode/api-v4'; +import { extendRegion } from 'support/util/regions'; describe('Manage Image Replicas', () => { /** @@ -20,8 +17,8 @@ describe('Manage Image Replicas', () => { */ it("updates an Image's regions", () => { const regionOptions: Partial = { - capabilities: ['Object Storage'], site_type: 'core', + capabilities: ['Object Storage'], }; const region1 = extendRegion(regionFactory.build(regionOptions)); const region2 = extendRegion(regionFactory.build(regionOptions)); @@ -29,13 +26,13 @@ describe('Manage Image Replicas', () => { const region4 = extendRegion(regionFactory.build(regionOptions)); const image = imageFactory.build({ + size: 50, + total_size: 100, capabilities: ['distributed-sites'], regions: [ { region: region1.id, status: 'available' }, { region: region2.id, status: 'available' }, ], - size: 50, - total_size: 100, }); mockGetRegions([region1, region2, region3, region4]).as('getRegions'); @@ -134,12 +131,12 @@ describe('Manage Image Replicas', () => { const updatedImage: Image = { ...image, + total_size: 150, regions: [ { region: region2.id, status: 'available' }, { region: region3.id, status: 'pending replication' }, { region: region4.id, status: 'pending replication' }, ], - total_size: 150, }; // mock the POST /v4/images/:id:regions response 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 ea6bda69636..a9cd4d5ceaa 100644 --- a/packages/manager/cypress/e2e/core/images/search-images.spec.ts +++ b/packages/manager/cypress/e2e/core/images/search-images.spec.ts @@ -1,12 +1,12 @@ import { createImage } from '@linode/api-v4/lib/images'; -import { authenticate } from 'support/api/authentication'; -import { interceptGetLinodeDisks } from 'support/intercepts/linodes'; -import { ui } from 'support/ui'; -import { cleanUp } from 'support/util/cleanup'; import { createTestLinode } from 'support/util/linodes'; -import { randomLabel } from 'support/util/random'; +import { ui } from 'support/ui'; +import { authenticate } from 'support/api/authentication'; +import { randomLabel } from 'support/util/random'; +import { cleanUp } from 'support/util/cleanup'; import type { Image, Linode } from '@linode/api-v4'; +import { interceptGetLinodeDisks } from 'support/intercepts/linodes'; authenticate(); describe('Search Images', () => { @@ -59,8 +59,8 @@ describe('Search Images', () => { // Search for the first image by label, confirm it's the only one shown. cy.findByPlaceholderText('Search Images').type(image1.label); - cy.contains(image1.label).should('be.visible'); - cy.contains(image2.label).should('not.exist'); + expect(cy.contains(image1.label).should('be.visible')); + expect(cy.contains(image2.label).should('not.exist')); // Clear search, confirm both images are shown. cy.findByTestId('clear-images-search').click(); diff --git a/packages/manager/cypress/e2e/core/images/smoke-create-image.spec.ts b/packages/manager/cypress/e2e/core/images/smoke-create-image.spec.ts index d46157d4c6c..59c107e474c 100644 --- a/packages/manager/cypress/e2e/core/images/smoke-create-image.spec.ts +++ b/packages/manager/cypress/e2e/core/images/smoke-create-image.spec.ts @@ -1,41 +1,45 @@ -import { linodeFactory, profileFactory } from '@linode/utilities'; -import { mockGetUser } from 'support/intercepts/account'; +import { + accountUserFactory, + eventFactory, + grantsFactory, + linodeFactory, + profileFactory, +} from 'src/factories'; +import { linodeDiskFactory } from 'src/factories/disk'; +import { imageFactory } from 'src/factories/images'; import { mockGetEvents } from 'support/intercepts/events'; import { mockCreateImage } from 'support/intercepts/images'; import { mockGetLinodeDisks, mockGetLinodes } from 'support/intercepts/linodes'; +import { ui } from 'support/ui'; +import { randomLabel, randomNumber, randomPhrase } from 'support/util/random'; import { mockGetProfile, mockGetProfileGrants, } from 'support/intercepts/profile'; -import { ui } from 'support/ui'; -import { randomLabel, randomNumber, randomPhrase } from 'support/util/random'; - -import { accountUserFactory, eventFactory, grantsFactory } from 'src/factories'; -import { linodeDiskFactory } from 'src/factories/disk'; -import { imageFactory } from 'src/factories/images'; +import { mockGetUser } from 'support/intercepts/account'; describe('create image (using mocks)', () => { it('create image from a linode', () => { const mockDisks = [ - linodeDiskFactory.build({ filesystem: 'ext4', label: 'Debian 12 Disk' }), + linodeDiskFactory.build({ label: 'Debian 12 Disk', filesystem: 'ext4' }), linodeDiskFactory.build({ - filesystem: 'swap', label: '512 MB Swap Image', + filesystem: 'swap', }), ]; const mockLinode = linodeFactory.build(); const mockNewImage = imageFactory.build({ - description: randomPhrase(), - eol: null, - expiry: null, id: `private/${randomNumber(1000, 99999)}`, - is_public: false, label: randomLabel(), - status: 'creating', + description: randomPhrase(), type: 'manual', + is_public: false, vendor: null, + expiry: null, + eol: null, + status: 'creating', }); mockGetLinodes([mockLinode]).as('getLinodes'); @@ -121,14 +125,14 @@ describe('create image (using mocks)', () => { // Mock setup for user profile, account user, and user grants with restricted permissions, // simulating a default user without the ability to add Linodes. const mockProfile = profileFactory.build({ - restricted: true, username: randomLabel(), + restricted: true, }); const mockUser = accountUserFactory.build({ + username: mockProfile.username, restricted: true, user_type: 'default', - username: mockProfile.username, }); const mockGrants = grantsFactory.build({ @@ -138,10 +142,10 @@ describe('create image (using mocks)', () => { }); const mockDisks = [ - linodeDiskFactory.build({ filesystem: 'ext4', label: 'Debian 12 Disk' }), + linodeDiskFactory.build({ label: 'Debian 12 Disk', filesystem: 'ext4' }), linodeDiskFactory.build({ - filesystem: 'swap', label: '512 MB Swap Image', + filesystem: 'swap', }), ]; @@ -195,14 +199,14 @@ describe('create image (using mocks)', () => { it('should not upload image for the restricted users', () => { const mockProfile = profileFactory.build({ - restricted: true, username: randomLabel(), + restricted: true, }); const mockUser = accountUserFactory.build({ + username: mockProfile.username, restricted: true, user_type: 'default', - username: mockProfile.username, }); const mockGrants = grantsFactory.build({ 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 3d3614114f7..80362860f46 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts @@ -1,71 +1,68 @@ /** * @file LKE creation end-to-end tests. */ +import { pluralize } from '@linode/utilities'; import { + accountFactory, dedicatedTypeFactory, + kubernetesClusterFactory, + kubernetesControlPlaneACLFactory, + kubernetesControlPlaneACLOptionsFactory, linodeTypeFactory, - pluralize, regionFactory, -} from '@linode/utilities'; -import { - dcPricingDocsLabel, - dcPricingDocsUrl, - dcPricingLkeCheckoutSummaryPlaceholder, - dcPricingLkeClusterPlans, - dcPricingLkeHAPlaceholder, - dcPricingMockLinodeTypes, - dcPricingPlanPlaceholder, -} from 'support/constants/dc-specific-pricing'; -import { - latestEnterpriseTierKubernetesVersion, - latestKubernetesVersion, -} from 'support/constants/lke'; -import { mockGetAccount } from 'support/intercepts/account'; -import { mockGetAccountBeta } from 'support/intercepts/betas'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; -import { mockGetLinodeTypes } from 'support/intercepts/linodes'; + nodePoolFactory, + kubeLinodeFactory, + lkeHighAvailabilityTypeFactory, +} from 'src/factories'; import { mockCreateCluster, - mockCreateClusterError, - mockGetApiEndpoints, mockGetCluster, - mockGetClusterPools, - mockGetClusters, + mockCreateClusterError, mockGetControlPlaneACL, + mockGetClusterPools, mockGetDashboardUrl, - mockGetKubernetesVersions, + mockGetApiEndpoints, + mockGetClusters, mockGetLKEClusterTypes, mockGetTieredKubernetesVersions, + mockGetKubernetesVersions, } from 'support/intercepts/lke'; +import { mockGetAccountBeta } from 'support/intercepts/betas'; +import { mockGetAccount } from 'support/intercepts/account'; import { - mockGetRegionAvailability, mockGetRegions, + mockGetRegionAvailability, } from 'support/intercepts/regions'; +import { getRegionById } from 'support/util/regions'; import { ui } from 'support/ui'; -import { randomItem, randomLabel, randomNumber } from 'support/util/random'; -import { chooseRegion, getRegionById } from 'support/util/regions'; - +import { randomLabel, randomNumber, randomItem } from 'support/util/random'; import { - accountBetaFactory, - accountFactory, - kubeLinodeFactory, - kubernetesClusterFactory, - kubernetesControlPlaneACLFactory, - kubernetesControlPlaneACLOptionsFactory, - lkeEnterpriseTypeFactory, - lkeHighAvailabilityTypeFactory, - nodePoolFactory, -} from 'src/factories'; + dcPricingLkeCheckoutSummaryPlaceholder, + dcPricingLkeHAPlaceholder, + dcPricingLkeClusterPlans, + dcPricingMockLinodeTypes, + dcPricingPlanPlaceholder, + dcPricingDocsLabel, + dcPricingDocsUrl, +} from 'support/constants/dc-specific-pricing'; +import { mockGetLinodeTypes } from 'support/intercepts/linodes'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { chooseRegion } from 'support/util/regions'; +import { getTotalClusterMemoryCPUAndStorage } from 'src/features/Kubernetes/kubeUtils'; import { CLUSTER_TIER_DOCS_LINK, CLUSTER_VERSIONS_DOCS_LINK, } from 'src/features/Kubernetes/constants'; -import { getTotalClusterMemoryCPUAndStorage } from 'src/features/Kubernetes/kubeUtils'; import { getTotalClusterPrice } from 'src/utilities/pricing/kubernetes'; -import type { PriceType } from '@linode/api-v4/lib/types'; import type { ExtendedType } from 'src/utilities/extendType'; import type { LkePlanDescription } from 'support/api/lke'; +import { PriceType } from '@linode/api-v4/lib/types'; +import { + latestEnterpriseTierKubernetesVersion, + latestKubernetesVersion, +} from 'support/constants/lke'; +import { lkeEnterpriseTypeFactory } from 'src/factories'; const dedicatedNodeCount = 4; const nanodeNodeCount = 3; @@ -421,13 +418,13 @@ describe('LKE Cluster Creation with APL enabled', () => { }, }).as('getFeatureFlags'); mockGetAccountBeta({ - description: - 'Akamai App Platform is a platform that combines developer and operations-centric tools, automation and self-service to streamline the application lifecycle when using Kubernetes. This process will pre-register you for an upcoming beta.', - ended: null, - enrolled: '2024-11-04T21:39:41', id: 'apl', label: 'Akamai App Platform Beta', + enrolled: '2024-11-04T21:39:41', + description: + 'Akamai App Platform is a platform that combines developer and operations-centric tools, automation and self-service to streamline the application lifecycle when using Kubernetes. This process will pre-register you for an upcoming beta.', started: '2024-10-31T18:00:00', + ended: null, }).as('getAccountBeta'); mockCreateCluster(mockedLKECluster).as('createCluster'); mockGetCluster(mockedLKECluster).as('getCluster'); @@ -461,7 +458,6 @@ describe('LKE Cluster Creation with APL enabled', () => { ui.regionSelect.find().click().type(`${clusterRegion.label}{enter}`); cy.findByTestId('apl-label').should('have.text', 'Akamai App Platform'); - cy.findByTestId('apl-beta-chip').should('have.text', 'BETA'); cy.findByTestId('apl-radio-button-yes').should('be.visible').click(); cy.findByTestId('ha-radio-button-yes').should('be.disabled'); cy.get( @@ -582,7 +578,6 @@ describe('LKE Cluster Creation with DC-specific pricing', () => { cy.focused().type(`${dcSpecificPricingRegion.label}{enter}`); // Confirm that HA price updates dynamically once region selection is made. - // eslint-disable-next-line sonarjs/slow-regex cy.contains(/\$.*\/month/).should('be.visible'); cy.get('[data-testid="ha-radio-button-yes"]').should('be.visible').click(); @@ -673,14 +668,14 @@ describe('LKE Cluster Creation with ACL', () => { }); const mockLinodeTypes = [ linodeTypeFactory.build({ - class: 'dedicated', id: 'dedicated-1', label: 'dedicated-1', + class: 'dedicated', }), linodeTypeFactory.build({ - class: 'dedicated', id: 'dedicated-2', label: 'dedicated-2', + class: 'dedicated', }), ]; const clusterVersion = '1.31'; @@ -716,10 +711,10 @@ describe('LKE Cluster Creation with ACL', () => { }, }); const mockCluster = kubernetesClusterFactory.build({ - control_plane: mockACL, - k8s_version: clusterVersion, label: clusterLabel, region: mockRegion.id, + k8s_version: clusterVersion, + control_plane: mockACL, }); mockCreateCluster(mockCluster).as('createCluster'); mockGetCluster(mockCluster).as('getCluster'); @@ -820,10 +815,10 @@ describe('LKE Cluster Creation with ACL', () => { }); const mockCluster = kubernetesClusterFactory.build({ - control_plane: mockACL, - k8s_version: clusterVersion, label: clusterLabel, region: mockRegion.id, + k8s_version: clusterVersion, + control_plane: mockACL, }); mockCreateCluster(mockCluster).as('createCluster'); mockGetCluster(mockCluster).as('getCluster'); @@ -872,7 +867,9 @@ describe('LKE Cluster Creation with ACL', () => { .should('be.visible') .should('be.enabled') .click(); - cy.get('[id="domain-transfer-ip-1"]').should('be.visible').click(); + cy.get('[id="ipv4-addresses-or-cidrs-ip-address-1"]') + .should('be.visible') + .click(); cy.focused().type('10.0.1.0/24'); cy.findByLabelText('IPv6 Addresses or CIDRs ip-address-0') .should('be.visible') @@ -934,223 +931,6 @@ describe('LKE Cluster Creation with ACL', () => { .should('be.enabled'); }); - /** - * - Confirms create flow for LKE-E cluster with ACL enabled by default - * - Confirms at least one IP must be provided for ACL unless acknowledgement is checked - * - Confirms the cluster details page shows ACL is enabled - */ - it('creates an LKE cluster with ACL enabled by default and handles IP address validation', () => { - const clusterLabel = randomLabel(); - const mockedEnterpriseCluster = kubernetesClusterFactory.build({ - k8s_version: latestEnterpriseTierKubernetesVersion.id, - label: clusterLabel, - region: 'us-iad', - tier: 'enterprise', - }); - const mockedEnterpriseClusterPools = [nanodeMemoryPool]; - const mockACL = kubernetesControlPlaneACLFactory.build({ - acl: { - addresses: { - ipv4: [], - ipv6: [], - }, - enabled: true, - 'revision-id': '', - }, - }); - mockGetControlPlaneACL(mockedEnterpriseCluster.id, mockACL).as( - 'getControlPlaneACL' - ); - mockGetAccount( - accountFactory.build({ - capabilities: [ - 'Kubernetes Enterprise', - 'LKE HA Control Planes', - 'LKE Network Access Control List (IP ACL)', - ], - }) - ).as('getAccount'); - mockGetTieredKubernetesVersions('enterprise', [ - latestEnterpriseTierKubernetesVersion, - ]).as('getTieredKubernetesVersions'); - mockGetKubernetesVersions([latestKubernetesVersion]).as( - 'getKubernetesVersions' - ); - mockGetLinodeTypes(mockedLKEClusterTypes).as('getLinodeTypes'); - mockGetLKEClusterTypes(mockedLKEEnterprisePrices).as( - 'getLKEEnterpriseClusterTypes' - ); - mockGetRegions([ - regionFactory.build({ - capabilities: ['Linodes', 'Kubernetes', 'Kubernetes Enterprise'], - id: 'us-iad', - label: 'Washington, DC', - }), - ]).as('getRegions'); - mockGetCluster(mockedEnterpriseCluster).as('getCluster'); - mockCreateCluster(mockedEnterpriseCluster).as('createCluster'); - mockGetClusters([mockedEnterpriseCluster]).as('getClusters'); - mockGetClusterPools( - mockedEnterpriseCluster.id, - mockedEnterpriseClusterPools - ).as('getClusterPools'); - mockGetDashboardUrl(mockedEnterpriseCluster.id).as('getDashboardUrl'); - mockGetApiEndpoints(mockedEnterpriseCluster.id).as('getApiEndpoints'); - - cy.visitWithLogin('/kubernetes/clusters'); - cy.wait(['@getAccount']); - - ui.button - .findByTitle('Create Cluster') - .should('be.visible') - .should('be.enabled') - .click(); - - cy.url().should('endWith', '/kubernetes/create'); - cy.wait(['@getKubernetesVersions', '@getTieredKubernetesVersions']); - - // Select enterprise tier. - cy.get(`[data-qa-select-card-heading="LKE Enterprise"]`) - .closest('[data-qa-selection-card]') - .click(); - - cy.wait(['@getLKEEnterpriseClusterTypes', '@getRegions']); - - // Select a supported region. - ui.regionSelect.find().clear().type('Washington, DC{enter}'); - - // Select an enterprise version. - ui.autocomplete - .findByLabel('Kubernetes Version') - .should('be.visible') - .click(); - - clusterPlans.forEach((clusterPlan) => { - const nodeCount = clusterPlan.nodeCount; - const planName = clusterPlan.planName; - // Click the right tab for the plan, and add a node pool with the desired - // number of nodes. - cy.findByText(clusterPlan.tab).should('be.visible').click(); - const quantityInput = '[name="Quantity"]'; - cy.findByText(planName) - .should('be.visible') - .closest('tr') - .within(() => { - cy.get(quantityInput).should('be.visible'); - cy.get(quantityInput).click(); - cy.get(quantityInput).type(`{selectall}${nodeCount}`); - - ui.button - .findByTitle('Add') - .should('be.visible') - .should('be.enabled') - .click(); - }); - }); - - // Confirm ACL is enabled by default. - cy.contains('Control Plane ACL').should('be.visible'); - ui.toggle - .find() - .should('have.attr', 'data-qa-toggle', 'true') - .should('be.visible'); - cy.findByRole('checkbox', { name: /Provide an ACL later/ }).should( - 'not.be.checked' - ); - - // Try to submit the form without the ACL acknowledgement checked. - cy.get('[data-testid="kube-checkout-bar"]') - .should('be.visible') - .within(() => { - ui.button - .findByTitle('Create Cluster') - .should('be.visible') - .should('be.enabled') - .click(); - }); - - // Confirm error validation requires an ACL IP. - cy.findByText( - 'At least one IP address or CIDR range is required for LKE Enterprise.' - ).should('be.visible'); - - // Add an IP, - cy.findByLabelText('IPv4 Addresses or CIDRs ip-address-0') - .should('be.visible') - .click(); - cy.focused().clear(); - cy.focused().type('10.0.0.0/24'); - - cy.get('[data-testid="kube-checkout-bar"]') - .should('be.visible') - .within(() => { - // Try to submit the form again. - ui.button - .findByTitle('Create Cluster') - .should('be.visible') - .should('be.enabled') - .click(); - }); - - // Confirm the validation message is gone. - cy.findByText( - 'At least one IP address or CIDR range is required for LKE Enterprise.' - ).should('not.exist'); - - // Check the acknowledgement to prevent IP validation. - cy.findByRole('checkbox', { name: /Provide an ACL later/ }).check(); - - // Clear the IP address field and check the acknowledgement to confirm the form can now submit without IP address validation. - cy.findByLabelText('IPv4 Addresses or CIDRs ip-address-0') - .should('be.visible') - .click(); - cy.focused().clear(); - cy.findByRole('checkbox', { name: /Provide an ACL later/ }).check(); - - // Finally, add a label, so the form will submit. - cy.findByLabelText('Cluster Label').should('be.visible').click(); - cy.focused().type(`${clusterLabel}{enter}`); - - cy.get('[data-testid="kube-checkout-bar"]') - .should('be.visible') - .within(() => { - // Try to submit the form. - ui.button - .findByTitle('Create Cluster') - .should('be.visible') - .should('be.enabled') - .click(); - }); - - // Confirm the validation message is gone. - cy.findByText( - 'At least one IP address or CIDR range is required for LKE Enterprise.' - ).should('not.exist'); - - cy.wait([ - '@getCluster', - '@getClusterPools', - '@createCluster', - '@getLKEEnterpriseClusterTypes', - '@getLinodeTypes', - '@getDashboardUrl', - '@getApiEndpoints', - '@getControlPlaneACL', - ]); - - cy.url().should( - 'endWith', - `/kubernetes/clusters/${mockedEnterpriseCluster.id}/summary` - ); - - // Confirms Summary panel displays as expected - cy.contains('Control Plane ACL').should('be.visible'); - ui.button - .findByTitle('Enabled (0 IP Addresses)') - .should('be.visible') - .should('be.enabled'); - }); - /** * - Confirms IP validation error appears when a bad IP is entered * - Confirms IP validation error disappears when a valid IP is entered @@ -1308,9 +1088,7 @@ describe('LKE Cluster Creation with LKE-E', () => { * - Confirms that HA is enabled by default with LKE-E selection * - Confirms an LKE-E supported region can be selected * - Confirms an LKE-E supported k8 version can be selected - * - Confirms the APL section is disabled while it remains unsupported - * - Confirms the VPC & Firewall placeholder section displays with correct copy - * - Confirms ACL is enabled by default + * - Confirms at least one IP must be provided for ACL * - Confirms the checkout bar displays the correct LKE-E info * - Confirms an enterprise cluster can be created with the correct chip, version, and price * - Confirms that the total node count for each pool is displayed @@ -1318,32 +1096,13 @@ describe('LKE Cluster Creation with LKE-E', () => { it('creates an LKE-E cluster with the account capability', () => { const clusterLabel = randomLabel(); const mockedEnterpriseCluster = kubernetesClusterFactory.build({ - k8s_version: latestEnterpriseTierKubernetesVersion.id, label: clusterLabel, region: 'us-iad', tier: 'enterprise', + k8s_version: latestEnterpriseTierKubernetesVersion.id, }); const mockedEnterpriseClusterPools = [nanodeMemoryPool, dedicatedCpuPool]; - const mockACL = kubernetesControlPlaneACLFactory.build({ - acl: { - addresses: { - ipv4: ['10.0.0.0/24'], - ipv6: [], - }, - enabled: true, - 'revision-id': '', - }, - }); - mockGetControlPlaneACL(mockedEnterpriseCluster.id, mockACL).as( - 'getControlPlaneACL' - ); - mockGetAccountBeta( - accountBetaFactory.build({ - id: 'apl', - label: 'Akamai App Platform Beta', - }) - ).as('getAccountBeta'); mockGetAccount( accountFactory.build({ capabilities: [ @@ -1458,23 +1217,6 @@ describe('LKE Cluster Creation with LKE-E', () => { .should('be.enabled') .click(); - // Confirm the APL section is disabled and unsupported. - cy.findByTestId('apl-label').should('be.visible'); - cy.findByTestId('apl-beta-chip').should( - 'have.text', - 'BETA - COMING SOON' - ); - cy.findByTestId('apl-radio-button-yes').should('be.disabled'); - cy.findByTestId('apl-radio-button-no').within(() => { - cy.findByRole('radio').should('be.disabled').should('be.checked'); - }); - - // Confirm the VPC/Firewall section displays. - cy.findByText('VPC & Firewall').should('be.visible'); - cy.findByText( - 'A VPC and Firewall are automatically generated for LKE Enterprise customers.' - ).should('be.visible'); - // Confirm the expected available plans display. validEnterprisePlanTabs.forEach((tab) => { ui.tabList.findTabByTitle(tab).should('be.visible'); @@ -1527,14 +1269,19 @@ describe('LKE Cluster Creation with LKE-E', () => { cy.findByText('Linode 2 GB Plan').should('be.visible'); cy.findByText('$15.00').should('be.visible'); cy.findByText('$459.00').should('be.visible'); + + // Try to submit the form + ui.button + .findByTitle('Create Cluster') + .should('be.visible') + .should('be.enabled') + .click(); }); - // Confirms ACL is enabled by default. - cy.contains('Control Plane ACL').should('be.visible'); - ui.toggle - .find() - .should('have.attr', 'data-qa-toggle', 'true') - .should('be.visible'); + // Confirm error validation requires an ACL IP + cy.findByText( + 'At least one IP address or CIDR range is required for LKE Enterprise.' + ).should('be.visible'); // Add an IP cy.findByLabelText('IPv4 Addresses or CIDRs ip-address-0') @@ -1554,6 +1301,10 @@ describe('LKE Cluster Creation with LKE-E', () => { .click(); }); + cy.findByText( + 'At least one IP address or CIDR range is required for LKE Enterprise.' + ).should('not.exist'); + // Wait for LKE cluster to be created and confirm that we are redirected // to the cluster summary page. cy.wait([ @@ -1564,7 +1315,6 @@ describe('LKE Cluster Creation with LKE-E', () => { '@getLinodeTypes', '@getDashboardUrl', '@getApiEndpoints', - '@getControlPlaneACL', ]); cy.url().should( diff --git a/packages/manager/cypress/e2e/core/kubernetes/lke-delete.spec.ts b/packages/manager/cypress/e2e/core/kubernetes/lke-delete.spec.ts index eabaacae666..6ea059173ed 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/lke-delete.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/lke-delete.spec.ts @@ -1,13 +1,12 @@ +import { kubernetesClusterFactory } from 'src/factories'; import { - mockDeleteCluster, mockGetCluster, mockGetClusters, + mockDeleteCluster, } from 'support/intercepts/lke'; import { ui } from 'support/ui'; import { randomLabel } from 'support/util/random'; -import { kubernetesClusterFactory } from 'src/factories'; - /* * Fills out and submits Type to Confirm deletion dialog for cluster with the given label. */ 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 96fb31a0c8a..659e5ad8c04 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 @@ -1,25 +1,23 @@ -import { mockGetAccount } from 'support/intercepts/account'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import type { KubernetesCluster } from '@linode/api-v4'; import { - mockGetClusterPools, mockGetClusters, + mockGetClusterPools, mockGetKubeconfig, mockGetKubernetesVersions, mockGetTieredKubernetesVersions, mockRecycleAllNodes, mockUpdateCluster, } from 'support/intercepts/lke'; -import { ui } from 'support/ui'; -import { readDownload } from 'support/util/downloads'; -import { getRegionById } from 'support/util/regions'; - import { accountFactory, kubernetesClusterFactory, nodePoolFactory, } from 'src/factories'; - -import type { KubernetesCluster } from '@linode/api-v4'; +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 { mockGetAccount } from 'support/intercepts/account'; describe('LKE landing page', () => { it('does not display a Disk Encryption info banner if the LDE feature is disabled', () => { diff --git a/packages/manager/cypress/e2e/core/kubernetes/lke-summary-page.spec.ts b/packages/manager/cypress/e2e/core/kubernetes/lke-summary-page.spec.ts index 2782745e969..b2ef694aa31 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/lke-summary-page.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/lke-summary-page.spec.ts @@ -1,24 +1,23 @@ +import { + kubernetesClusterFactory, + kubernetesControlPlaneACLFactory, + nodePoolFactory, +} from 'src/factories'; import { latestKubernetesVersion } from 'support/constants/lke'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { - mockGetApiEndpoints, mockGetCluster, - mockGetClusterPools, - mockGetControlPlaneACL, - mockGetDashboardUrl, mockGetKubeconfig, mockGetKubernetesVersions, + mockGetClusterPools, + mockGetDashboardUrl, + mockGetApiEndpoints, + mockGetControlPlaneACL, mockUpdateCluster, } from 'support/intercepts/lke'; -import { ui } from 'support/ui'; -import { readDownload } from 'support/util/downloads'; import { randomLabel } from 'support/util/random'; - -import { - kubernetesClusterFactory, - kubernetesControlPlaneACLFactory, - nodePoolFactory, -} from 'src/factories'; +import { readDownload } from 'support/util/downloads'; +import { ui } from 'support/ui'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; const mockKubeconfigContents = '---'; // Valid YAML. const mockKubeconfigResponse = { @@ -89,8 +88,8 @@ describe('LKE summary page', () => { }, }); const mockCluster = kubernetesClusterFactory.build({ - control_plane: mockACL, k8s_version: latestKubernetesVersion, + control_plane: mockACL, }); const tag = randomLabel(); const mockClusterUpdated = { @@ -151,8 +150,8 @@ describe('LKE summary page', () => { const tagNew = randomLabel(); const mockCluster = kubernetesClusterFactory.build({ - control_plane: mockACL, k8s_version: latestKubernetesVersion, + control_plane: mockACL, tags: tagsExisting, }); @@ -221,8 +220,8 @@ describe('LKE summary page', () => { }); const tagExisting = randomLabel(); const mockCluster = kubernetesClusterFactory.build({ - control_plane: mockACL, k8s_version: latestKubernetesVersion, + control_plane: mockACL, tags: [tagExisting], }); @@ -274,8 +273,8 @@ describe('LKE summary page', () => { }); const tagsExisting = buildTags(2); const mockCluster = kubernetesClusterFactory.build({ - control_plane: mockACL, k8s_version: latestKubernetesVersion, + control_plane: mockACL, tags: tagsExisting, }); diff --git a/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts b/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts index 7cbe9d211d9..edb21bc4286 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts @@ -1,52 +1,52 @@ -import { linodeFactory, linodeTypeFactory } from '@linode/utilities'; -import { DateTime } from 'luxon'; -import { dcPricingMockLinodeTypes } from 'support/constants/dc-specific-pricing'; -import { latestKubernetesVersion } from 'support/constants/lke'; -import { mockGetAccount } from 'support/intercepts/account'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { - mockGetLinodeType, - mockGetLinodeTypes, - mockGetLinodes, -} from 'support/intercepts/linodes'; + accountFactory, + kubernetesClusterFactory, + nodePoolFactory, + kubeLinodeFactory, + linodeFactory, + kubernetesControlPlaneACLFactory, + kubernetesControlPlaneACLOptionsFactory, + linodeTypeFactory, +} from 'src/factories'; +import { extendType } from 'src/utilities/extendType'; +import { mockGetAccount } from 'support/intercepts/account'; +import { latestKubernetesVersion } from 'support/constants/lke'; import { + mockGetCluster, + mockGetKubernetesVersions, + mockGetClusterPools, + mockResetKubeconfig, + mockUpdateCluster, mockAddNodePool, + mockUpdateNodePool, mockDeleteNodePool, + mockRecycleNode, + mockRecycleNodePool, + mockRecycleAllNodes, + mockGetDashboardUrl, mockGetApiEndpoints, - mockGetCluster, - mockGetClusterPools, + mockUpdateControlPlaneACL, mockGetControlPlaneACL, + mockUpdateControlPlaneACLError, mockGetControlPlaneACLError, - mockGetDashboardUrl, - mockGetKubernetesVersions, mockGetTieredKubernetesVersions, - mockRecycleAllNodes, - mockRecycleNode, - mockRecycleNodePool, - mockResetKubeconfig, - mockUpdateCluster, mockUpdateClusterError, - mockUpdateControlPlaneACL, - mockUpdateControlPlaneACLError, - mockUpdateNodePool, mockUpdateNodePoolError, } from 'support/intercepts/lke'; +import { + mockGetLinodeType, + mockGetLinodeTypes, + mockGetLinodes, +} from 'support/intercepts/linodes'; +import type { PoolNodeResponse, Linode, Taint, Label } from '@linode/api-v4'; import { ui } from 'support/ui'; -import { buildArray } from 'support/util/arrays'; -import { randomIp, randomLabel, randomString } from 'support/util/random'; +import { randomIp, randomLabel } from 'support/util/random'; import { getRegionById } from 'support/util/regions'; - -import { - accountFactory, - kubeLinodeFactory, - kubernetesClusterFactory, - kubernetesControlPlaneACLFactory, - kubernetesControlPlaneACLOptionsFactory, - nodePoolFactory, -} from 'src/factories'; -import { extendType } from 'src/utilities/extendType'; - -import type { Label, Linode, PoolNodeResponse, Taint } from '@linode/api-v4'; +import { dcPricingMockLinodeTypes } from 'support/constants/dc-specific-pricing'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { randomString } from 'support/util/random'; +import { buildArray } from 'support/util/arrays'; +import { DateTime } from 'luxon'; const mockNodePools = nodePoolFactory.buildList(2); @@ -67,10 +67,10 @@ describe('LKE cluster updates', () => { */ it('can upgrade to high availability', () => { const mockCluster = kubernetesClusterFactory.build({ + k8s_version: latestKubernetesVersion, control_plane: { high_availability: false, }, - k8s_version: latestKubernetesVersion, }); const mockClusterWithHA = { @@ -387,13 +387,13 @@ describe('LKE cluster updates', () => { const mockNodePool = nodePoolFactory.build({ count: 1, - nodes: [mockKubeLinode], type: 'g6-standard-1', + nodes: [mockKubeLinode], }); const mockLinode = linodeFactory.build({ - id: mockKubeLinode.instance_id ?? undefined, label: randomLabel(), + id: mockKubeLinode.instance_id ?? undefined, }); const recycleWarningSubstrings = [ @@ -520,16 +520,16 @@ describe('LKE cluster updates', () => { const mockNodePool = nodePoolFactory.build({ count: 1, - nodes: kubeLinodeFactory.buildList(1), type: 'g6-standard-1', + nodes: kubeLinodeFactory.buildList(1), }); const mockNodePoolAutoscale = { ...mockNodePool, autoscaler: { enabled: true, - max: autoscaleMax, min: autoscaleMin, + max: autoscaleMax, }, }; @@ -638,8 +638,8 @@ describe('LKE cluster updates', () => { const mockNodePoolResized = nodePoolFactory.build({ count: 3, - nodes: kubeLinodeFactory.buildList(3), type: 'g6-standard-1', + nodes: kubeLinodeFactory.buildList(3), }); const mockNodePoolInitial = { @@ -1040,8 +1040,8 @@ describe('LKE cluster updates', () => { const mockNodePoolNoTags = nodePoolFactory.build({ id: 1, - nodes: mockNodes, type: mockType.id, + nodes: mockNodes, }); const mockNodePoolWithTags = { @@ -1151,10 +1151,11 @@ describe('LKE cluster updates', () => { const mockNodePoolInitial = nodePoolFactory.build({ id: 1, + type: mockType.id, + nodes: mockNodes, labels: { ['example.com/my-app']: 'teams', }, - nodes: mockNodes, taints: [ { effect: 'NoSchedule', @@ -1162,7 +1163,6 @@ describe('LKE cluster updates', () => { value: 'teamA', }, ], - type: mockType.id, }); const mockDrawerTitle = 'Labels and Taints: Linode 2 GB Plan'; @@ -1185,10 +1185,10 @@ describe('LKE cluster updates', () => { it('can delete labels and taints', () => { const mockNodePoolUpdated = nodePoolFactory.build({ id: 1, - labels: {}, + type: mockType.id, nodes: mockNodes, taints: [], - type: mockType.id, + labels: {}, }); cy.visitWithLogin(`/kubernetes/clusters/${mockCluster.id}`); @@ -1309,24 +1309,24 @@ describe('LKE cluster updates', () => { const mockNewSimpleLabel = 'my_label.-key: my_label.-value'; const mockNewDNSLabel = 'my_label-key.io/app: my_label.-value'; const mockNewTaint: Taint = { - effect: 'NoSchedule', key: 'my_taint.-key', value: 'my_taint.-value', + effect: 'NoSchedule', }; const mockNewDNSTaint: Taint = { - effect: 'NoSchedule', key: 'my_taint-key.io/app', value: 'my_taint.-value', + effect: 'NoSchedule', }; const mockNodePoolUpdated = nodePoolFactory.build({ id: 1, + type: mockType.id, + nodes: mockNodes, + taints: [mockNewTaint, mockNewDNSTaint], labels: { 'my_label-key': 'my_label.-value', 'my_label-key.io/app': 'my_label.-value', }, - nodes: mockNodes, - taints: [mockNewTaint, mockNewDNSTaint], - type: mockType.id, }); cy.visitWithLogin(`/kubernetes/clusters/${mockCluster.id}`); @@ -1717,12 +1717,12 @@ describe('LKE cluster updates', () => { }); const mockNodePools = [ nodePoolFactory.build({ - count: 10, nodes: kubeLinodeFactory.buildList(10), + count: 10, }), nodePoolFactory.build({ - count: 5, nodes: kubeLinodeFactory.buildList(5), + count: 5, }), nodePoolFactory.build({ nodes: [kubeLinodeFactory.build()] }), ]; @@ -1798,8 +1798,8 @@ describe('LKE cluster updates', () => { it('filters the node tables based on selected status filter', () => { const mockCluster = kubernetesClusterFactory.build({ - created: DateTime.local().toISO(), k8s_version: latestKubernetesVersion, + created: DateTime.local().toISO(), tier: 'enterprise', }); const mockNodePools = [ @@ -1932,17 +1932,17 @@ describe('LKE cluster updates', () => { const mockPlanType = extendType(dcPricingMockLinodeTypes[0]); const mockCluster = kubernetesClusterFactory.build({ + k8s_version: latestKubernetesVersion, + region: dcSpecificPricingRegion.id, control_plane: { high_availability: false, }, - k8s_version: latestKubernetesVersion, - region: dcSpecificPricingRegion.id, }); const mockNodePoolResized = nodePoolFactory.build({ count: 3, - nodes: kubeLinodeFactory.buildList(3), type: mockPlanType.id, + nodes: kubeLinodeFactory.buildList(3), }); const mockNodePoolInitial = { @@ -2076,25 +2076,25 @@ describe('LKE cluster updates', () => { const dcSpecificPricingRegion = getRegionById('us-east'); const mockCluster = kubernetesClusterFactory.build({ + k8s_version: latestKubernetesVersion, + region: dcSpecificPricingRegion.id, control_plane: { high_availability: false, }, - k8s_version: latestKubernetesVersion, - region: dcSpecificPricingRegion.id, }); const mockPlanType = extendType(dcPricingMockLinodeTypes[0]); const mockNewNodePool = nodePoolFactory.build({ count: 2, - nodes: kubeLinodeFactory.buildList(2), type: mockPlanType.id, + nodes: kubeLinodeFactory.buildList(2), }); const mockNodePool = nodePoolFactory.build({ count: 1, - nodes: kubeLinodeFactory.buildList(1), type: mockPlanType.id, + nodes: kubeLinodeFactory.buildList(1), }); mockGetCluster(mockCluster).as('getCluster'); @@ -2182,17 +2182,17 @@ describe('LKE cluster updates', () => { const mockPlanType = extendType(dcPricingMockLinodeTypes[2]); const mockCluster = kubernetesClusterFactory.build({ + k8s_version: latestKubernetesVersion, + region: dcSpecificPricingRegion.id, control_plane: { high_availability: false, }, - k8s_version: latestKubernetesVersion, - region: dcSpecificPricingRegion.id, }); const mockNodePoolResized = nodePoolFactory.build({ count: 3, - nodes: kubeLinodeFactory.buildList(3), type: mockPlanType.id, + nodes: kubeLinodeFactory.buildList(3), }); const mockNodePoolInitial = { @@ -2319,23 +2319,23 @@ describe('LKE cluster updates', () => { const mockPlanType = extendType(dcPricingMockLinodeTypes[2]); const mockCluster = kubernetesClusterFactory.build({ + k8s_version: latestKubernetesVersion, + region: dcSpecificPricingRegion.id, control_plane: { high_availability: false, }, - k8s_version: latestKubernetesVersion, - region: dcSpecificPricingRegion.id, }); const mockNewNodePool = nodePoolFactory.build({ count: 2, - nodes: kubeLinodeFactory.buildList(2), type: mockPlanType.id, + nodes: kubeLinodeFactory.buildList(2), }); const mockNodePool = nodePoolFactory.build({ count: 1, - nodes: kubeLinodeFactory.buildList(1), type: mockPlanType.id, + nodes: kubeLinodeFactory.buildList(1), }); mockGetCluster(mockCluster).as('getCluster'); @@ -2450,22 +2450,24 @@ describe('LKE ACL updates', () => { */ it('can enable ACL on an LKE cluster with ACL pre-installed and edit IPs', () => { const mockACLOptions = kubernetesControlPlaneACLOptionsFactory.build({ - addresses: { ipv4: ['10.0.3.0/24'], ipv6: undefined }, enabled: false, + addresses: { ipv4: ['10.0.3.0/24'], ipv6: undefined }, }); - const mockUpdatedACLOptions1 = - kubernetesControlPlaneACLOptionsFactory.build({ - addresses: { ipv4: ['10.0.0.0/24'], ipv6: undefined }, + const mockUpdatedACLOptions1 = kubernetesControlPlaneACLOptionsFactory.build( + { enabled: true, 'revision-id': mockRevisionId, - }); + addresses: { ipv4: ['10.0.0.0/24'], ipv6: undefined }, + } + ); const mockControlPaneACL = kubernetesControlPlaneACLFactory.build({ acl: mockACLOptions, }); - const mockUpdatedControlPlaneACL1 = - kubernetesControlPlaneACLFactory.build({ + const mockUpdatedControlPlaneACL1 = kubernetesControlPlaneACLFactory.build( + { acl: mockUpdatedACLOptions1, - }); + } + ); mockGetCluster(mockCluster).as('getCluster'); mockGetControlPlaneACL(mockCluster.id, mockControlPaneACL).as( @@ -2546,8 +2548,10 @@ describe('LKE ACL updates', () => { .click(); // update mocks - const mockUpdatedACLOptions2 = - kubernetesControlPlaneACLOptionsFactory.build({ + const mockUpdatedACLOptions2 = kubernetesControlPlaneACLOptionsFactory.build( + { + enabled: true, + 'revision-id': mockRevisionId, addresses: { ipv4: ['10.0.0.0/24'], ipv6: [ @@ -2555,13 +2559,13 @@ describe('LKE ACL updates', () => { 'f4a2:b849:4a24:d0d9:15f0:704b:f943:718f', ], }, - enabled: true, - 'revision-id': mockRevisionId, - }); - const mockUpdatedControlPlaneACL2 = - kubernetesControlPlaneACLFactory.build({ + } + ); + const mockUpdatedControlPlaneACL2 = kubernetesControlPlaneACLFactory.build( + { acl: mockUpdatedACLOptions2, - }); + } + ); mockUpdateControlPlaneACL(mockCluster.id, mockUpdatedControlPlaneACL2).as( 'updateControlPlaneACL' ); @@ -2642,31 +2646,28 @@ describe('LKE ACL updates', () => { /** * - Confirms ACL can be disabled from the summary page (for standard tier only) + * - Confirms both IPv4 and IPv6 can be updated and that drawer updates as a result */ - it('can disable ACL on a standard tier cluster', () => { + it('can disable ACL on a standard tier cluster and edit IPs', () => { const mockACLOptions = kubernetesControlPlaneACLOptionsFactory.build({ - addresses: { - ipv4: [], - ipv6: [], - }, enabled: true, + addresses: { ipv4: undefined, ipv6: undefined }, }); - - const mockDisabledACLOptions = - kubernetesControlPlaneACLOptionsFactory.build({ + const mockUpdatedACLOptions1 = kubernetesControlPlaneACLOptionsFactory.build( + { + enabled: false, addresses: { - ipv4: [''], - ipv6: [''], + ipv4: ['10.0.0.0/24'], + ipv6: ['8e61:f9e9:8d40:6e0a:cbff:c97a:2692:827e'], }, - enabled: false, - 'revision-id': '', - }); + } + ); const mockControlPaneACL = kubernetesControlPlaneACLFactory.build({ acl: mockACLOptions, }); - const mockUpdatedControlPlaneACL = kubernetesControlPlaneACLFactory.build( + const mockUpdatedControlPlaneACL1 = kubernetesControlPlaneACLFactory.build( { - acl: mockDisabledACLOptions, + acl: mockUpdatedACLOptions1, } ); @@ -2674,7 +2675,7 @@ describe('LKE ACL updates', () => { mockGetControlPlaneACL(mockCluster.id, mockControlPaneACL).as( 'getControlPlaneACL' ); - mockUpdateControlPlaneACL(mockCluster.id, mockUpdatedControlPlaneACL).as( + mockUpdateControlPlaneACL(mockCluster.id, mockUpdatedControlPlaneACL1).as( 'updateControlPlaneACL' ); @@ -2718,16 +2719,27 @@ describe('LKE ACL updates', () => { // confirm Revision ID section cy.findByLabelText('Revision ID').should( 'have.value', - mockDisabledACLOptions['revision-id'] + mockACLOptions['revision-id'] ); - // confirm IPv4 and IPv6 address sections + // Addresses Section: update IPv4 cy.findByLabelText('IPv4 Addresses or CIDRs ip-address-0') .should('be.visible') - .should('have.value', mockDisabledACLOptions.addresses?.ipv4?.[0]); + .click(); + cy.focused().type('10.0.0.0/24'); + cy.findByText('Add IPv4 Address') + .should('be.visible') + .should('be.enabled') + .click(); + // update IPv6 cy.findByLabelText('IPv6 Addresses or CIDRs ip-address-0') .should('be.visible') - .should('have.value', mockDisabledACLOptions.addresses?.ipv6?.[0]); + .click(); + cy.focused().type('8e61:f9e9:8d40:6e0a:cbff:c97a:2692:827e'); + cy.findByText('Add IPv6 Address') + .should('be.visible') + .should('be.enabled') + .click(); // submit ui.button @@ -2742,7 +2754,7 @@ describe('LKE ACL updates', () => { // confirm summary panel updates cy.contains('Control Plane ACL').should('be.visible'); - cy.findByText('Enabled (0 IP Addresses)').should('not.exist'); + cy.findByText('Enabled (O IP Addresses)').should('not.exist'); ui.button .findByTitle('Enable') .should('be.visible') @@ -2760,19 +2772,11 @@ describe('LKE ACL updates', () => { .should('have.attr', 'data-qa-toggle', 'false') .should('be.visible'); - // confirm Revision ID section remains empty - cy.findByLabelText('Revision ID').should( - 'have.value', - mockDisabledACLOptions['revision-id'] - ); - - // confirm IPv4 and IPv6 address sections remain empty - cy.findByLabelText('IPv4 Addresses or CIDRs ip-address-0') - .should('be.visible') - .should('have.value', mockDisabledACLOptions.addresses?.ipv4?.[0]); - cy.findByLabelText('IPv6 Addresses or CIDRs ip-address-0') - .should('be.visible') - .should('have.value', mockDisabledACLOptions.addresses?.ipv6?.[0]); + // confirm updated IP addresses display + cy.findByDisplayValue('10.0.0.0/24').should('be.visible'); + cy.findByDisplayValue( + '8e61:f9e9:8d40:6e0a:cbff:c97a:2692:827e' + ).should('be.visible'); }); }); @@ -2783,8 +2787,8 @@ describe('LKE ACL updates', () => { */ it('can enable ACL on an LKE cluster with ACL not pre-installed and edit IPs', () => { const mockACLOptions = kubernetesControlPlaneACLOptionsFactory.build({ - addresses: { ipv4: ['10.0.0.0/24'] }, enabled: true, + addresses: { ipv4: ['10.0.0.0/24'] }, }); const mockControlPaneACL = kubernetesControlPlaneACLFactory.build({ acl: mockACLOptions, @@ -2877,8 +2881,8 @@ describe('LKE ACL updates', () => { */ it('can handle validation and API errors', () => { const mockACLOptions = kubernetesControlPlaneACLOptionsFactory.build({ - addresses: { ipv4: undefined, ipv6: undefined }, enabled: true, + addresses: { ipv4: undefined, ipv6: undefined }, }); const mockControlPaneACL = kubernetesControlPlaneACLFactory.build({ acl: mockACLOptions, @@ -2963,38 +2967,24 @@ describe('LKE ACL updates', () => { tier: 'enterprise', }); const mockACLOptions = kubernetesControlPlaneACLOptionsFactory.build({ - addresses: { ipv4: [], ipv6: [] }, - enabled: true, - }); - const mockUpdatedOptions = kubernetesControlPlaneACLOptionsFactory.build({ - addresses: { - ipv4: [], - ipv6: ['8e61:f9e9:8d40:6e0a:cbff:c97a:2692:827e'], - }, + addresses: { ipv4: ['127.0.0.1'], ipv6: undefined }, enabled: true, }); const mockControlPaneACL = kubernetesControlPlaneACLFactory.build({ acl: mockACLOptions, }); - const mockUpdatedControlPaneACL = kubernetesControlPlaneACLFactory.build({ - acl: mockUpdatedOptions, - }); mockGetCluster(mockEnterpriseCluster).as('getCluster'); mockGetControlPlaneACL(mockEnterpriseCluster.id, mockControlPaneACL).as( 'getControlPlaneACL' ); - mockUpdateControlPlaneACL( - mockEnterpriseCluster.id, - mockUpdatedControlPaneACL - ).as('updateControlPlaneACL'); cy.visitWithLogin(`/kubernetes/clusters/${mockEnterpriseCluster.id}`); cy.wait(['@getAccount', '@getCluster', '@getControlPlaneACL']); cy.contains('Control Plane ACL').should('be.visible'); ui.button - .findByTitle('Enabled (0 IP Addresses)') + .findByTitle('Enabled (1 IP Address)') .should('be.visible') .should('be.enabled') .click(); @@ -3003,14 +2993,11 @@ describe('LKE ACL updates', () => { .findByTitle(`Control Plane ACL for ${mockEnterpriseCluster.label}`) .should('be.visible') .within(() => { - // Confirm the checkbox is not checked by default - cy.findByRole('checkbox', { name: /Provide an ACL later/ }).should( - 'not.be.checked' - ); - - cy.findByLabelText('Revision ID').click(); + // Clear the existing IP + cy.findByLabelText('IPv4 Addresses or CIDRs ip-address-0') + .should('be.visible') + .click(); cy.focused().clear(); - cy.focused().type('1'); // Try to submit the form without any IPs ui.button @@ -3026,7 +3013,6 @@ describe('LKE ACL updates', () => { ).should('be.visible'); // Add at least one IP - cy.findByText('Add IPv6 Address').click(); cy.findByLabelText('IPv6 Addresses or CIDRs ip-address-0') .should('be.visible') .click(); @@ -3046,41 +3032,6 @@ describe('LKE ACL updates', () => { 'At least one IP address or CIDR range is required for LKE Enterprise.' ).should('not.exist'); }); - - cy.wait('@updateControlPlaneACL'); - - ui.button - .findByTitle('Enabled (1 IP Address)') - .should('be.visible') - .should('be.enabled') - .click(); - - ui.drawer - .findByTitle(`Control Plane ACL for ${mockEnterpriseCluster.label}`) - .should('be.visible') - .within(() => { - // Clear the existing IP - cy.findByLabelText('IPv6 Addresses or CIDRs ip-address-0') - .should('be.visible') - .click(); - cy.focused().clear(); - - // Check the acknowledgement checkbox - cy.findByRole('checkbox', { name: /Provide an ACL later/ }).click(); - - // Confirm the form can submit without any IPs if the acknowledgement is checked - ui.button - .findByTitle('Update') - .scrollIntoView() - .should('be.visible') - .should('be.enabled') - .click(); - - // Confirm error message disappears - cy.contains( - 'At least one IP address or CIDR range is required for LKE Enterprise.' - ).should('not.exist'); - }); }); }); }); diff --git a/packages/manager/cypress/e2e/core/kubernetes/smoke-lke-create.spec.ts b/packages/manager/cypress/e2e/core/kubernetes/smoke-lke-create.spec.ts index f6b99c5ab16..124c9d04659 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/smoke-lke-create.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/smoke-lke-create.spec.ts @@ -1,17 +1,7 @@ -import { profileFactory } from '@linode/utilities'; -import { - accountUserFactory, - grantsFactory, - kubernetesClusterFactory, -} from '@src/factories'; -import { mockGetUser } from 'support/intercepts/account'; +import { kubernetesClusterFactory } from '@src/factories'; +import { randomLabel, randomNumber } from 'support/util/random'; import { mockCreateCluster } from 'support/intercepts/lke'; -import { - mockGetProfile, - mockGetProfileGrants, -} from 'support/intercepts/profile'; import { ui } from 'support/ui'; -import { randomLabel, randomNumber } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; /** @@ -64,8 +54,8 @@ const minimumNodeNotice = describe('LKE Create Cluster', () => { it('Simple Page Check', () => { const mockCluster = kubernetesClusterFactory.build({ - id: randomNumber(10000, 99999), label: randomLabel(), + id: randomNumber(10000, 99999), }); mockCreateCluster(mockCluster).as('createCluster'); cy.visitWithLogin('/kubernetes/create'); @@ -106,68 +96,4 @@ describe('LKE Create Cluster', () => { `/kubernetes/clusters/${mockCluster.id}/summary` ); }); - - it('should not allow creating cluster for restricted users', () => { - // Mock setup for user profile, account user, and user grants with restricted permissions, - // simulating a default user without the ability to add Linodes. - const mockProfile = profileFactory.build({ - restricted: true, - username: randomLabel(), - }); - - const mockUser = accountUserFactory.build({ - restricted: true, - user_type: 'default', - username: mockProfile.username, - }); - - const mockGrants = grantsFactory.build({ - global: { - add_kubernetes: false, - }, - }); - - const mockCluster = kubernetesClusterFactory.build({ - id: randomNumber(10000, 99999), - label: randomLabel(), - }); - - mockGetProfile(mockProfile); - mockGetProfileGrants(mockGrants); - mockGetUser(mockUser); - mockCreateCluster(mockCluster).as('createCluster'); - - cy.visitWithLogin('/kubernetes/create'); - cy.findByText('Add Node Pools').should('be.visible'); - - // Confirm that a notice should be shown informing the user they do not have permission to create a Cluster. - cy.findByText( - "You don't have permissions to create LKE Clusters. Please contact your account administrator to request the necessary permissions." - ).should('be.visible'); - - // Confirm that "Cluster Label" field is disabled. - cy.findByLabelText('Cluster Label') - .should('be.visible') - .should('be.disabled'); - - // Confirm that "Region" field is disabled. - ui.regionSelect.find().should('be.visible').should('be.disabled'); - - // Confirm that "Kubernetes Version" field is disabled. - cy.get('[data-qa-autocomplete="Kubernetes Version"] input') - .should('be.visible') - .should('be.disabled'); - - // Confirm that "HA" field is disabled. - cy.get('[data-testid="ha-radio-button-yes"]').should('be.visible').click(); - cy.get('[data-testid="ha-radio-button-yes"]').should('not.be.checked'); - - cy.get('[data-testid="kube-checkout-bar"]').within(() => { - // Confirm that "Create Cluster" field is disabled. - ui.button - .findByTitle('Create Cluster') - .should('be.visible') - .should('be.disabled'); - }); - }); }); 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 4a914e6eff8..2408d15a7db 100644 --- a/packages/manager/cypress/e2e/core/linodes/backup-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/backup-linode.spec.ts @@ -1,35 +1,34 @@ /* eslint-disable sonarjs/no-duplicate-string */ +import type { Linode } from '@linode/api-v4'; import { - createLinodeRequestFactory, - linodeBackupsFactory, linodeFactory, -} from '@linode/utilities'; -import { accountSettingsFactory } from '@src/factories'; + linodeBackupsFactory, + accountSettingsFactory, + createLinodeRequestFactory, +} from '@src/factories'; import { authenticate } from 'support/api/authentication'; -import { expectManagedDisabled } from 'support/api/managed'; -import { dcPricingMockLinodeTypesForBackups } from 'support/constants/dc-specific-pricing'; -import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; import { mockGetAccountSettings, mockUpdateAccountSettings, } from 'support/intercepts/account'; import { - interceptCancelLinodeBackups, - interceptCreateLinodeSnapshot, - interceptEnableLinodeBackups, - interceptGetLinode, + mockGetLinodes, mockEnableLinodeBackups, mockGetLinodeType, mockGetLinodeTypes, - mockGetLinodes, + interceptEnableLinodeBackups, + interceptGetLinode, + interceptCreateLinodeSnapshot, + interceptCancelLinodeBackups, } from 'support/intercepts/linodes'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; -import { createTestLinode } from 'support/util/linodes'; import { randomLabel } from 'support/util/random'; +import { dcPricingMockLinodeTypesForBackups } from 'support/constants/dc-specific-pricing'; import { chooseRegion } from 'support/util/regions'; - -import type { Linode } from '@linode/api-v4'; +import { expectManagedDisabled } from 'support/api/managed'; +import { createTestLinode } from 'support/util/linodes'; +import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; const BackupsCancellationNote = 'Once backups for this Linode have been canceled, you cannot re-enable them for 24 hours.'; @@ -58,10 +57,10 @@ describe('linode backups', () => { // Create a Linode that is not booted and which does not have backups enabled. const createLinodeRequest = createLinodeRequestFactory.build({ - backups_enabled: false, - booted: false, label: randomLabel(), region: chooseRegion().id, + backups_enabled: false, + booted: false, }); cy.defer( @@ -98,7 +97,6 @@ describe('linode backups', () => { .should('be.visible') .within(() => { // Confirm that user is warned of additional backup charges. - // eslint-disable-next-line sonarjs/slow-regex cy.contains(/.* This will add .* to your monthly bill\./).should( 'be.visible' ); @@ -168,10 +166,10 @@ describe('linode backups', () => { cy.tag('method:e2e'); // Create a Linode that is not booted and which has backups enabled. const createLinodeRequest = createLinodeRequestFactory.build({ - backups_enabled: true, - booted: false, label: randomLabel(), region: chooseRegion().id, + backups_enabled: true, + booted: false, }); const snapshotName = randomLabel(); @@ -329,27 +327,27 @@ describe('"Enable Linode Backups" banner', () => { // // See `dcPricingMockLinodeTypes` exported from `support/constants/dc-specific-pricing.ts`. linodeFactory.build({ - backups: { enabled: false }, label: randomLabel(), region: 'us-ord', + backups: { enabled: false }, type: dcPricingMockLinodeTypesForBackups[0].id, }), linodeFactory.build({ - backups: { enabled: false }, label: randomLabel(), region: 'us-east', + backups: { enabled: false }, type: dcPricingMockLinodeTypesForBackups[1].id, }), linodeFactory.build({ - backups: { enabled: false }, label: randomLabel(), region: 'us-west', + backups: { enabled: false }, type: dcPricingMockLinodeTypesForBackups[2].id, }), linodeFactory.build({ - backups: { enabled: false }, label: randomLabel(), region: 'us-central', + backups: { enabled: false }, type: 'g6-nanode-1', }), ]; 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 5bd831eddaf..e964c7a2046 100644 --- a/packages/manager/cypress/e2e/core/linodes/clone-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/clone-linode.spec.ts @@ -1,50 +1,47 @@ import { createLinodeRequestFactory, - linodeConfigInterfaceFactory, + linodeConfigFactory, + LinodeConfigInterfaceFactory, linodeFactory, -} from '@linode/utilities'; -import { VLANFactory, - linodeConfigFactory, volumeFactory, } from '@src/factories'; -import { authenticate } from 'support/api/authentication'; -import { - dcPricingDocsLabel, - dcPricingDocsUrl, - dcPricingMockLinodeTypes, - dcPricingRegionDifferenceNotice, -} from 'support/constants/dc-specific-pricing'; -import { - LINODE_CLONE_TIMEOUT, - LINODE_CREATE_TIMEOUT, -} from 'support/constants/linodes'; -import { mockGetLinodeConfigs } from 'support/intercepts/configs'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { interceptCloneLinode, - mockCloneLinode, - mockCreateLinode, mockGetLinodeDetails, + mockGetLinodes, mockGetLinodeType, mockGetLinodeTypes, + mockCreateLinode, + mockCloneLinode, mockGetLinodeVolumes, - mockGetLinodes, } from 'support/intercepts/linodes'; +import { linodeCreatePage } from 'support/ui/pages'; import { mockGetVLANs } from 'support/intercepts/vlans'; import { ui } from 'support/ui'; -import { linodeCreatePage } from 'support/ui/pages'; -import { cleanUp } from 'support/util/cleanup'; -import { createTestLinode } from 'support/util/linodes'; import { - randomIp, + dcPricingMockLinodeTypes, + dcPricingRegionDifferenceNotice, + dcPricingDocsLabel, + dcPricingDocsUrl, +} from 'support/constants/dc-specific-pricing'; +import { chooseRegion, getRegionById } from 'support/util/regions'; +import { randomLabel, randomNumber, randomString, + randomIp, } from 'support/util/random'; -import { chooseRegion, getRegionById } from 'support/util/regions'; - +import { authenticate } from 'support/api/authentication'; +import { cleanUp } from 'support/util/cleanup'; +import { createTestLinode } from 'support/util/linodes'; +import { + LINODE_CLONE_TIMEOUT, + LINODE_CREATE_TIMEOUT, +} from 'support/constants/linodes'; import type { Linode } from '@linode/api-v4'; +import { mockGetLinodeConfigs } from 'support/intercepts/configs'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; /** * Returns the Cloud Manager URL to clone a given Linode. @@ -78,9 +75,9 @@ describe('clone linode', () => { cy.tag('method:e2e', 'purpose:dcTesting'); const linodeRegion = chooseRegion({ capabilities: ['Vlans'] }); const linodePayload = createLinodeRequestFactory.build({ - booted: false, label: randomLabel(), region: linodeRegion.id, + booted: false, type: 'g6-nanode-1', }); @@ -159,7 +156,7 @@ describe('clone linode', () => { type: null, }); const mockVolume = volumeFactory.build(); - const mockPublicConfigInterface = linodeConfigInterfaceFactory.build({ + const mockPublicConfigInterface = LinodeConfigInterfaceFactory.build({ ipam_address: null, purpose: 'public', }); @@ -171,17 +168,17 @@ describe('clone linode', () => { ], }); const mockVlan = VLANFactory.build({ - cidr_block: `${randomIp()}/24`, id: randomNumber(), label: randomLabel(), - linodes: [], region: mockLinodeRegion.id, + cidr_block: `${randomIp()}/24`, + linodes: [], }); const linodeNullTypePayload = createLinodeRequestFactory.build({ - booted: false, label: mockLinode.label, region: mockLinodeRegion.id, + booted: false, }); const newLinodeLabel = `${linodeNullTypePayload.label}-clone`; const clonedLinode = { diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-in-core-region.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-in-core-region.spec.ts index df59f00ebe3..cc0b2676087 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-in-core-region.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-in-core-region.spec.ts @@ -1,4 +1,4 @@ -import { linodeFactory, regionFactory } from '@linode/utilities'; +import { linodeFactory, regionFactory } from 'src/factories'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { mockCreateLinode } from 'support/intercepts/linodes'; import { diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-in-distributed-region.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-in-distributed-region.spec.ts index 8f1ccfd6833..9c142dca96c 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-in-distributed-region.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-in-distributed-region.spec.ts @@ -1,8 +1,5 @@ -import { - linodeFactory, - linodeTypeFactory, - regionFactory, -} from '@linode/utilities'; +import { Region } from '@linode/api-v4'; +import { linodeFactory, linodeTypeFactory, regionFactory } from 'src/factories'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { mockCreateLinode, @@ -17,8 +14,6 @@ import { linodeCreatePage } from 'support/ui/pages'; import { randomLabel, randomString } from 'support/util/random'; import { extendRegion } from 'support/util/regions'; -import type { Region } from '@linode/api-v4'; - describe('Create Linode in Distributed Region', () => { /* * - Confirms Linode create flow can be completed with a distributed region @@ -33,9 +28,9 @@ describe('Create Linode in Distributed Region', () => { const mockRegion = extendRegion(regionFactory.build(mockRegionOptions)); const mockLinodeTypes = [ linodeTypeFactory.build({ - class: 'nanode', id: 'nanode-edge-1', label: 'Nanode 1GB', + class: 'nanode', }), ]; const mockLinode = linodeFactory.build({ 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 ffc2939ce95..b456ec593aa 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 @@ -2,13 +2,13 @@ * @file Smoke tests for Linode Create flow across common mobile viewport sizes. */ -import { linodeFactory } from '@linode/utilities'; +import { linodeFactory } from 'src/factories'; import { MOBILE_VIEWPORTS } from 'support/constants/environment'; -import { mockCreateLinode } from 'support/intercepts/linodes'; -import { ui } from 'support/ui'; import { linodeCreatePage } from 'support/ui/pages'; import { randomLabel, randomNumber, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; +import { ui } from 'support/ui'; +import { mockCreateLinode } from 'support/intercepts/linodes'; describe('Linode create mobile smoke', () => { MOBILE_VIEWPORTS.forEach((viewport) => { 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 index 3f7d28c425c..069fc806b06 100644 --- 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 @@ -1,6 +1,6 @@ -import { regionFactory } from '@linode/utilities'; -import { mockGetRegions } from 'support/intercepts/regions'; 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'; @@ -86,6 +86,6 @@ describe('Linode Create Region Select', () => { cy.findByLabelText('Region').should('have.value', 'UK, London (eu-west)'); // Confirm that selecting a valid region updates the Plan Selection panel. - cy.get('[data-testid="table-row-empty"]').should('not.exist'); + 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 b74046af2fc..7d6440ee412 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 @@ -2,11 +2,11 @@ * @file Linode Create view code snippets tests. */ -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { ui } from 'support/ui'; -import { linodeCreatePage } from 'support/ui/pages'; + import { randomLabel, randomString } from 'support/util/random'; -import { chooseRegion } from 'support/util/regions'; +import { linodeCreatePage } from 'support/ui/pages'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; describe('Create Linode flow to validate code snippet modal', () => { beforeEach(() => { @@ -21,15 +21,13 @@ describe('Create Linode flow to validate code snippet modal', () => { it(`view code snippets in create linode flow`, () => { const linodeLabel = randomLabel(); const rootPass = randomString(32); - const mockLinodeRegion = chooseRegion({ - capabilities: ['Linodes'], - }); + cy.visitWithLogin('/linodes/create'); // Set Linode label, distribution, plan type, password, etc. linodeCreatePage.setLabel(linodeLabel); linodeCreatePage.selectImage('Debian 12'); - linodeCreatePage.selectRegionById(mockLinodeRegion.id); + linodeCreatePage.selectRegionById('us-east'); linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); linodeCreatePage.setRootPassword(rootPass); 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 125d3673317..8e87638090f 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,4 +1,4 @@ -import { linodeFactory } from '@linode/utilities'; +import { linodeFactory } from 'src/factories'; import { mockCreateLinode, mockGetLinodeDetails, 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 index 8a7efe8c078..bb2b40b205d 100644 --- 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 @@ -1,18 +1,18 @@ -import { linodeFactory } from '@linode/utilities'; +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, - dcPricingMockLinodeTypes, - dcPricingPlanPlaceholder, } from 'support/constants/dc-specific-pricing'; import { mockCreateLinode, mockGetLinodeType, mockGetLinodeTypes, } from 'support/intercepts/linodes'; -import { ui } from 'support/ui'; -import { randomLabel } from 'support/util/random'; -import { getRegionById } from 'support/util/regions'; describe('Create Linode with DC-specific pricing', () => { /* 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 index 093b89bd120..884fc31471c 100644 --- 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 @@ -1,9 +1,9 @@ import { + accountFactory, linodeFactory, linodeTypeFactory, regionFactory, -} from '@linode/utilities'; -import { accountFactory } from '@src/factories'; +} from '@src/factories'; import { mockGetAccount } from 'support/intercepts/account'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { @@ -28,7 +28,7 @@ import { import type { Region } from '@linode/api-v4'; 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', () => { + it('should not have a "Disk Encryption" section visible if the feature flag is off, user does not have capability, and the selected region does not support LDE', () => { // Mock feature flag -- @TODO LDE: Remove feature flag once LDE is fully rolled out mockAppendFeatureFlags({ linodeDiskEncryption: makeFeatureFlagData(false), @@ -41,10 +41,24 @@ describe('Create Linode with Disk Encryption', () => { mockGetAccount(mockAccount).as('getAccount'); + // Mock regions response + const mockRegion = regionFactory.build({ + capabilities: ['Linodes'], + }); + + const mockRegions = [mockRegion]; + + mockGetRegions(mockRegions); + // intercept request cy.visitWithLogin('/linodes/create'); cy.wait(['@getFeatureFlags', '@getAccount']); + ui.regionSelect.find().click(); + ui.regionSelect + .findItemByRegionLabel(mockRegion.label, mockRegions) + .click(); + // Check if section is visible cy.get(`[data-testid=${headerTestId}]`).should('not.exist'); }); @@ -96,6 +110,54 @@ describe('Create Linode with Disk Encryption', () => { cy.get(`[data-testid="${checkboxTestId}"]`).should('be.enabled'); }); + it('should have a "Disk Encryption" section visible if the feature flag is off, user does not have the capability, but the selected region has the "Disk Encryption" capability', () => { + // Situation where LDE is in GA in the selected region/DC + + // 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'], + }); + + 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']); + + // "Disk Encryption" section should not be visible if a region that does not support LDE is selected + ui.regionSelect.find().click(); + ui.regionSelect + .findItemByRegionLabel(mockRegionWithoutDiskEncryption.label, mockRegions) + .click(); + + cy.get(`[data-testid="${headerTestId}"]`).should('not.exist'); + + // "Disk Encryption" section should be visible if a region that supports LDE is selected + ui.regionSelect.find().click(); + ui.regionSelect + .findItemByRegionLabel(mockRegion.label, mockRegions) + .click(); + + cy.get(`[data-testid="${headerTestId}"]`).should('exist'); + cy.get(`[data-testid="${checkboxTestId}"]`).should('be.enabled'); // "Encrypt Disk" checkbox should be enabled + }); + // Confirm Linode Disk Encryption features when using Distributed Regions. describe('Distributed regions', () => { const encryptionTooltipMessage = @@ -209,7 +271,7 @@ describe('Create Linode with Disk Encryption', () => { const requestPayload = xhr.request.body; const regionId = requestPayload['region']; expect(regionId).to.equal(mockLinode.region); - expect(requestPayload['disk_encryption']).to.equal(undefined); + expect(requestPayload['disk_encryption']).to.be.undefined; }); }); }); 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 35865e33a6e..087001c9125 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 @@ -1,21 +1,23 @@ -import { linodeFactory } from '@linode/utilities'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { - mockCreateFirewall, - mockCreateFirewallError, - mockGetFirewalls, - mockGetTemplate, -} from 'support/intercepts/firewalls'; + linodeFactory, + firewallFactory, + firewallTemplateFactory, +} from 'src/factories'; import { mockCreateLinode, mockGetLinodeDetails, } from 'support/intercepts/linodes'; +import { + mockGetFirewalls, + mockCreateFirewall, + mockGetTemplate, + mockCreateFirewallError, +} from 'support/intercepts/firewalls'; import { ui } from 'support/ui'; import { linodeCreatePage } from 'support/ui/pages'; import { randomLabel, randomNumber, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; - -import { firewallFactory, firewallTemplateFactory } from 'src/factories'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; describe('Create Linode with Firewall', () => { beforeEach(() => { 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 fac8dd00c69..1ad12b3382b 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 @@ -1,13 +1,15 @@ -import { linodeFactory, sshKeyFactory } from '@linode/utilities'; +import { + accountUserFactory, + linodeFactory, + sshKeyFactory, +} from 'src/factories'; +import { randomLabel, randomNumber, randomString } from 'support/util/random'; +import { chooseRegion } from 'support/util/regions'; import { mockGetUser, mockGetUsers } from 'support/intercepts/account'; import { mockCreateLinode } from 'support/intercepts/linodes'; -import { mockCreateSSHKey } from 'support/intercepts/profile'; -import { ui } from 'support/ui'; import { linodeCreatePage } from 'support/ui/pages'; -import { randomLabel, randomNumber, randomString } from 'support/util/random'; -import { chooseRegion } from 'support/util/regions'; - -import { accountUserFactory } from 'src/factories'; +import { ui } from 'support/ui'; +import { mockCreateSSHKey } from 'support/intercepts/profile'; describe('Create Linode with SSH Key', () => { /* @@ -28,8 +30,8 @@ describe('Create Linode with SSH Key', () => { }); const mockUser = accountUserFactory.build({ - ssh_keys: [mockSshKey.label], username: randomLabel(), + ssh_keys: [mockSshKey.label], }); mockGetUsers([mockUser]); @@ -88,8 +90,8 @@ describe('Create Linode with SSH Key', () => { }); const mockUser = accountUserFactory.build({ - ssh_keys: [], username: randomLabel(), + ssh_keys: [], }); const mockUserWithKey = { 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 9fb53cba649..009aa74305d 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,4 @@ -import { linodeFactory, regionFactory } from '@linode/utilities'; +import { imageFactory, linodeFactory, regionFactory } from 'src/factories'; import { mockGetAllImages, mockGetImage } from 'support/intercepts/images'; import { mockCreateLinode, @@ -10,8 +10,6 @@ import { linodeCreatePage } from 'support/ui/pages'; import { randomLabel, randomNumber, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import { imageFactory } from 'src/factories'; - describe('Create Linode with user data', () => { /* * - Confirms UI flow to create a Linode with cloud-init user data specified. @@ -113,15 +111,15 @@ describe('Create Linode with user data', () => { region: linodeRegion.id, }); const mockImage = imageFactory.build({ + id: `linode/${randomLabel()}`, + label: randomLabel(), + created_by: 'linode', + is_public: true, + vendor: 'Debian', // `cloud-init` is omitted from Image capabilities. capabilities: [], - created_by: 'linode', // null eol so that the image is not deprecated eol: null, - id: `linode/${randomLabel()}`, - is_public: true, - label: randomLabel(), - vendor: 'Debian', }); 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 3a3be95d502..12929833fe4 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 @@ -1,19 +1,17 @@ -import { linodeFactory, regionFactory } from '@linode/utilities'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; -import { mockCreateLinode } from 'support/intercepts/linodes'; +import { linodeFactory, regionFactory, VLANFactory } from 'src/factories'; import { mockGetRegions } from 'support/intercepts/regions'; -import { mockGetVLANs } from 'support/intercepts/vlans'; import { ui } from 'support/ui'; import { linodeCreatePage } from 'support/ui/pages'; +import { chooseRegion } from 'support/util/regions'; import { randomIp, randomLabel, randomNumber, randomString, } from 'support/util/random'; -import { chooseRegion } from 'support/util/regions'; - -import { VLANFactory } from 'src/factories'; +import { mockGetVLANs } from 'support/intercepts/vlans'; +import { mockCreateLinode } from 'support/intercepts/linodes'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; describe('Create Linode with VLANs', () => { beforeEach(() => { @@ -38,11 +36,11 @@ describe('Create Linode with VLANs', () => { }); const mockVlan = VLANFactory.build({ - cidr_block: `${randomIp()}/24`, id: randomNumber(), label: randomLabel(), - linodes: [], region: mockLinodeRegion.id, + cidr_block: `${randomIp()}/24`, + linodes: [], }); mockGetVLANs([mockVlan]); @@ -125,11 +123,11 @@ describe('Create Linode with VLANs', () => { }); const mockVlan = VLANFactory.build({ - cidr_block: `${randomIp()}/24`, id: randomNumber(), label: randomLabel(), - linodes: [], region: mockLinodeRegion.id, + cidr_block: `${randomIp()}/24`, + linodes: [], }); mockGetVLANs([]); 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 21165a9d520..7ecdd0a8f67 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 @@ -1,10 +1,12 @@ import { - linodeConfigInterfaceFactoryWithVPC, linodeFactory, regionFactory, -} from '@linode/utilities'; -import { mockGetLinodeConfig } from 'support/intercepts/configs'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; + subnetFactory, + vpcFactory, + linodeConfigFactory, + LinodeConfigInterfaceFactoryWithVPC, +} from 'src/factories'; +import { mockGetLinodeConfigs } from 'support/intercepts/configs'; import { mockCreateLinode, mockGetLinodeDetails, @@ -27,9 +29,8 @@ import { randomString, } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; - -import { linodeConfigFactory, subnetFactory, vpcFactory } from 'src/factories'; import { WARNING_ICON_UNRECOMMENDED_CONFIG } from 'src/features/VPCs/constants'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; describe('Create Linode with VPCs', () => { beforeEach(() => { @@ -48,9 +49,9 @@ describe('Create Linode with VPCs', () => { const mockSubnet = subnetFactory.build({ id: randomNumber(), - ipv4: `${randomIp()}/0`, label: randomLabel(), linodes: [], + ipv4: `${randomIp()}/0`, }); const mockVPC = vpcFactory.build({ @@ -66,11 +67,11 @@ describe('Create Linode with VPCs', () => { region: linodeRegion.id, }); - const mockInterface = linodeConfigInterfaceFactoryWithVPC.build({ - active: true, - primary: true, - subnet_id: mockSubnet.id, + const mockInterface = LinodeConfigInterfaceFactoryWithVPC.build({ vpc_id: mockVPC.id, + subnet_id: mockSubnet.id, + primary: true, + active: true, }); const mockLinodeConfig = linodeConfigFactory.build({ @@ -82,13 +83,7 @@ describe('Create Linode with VPCs', () => { linodes: [ { id: mockLinode.id, - interfaces: [ - { - active: true, - config_id: mockLinodeConfig.id, - id: mockInterface.id, - }, - ], + interfaces: [{ id: mockInterface.id, active: true, config_id: 1 }], }, ], }; @@ -140,7 +135,7 @@ describe('Create Linode with VPCs', () => { // Confirm that request payload includes VPC interface. expect(expectedVpcInterface['vpc_id']).to.equal(mockVPC.id); - expect(expectedVpcInterface['ipv4']).to.deep.equal({}); + expect(expectedVpcInterface['ipv4']).to.be.an('object').that.is.empty; expect(expectedVpcInterface['subnet_id']).to.equal(mockSubnet.id); expect(expectedVpcInterface['purpose']).to.equal('vpc'); // Confirm that VPC interfaces are always marked as the primary interface @@ -156,15 +151,13 @@ describe('Create Linode with VPCs', () => { mockGetVPC(mockVPC).as('getVPC'); mockGetSubnets(mockVPC.id, [mockUpdatedSubnet]).as('getSubnets'); mockGetLinodeDetails(mockLinode.id, mockLinode).as('getLinode'); - mockGetLinodeConfig({ - config: mockLinodeConfig, - configId: mockLinodeConfig.id, - linodeId: mockLinode.id, - }).as('getLinodeConfig'); + mockGetLinodeConfigs(mockLinode.id, [mockLinodeConfig]).as( + 'getLinodeConfigs' + ); cy.visit(`/vpcs/${mockVPC.id}`); cy.findByLabelText(`expand ${mockSubnet.label} row`).click(); - cy.wait('@getLinodeConfig'); + cy.wait('@getLinodeConfigs'); cy.findByTestId(WARNING_ICON_UNRECOMMENDED_CONFIG).should('not.exist'); }); @@ -182,14 +175,14 @@ describe('Create Linode with VPCs', () => { const mockSubnet = subnetFactory.build({ id: randomNumber(), - ipv4: '10.0.0.0/24', label: randomLabel(), linodes: [], + ipv4: '10.0.0.0/24', }); const mockVPC = vpcFactory.build({ - description: randomPhrase(), id: randomNumber(), + description: randomPhrase(), label: randomLabel(), region: linodeRegion.id, subnets: [mockSubnet], @@ -201,11 +194,11 @@ describe('Create Linode with VPCs', () => { region: linodeRegion.id, }); - const mockInterface = linodeConfigInterfaceFactoryWithVPC.build({ - active: true, - primary: true, - subnet_id: mockSubnet.id, + const mockInterface = LinodeConfigInterfaceFactoryWithVPC.build({ vpc_id: mockVPC.id, + subnet_id: mockSubnet.id, + primary: true, + active: true, }); const mockLinodeConfig = linodeConfigFactory.build({ @@ -217,13 +210,7 @@ describe('Create Linode with VPCs', () => { linodes: [ { id: mockLinode.id, - interfaces: [ - { - active: true, - config_id: mockLinodeConfig.id, - id: mockInterface.id, - }, - ], + interfaces: [{ id: mockInterface.id, active: true, config_id: 1 }], }, ], }; @@ -328,15 +315,13 @@ describe('Create Linode with VPCs', () => { mockGetVPC(mockVPC).as('getVPC'); mockGetSubnets(mockVPC.id, [mockUpdatedSubnet]).as('getSubnets'); mockGetLinodeDetails(mockLinode.id, mockLinode).as('getLinode'); - mockGetLinodeConfig({ - config: mockLinodeConfig, - configId: mockLinodeConfig.id, - linodeId: mockLinode.id, - }).as('getLinodeConfig'); + mockGetLinodeConfigs(mockLinode.id, [mockLinodeConfig]).as( + 'getLinodeConfigs' + ); cy.visit(`/vpcs/${mockVPC.id}`); cy.findByLabelText(`expand ${mockSubnet.label} row`).click(); - cy.wait('@getLinodeConfig'); + cy.wait('@getLinodeConfigs'); cy.findByTestId(WARNING_ICON_UNRECOMMENDED_CONFIG).should('not.exist'); }); 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 f5f0cff79a4..30bd5352499 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts @@ -2,40 +2,52 @@ * @file Linode Create end-to-end tests. */ -import { - linodeFactory, - linodeTypeFactory, - profileFactory, - regionFactory, -} from '@linode/utilities'; -import { authenticate } from 'support/api/authentication'; +import { ui } from 'support/ui'; +import { chooseRegion } from 'support/util/regions'; +import { randomLabel, randomString, randomNumber } from 'support/util/random'; import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; -import { mockGetAccount, mockGetUser } from 'support/intercepts/account'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { cleanUp } from 'support/util/cleanup'; +import { linodeCreatePage } from 'support/ui/pages'; +import { authenticate } from 'support/api/authentication'; import { interceptCreateLinode, - mockCreateLinode, mockCreateLinodeError, + mockCreateLinode, + mockGetLinodeDisks, + mockGetLinodeType, mockGetLinodeTypes, + mockGetLinodeVolumes, } from 'support/intercepts/linodes'; -import { - interceptGetProfile, - mockGetProfile, - mockGetProfileGrants, -} from 'support/intercepts/profile'; -import { mockGetRegions } from 'support/intercepts/regions'; -import { ui } from 'support/ui'; -import { linodeCreatePage } from 'support/ui/pages'; -import { cleanUp } from 'support/util/cleanup'; -import { randomLabel, randomNumber, randomString } from 'support/util/random'; -import { chooseRegion } from 'support/util/regions'; -import { skip } from 'support/util/skip'; - +import { interceptGetProfile } from 'support/intercepts/profile'; +import { Region, VLAN, Config, Disk } from '@linode/api-v4'; +import { getRegionById } from 'support/util/regions'; import { accountFactory, accountUserFactory, + profileFactory, grantsFactory, + linodeFactory, + linodeConfigFactory, + linodeTypeFactory, + VLANFactory, + vpcFactory, + subnetFactory, + regionFactory, + LinodeConfigInterfaceFactory, + LinodeConfigInterfaceFactoryWithVPC, } from 'src/factories'; +import { dcPricingMockLinodeTypes } from 'support/constants/dc-specific-pricing'; +import { mockGetAccount } from 'support/intercepts/account'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { mockGetRegions } from 'support/intercepts/regions'; +import { mockGetVLANs } from 'support/intercepts/vlans'; +import { mockGetVPC, mockGetVPCs } from 'support/intercepts/vpc'; +import { mockGetLinodeConfigs } from 'support/intercepts/configs'; +import { + mockGetProfile, + mockGetProfileGrants, +} from 'support/intercepts/profile'; +import { mockGetUser } from 'support/intercepts/account'; let username: string; @@ -59,19 +71,24 @@ describe('Create Linode', () => { describe('By plan type', () => { [ { - planId: 'g6-nanode-1', - planLabel: 'Nanode 1 GB', planType: 'Shared CPU', + planLabel: 'Nanode 1 GB', + planId: 'g6-nanode-1', }, { - planId: 'g6-dedicated-2', - planLabel: 'Dedicated 4 GB', planType: 'Dedicated CPU', + planLabel: 'Dedicated 4 GB', + planId: 'g6-dedicated-2', }, { - planId: 'g7-highmem-1', - planLabel: 'Linode 24 GB', planType: 'High Memory', + planLabel: 'Linode 24 GB', + planId: 'g7-highmem-1', + }, + { + planType: 'Premium CPU', + planLabel: 'Premium 4 GB', + planId: 'g7-premium-2', }, // TODO Include GPU plan types. // TODO Include Accelerated plan types (when they're no longer as restricted) @@ -82,9 +99,8 @@ describe('Create Linode', () => { */ it(`creates a ${planConfig.planType} Linode`, () => { const linodeRegion = chooseRegion({ - capabilities: ['Linodes', 'Vlans'], + capabilities: ['Linodes', 'Premium Plans', 'Vlans'], }); - const linodeLabel = randomLabel(); interceptGetProfile().as('getProfile'); @@ -157,84 +173,6 @@ describe('Create Linode', () => { }); }); }); - - /* - * - Confirms Premium Plan Linode can be created end-to-end. - * - Confirms creation flow, that Linode boots, and that UI reflects status. - */ - it(`creates a Premium CPU Linode`, () => { - cy.tag('env:premiumPlans'); - - // TODO Allow `chooseRegion` to be configured not to throw. - const linodeRegion = (() => { - try { - return chooseRegion({ - capabilities: ['Linodes', 'Premium Plans', 'Vlans'], - }); - } catch { - skip(); - } - return; - })()!; - - const linodeLabel = randomLabel(); - const planId = 'g7-premium-2'; - const planLabel = 'Premium 4 GB'; - const planType = 'Premium CPU'; - - interceptGetProfile().as('getProfile'); - interceptCreateLinode().as('createLinode'); - cy.visitWithLogin('/linodes/create'); - - // Set Linode label, OS, plan type, password, etc. - linodeCreatePage.setLabel(linodeLabel); - linodeCreatePage.selectImage('Debian 12'); - linodeCreatePage.selectRegionById(linodeRegion.id); - linodeCreatePage.selectPlan(planType, planLabel); - linodeCreatePage.setRootPassword(randomString(32)); - - // Confirm information in summary is shown as expected. - cy.get('[data-qa-linode-create-summary]').scrollIntoView(); - cy.get('[data-qa-linode-create-summary]').within(() => { - cy.findByText('Debian 12').should('be.visible'); - cy.findByText(linodeRegion.label).should('be.visible'); - cy.findByText(planLabel).should('be.visible'); - }); - - // Create Linode and confirm it's provisioned as expected. - ui.button - .findByTitle('Create Linode') - .should('be.visible') - .should('be.enabled') - .click(); - - cy.wait('@createLinode').then((xhr) => { - const requestPayload = xhr.request.body; - const responsePayload = xhr.response?.body; - - // Confirm that API request and response contain expected data - expect(requestPayload['label']).to.equal(linodeLabel); - expect(requestPayload['region']).to.equal(linodeRegion.id); - expect(requestPayload['type']).to.equal(planId); - - expect(responsePayload['label']).to.equal(linodeLabel); - expect(responsePayload['region']).to.equal(linodeRegion.id); - expect(responsePayload['type']).to.equal(planId); - - // Confirm that Cloud redirects to details page - cy.url().should('endWith', `/linodes/${responsePayload['id']}`); - }); - - cy.wait('@getProfile').then((xhr) => { - username = xhr.response?.body.username; - }); - - // Confirm toast notification should appear on Linode create. - ui.toast.assertMessage(`Your Linode ${linodeLabel} is being created.`); - cy.findByText('RUNNING', { timeout: LINODE_CREATE_TIMEOUT }).should( - 'be.visible' - ); - }); }); }); @@ -257,9 +195,9 @@ describe('Create Linode', () => { }); const mockAcceleratedType = [ linodeTypeFactory.build({ - class: 'accelerated', id: 'accelerated-1', label: 'accelerated-1', + class: 'accelerated', }), ]; const mockRegions = [ @@ -338,6 +276,181 @@ describe('Create Linode', () => { }); }); + it('adds an SSH key to the linode during create flow', () => { + const rootpass = randomString(32); + const sshPublicKeyLabel = randomLabel(); + const randomKey = randomString(400, { + uppercase: true, + lowercase: true, + numbers: true, + spaces: false, + symbols: false, + }); + const sshPublicKey = `ssh-rsa e2etestkey${randomKey} e2etest@linode`; + 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], + }); + 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'); + + 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'); + + 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}`); + + // Verify VPCs get fetched once a region is selected + cy.wait('@getVPCs'); + + cy.findByText('Shared CPU').click(); + cy.get(`[id="${dcPricingMockLinodeTypes[0].id}"]`).click(); + + // the "VPC" section is present, and the VPC in the same region of + // the linode can be selected. + cy.get('[data-testid="vpc-panel"]') + .should('be.visible') + .within(() => { + cy.contains('Assign this Linode to an existing VPC.').should( + 'be.visible' + ); + // select VPC + cy.findByLabelText('Assign VPC').should('be.visible').focus(); + cy.focused().type(`${mockVPC.label}{downArrow}{enter}`); + // select subnet + cy.findByPlaceholderText('Select Subnet') + .should('be.visible') + .type(`${mockSubnet.label}{downArrow}{enter}`); + }); + + // The drawer opens when clicking "Add an SSH Key" button + ui.button + .findByTitle('Add an SSH Key') + .should('be.visible') + .should('be.enabled') + .click(); + ui.drawer + .findByTitle('Add SSH Key') + .should('be.visible') + .within(() => { + cy.get('[id="label"]').clear(); + cy.focused().type(sshPublicKeyLabel); + + // An alert displays when the format of SSH key is incorrect + cy.get('[id="ssh-public-key"]').clear(); + cy.focused().type('WrongFormatSshKey'); + ui.button + .findByTitle('Add Key') + .should('be.visible') + .should('be.enabled') + .click(); + cy.findAllByText( + 'SSH Key key-type must be ssh-dss, ssh-rsa, ecdsa-sha2-nistp, ssh-ed25519, or sk-ecdsa-sha2-nistp256.' + ).should('be.visible'); + + // Create a new ssh key + cy.get('[id="ssh-public-key"]').clear(); + cy.focused().type(sshPublicKey); + ui.button + .findByTitle('Add Key') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + // When a user creates an SSH key, a toast notification appears that says "Successfully created SSH key." + ui.toast.assertMessage('Successfully created SSH key.'); + + // When a user creates an SSH key, the list of SSH keys for each user updates to show the new key for the signed in user + cy.findByText(sshPublicKeyLabel, { exact: false }).should('be.visible'); + + cy.get('#linode-label').clear(); + cy.focused().type(linodeLabel); + cy.focused().click(); + cy.get('#root-password').type(rootpass); + + ui.button.findByTitle('Create Linode').click(); + + cy.wait('@linodeCreated').its('response.statusCode').should('eq', 200); + cy.findByText(linodeLabel).should('be.visible'); + cy.contains('RUNNING', { timeout: 300000 }).should('be.visible'); + }); + /* * - Confirms error message can show up during Linode create flow. * - Confirms Linode can be created after retry. @@ -422,14 +535,14 @@ describe('Create Linode', () => { // Mock setup for user profile, account user, and user grants with restricted permissions, // simulating a default user without the ability to add Linodes. const mockProfile = profileFactory.build({ - restricted: true, username: randomLabel(), + restricted: true, }); const mockUser = accountUserFactory.build({ + username: mockProfile.username, restricted: true, user_type: 'default', - username: mockProfile.username, }); const mockGrants = grantsFactory.build({ 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 e496a549c68..487e78259f4 100644 --- a/packages/manager/cypress/e2e/core/linodes/linode-config.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/linode-config.spec.ts @@ -1,61 +1,58 @@ -import { - linodeConfigInterfaceFactory, - linodeConfigInterfaceFactoryWithVPC, - linodeFactory, -} from '@linode/utilities'; -import { - VLANFactory, - kernelFactory, - linodeConfigFactory, - subnetFactory, - vpcFactory, -} from '@src/factories'; +import { createTestLinode } from 'support/util/linodes'; +import { ui } from 'support/ui'; import { authenticate } from 'support/api/authentication'; +import { cleanUp } from 'support/util/cleanup'; +import { mockGetVPC, mockGetVPCs } from 'support/intercepts/vpc'; import { dcPricingMockLinodeTypes } from 'support/constants/dc-specific-pricing'; import { LINODE_CLONE_TIMEOUT } from 'support/constants/linodes'; -import { - interceptCreateLinodeConfigs, - interceptDeleteLinodeConfig, - interceptGetLinodeConfigs, - interceptUpdateLinodeConfigs, - mockCreateLinodeConfigs, - mockGetLinodeConfigs, - mockUpdateLinodeConfigs, -} from 'support/intercepts/configs'; +import { chooseRegion, getRegionById } from 'support/util/regions'; +import { mockGetVLANs } from 'support/intercepts/vlans'; import { interceptRebootLinode, mockGetLinodeDetails, mockGetLinodeDisks, - mockGetLinodeFirewalls, - mockGetLinodeKernel, mockGetLinodeKernels, mockGetLinodeVolumes, + mockGetLinodeKernel, } from 'support/intercepts/linodes'; -import { mockGetVLANs } from 'support/intercepts/vlans'; -import { mockGetVPC, mockGetVPCs } from 'support/intercepts/vpc'; -import { ui } from 'support/ui'; -import { cleanUp } from 'support/util/cleanup'; +import { + interceptGetLinodeConfigs, + interceptDeleteLinodeConfig, + interceptCreateLinodeConfigs, + interceptUpdateLinodeConfigs, + mockGetLinodeConfigs, + mockCreateLinodeConfigs, + mockUpdateLinodeConfigs, +} from 'support/intercepts/configs'; +import { fetchLinodeConfigs } from 'support/util/linodes'; +import { + kernelFactory, + vpcFactory, + linodeFactory, + linodeConfigFactory, + VLANFactory, + LinodeConfigInterfaceFactory, + LinodeConfigInterfaceFactoryWithVPC, + subnetFactory, +} from '@src/factories'; +import { randomNumber, randomLabel, randomIp } from 'support/util/random'; import { fetchAllKernels, findKernelById } from 'support/util/kernels'; -import { createTestLinode, fetchLinodeConfigs } from 'support/util/linodes'; -import { randomIp, randomLabel, randomNumber } from 'support/util/random'; -import { chooseRegion, getRegionById } from 'support/util/regions'; - import { LINODE_UNREACHABLE_HELPER_TEXT, NATTED_PUBLIC_IP_HELPER_TEXT, NOT_NATTED_HELPER_TEXT, } from 'src/features/VPCs/constants'; +import type { CreateTestLinodeOptions } from 'support/util/linodes'; import type { Config, CreateLinodeRequest, InterfacePurpose, - Kernel, Linode, - Region, VLAN, + Region, + Kernel, } from '@linode/api-v4'; -import type { CreateTestLinodeOptions } from 'support/util/linodes'; /** * Returns a Promise that resolves to a new test Linode and its first config object. @@ -258,7 +255,7 @@ describe('Linode Config management', () => { () => createLinodeAndGetConfig( { booted: true }, - { securityMethod: 'vlan_no_internet', waitForBoot: true } + { waitForBoot: true, securityMethod: 'vlan_no_internet' } ), 'Creating and booting test Linode' ).then(([linode, config]: [Linode, Config]) => { @@ -316,7 +313,7 @@ describe('Linode Config management', () => { ), createTestLinode( { booted: true }, - { securityMethod: 'vlan_no_internet', waitForBoot: true } + { securityMethod: 'vlan_no_internet' } ), ]); }; @@ -395,8 +392,7 @@ describe('Linode Config management', () => { // Confirm toast message and that UI updates to reflect clone in progress. ui.toast.assertMessage( - `Linode ${sourceLinode.label} has been cloned to ${destLinode.label}.`, - { timeout: LINODE_CLONE_TIMEOUT } + `Linode ${sourceLinode.label} has been cloned to ${destLinode.label}.` ); cy.findByText(/CLONING \(\d+%\)/).should('be.visible'); }); @@ -469,19 +465,19 @@ describe('Linode Config management', () => { // Mock config with public internet for eth0 and VLAN for eth1. const mockConfig: Config = linodeConfigFactory.build({ id: randomNumber(), + label: randomLabel(), + kernel: mockKernel.id, interfaces: [ - linodeConfigInterfaceFactory.build({ + LinodeConfigInterfaceFactory.build({ ipam_address: null, - label: null, purpose: 'public', + label: null, }), - linodeConfigInterfaceFactory.build({ + LinodeConfigInterfaceFactory.build({ label: randomLabel(), purpose: 'vlan', }), ], - kernel: mockKernel.id, - label: randomLabel(), }); const mockVLANs: VLAN[] = VLANFactory.buildList(2); @@ -502,10 +498,10 @@ describe('Linode Config management', () => { const mockConfigWithVpc: Config = { ...mockConfig, interfaces: [ - linodeConfigInterfaceFactoryWithVPC.build({ + LinodeConfigInterfaceFactoryWithVPC.build({ + vpc_id: mockVPC.id, active: false, label: null, - vpc_id: mockVPC.id, }), ], }; @@ -615,10 +611,10 @@ describe('Linode Config management', () => { ...mockConfig, interfaces: [ ...mockConfigInterfaces, - linodeConfigInterfaceFactoryWithVPC.build({ - active: false, + LinodeConfigInterfaceFactoryWithVPC.build({ label: undefined, vpc_id: mockVPC.id, + active: false, }), ], }; @@ -701,9 +697,9 @@ describe('Linode Config management', () => { }); const mockSubnet = subnetFactory.build({ id: randomNumber(), - ipv4: `${randomIp()}/0`, label: randomLabel(), linodes: [], + ipv4: `${randomIp()}/0`, }); const mockVPC = vpcFactory.build({ id: randomNumber(), @@ -716,30 +712,28 @@ describe('Linode Config management', () => { const mockConfigWithVpc: Config = { ...mockConfig, interfaces: [ - linodeConfigInterfaceFactory.build({ + LinodeConfigInterfaceFactory.build({ ipam_address: null, - label: null, purpose: 'public', + label: null, }), - linodeConfigInterfaceFactoryWithVPC.build({ + LinodeConfigInterfaceFactoryWithVPC.build({ + vpc_id: mockVPC.id, active: false, label: null, - vpc_id: mockVPC.id, }), ], }; // Mock a Linode with no existing configs, then visit its details page. mockGetLinodeKernel(mockKernel.id, mockKernel); - mockGetLinodeKernels([mockKernel]).as('getKernels'); + mockGetLinodeKernels([mockKernel]); mockGetLinodeDetails(mockLinode.id, mockLinode).as('getLinode'); mockGetLinodeDisks(mockLinode.id, []).as('getDisks'); mockGetLinodeVolumes(mockLinode.id, []).as('getVolumes'); mockGetLinodeConfigs(mockLinode.id, []).as('getConfigs'); - mockGetLinodeFirewalls(mockLinode.id, []); mockGetVPC(mockVPC).as('getVPC'); mockGetVPCs([mockVPC]).as('getVPCs'); - mockGetVLANs([]).as('getVLANs'); cy.visitWithLogin(`/linodes/${mockLinode.id}/configurations`); cy.wait(['@getConfigs', '@getDisks', '@getLinode', '@getVolumes']); @@ -757,10 +751,8 @@ describe('Linode Config management', () => { 'getLinodeConfigs' ); - // Create new config. Wait for VLAN GET response before interacting with form. + // Create new config. cy.findByText('Add Configuration').click(); - cy.wait('@getVLANs'); - ui.dialog .findByTitle('Add Configuration') .should('be.visible') diff --git a/packages/manager/cypress/e2e/core/linodes/linode-network.spec.ts b/packages/manager/cypress/e2e/core/linodes/linode-network.spec.ts index 5d7d27f4e44..c960beada73 100644 --- a/packages/manager/cypress/e2e/core/linodes/linode-network.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/linode-network.spec.ts @@ -1,33 +1,24 @@ -import { linodeFactory } from '@linode/utilities'; import { - linodeInterfaceFactoryPublic, - linodeInterfaceFactoryVPC, -} from '@linode/utilities'; -import { - firewallDeviceFactory, - firewallFactory, + linodeFactory, ipAddressFactory, - subnetFactory, - vpcFactory, + firewallFactory, + firewallDeviceFactory, } from '@src/factories'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; -import { - mockAddFirewallDevice, - mockGetFirewalls, - mockGetLinodeInterfaceFirewalls, -} from 'support/intercepts/firewalls'; + +import type { IPRange } from '@linode/api-v4'; + import { - mockCreateLinodeInterface, mockGetLinodeDetails, - mockGetLinodeFirewalls, mockGetLinodeIPAddresses, - mockGetLinodeInterfaces, + mockGetLinodeFirewalls, } from 'support/intercepts/linodes'; import { mockUpdateIPAddress } from 'support/intercepts/networking'; -import { mockGetVPCs } from 'support/intercepts/vpc'; import { ui } from 'support/ui'; - -import type { IPRange } from '@linode/api-v4'; +import { + mockAddFirewallDevice, + mockGetFirewalls, +} from 'support/intercepts/firewalls'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; describe('IP Addresses', () => { const mockLinode = linodeFactory.build(); @@ -62,10 +53,10 @@ describe('IP Addresses', () => { mockGetLinodeFirewalls(mockLinode.id, []).as('getLinodeFirewalls'); mockGetLinodeIPAddresses(mockLinode.id, { ipv4: { - private: [], public: [ipAddress], - reserved: [], + private: [], shared: [], + reserved: [], }, ipv6: { global: [_ipv6Range], @@ -83,7 +74,7 @@ describe('IP Addresses', () => { * - Confirms the success toast message after editing RDNS */ it('checks for the toast message upon editing an RDNS', () => { - cy.findByLabelText('Linode IP Addresses') + cy.findByLabelText('IPv4 Addresses') .should('be.visible') .within(() => { // confirm table headers @@ -99,7 +90,7 @@ describe('IP Addresses', () => { .should('be.visible') .closest('tr') .within(() => { - cy.findByText('Public – IPv4').should('be.visible'); + cy.findByText('IPv4 – Public').should('be.visible'); cy.findByText(mockRDNS).should('be.visible'); // open up the edit RDNS drawer @@ -136,7 +127,7 @@ describe('IP Addresses', () => { .should('be.visible') .closest('tr') .within(() => { - cy.findByText('Public – IPv4').should('be.visible'); + cy.findByText('IPv4 – Public').should('be.visible'); ui.actionMenu .findByTitle(`Action menu for IP Address ${linodeIPv4}`) .should('be.visible'); @@ -147,7 +138,7 @@ describe('IP Addresses', () => { .should('be.visible') .closest('tr') .within(() => { - cy.findByText('Range – IPv6').should('be.visible'); + cy.findByText('IPv6 – Range').should('be.visible'); ui.actionMenu .findByTitle(`Action menu for IP Address ${_ipv6Range.range}`) .should('be.visible'); @@ -245,168 +236,3 @@ describe('Firewalls', () => { .should('be.disabled'); }); }); - -describe('Linode Interfaces', () => { - beforeEach(() => { - mockAppendFeatureFlags({ - linodeInterfaces: { enabled: true }, - }); - }); - - it('allows the user to add a public network interface with a firewall', () => { - const linode = linodeFactory.build({ interface_generation: 'linode' }); - const firewalls = firewallFactory.buildList(3); - const linodeInterface = linodeInterfaceFactoryPublic.build(); - - const selectedFirewall = firewalls[1]; - - mockGetLinodeDetails(linode.id, linode).as('getLinode'); - mockGetLinodeInterfaces(linode.id, { interfaces: [] }).as('getInterfaces'); - mockGetFirewalls(firewalls).as('getFirewalls'); - mockCreateLinodeInterface(linode.id, linodeInterface).as('createInterface'); - mockGetLinodeInterfaceFirewalls(linode.id, linodeInterface.id, [ - selectedFirewall, - ]).as('getInterfaceFirewalls'); - - cy.visitWithLogin(`/linodes/${linode.id}/networking`); - - cy.wait(['@getLinode', '@getInterfaces']); - - ui.button.findByTitle('Add Network Interface').scrollIntoView().click(); - - ui.drawer.findByTitle('Add Network Interface').within(() => { - // Verify firewalls fetch - cy.wait('@getFirewalls'); - - // Try submitting the form - ui.button.findByAttribute('type', 'submit').should('be.enabled').click(); - - // Verify a validation error shows - cy.findByText('You must selected an Interface type.').should( - 'be.visible' - ); - - // Select the public interface type - cy.findByLabelText('Public').click(); - - // Verify a validation error goes away - cy.findByText('You must selected an Interface type.').should('not.exist'); - - // Select a Firewall - ui.autocomplete.findByLabel('Firewall').click(); - ui.autocompletePopper.findByTitle(selectedFirewall.label).click(); - - mockGetLinodeInterfaces(linode.id, { interfaces: [linodeInterface] }); - - ui.button.findByAttribute('type', 'submit').should('be.enabled').click(); - }); - - cy.wait('@createInterface').then((xhr) => { - const requestPayload = xhr.request.body; - - // Confirm that request payload includes a Public interface only - expect(requestPayload['public']).to.be.an('object'); - expect(requestPayload['vpc']).to.equal(null); - expect(requestPayload['vlan']).to.equal(null); - }); - - ui.toast.assertMessage('Successfully added network interface.'); - - // Verify the interface row shows upon creation - cy.findByText(linodeInterface.mac_address) - .closest('tr') - .within(() => { - // Verify we fetch the interfaces firewalls and the label shows - cy.wait('@getInterfaceFirewalls'); - cy.findByText(selectedFirewall.label).should('be.visible'); - - // Verify the interface type shows - cy.findByText('Public').should('be.visible'); - }); - }); - - it('allows the user to add a VPC network interface with a firewall', () => { - const linode = linodeFactory.build({ interface_generation: 'linode' }); - const firewalls = firewallFactory.buildList(3); - const subnets = subnetFactory.buildList(3); - const vpcs = vpcFactory.buildList(3, { subnets }); - const linodeInterface = linodeInterfaceFactoryVPC.build(); - - const selectedFirewall = firewalls[1]; - const selectedVPC = vpcs[1]; - const selectedSubnet = selectedVPC.subnets[0]; - - mockGetLinodeDetails(linode.id, linode).as('getLinode'); - mockGetLinodeInterfaces(linode.id, { interfaces: [] }).as('getInterfaces'); - mockGetFirewalls(firewalls).as('getFirewalls'); - mockGetVPCs(vpcs).as('getVPCs'); - mockCreateLinodeInterface(linode.id, linodeInterface).as('createInterface'); - mockGetLinodeInterfaceFirewalls(linode.id, linodeInterface.id, [ - selectedFirewall, - ]).as('getInterfaceFirewalls'); - - cy.visitWithLogin(`/linodes/${linode.id}/networking`); - - cy.wait(['@getLinode', '@getInterfaces']); - - ui.button.findByTitle('Add Network Interface').scrollIntoView().click(); - - ui.drawer.findByTitle('Add Network Interface').within(() => { - // Verify firewalls fetch - cy.wait('@getFirewalls'); - - cy.findByLabelText('VPC').click(); - - // Verify VPCs fetch - cy.wait('@getVPCs'); - - // Select a VPC - ui.autocomplete.findByLabel('VPC').click(); - ui.autocompletePopper.findByTitle(selectedVPC.label).click(); - - // Select a Firewall - ui.autocomplete.findByLabel('Firewall').click(); - ui.autocompletePopper.findByTitle(selectedFirewall.label).click(); - - // Submit the form - ui.button.findByAttribute('type', 'submit').should('be.enabled').click(); - - // Verify an error shows because a subnet is not selected - cy.findByText('Subnet is required.').should('be.visible'); - - // Select a Subnet - ui.autocomplete.findByLabel('Subnet').click(); - ui.autocompletePopper.findByTitle(selectedSubnet.label).click(); - - // Verify the error goes away - cy.findByText('Subnet is required.').should('not.exist'); - - mockGetLinodeInterfaces(linode.id, { interfaces: [linodeInterface] }); - - ui.button.findByAttribute('type', 'submit').should('be.enabled').click(); - }); - - cy.wait('@createInterface').then((xhr) => { - const requestPayload = xhr.request.body; - - // Confirm that request payload includes VPC interface only - expect(requestPayload['public']).to.equal(null); - expect(requestPayload['vpc']['subnet_id']).to.equal(selectedSubnet.id); - expect(requestPayload['vlan']).to.equal(null); - }); - - ui.toast.assertMessage('Successfully added network interface.'); - - // Verify the interface row shows upon creation - cy.findByText(linodeInterface.mac_address) - .closest('tr') - .within(() => { - // Verify we fetch the interfaces firewalls and the label shows - cy.wait('@getInterfaceFirewalls'); - cy.findByText(selectedFirewall.label).should('be.visible'); - - // Verify the interface type shows - cy.findByText('VPC').should('be.visible'); - }); - }); -}); 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 e6ec268d9c0..2a2d971c0cc 100644 --- a/packages/manager/cypress/e2e/core/linodes/linode-storage.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/linode-storage.spec.ts @@ -1,16 +1,15 @@ /* eslint-disable sonarjs/no-duplicate-string */ -import { authenticate } from 'support/api/authentication'; import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; +import { Linode } from '@linode/api-v4'; +import { authenticate } from 'support/api/authentication'; +import { createTestLinode } from 'support/util/linodes'; +import { ui } from 'support/ui'; +import { cleanUp } from 'support/util/cleanup'; import { - interceptAddDisks, interceptDeleteDisks, + interceptAddDisks, interceptResizeDisks, } from 'support/intercepts/linodes'; -import { ui } from 'support/ui'; -import { cleanUp } from 'support/util/cleanup'; -import { createTestLinode } from 'support/util/linodes'; - -import type { Linode } from '@linode/api-v4'; /** * Waits for a Linode to finish provisioning by checking the details page status indicator. diff --git a/packages/manager/cypress/e2e/core/linodes/migrate-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/migrate-linode.spec.ts index a9c45bc260f..9e00b502363 100644 --- a/packages/manager/cypress/e2e/core/linodes/migrate-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/migrate-linode.spec.ts @@ -1,21 +1,21 @@ -import { linodeFactory } from '@linode/utilities'; -import { linodeDiskFactory } from '@src/factories'; import { authenticate } from 'support/api/authentication'; import { - dcPricingCurrentPriceLabel, - dcPricingMockLinodeTypes, - dcPricingNewPriceLabel, -} from 'support/constants/dc-specific-pricing'; -import { - mockGetLinodeDetails, mockGetLinodeDisks, - mockGetLinodeType, mockGetLinodeVolumes, mockMigrateLinode, } from 'support/intercepts/linodes'; import { ui } from 'support/ui'; import { apiMatcher } from 'support/util/intercepts'; +import { linodeFactory } from '@src/factories'; +import { mockGetLinodeDetails } from 'support/intercepts/linodes'; import { getRegionById } from 'support/util/regions'; +import { + dcPricingMockLinodeTypes, + dcPricingCurrentPriceLabel, + dcPricingNewPriceLabel, +} from 'support/constants/dc-specific-pricing'; +import { mockGetLinodeType } from 'support/intercepts/linodes'; +import { linodeDiskFactory } from '@src/factories'; authenticate(); describe('Migrate linodes', () => { diff --git a/packages/manager/cypress/e2e/core/linodes/plan-selection.spec.ts b/packages/manager/cypress/e2e/core/linodes/plan-selection.spec.ts index 07ef3c82d45..30ecd433839 100644 --- a/packages/manager/cypress/e2e/core/linodes/plan-selection.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/plan-selection.spec.ts @@ -1,20 +1,20 @@ // TODO: Cypress +// Move this to cypress component testing once the setup is complete - see https://github.com/linode/manager/pull/10134 +import { ui } from 'support/ui'; import { + accountFactory, linodeTypeFactory, - regionAvailabilityFactory, regionFactory, -} from '@linode/utilities'; -import { accountFactory } from '@src/factories'; + regionAvailabilityFactory, +} from '@src/factories'; import { authenticate } from 'support/api/authentication'; -import { mockGetAccount } from 'support/intercepts/account'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; -import { mockGetLinodeTypes } from 'support/intercepts/linodes'; import { - mockGetRegionAvailability, mockGetRegions, + mockGetRegionAvailability, } from 'support/intercepts/regions'; -// Move this to cypress component testing once the setup is complete - see https://github.com/linode/manager/pull/10134 -import { ui } from 'support/ui'; +import { mockGetLinodeTypes } from 'support/intercepts/linodes'; +import { mockGetAccount } from 'support/intercepts/account'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; const mockRegions = [ regionFactory.build({ @@ -26,71 +26,71 @@ const mockRegions = [ const mockDedicatedLinodeTypes = [ linodeTypeFactory.build({ - class: 'dedicated', id: 'dedicated-1', label: 'dedicated-1', + class: 'dedicated', }), linodeTypeFactory.build({ - class: 'dedicated', id: 'dedicated-2', label: 'dedicated-2', + class: 'dedicated', }), linodeTypeFactory.build({ - class: 'dedicated', id: 'dedicated-3', label: 'dedicated-3', + class: 'dedicated', }), linodeTypeFactory.build({ - class: 'dedicated', id: 'dedicated-4', label: 'dedicated-4', + class: 'dedicated', }), ]; const mockSharedLinodeTypes = [ linodeTypeFactory.build({ - class: 'standard', id: 'shared-1', label: 'shared-1', + class: 'standard', }), linodeTypeFactory.build({ - class: 'standard', id: 'shared-2', label: 'shared-2', + class: 'standard', }), linodeTypeFactory.build({ - class: 'standard', id: 'shared-3', label: 'shared-3', + class: 'standard', }), ]; const mockHighMemoryLinodeTypes = [ linodeTypeFactory.build({ - class: 'highmem', id: 'highmem-1', label: 'highmem-1', + class: 'highmem', }), ]; const mockGPUType = [ linodeTypeFactory.build({ - class: 'gpu', id: 'gpu-1', label: 'gpu-1', + class: 'gpu', }), linodeTypeFactory.build({ - class: 'gpu', id: 'gpu-2', label: 'gpu-2 Ada', + class: 'gpu', }), ]; const mockAcceleratedType = [ linodeTypeFactory.build({ - class: 'accelerated', id: 'accelerated-1', label: 'accelerated-1', + class: 'accelerated', }), ]; @@ -104,23 +104,23 @@ const mockLinodeTypes = [ const mockRegionAvailability = [ regionAvailabilityFactory.build({ - available: false, plan: 'dedicated-3', + available: false, region: 'us-east', }), regionAvailabilityFactory.build({ - available: false, plan: 'dedicated-4', + available: false, region: 'us-east', }), regionAvailabilityFactory.build({ - available: false, plan: 'highmem-1', + available: false, region: 'us-east', }), regionAvailabilityFactory.build({ - available: false, plan: 'shared-3', + available: false, region: 'us-east', }), ]; @@ -370,9 +370,9 @@ describe('displays specific linode plans for GPU', () => { ); mockAppendFeatureFlags({ gpuv2: { - egressBanner: true, - planDivider: true, transferBanner: true, + planDivider: true, + egressBanner: true, }, }).as('getFeatureFlags'); }); @@ -418,9 +418,9 @@ describe('displays specific kubernetes plans for GPU', () => { ); mockAppendFeatureFlags({ gpuv2: { - egressBanner: true, - planDivider: true, transferBanner: true, + planDivider: true, + egressBanner: true, }, }).as('getFeatureFlags'); }); 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 fb20dcb2627..568dfffba1f 100644 --- a/packages/manager/cypress/e2e/core/linodes/rebuild-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/rebuild-linode.spec.ts @@ -1,31 +1,22 @@ +import { CreateLinodeRequest, Linode } from '@linode/api-v4'; +import { ui } from 'support/ui'; +import { randomString, randomLabel } from 'support/util/random'; +import { authenticate } from 'support/api/authentication'; import { createStackScript } from '@linode/api-v4/lib'; import { - createLinodeRequestFactory, - linodeFactory, - regionFactory, -} from '@linode/utilities'; -import { imageFactory } from '@src/factories'; -import { authenticate } from 'support/api/authentication'; -import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; -import { mockGetAllImages, mockGetImage } from 'support/intercepts/images'; + interceptGetStackScript, + interceptGetStackScripts, +} from 'support/intercepts/stackscripts'; +import { createLinodeRequestFactory, linodeFactory } from '@src/factories'; +import { cleanUp } from 'support/util/cleanup'; +import { chooseRegion } from 'support/util/regions'; import { interceptRebuildLinode, mockGetLinodeDetails, - mockRebuildLinode, mockRebuildLinodeError, } from 'support/intercepts/linodes'; -import { mockGetRegions } from 'support/intercepts/regions'; -import { - interceptGetStackScript, - interceptGetStackScripts, -} from 'support/intercepts/stackscripts'; -import { ui } from 'support/ui'; -import { cleanUp } from 'support/util/cleanup'; import { createTestLinode } from 'support/util/linodes'; -import { randomLabel, randomString } from 'support/util/random'; -import { chooseRegion } from 'support/util/regions'; - -import type { CreateLinodeRequest, Linode } from '@linode/api-v4'; +import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; /** * Creates a Linode and StackScript. @@ -82,7 +73,7 @@ const findRebuildDialog = (linodeLabel: string) => { */ const assertPasswordComplexity = ( desiredPassword: string, - passwordStrength: 'Fair' | 'Good' | 'Weak' + passwordStrength: 'Weak' | 'Fair' | 'Good' ) => { cy.findByLabelText('Root Password').should('be.visible').clear(); cy.focused().type(desiredPassword); @@ -103,7 +94,6 @@ const submitRebuild = () => { }; // Error message that is displayed when desired password is not strong enough. -// eslint-disable-next-line sonarjs/no-hardcoded-passwords const passwordComplexityError = 'Password does not meet strength requirement.'; authenticate(); @@ -177,7 +167,7 @@ describe('rebuild linode', () => { * - Confirms that a Linode can be rebuilt using a Community StackScript. */ it('rebuilds a linode from Community StackScript', () => { - cy.tag('method:e2e', 'env:stackScripts'); + cy.tag('method:e2e'); const stackScriptId = 443929; const stackScriptName = 'OpenLiteSpeed-WordPress'; const image = 'AlmaLinux 9'; @@ -248,22 +238,22 @@ describe('rebuild linode', () => { // Create a StackScript to rebuild a Linode. const linodeRequest = createLinodeRequestFactory.build({ - image: 'linode/alpine3.18', label: randomLabel(), - region, + region: region, + image: 'linode/alpine3.18', root_pass: randomString(16), }); const stackScriptRequest = { - deployments_active: 0, - deployments_total: 0, + label: randomLabel(), description: randomString(), + ordinal: 0, + logo_url: '', images: ['linode/alpine3.18'], + deployments_total: 0, + deployments_active: 0, is_public: false, - label: randomLabel(), - logo_url: '', mine: true, - ordinal: 0, rev_note: '', script: '#!/bin/bash\n\necho "Hello, world!"', user_defined_fields: [], @@ -349,69 +339,4 @@ describe('rebuild linode', () => { cy.findByText(mockErrorMessage); }); }); - - it('can rebuild a Linode reusing existing user data', () => { - const region = regionFactory.build({ capabilities: ['Metadata'] }); - const linode = linodeFactory.build({ - region: region.id, - // has_user_data: true - add this when we add the type to make this test more realistic - }); - const image = imageFactory.build({ - capabilities: ['cloud-init'], - is_public: true, - }); - - mockRebuildLinode(linode.id, linode).as('rebuildLinode'); - mockGetLinodeDetails(linode.id, linode).as('getLinode'); - mockGetRegions([region]); - mockGetAllImages([image]); - mockGetImage(image.id, image); - - cy.visitWithLogin(`/linodes/${linode.id}?rebuild=true`); - - findRebuildDialog(linode.label).within(() => { - // Select an Image - ui.autocomplete.findByLabel('Image').should('be.visible').click(); - ui.autocompletePopper - .findByTitle(image.label, { exact: false }) - .should('be.visible') - .click(); - - // Type a root password - assertPasswordComplexity(rootPassword, 'Good'); - - // Open the User Data accordion - ui.accordionHeading.findByTitle('Add User Data').scrollIntoView().click(); - - // Verify the reuse checkbox is not checked by default and check it - cy.findByLabelText( - `Reuse user data previously provided for ${linode.label}` - ) - .should('not.be.checked') - .click(); - - // Verify the checkbox becomes checked - cy.findByLabelText( - `Reuse user data previously provided for ${linode.label}` - ).should('be.checked'); - - // Type to confirm - cy.findByLabelText('Linode Label').should('be.visible').click(); - cy.focused().type(linode.label); - - submitRebuild(); - }); - - cy.wait('@rebuildLinode').then((xhr) => { - // Confirm that metadata is NOT in the payload. - // If we omit metadata from the payload, the API will reuse previously provided userdata. - expect(xhr.request.body.metadata).to.equal(undefined); - - // Verify other expected values are in the request - expect(xhr.request.body.image).to.equal(image.id); - expect(xhr.request.body.root_pass).to.be.a('string'); - }); - - ui.toast.assertMessage('Linode rebuild started.'); - }); }); 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 7d3cb5354f9..636dc15a9c9 100644 --- a/packages/manager/cypress/e2e/core/linodes/rescue-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/rescue-linode.spec.ts @@ -1,6 +1,6 @@ -import { createLinodeRequestFactory, linodeFactory } from '@linode/utilities'; +import type { Linode } from '@linode/api-v4'; +import { createLinodeRequestFactory, linodeFactory } from '@src/factories'; import { authenticate } from 'support/api/authentication'; -import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; import { interceptGetLinodeDetails, interceptRebootLinodeIntoRescueMode, @@ -11,12 +11,11 @@ import { } from 'support/intercepts/linodes'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; +import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; import { createTestLinode } from 'support/util/linodes'; import { randomLabel } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import type { Linode } from '@linode/api-v4'; - // Submits the Rescue Linode dialog, initiating reboot into rescue mode. const rebootInRescueMode = () => { ui.button 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 c70b301fd7e..5f7486eb352 100644 --- a/packages/manager/cypress/e2e/core/linodes/resize-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/resize-linode.spec.ts @@ -1,9 +1,9 @@ -import { authenticate } from 'support/api/authentication'; -import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; -import { interceptLinodeResize } from 'support/intercepts/linodes'; +import { createTestLinode } from 'support/util/linodes'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; -import { createTestLinode } from 'support/util/linodes'; +import { authenticate } from 'support/api/authentication'; +import { interceptLinodeResize } from 'support/intercepts/linodes'; +import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; authenticate(); describe('resize linode', () => { diff --git a/packages/manager/cypress/e2e/core/linodes/search-linodes.spec.ts b/packages/manager/cypress/e2e/core/linodes/search-linodes.spec.ts index 265617b56e1..5fa1ff3dda9 100644 --- a/packages/manager/cypress/e2e/core/linodes/search-linodes.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/search-linodes.spec.ts @@ -1,8 +1,7 @@ -import { authenticate } from 'support/api/authentication'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; +import { authenticate } from 'support/api/authentication'; import { createTestLinode } from 'support/util/linodes'; - import type { Linode } from '@linode/api-v4'; authenticate(); 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 61ecc709e97..bb1310c840c 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 @@ -1,14 +1,13 @@ -import { createLinodeRequestFactory } from '@linode/utilities'; -import { accountSettingsFactory } from '@src/factories/accountSettings'; import { authenticate } from 'support/api/authentication'; -import { mockGetAccountSettings } from 'support/intercepts/account'; -import { interceptDeleteLinode } from 'support/intercepts/linodes'; +import { createTestLinode } from 'support/util/linodes'; +import { createLinodeRequestFactory } from '@src/factories/linodes'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; -import { createTestLinode } from 'support/util/linodes'; +import { Linode } from '@linode/api-v4'; +import { accountSettingsFactory } from '@src/factories/accountSettings'; import { randomLabel } from 'support/util/random'; - -import type { Linode } from '@linode/api-v4'; +import { interceptDeleteLinode } from 'support/intercepts/linodes'; +import { mockGetAccountSettings } from 'support/intercepts/account'; const confirmDeletion = (linodeLabel: string) => { cy.url().should('endWith', '/linodes'); @@ -47,14 +46,14 @@ const deleteLinodeFromActionMenu = (linodeLabel: string) => { }; const preferenceOverrides = { - desktop_sidebar_open: false, - linodes_group_by_tag: false, linodes_view_style: 'list', + linodes_group_by_tag: false, + volumes_group_by_tag: false, + desktop_sidebar_open: false, sortKeys: { 'linodes-landing': { order: 'asc', orderBy: 'label' }, volume: { order: 'asc', orderBy: 'label' }, }, - volumes_group_by_tag: false, }; authenticate(); diff --git a/packages/manager/cypress/e2e/core/linodes/smoke-linode-landing-table.spec.ts b/packages/manager/cypress/e2e/core/linodes/smoke-linode-landing-table.spec.ts index 8dcfd160e40..8826771ff3d 100644 --- a/packages/manager/cypress/e2e/core/linodes/smoke-linode-landing-table.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/smoke-linode-landing-table.spec.ts @@ -1,45 +1,41 @@ /* eslint-disable sonarjs/no-duplicate-string */ -import { - linodeFactory, - profileFactory, - userPreferencesFactory, -} from '@linode/utilities'; +import { Linode } from '@linode/api-v4'; import { accountSettingsFactory } from '@src/factories/accountSettings'; -import { accountUserFactory } from '@src/factories/accountUsers'; -import { grantsFactory } from '@src/factories/grants'; +import { linodeFactory } from '@src/factories/linodes'; import { makeResourcePage } from '@src/mocks/serverHandlers'; +import { ui } from 'support/ui'; +import { routes } from 'support/ui/constants'; +import { apiMatcher } from 'support/util/intercepts'; +import { chooseRegion, getRegionById } from 'support/util/regions'; import { authenticate } from 'support/api/authentication'; -import { mockGetUser } from 'support/intercepts/account'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { - mockGetLinodeFirewalls, mockGetLinodes, + mockGetLinodeFirewalls, } from 'support/intercepts/linodes'; +import { userPreferencesFactory, profileFactory } from '@src/factories'; +import { accountUserFactory } from '@src/factories/accountUsers'; +import { grantsFactory } from '@src/factories/grants'; +import { mockGetUser } from 'support/intercepts/account'; import { - mockGetProfile, - mockGetProfileGrants, mockGetUserPreferences, mockUpdateUserPreferences, + mockGetProfile, + mockGetProfileGrants, } from 'support/intercepts/profile'; -import { ui } from 'support/ui'; -import { routes } from 'support/ui/constants'; +import { randomLabel } from 'support/util/random'; import * as commonLocators from 'support/ui/locators/common-locators'; import * as linodeLocators from 'support/ui/locators/linode-locators'; -import { apiMatcher } from 'support/util/intercepts'; -import { randomLabel } from 'support/util/random'; -import { chooseRegion, getRegionById } from 'support/util/regions'; - -import type { Linode } from '@linode/api-v4'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; -const mockLinodes = new Array(5) - .fill(null) - .map((_item: null, index: number): Linode => { +const mockLinodes = new Array(5).fill(null).map( + (_item: null, index: number): Linode => { return linodeFactory.build({ label: `Linode ${index}`, region: chooseRegion().id, tags: [index % 2 == 0 ? 'even' : 'odd', 'nums'], }); - }); + } +); const mockLinodesData = makeResourcePage(mockLinodes); @@ -56,14 +52,14 @@ const linodeLabel = (index: number) => { }; const preferenceOverrides = { - desktop_sidebar_open: false, - linodes_group_by_tag: false, linodes_view_style: 'list', + linodes_group_by_tag: false, + volumes_group_by_tag: false, + desktop_sidebar_open: false, sortKeys: { 'linodes-landing': { order: 'asc', orderBy: 'label' }, volume: { order: 'asc', orderBy: 'label' }, }, - volumes_group_by_tag: false, }; authenticate(); @@ -77,7 +73,7 @@ describe('linode landing checks', () => { req.reply(mockAccountSettings); }).as('getAccountSettings'); cy.intercept('GET', apiMatcher('profile')).as('getProfile'); - cy.intercept('GET', apiMatcher('linode/instances*'), (req) => { + cy.intercept('GET', apiMatcher('linode/instances/*'), (req) => { req.reply(mockLinodesData); }).as('getLinodes'); cy.visitWithLogin('/', { preferenceOverrides }); @@ -118,10 +114,7 @@ describe('linode landing checks', () => { ); ui.mainSearch.find().should('be.visible'); - cy.findByTestId('top-menu-help-and-support') - .should('be.visible') - .should('be.enabled') - .click(); + cy.findByLabelText('Help & Support').should('be.enabled').click(); cy.url().should('endWith', '/support'); cy.go('back'); @@ -496,7 +489,7 @@ describe('linode landing checks for empty state', () => { .should('be.visible') .should('have.text', 'Cloud-based virtual machines'); - // Assert that recommended section is visible - Getting Started Guides, Deploy an App and Video Playlist + //Assert that recommended section is visible - Getting Started Guides, Deploy an App and Video Playlist cy.get('@resourcesSection') .contains('h2', 'Getting Started Guides') .should('be.visible'); @@ -529,14 +522,14 @@ describe('linode landing checks for empty state', () => { // Mock setup for user profile, account user, and user grants with restricted permissions, // simulating a default user without the ability to add Linodes. const mockProfile = profileFactory.build({ - restricted: true, username: randomLabel(), + restricted: true, }); const mockUser = accountUserFactory.build({ + username: mockProfile.username, restricted: true, user_type: 'default', - username: mockProfile.username, }); const mockGrants = grantsFactory.build({ @@ -573,15 +566,15 @@ describe('linode landing checks for empty state', () => { describe('linode landing checks for non-empty state with restricted user', () => { beforeEach(() => { // Mock setup to display the Linode landing page in an non-empty state - const mockLinodes: Linode[] = new Array(1) - .fill(null) - .map((_item: null, index: number): Linode => { + const mockLinodes: Linode[] = new Array(1).fill(null).map( + (_item: null, index: number): Linode => { return linodeFactory.build({ label: `Linode ${index}`, region: chooseRegion().id, tags: [index % 2 == 0 ? 'even' : 'odd', 'nums'], }); - }); + } + ); mockGetLinodes(mockLinodes).as('getLinodes'); @@ -593,8 +586,8 @@ describe('linode landing checks for non-empty state with restricted user', () => // Mock setup for user profile, account user, and user grants with restricted permissions, // simulating a default user without the ability to add Linodes. const mockProfile = profileFactory.build({ - restricted: true, username: randomLabel(), + restricted: true, }); const mockGrants = grantsFactory.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 ae678525720..173a1fe4ec3 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 @@ -1,9 +1,8 @@ +import { ui } from 'support/ui'; import { authenticate } from 'support/api/authentication'; import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; -import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; import { createTestLinode } from 'support/util/linodes'; - import type { Linode } from '@linode/api-v4'; authenticate(); 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 82e3eba1a0e..64f64742ab0 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 @@ -1,8 +1,8 @@ -import { authenticate } from 'support/api/authentication'; -import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; +import { createTestLinode } from 'support/util/linodes'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; -import { createTestLinode } from 'support/util/linodes'; +import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; +import { authenticate } from 'support/api/authentication'; import { randomLabel } from 'support/util/random'; authenticate(); diff --git a/packages/manager/cypress/e2e/core/longview/longview-plan.spec.ts b/packages/manager/cypress/e2e/core/longview/longview-plan.spec.ts index 84977989f93..def2a6a8b67 100644 --- a/packages/manager/cypress/e2e/core/longview/longview-plan.spec.ts +++ b/packages/manager/cypress/e2e/core/longview/longview-plan.spec.ts @@ -1,3 +1,5 @@ +import type { ActiveLongviewPlan } from '@linode/api-v4'; +import { longviewActivePlanFactory } from 'src/factories'; import { authenticate } from 'support/api/authentication'; import { mockGetLongviewPlan, @@ -6,10 +8,6 @@ import { import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; -import { longviewActivePlanFactory } from 'src/factories'; - -import type { ActiveLongviewPlan } from '@linode/api-v4'; - authenticate(); describe('longview plan', () => { before(() => { diff --git a/packages/manager/cypress/e2e/core/longview/longview.spec.ts b/packages/manager/cypress/e2e/core/longview/longview.spec.ts index e14eac42563..78db13b4ed6 100644 --- a/packages/manager/cypress/e2e/core/longview/longview.spec.ts +++ b/packages/manager/cypress/e2e/core/longview/longview.spec.ts @@ -1,32 +1,30 @@ +import type { LongviewClient } from '@linode/api-v4'; import { DateTime } from 'luxon'; +import { + longviewResponseFactory, + longviewClientFactory, + longviewAppsFactory, + longviewLatestStatsFactory, + longviewPackageFactory, +} from 'src/factories'; import { authenticate } from 'support/api/authentication'; import { - longviewAddClientButtonText, - longviewEmptyStateMessage, longviewStatusTimeout, + longviewEmptyStateMessage, + longviewAddClientButtonText, } from 'support/constants/longview'; import { interceptFetchLongviewStatus, + mockGetLongviewClients, + mockFetchLongviewStatus, mockCreateLongviewClient, mockDeleteLongviewClient, - mockFetchLongviewStatus, - mockGetLongviewClients, mockUpdateLongviewClient, } from 'support/intercepts/longview'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; import { randomLabel } from 'support/util/random'; -import { - longviewAppsFactory, - longviewClientFactory, - longviewLatestStatsFactory, - longviewPackageFactory, - longviewResponseFactory, -} from 'src/factories'; - -import type { LongviewClient } from '@linode/api-v4'; - /** * Returns the command used to install Longview which is shown in Cloud's UI. * diff --git a/packages/manager/cypress/e2e/core/managed/managed-contacts.spec.ts b/packages/manager/cypress/e2e/core/managed/managed-contacts.spec.ts index 95ad21d6feb..63d853cf8a8 100644 --- a/packages/manager/cypress/e2e/core/managed/managed-contacts.spec.ts +++ b/packages/manager/cypress/e2e/core/managed/managed-contacts.spec.ts @@ -2,17 +2,16 @@ * @file Integration tests for Managed contacts. */ +import { contactFactory } from 'src/factories/managed'; import { visitUrlWithManagedEnabled } from 'support/api/managed'; import { - mockCreateContact, - mockDeleteContact, mockGetContacts, + mockCreateContact, mockUpdateContact, + mockDeleteContact, } from 'support/intercepts/managed'; import { ui } from 'support/ui'; -import { randomPhoneNumber, randomString } from 'support/util/random'; - -import { contactFactory } from 'src/factories/managed'; +import { randomString, randomPhoneNumber } from 'support/util/random'; // Message that's shown when there are no Managed contacts. const noContactsMessage = "You don't have any Contacts on your account."; @@ -26,9 +25,9 @@ describe('Managed Contacts tab', () => { const contactIds = [1, 2, 3, 4, 5]; const contacts = contactIds.map((id) => { return contactFactory.build({ - email: `contact-email-${id}@example.com`, - id, name: `Managed Contact ${id}`, + email: `contact-email-${id}@example.com`, + id: id, }); }); @@ -60,9 +59,9 @@ describe('Managed Contacts tab', () => { const contactPrimaryPhone = randomPhoneNumber(); const contactEmail = `${contactName}@example.com`; const contact = contactFactory.build({ - email: contactEmail, id: contactId, name: contactName, + email: contactEmail, phone: { primary: contactPrimaryPhone, secondary: null, @@ -130,9 +129,9 @@ describe('Managed Contacts tab', () => { const contactNewPrimaryPhone = randomPhoneNumber(); const contact = contactFactory.build({ - email: contactOldEmail, id: contactId, name: contactOldName, + email: contactOldEmail, phone: { primary: contactOldPrimaryPhone, }, @@ -140,8 +139,8 @@ describe('Managed Contacts tab', () => { const updatedContact = { ...contact, - email: contactNewEmail, name: contactNewName, + email: contactNewEmail, phone: { ...contact.phone, primary: contactNewPrimaryPhone, diff --git a/packages/manager/cypress/e2e/core/managed/managed-credentials.spec.ts b/packages/manager/cypress/e2e/core/managed/managed-credentials.spec.ts index d0a068557d0..9db0fe50925 100644 --- a/packages/manager/cypress/e2e/core/managed/managed-credentials.spec.ts +++ b/packages/manager/cypress/e2e/core/managed/managed-credentials.spec.ts @@ -2,6 +2,7 @@ * @file Integration tests for Managed credentials. */ +import { credentialFactory } from 'src/factories/managed'; import { visitUrlWithManagedEnabled } from 'support/api/managed'; import { mockCreateCredential, @@ -13,8 +14,6 @@ import { import { ui } from 'support/ui'; import { randomLabel, randomString } from 'support/util/random'; -import { credentialFactory } from 'src/factories/managed'; - // Message that's shown when there are no Managed credentials. const noCredentialsMessage = "You don't have any Credentials on your account."; diff --git a/packages/manager/cypress/e2e/core/managed/managed-monitors.spec.ts b/packages/manager/cypress/e2e/core/managed/managed-monitors.spec.ts index 10149790052..33227ae887a 100644 --- a/packages/manager/cypress/e2e/core/managed/managed-monitors.spec.ts +++ b/packages/manager/cypress/e2e/core/managed/managed-monitors.spec.ts @@ -2,6 +2,7 @@ * @file Integration tests for Managed monitors. */ +import { monitorFactory } from 'src/factories/managed'; import { visitUrlWithManagedEnabled } from 'support/api/managed'; import { mockCreateServiceMonitor, @@ -14,8 +15,6 @@ import { import { ui } from 'support/ui'; import { randomLabel } from 'support/util/random'; -import { monitorFactory } from 'src/factories/managed'; - // Message that's shown when no Managed service monitors are set up. const noMonitorsMessage = "You don't have any Monitors on your account."; @@ -43,9 +42,9 @@ describe('Managed Monitors tab', () => { // Confirm that each monitor is listed and shows the correct status. [ - { expectedStatus: 'Verified', label: 'OK Test Monitor' }, - { expectedStatus: 'Pending', label: 'Pending Test Monitor' }, - { expectedStatus: 'Failed', label: 'Problem Test Monitor' }, + { label: 'OK Test Monitor', expectedStatus: 'Verified' }, + { label: 'Pending Test Monitor', expectedStatus: 'Pending' }, + { label: 'Problem Test Monitor', expectedStatus: 'Failed' }, ].forEach((monitorInfo) => { cy.findByText(monitorInfo.label) .should('be.visible') @@ -68,11 +67,11 @@ describe('Managed Monitors tab', () => { const monitorMenuLabel = 'Action menu for Monitor New Monitor'; const originalMonitor = monitorFactory.build({ - body: '200', - credentials: [], id: monitorId, + body: '200', label: originalLabel, status: 'ok', + credentials: [], }); const newMonitor = { @@ -158,8 +157,8 @@ describe('Managed Monitors tab', () => { const monitorLabel = randomLabel(); const monitorUrl = 'https://www.example.com'; const newMonitor = monitorFactory.build({ - address: monitorUrl, label: monitorLabel, + address: monitorUrl, }); mockGetServiceMonitors([]).as('getMonitors'); @@ -217,9 +216,9 @@ describe('Managed Monitors tab', () => { const monitorMenuLabel = `Action menu for Monitor ${monitorLabel}`; const originalMonitor = monitorFactory.build({ - address: monitorUrl, id: monitorId, label: monitorLabel, + address: monitorUrl, status: 'ok', }); diff --git a/packages/manager/cypress/e2e/core/managed/managed-navigation.spec.ts b/packages/manager/cypress/e2e/core/managed/managed-navigation.spec.ts index 07ea0214e64..cc6f2ecefaa 100644 --- a/packages/manager/cypress/e2e/core/managed/managed-navigation.spec.ts +++ b/packages/manager/cypress/e2e/core/managed/managed-navigation.spec.ts @@ -2,13 +2,13 @@ * @file Integration tests for Managed navigation. */ -import { userPreferencesFactory } from '@linode/utilities'; import { - managedAccount, - nonManagedAccount, - visitUrlWithManagedDisabled, - visitUrlWithManagedEnabled, -} from 'support/api/managed'; + contactFactory, + credentialFactory, + managedIssueFactory, + monitorFactory, +} from 'src/factories/managed'; +import { userPreferencesFactory } from 'src/factories/profile'; import { mockGetAccountSettings } from 'support/intercepts/account'; import { mockGetContacts, @@ -20,14 +20,12 @@ import { } from 'support/intercepts/managed'; import { mockGetUserPreferences } from 'support/intercepts/profile'; import { ui } from 'support/ui'; - import { - contactFactory, - credentialFactory, - managedIssueFactory, - monitorFactory, -} from 'src/factories/managed'; - + managedAccount, + nonManagedAccount, + visitUrlWithManagedDisabled, + visitUrlWithManagedEnabled, +} from 'support/api/managed'; import type { UserPreferences } from '@linode/api-v4'; // Array of URLs to all Managed-related pages. diff --git a/packages/manager/cypress/e2e/core/managed/managed-ssh.spec.ts b/packages/manager/cypress/e2e/core/managed/managed-ssh.spec.ts index 2cf18f42d39..dab96aaae79 100644 --- a/packages/manager/cypress/e2e/core/managed/managed-ssh.spec.ts +++ b/packages/manager/cypress/e2e/core/managed/managed-ssh.spec.ts @@ -2,6 +2,11 @@ * @file Integration tests for Managed SSH access. */ +import type { ManagedLinodeSetting } from '@linode/api-v4'; +import { + managedLinodeSettingFactory, + managedSSHSettingFactory, +} from 'src/factories/managed'; import { visitUrlWithManagedEnabled } from 'support/api/managed'; import { mockGetLinodeSettings, @@ -16,13 +21,6 @@ import { randomString, } from 'support/util/random'; -import { - managedLinodeSettingFactory, - managedSSHSettingFactory, -} from 'src/factories/managed'; - -import type { ManagedLinodeSetting } from '@linode/api-v4'; - // Message that is shown when no Linodes are listed. const noLinodesMessage = "You don't have any Linodes on your account."; @@ -33,11 +31,11 @@ const noLinodesMessage = "You don't have any Linodes on your account."; */ const randomPublicSshKey = (): string => { const randomKey = randomString(400, { + uppercase: true, lowercase: true, numbers: true, spaces: false, symbols: false, - uppercase: true, }); return `ssh-rsa e2etestkey${randomKey} managedservices@linode`; @@ -98,9 +96,9 @@ describe('Managed SSH Access tab', () => { ...originalLinodeSettings, ssh: { ...originalLinodeSettings.ssh, - ip: 'any', - port: newPort, user: newUser, + port: newPort, + ip: 'any', }, }; diff --git a/packages/manager/cypress/e2e/core/managed/managed-summary.spec.ts b/packages/manager/cypress/e2e/core/managed/managed-summary.spec.ts index fa9293e9cac..de7870de378 100644 --- a/packages/manager/cypress/e2e/core/managed/managed-summary.spec.ts +++ b/packages/manager/cypress/e2e/core/managed/managed-summary.spec.ts @@ -1,11 +1,10 @@ -import { visitUrlWithManagedEnabled } from 'support/api/managed'; import { mockGetIssues, mockGetServiceMonitors, mockGetStats, } from 'support/intercepts/managed'; - import { managedIssueFactory, monitorFactory } from 'src/factories/managed'; +import { visitUrlWithManagedEnabled } from 'support/api/managed'; describe('Managed Summary tab', () => { /** diff --git a/packages/manager/cypress/e2e/core/nodebalancers/nodebalancer-settings.spec.ts b/packages/manager/cypress/e2e/core/nodebalancers/nodebalancer-settings.spec.ts index 5090e7004eb..3356f5b72f7 100644 --- a/packages/manager/cypress/e2e/core/nodebalancers/nodebalancer-settings.spec.ts +++ b/packages/manager/cypress/e2e/core/nodebalancers/nodebalancer-settings.spec.ts @@ -1,4 +1,8 @@ -import { nodeBalancerFactory } from '@linode/utilities'; +import { + firewallDeviceFactory, + firewallFactory, + nodeBalancerFactory, +} from 'src/factories'; import { mockAddFirewallDevice, mockGetFirewalls, @@ -9,8 +13,6 @@ import { } from 'support/intercepts/nodebalancers'; import { ui } from 'support/ui'; -import { firewallDeviceFactory, firewallFactory } from 'src/factories'; - describe('Firewalls', () => { it('allows the user to assign a Firewall from the NodeBalancer settings page', () => { const nodebalancer = nodeBalancerFactory.build(); 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 index dd797581165..f5e22d67fe4 100644 --- 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 @@ -1,14 +1,13 @@ -import { nodeBalancerFactory } from '@linode/utilities'; -import { authenticate } from 'support/api/authentication'; import { entityTag } from 'support/constants/cypress'; -import { interceptCreateNodeBalancer } from 'support/intercepts/nodebalancers'; -import { ui } from 'support/ui'; -import { cleanUp } from 'support/util/cleanup'; import { createTestLinode } from 'support/util/linodes'; import { randomLabel } from 'support/util/random'; import { chooseRegion } 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', () => { @@ -23,14 +22,14 @@ describe('create NodeBalancer to test the submission of multiple nodes and multi it('creates a NodeBalancer with multiple Backend Nodes', () => { const region = chooseRegion(); const linodePayload = { + region: region.id, // NodeBalancers require Linodes with private IPs. private_ip: true, - region: region.id, }; const linodePayload_2 = { - private_ip: true, region: region.id, + private_ip: true, }; const createTestLinodes = async () => { @@ -43,15 +42,15 @@ describe('create NodeBalancer to test the submission of multiple nodes and multi cy.defer(createTestLinodes, 'Creating 2 test Linodes').then( ([linode, linode2]: [Linode, Linode]) => { const nodeBal = nodeBalancerFactory.build({ - ipv4: linode.ipv4[1], label: randomLabel(), region: region.id, + ipv4: linode.ipv4[1], }); const nodeBal_2 = nodeBalancerFactory.build({ - ipv4: linode2.ipv4[1], label: randomLabel(), region: region.id, + ipv4: linode2.ipv4[1], }); interceptCreateNodeBalancer().as('createNodeBalancer'); @@ -115,14 +114,14 @@ describe('create NodeBalancer to test the submission of multiple nodes and multi it('creates a NodeBalancer with an additional config', () => { const region = chooseRegion(); const linodePayload = { + region: region.id, // NodeBalancers require Linodes with private IPs. private_ip: true, - region: region.id, }; const linodePayload_2 = { - private_ip: true, region: region.id, + private_ip: true, }; const createTestLinodes = async () => { @@ -135,15 +134,15 @@ describe('create NodeBalancer to test the submission of multiple nodes and multi cy.defer(createTestLinodes, 'Creating 2 test Linodes').then( ([linode, linode2]: [Linode, Linode]) => { const nodeBal = nodeBalancerFactory.build({ - ipv4: linode.ipv4[1], label: randomLabel(), region: region.id, + ipv4: linode.ipv4[1], }); const nodeBal_2 = nodeBalancerFactory.build({ - ipv4: linode2.ipv4[1], label: randomLabel(), region: region.id, + ipv4: linode2.ipv4[1], }); interceptCreateNodeBalancer().as('createNodeBalancer'); @@ -205,9 +204,9 @@ describe('create NodeBalancer to test the submission of multiple nodes and multi it('displays errors during adding new config', () => { const region = chooseRegion(); const linodePayload = { + region: region.id, // NodeBalancers require Linodes with private IPs. private_ip: true, - region: region.id, }; cy.defer( @@ -215,9 +214,9 @@ describe('create NodeBalancer to test the submission of multiple nodes and multi 'Creating test Linode' ).then((linode: Linode) => { const nodeBal = nodeBalancerFactory.build({ - ipv4: linode.ipv4[1], label: randomLabel(), region: region.id, + ipv4: linode.ipv4[1], }); cy.visitWithLogin('/nodebalancers/create'); diff --git a/packages/manager/cypress/e2e/core/nodebalancers/nodebalancers-empty-landing-page.spec.ts b/packages/manager/cypress/e2e/core/nodebalancers/nodebalancers-empty-landing-page.spec.ts index 15ebe82c551..b93401cab6c 100644 --- a/packages/manager/cypress/e2e/core/nodebalancers/nodebalancers-empty-landing-page.spec.ts +++ b/packages/manager/cypress/e2e/core/nodebalancers/nodebalancers-empty-landing-page.spec.ts @@ -1,5 +1,5 @@ -import { mockGetNodeBalancers } from 'support/intercepts/nodebalancers'; import { ui } from 'support/ui'; +import { mockGetNodeBalancers } from 'support/intercepts/nodebalancers'; describe('NodeBalancers empty landing page', () => { /** diff --git a/packages/manager/cypress/e2e/core/nodebalancers/smoke-create-nodebal.spec.ts b/packages/manager/cypress/e2e/core/nodebalancers/smoke-create-nodebal.spec.ts index 2cd99a49615..9022ab6b9f7 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 @@ -1,14 +1,15 @@ -import { authenticate } from 'support/api/authentication'; 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 { dcPricingDocsLabel, dcPricingDocsUrl, } from 'support/constants/dc-specific-pricing'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; -import { createTestLinode } from 'support/util/linodes'; -import { randomLabel } from 'support/util/random'; -import { chooseRegion, getRegionById } from 'support/util/regions'; +import { authenticate } from 'support/api/authentication'; import type { NodeBalancer } from '@linode/api-v4'; @@ -20,10 +21,10 @@ import { linodeFactory, nodeBalancerFactory, regionFactory, -} from '@linode/utilities'; -import { mockGetLinodes } from 'support/intercepts/linodes'; +} from 'src/factories'; import { interceptCreateNodeBalancer } from 'support/intercepts/nodebalancers'; import { mockGetRegions } from 'support/intercepts/regions'; +import { mockGetLinodes } from 'support/intercepts/linodes'; const createNodeBalancerWithUI = ( nodeBal: NodeBalancer, @@ -90,16 +91,16 @@ describe('create NodeBalancer', () => { it('creates a NodeBalancer in a region with base pricing', () => { const region = chooseRegion(); const linodePayload = { + region: region.id, // NodeBalancers require Linodes with private IPs. private_ip: true, - region: region.id, }; cy.defer(() => createTestLinode(linodePayload)).then((linode) => { const nodeBal = nodeBalancerFactory.build({ - ipv4: linode.ipv4[1], label: randomLabel(), region: region.id, + ipv4: linode.ipv4[1], }); // catch request interceptCreateNodeBalancer().as('createNodeBalancer'); @@ -173,15 +174,15 @@ describe('create NodeBalancer', () => { it('shows DC-specific pricing information when creating a NodeBalancer', () => { const initialRegion = getRegionById('us-west'); const linodePayload = { + region: initialRegion.id, // NodeBalancers require Linodes with private IPs. private_ip: true, - region: initialRegion.id, }; cy.defer(() => createTestLinode(linodePayload)).then((linode) => { const nodeBal = nodeBalancerFactory.build({ - ipv4: linode.ipv4[1], label: randomLabel(), region: initialRegion.id, + ipv4: linode.ipv4[1], }); // catch request 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 4bcf0b1f6ed..47de9225d8a 100644 --- a/packages/manager/cypress/e2e/core/notificationsAndEvents/events-fetching.spec.ts +++ b/packages/manager/cypress/e2e/core/notificationsAndEvents/events-fetching.spec.ts @@ -2,13 +2,11 @@ * @file Integration tests for Cloud Manager's events fetching and polling behavior. */ -import { DateTime } from 'luxon'; import { mockGetEvents, mockGetEventsPolling } from 'support/intercepts/events'; -import { mockGetVolumes } from 'support/intercepts/volumes'; -import { randomNumber } from 'support/util/random'; - +import { DateTime } from 'luxon'; import { eventFactory } from 'src/factories'; - +import { randomNumber } from 'support/util/random'; +import { mockGetVolumes } from 'support/intercepts/volumes'; import type { Interception } from 'support/cypress-exports'; describe('Event fetching and polling', () => { @@ -54,15 +52,15 @@ describe('Event fetching and polling', () => { */ 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, - id: randomNumber(10000, 99999), - percent_complete: null, rate: null, + percent_complete: null, }); mockGetEvents([mockEvent]).as('getEvents'); @@ -111,13 +109,13 @@ describe('Event fetching and polling', () => { .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, - id: randomNumber(10000, 99999), - percent_complete: null, rate: null, + percent_complete: null, }); mockGetEvents([mockEvent]).as('getEventsInitialFetches'); @@ -166,22 +164,22 @@ describe('Event fetching and polling', () => { .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, - id: randomNumber(10000, 99999), - percent_complete: null, rate: null, + percent_complete: null, }); const mockEventInProgress = eventFactory.build({ + id: randomNumber(10000, 99999), created: DateTime.now().minus({ minutes: 6 }).toISO(), duration: 0, - id: randomNumber(10000, 99999), - percent_complete: 50, rate: null, + percent_complete: 50, }); const mockEvents = [mockEventBasic, mockEventInProgress]; diff --git a/packages/manager/cypress/e2e/core/notificationsAndEvents/events-menu.spec.ts b/packages/manager/cypress/e2e/core/notificationsAndEvents/events-menu.spec.ts index 6f6239455fb..909218b1afd 100644 --- a/packages/manager/cypress/e2e/core/notificationsAndEvents/events-menu.spec.ts +++ b/packages/manager/cypress/e2e/core/notificationsAndEvents/events-menu.spec.ts @@ -2,14 +2,13 @@ * @file Integration tests for Cloud Manager's events menu. */ -import { DateTime } from 'luxon'; 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'; -import { eventFactory } from 'src/factories'; - describe('Notifications Menu', () => { /* * - Confirms that the notification menu shows all events when 20 or fewer exist. @@ -20,17 +19,17 @@ describe('Notifications Menu', () => { 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}`, }, - percent_complete: null, - rate: null, - seen: false, - status: 'scheduled', username: randomLabel(), }); }); @@ -69,17 +68,17 @@ describe('Notifications Menu', () => { 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}`, }, - percent_complete: null, - rate: null, - seen: false, - status: 'scheduled', username: randomLabel(), }); }); @@ -149,22 +148,22 @@ describe('Notifications Menu', () => { 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}`, }, - // The event with the highest ID is expected to come first in the array. - id: 5000 - index, - percent_complete: null, - rate: null, - seen: false, - status: 'scheduled', username: randomLabel(), }); }); @@ -248,23 +247,23 @@ describe('Notifications 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, - // To simulate multiple events occurring simultaneously, give all + 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}`, }, - // Events are not guaranteed to be ordered by ID; simulate this by using random IDs. - id: randomNumber(1000, 9999), - percent_complete: null, - rate: null, - seen: false, - status: 'scheduled', username: randomLabel(), }); }); diff --git a/packages/manager/cypress/e2e/core/notificationsAndEvents/events.spec.ts b/packages/manager/cypress/e2e/core/notificationsAndEvents/events.spec.ts index 7cdaa021cc8..852d60c297e 100644 --- a/packages/manager/cypress/e2e/core/notificationsAndEvents/events.spec.ts +++ b/packages/manager/cypress/e2e/core/notificationsAndEvents/events.spec.ts @@ -1,17 +1,17 @@ -import { EventActionKeys } from '@linode/api-v4'; import { eventFactory } from '@src/factories/events'; import { mockGetEvents } from 'support/intercepts/events'; +import { EventActionKeys } from '@linode/api-v4'; import type { Event } from '@linode/api-v4'; const events: Event[] = EventActionKeys.map((action) => { return eventFactory.build({ action, - entity: { id: 0, label: 'linode-0' }, message: `${action + ' message'}`, - percent_complete: null, - read: false, seen: false, + read: false, + percent_complete: null, + entity: { id: 0, label: 'linode-0' }, }); }); diff --git a/packages/manager/cypress/e2e/core/notificationsAndEvents/notifications.spec.ts b/packages/manager/cypress/e2e/core/notificationsAndEvents/notifications.spec.ts index c7d270fede2..bf3ce8cb83f 100644 --- a/packages/manager/cypress/e2e/core/notificationsAndEvents/notifications.spec.ts +++ b/packages/manager/cypress/e2e/core/notificationsAndEvents/notifications.spec.ts @@ -1,21 +1,20 @@ +import { Notification } from '@linode/api-v4'; import { notificationFactory } from '@src/factories/notification'; import { mockGetNotifications } from 'support/intercepts/events'; -import type { Notification } from '@linode/api-v4'; - const notifications: Notification[] = [ notificationFactory.build({ - severity: 'critical', type: 'migration_scheduled', + severity: 'critical', }), - notificationFactory.build({ severity: 'major', type: 'migration_pending' }), - notificationFactory.build({ severity: 'minor', type: 'reboot_scheduled' }), - notificationFactory.build({ severity: 'critical', type: 'outage' }), - notificationFactory.build({ severity: 'minor', type: 'ticket_important' }), - notificationFactory.build({ severity: 'critical', type: 'ticket_abuse' }), - notificationFactory.build({ severity: 'major', type: 'notice' }), - notificationFactory.build({ severity: 'minor', type: 'maintenance' }), - notificationFactory.build({ severity: 'critical', type: 'promotion' }), + notificationFactory.build({ type: 'migration_pending', severity: 'major' }), + notificationFactory.build({ type: 'reboot_scheduled', severity: 'minor' }), + notificationFactory.build({ type: 'outage', severity: 'critical' }), + notificationFactory.build({ type: 'ticket_important', severity: 'minor' }), + notificationFactory.build({ type: 'ticket_abuse', severity: 'critical' }), + notificationFactory.build({ type: 'notice', severity: 'major' }), + notificationFactory.build({ type: 'maintenance', severity: 'minor' }), + notificationFactory.build({ type: 'promotion', severity: 'critical' }), ]; describe('verify notification types and icons', () => { 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 dcabea27aba..5f851b1f7cc 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 @@ -2,20 +2,19 @@ * @file End-to-end tests for Object Storage Access Key operations. */ -import { createBucket } from '@linode/api-v4/lib/object-storage'; +import { createObjectStorageBucketFactoryLegacy } from 'src/factories/objectStorage'; import { authenticate } from 'support/api/authentication'; -import { mockGetAccount } from 'support/intercepts/account'; +import { createBucket } from '@linode/api-v4/lib/object-storage'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { - interceptCreateAccessKey, interceptGetAccessKeys, + interceptCreateAccessKey, } from 'support/intercepts/object-storage'; +import { randomLabel } from 'support/util/random'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; -import { randomLabel } from 'support/util/random'; - +import { mockGetAccount } from 'support/intercepts/account'; import { accountFactory } from 'src/factories'; -import { createObjectStorageBucketFactoryLegacy } from 'src/factories/objectStorage'; authenticate(); describe('object storage access key end-to-end tests', () => { @@ -122,8 +121,8 @@ describe('object storage access key end-to-end tests', () => { const bucketLabel = randomLabel(); const bucketCluster = 'us-east-1'; const bucketRequest = createObjectStorageBucketFactoryLegacy.build({ - cluster: bucketCluster, label: bucketLabel, + cluster: bucketCluster, // Default factory sets `cluster` and `region`, but API does not accept `region` yet. region: undefined, }); 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 7b628c96e03..6f0710a2aa3 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 @@ -2,18 +2,17 @@ * @file Smoke tests for crucial Object Storage Access Keys operations. */ -import { mockGetAccount } from 'support/intercepts/account'; +import { objectStorageKeyFactory } from 'src/factories/objectStorage'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { mockCreateAccessKey, mockDeleteAccessKey, mockGetAccessKeys, } from 'support/intercepts/object-storage'; -import { ui } from 'support/ui'; import { randomLabel, randomNumber, randomString } from 'support/util/random'; - +import { ui } from 'support/ui'; import { accountFactory } from 'src/factories'; -import { objectStorageKeyFactory } from 'src/factories/objectStorage'; +import { mockGetAccount } from 'support/intercepts/account'; describe('object storage access keys smoke tests', () => { /* @@ -24,8 +23,8 @@ describe('object storage access keys smoke tests', () => { */ it('can create access key - smoke', () => { const mockAccessKey = objectStorageKeyFactory.build({ - access_key: randomString(20), label: randomLabel(), + access_key: randomString(20), secret_key: randomString(39), }); @@ -98,9 +97,9 @@ describe('object storage access keys smoke tests', () => { */ it('can revoke access key - smoke', () => { const accessKey = objectStorageKeyFactory.build({ - access_key: randomString(20), - id: randomNumber(1, 99999), label: randomLabel(), + id: randomNumber(1, 99999), + access_key: randomString(20), secret_key: randomString(39), }); 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 5a4c4b4ace3..7772498eabc 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 @@ -1,50 +1,50 @@ /** * @file Cypress integration tests for OBJ enrollment and cancellation. */ -import { profileFactory, regionFactory } from '@linode/utilities'; + +import type { + AccountSettings, + ObjectStorageCluster, + ObjectStorageClusterID, + Region, +} from '@linode/api-v4'; import { - accountFactory, accountSettingsFactory, objectStorageClusterFactory, + profileFactory, + regionFactory, objectStorageKeyFactory, + accountFactory, } from '@src/factories'; import { mockGetAccount, mockGetAccountSettings, } from 'support/intercepts/account'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { mockCancelObjectStorage, mockCreateAccessKey, - mockGetAccessKeys, mockGetBuckets, mockGetClusters, } from 'support/intercepts/object-storage'; import { mockGetProfile } from 'support/intercepts/profile'; -import { mockGetRegions } from 'support/intercepts/regions'; import { ui } from 'support/ui'; import { randomLabel } from 'support/util/random'; - -import type { - AccountSettings, - ObjectStorageCluster, - ObjectStorageClusterID, - Region, -} from '@linode/api-v4'; +import { mockGetRegions } from 'support/intercepts/regions'; +import { mockGetAccessKeys } from 'support/intercepts/object-storage'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; // Various messages, notes, and warnings that may be shown when enabling Object Storage // under different circumstances. const objNotes = { - // Information regarding the Object Storage cancellation process. - cancellationExplanation: - /To discontinue billing, you.*ll need to cancel Object Storage in your Account Settings./, + // When enabling OBJ, in both the Access Key flow and Create Bucket flow, when OBJ DC-specific pricing is enabled. + objDCPricing: + 'Object Storage costs a flat rate of $5/month, and includes 250 GB of storage. When you enable Object Storage, 1 TB of outbound data transfer will be added to your global network transfer pool.', // Link to further DC-specific pricing information. dcPricingLearnMoreNote: 'Learn more about pricing and specifications.', - // When enabling OBJ, in both the Access Key flow and Create Bucket flow, when OBJ DC-specific pricing is enabled. - objDCPricing: - 'Object Storage costs a flat rate of $5/month, and includes 250 GB of storage. When you enable Object Storage, 1 TB of outbound data transfer will be added to your global network transfer pool.', + // Information regarding the Object Storage cancellation process. + cancellationExplanation: /To discontinue billing, you.*ll need to cancel Object Storage in your Account Settings./, }; describe('Object Storage enrollment', () => { @@ -75,18 +75,18 @@ describe('Object Storage enrollment', () => { const mockRegions: Region[] = [ regionFactory.build({ capabilities: ['Object Storage'], - id: 'us-east', label: 'Newark, NJ', + id: 'us-east', }), regionFactory.build({ capabilities: ['Object Storage'], - id: 'br-gru', label: 'Sao Paulo, BR', + id: 'br-gru', }), regionFactory.build({ capabilities: ['Object Storage'], - id: 'id-cgk', label: 'Jakarta, ID', + id: 'id-cgk', }), ]; @@ -145,7 +145,7 @@ describe('Object Storage enrollment', () => { .findByTitle('Create Bucket') .should('be.visible') .within(() => { - cy.findByLabelText('Bucket Name (required)') + cy.findByLabelText('Label (required)') .should('be.visible') .type(randomLabel()); 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 6a014990a23..3792b228802 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 @@ -3,27 +3,26 @@ */ import { createBucket } from '@linode/api-v4/lib/object-storage'; +import { + accountFactory, + createObjectStorageBucketFactoryLegacy, +} from 'src/factories'; import { authenticate } from 'support/api/authentication'; import { interceptGetNetworkUtilization, mockGetAccount, } from 'support/intercepts/account'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { interceptCreateBucket, interceptDeleteBucket, - interceptGetBucketAccess, interceptGetBuckets, + interceptGetBucketAccess, interceptUpdateBucketAccess, } from 'support/intercepts/object-storage'; import { ui } from 'support/ui'; -import { cleanUp } from 'support/util/cleanup'; import { randomLabel } from 'support/util/random'; - -import { - accountFactory, - createObjectStorageBucketFactoryLegacy, -} from 'src/factories'; +import { cleanUp } from 'support/util/cleanup'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; /** * Create a bucket with the given label and cluster. @@ -44,9 +43,9 @@ const setUpBucket = ( ) => { return createBucket( createObjectStorageBucketFactoryLegacy.build({ + label, cluster, cors_enabled, - label, // API accepts either `cluster` or `region`, but not both. Our factory // populates both fields, so we have to manually set `region` to `undefined` @@ -96,17 +95,19 @@ describe('object storage end-to-end tests', () => { // Wait for loader to disappear, indicating that all buckets have been loaded. // Mitigates test failures stemming from M3-7833. - cy.findByTestId('Buckets').within(() => { + cy.findByLabelText('Buckets').within(() => { cy.findByLabelText('Content is loading').should('not.exist'); }); - ui.button.findByTitle('Create Bucket').should('be.visible').click(); + ui.entityHeader.find().within(() => { + ui.button.findByTitle('Create Bucket').should('be.visible').click(); + }); ui.drawer .findByTitle('Create Bucket') .should('be.visible') .within(() => { - cy.findByLabelText('Bucket Name (required)').click(); + cy.findByText('Label').click(); cy.focused().type(bucketLabel); ui.regionSelect.find().click(); cy.focused().type(`${bucketRegion}{enter}`); @@ -168,31 +169,31 @@ describe('object storage end-to-end tests', () => { interceptUpdateBucketAccess(bucketLabel, bucketCluster).as( 'updateBucketAccess' ); + }); - // Navigate to new bucket page, upload and delete an object. - cy.visitWithLogin(bucketAccessPage); + // Navigate to new bucket page, upload and delete an object. + cy.visitWithLogin(bucketAccessPage); - cy.wait('@getBucketAccess'); + cy.wait('@getBucketAccess'); - // Make object public, confirm it can be accessed. - cy.findByLabelText('Access Control List (ACL)') - .should('be.visible') - .should('not.have.value', 'Loading access...') - .should('have.value', 'Private') - .click(); - cy.focused().type('Public Read'); + // Make object public, confirm it can be accessed. + cy.findByLabelText('Access Control List (ACL)') + .should('be.visible') + .should('not.have.value', 'Loading access...') + .should('have.value', 'Private') + .click(); + cy.focused().type('Public Read'); - ui.autocompletePopper - .findByTitle('Public Read') - .should('be.visible') - .click(); + ui.autocompletePopper + .findByTitle('Public Read') + .should('be.visible') + .click(); - ui.button.findByTitle('Save').should('be.visible').click(); + ui.button.findByTitle('Save').should('be.visible').click(); - // TODO Confirm that outgoing API request contains expected values. - cy.wait('@updateBucketAccess'); + // TODO Confirm that outgoing API request contains expected values. + cy.wait('@updateBucketAccess'); - cy.findByText('Bucket access updated successfully.'); - }); + cy.findByText('Bucket access updated successfully.'); }); }); 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 8f9e618c2c9..21edeaed94c 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 @@ -3,23 +3,22 @@ */ import 'cypress-file-upload'; -import { mockGetAccount } from 'support/intercepts/account'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { objectStorageBucketFactory } from 'src/factories/objectStorage'; import { mockCreateBucket, mockDeleteBucket, mockDeleteBucketObject, mockDeleteBucketObjectS3, - mockGetBucketObjects, mockGetBuckets, + mockGetBucketObjects, mockUploadBucketObject, mockUploadBucketObjectS3, } from 'support/intercepts/object-storage'; -import { ui } from 'support/ui'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { randomLabel } from 'support/util/random'; - +import { ui } from 'support/ui'; import { accountFactory } from 'src/factories'; -import { objectStorageBucketFactory } from 'src/factories/objectStorage'; +import { mockGetAccount } from 'support/intercepts/account'; describe('object storage smoke tests', () => { /* @@ -34,16 +33,16 @@ describe('object storage smoke tests', () => { const bucketHostname = `${bucketLabel}.${bucketCluster}.linodeobjects.com`; const mockBucket = objectStorageBucketFactory.build({ + label: bucketLabel, cluster: bucketCluster, hostname: bucketHostname, - label: bucketLabel, }); mockGetAccount(accountFactory.build({ capabilities: ['Object Storage'] })); mockAppendFeatureFlags({ - gecko2: false, objMultiCluster: false, objectStorageGen2: { enabled: false }, + gecko2: false, }).as('getFeatureFlags'); mockGetBuckets([]).as('getBuckets'); @@ -63,7 +62,7 @@ describe('object storage smoke tests', () => { .findByTitle('Create Bucket') .should('be.visible') .within(() => { - cy.findByLabelText('Bucket Name (required)').click(); + cy.findByText('Label').click(); cy.focused().type(bucketLabel); ui.regionSelect.find().click(); cy.focused().type(`${bucketRegion}{enter}`); @@ -173,9 +172,9 @@ describe('object storage smoke tests', () => { const bucketLabel = randomLabel(); const bucketCluster = 'us-southeast-1'; const bucketMock = objectStorageBucketFactory.build({ + label: bucketLabel, cluster: bucketCluster, hostname: `${bucketLabel}.${bucketCluster}.linodeobjects.com`, - label: bucketLabel, objects: 0, }); diff --git a/packages/manager/cypress/e2e/core/objectStorageGen2/bucket-access-keys-gen2.spec.ts b/packages/manager/cypress/e2e/core/objectStorageGen2/bucket-access-keys-gen2.spec.ts index e601bedd47a..a031285e701 100644 --- a/packages/manager/cypress/e2e/core/objectStorageGen2/bucket-access-keys-gen2.spec.ts +++ b/packages/manager/cypress/e2e/core/objectStorageGen2/bucket-access-keys-gen2.spec.ts @@ -1,11 +1,10 @@ -import { profileFactory } from '@linode/utilities'; import { mockGetAccount } from 'support/intercepts/account'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; -import { mockGetAccessKeys } from 'support/intercepts/object-storage'; import { mockGetProfile } from 'support/intercepts/profile'; -import { ui } from 'support/ui'; - +import { mockGetAccessKeys } from 'support/intercepts/object-storage'; import { accountFactory, objectStorageKeyFactory } from 'src/factories'; +import { profileFactory } from 'src/factories/profile'; +import { ui } from 'support/ui'; describe('Object Storage gen2 access keys tests', () => { /** @@ -29,27 +28,27 @@ describe('Object Storage gen2 access keys tests', () => { const mockAccessKey1 = objectStorageKeyFactory.build({ regions: [ - { endpoint_type: 'E3', id: 'us-east', s3_endpoint: 'us-east.com' }, + { id: 'us-east', s3_endpoint: 'us-east.com', endpoint_type: 'E3' }, ], }); const mockAccessKey2 = objectStorageKeyFactory.build({ regions: [ { - endpoint_type: 'E3', id: 'us-southeast', s3_endpoint: 'us-southeast.com', + endpoint_type: 'E3', }, - { endpoint_type: 'E2', id: 'in-maa', s3_endpoint: 'in-maa.com' }, - { endpoint_type: 'E1', id: 'us-mia', s3_endpoint: 'us-mia.com' }, - { endpoint_type: 'E0', id: 'it-mil', s3_endpoint: 'it-mil.com' }, + { id: 'in-maa', s3_endpoint: 'in-maa.com', endpoint_type: 'E2' }, + { id: 'us-mia', s3_endpoint: 'us-mia.com', endpoint_type: 'E1' }, + { id: 'it-mil', s3_endpoint: 'it-mil.com', endpoint_type: 'E0' }, ], }); mockGetAccessKeys([mockAccessKey1, mockAccessKey2]).as( 'getObjectStorageAccessKeys' - ); - cy.visitWithLogin('/object-storage/access-keys'); + ), + cy.visitWithLogin('/object-storage/access-keys'); cy.wait(['@getFeatureFlags', '@getAccount', '@getObjectStorageAccessKeys']); 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 5592400f299..dfec433327e 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 @@ -1,27 +1,26 @@ -import { profileFactory, regionFactory } from '@linode/utilities'; import { mockGetAccount } from 'support/intercepts/account'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { - mockCreateBucket, - mockCreateBucketError, + mockGetObjectStorageEndpoints, + mockGetBuckets, mockDeleteBucket, + mockCreateBucket, mockGetBucketAccess, - mockGetBuckets, - mockGetObjectStorageEndpoints, + mockCreateBucketError, } from 'support/intercepts/object-storage'; import { mockGetProfile } from 'support/intercepts/profile'; import { mockGetRegions } from 'support/intercepts/regions'; import { ui } from 'support/ui'; import { checkRateLimitsTable } from 'support/util/object-storage-gen2'; import { randomLabel } from 'support/util/random'; -import { chooseRegion } from 'support/util/regions'; - import { accountFactory, objectStorageBucketFactoryGen2, objectStorageEndpointsFactory, + regionFactory, } from 'src/factories'; - +import { profileFactory } from 'src/factories/profile'; +import { chooseRegion } from 'support/util/regions'; import type { ACLType, ObjectStorageEndpoint } from '@linode/api-v4'; describe('Object Storage Gen2 create bucket tests', () => { @@ -98,6 +97,8 @@ describe('Object Storage Gen2 create bucket tests', () => { endpointType === 'Standard (E3)' || endpointType === 'Standard (E2)' ) { + cy.contains(bucketRateLimitsNotice).should('be.visible'); + cy.get('[data-testid="bucket-rate-limit-table"]').should('be.visible'); cy.contains(CORSNotice).should('be.visible'); ui.toggle.find().should('not.exist'); } else { @@ -151,9 +152,9 @@ describe('Object Storage Gen2 create bucket tests', () => { mockGetBuckets([]).as('getBuckets'); mockDeleteBucket(bucketLabel, mockRegion.id).as('deleteBucket'); mockCreateBucket({ - cors_enabled: true, - endpoint_type: 'E0', label: bucketLabel, + endpoint_type: 'E0', + cors_enabled: true, region: mockRegion.id, }).as('createBucket'); @@ -175,9 +176,9 @@ describe('Object Storage Gen2 create bucket tests', () => { ]); const mockBucket = objectStorageBucketFactoryGen2.build({ - endpoint_type: 'E0', label: bucketLabel, region: mockRegion.id, + endpoint_type: 'E0', s3_endpoint: undefined, }); @@ -185,7 +186,7 @@ describe('Object Storage Gen2 create bucket tests', () => { .findByTitle('Create Bucket') .should('be.visible') .within(() => { - cy.findByLabelText('Bucket Name (required)').click(); + cy.findByText('Label').click(); cy.focused().type(bucketLabel); ui.regionSelect.find().click(); cy.focused().type(`${mockRegion.label}{enter}`); @@ -292,9 +293,9 @@ describe('Object Storage Gen2 create bucket tests', () => { mockGetBuckets([]).as('getBuckets'); mockDeleteBucket(bucketLabel, mockRegion.id).as('deleteBucket'); mockCreateBucket({ - cors_enabled: true, - endpoint_type: 'E1', label: bucketLabel, + endpoint_type: 'E1', + cors_enabled: true, region: mockRegion.id, }).as('createBucket'); @@ -316,9 +317,9 @@ describe('Object Storage Gen2 create bucket tests', () => { ]); const mockBucket = objectStorageBucketFactoryGen2.build({ - endpoint_type: 'E1', label: bucketLabel, region: mockRegion.id, + endpoint_type: 'E1', s3_endpoint: 'us-sea-1.linodeobjects.com', }); @@ -326,7 +327,7 @@ describe('Object Storage Gen2 create bucket tests', () => { .findByTitle('Create Bucket') .should('be.visible') .within(() => { - cy.findByLabelText('Bucket Name (required)').click(); + cy.findByText('Label').click(); cy.focused().type(bucketLabel); ui.regionSelect.find().click(); cy.focused().type(`${mockRegion.label}{enter}`); @@ -418,9 +419,9 @@ describe('Object Storage Gen2 create bucket tests', () => { mockGetBuckets([]).as('getBuckets'); mockDeleteBucket(bucketLabel, mockRegion.id).as('deleteBucket'); mockCreateBucket({ - cors_enabled: true, - endpoint_type: 'E2', label: bucketLabel, + endpoint_type: 'E2', + cors_enabled: true, region: mockRegion.id, }).as('createBucket'); @@ -442,9 +443,9 @@ describe('Object Storage Gen2 create bucket tests', () => { ]); const mockBucket = objectStorageBucketFactoryGen2.build({ - endpoint_type: 'E2', label: bucketLabel, region: mockRegion.id, + endpoint_type: 'E2', s3_endpoint: undefined, }); @@ -452,7 +453,7 @@ describe('Object Storage Gen2 create bucket tests', () => { .findByTitle('Create Bucket') .should('be.visible') .within(() => { - cy.findByLabelText('Bucket Name (required)').click(); + cy.findByText('Label').click(); cy.focused().type(bucketLabel); ui.regionSelect.find().click(); cy.focused().type(`${mockRegion.label}{enter}`); @@ -542,9 +543,9 @@ describe('Object Storage Gen2 create bucket tests', () => { mockGetBuckets([]).as('getBuckets'); mockDeleteBucket(bucketLabel, mockRegion.id).as('deleteBucket'); mockCreateBucket({ - cors_enabled: false, - endpoint_type: 'E3', label: bucketLabel, + endpoint_type: 'E3', + cors_enabled: false, region: mockRegion.id, }).as('createBucket'); @@ -566,9 +567,9 @@ describe('Object Storage Gen2 create bucket tests', () => { ]); const mockBucket = objectStorageBucketFactoryGen2.build({ - endpoint_type: 'E3', label: bucketLabel, region: mockRegion.id, + endpoint_type: 'E3', s3_endpoint: undefined, }); @@ -576,7 +577,7 @@ describe('Object Storage Gen2 create bucket tests', () => { .findByTitle('Create Bucket') .should('be.visible') .within(() => { - cy.findByLabelText('Bucket Name (required)').click(); + cy.findByText('Label').click(); cy.focused().type(bucketLabel); ui.regionSelect.find().click(); cy.focused().type(`${mockRegion.label}{enter}`); @@ -713,10 +714,10 @@ describe('Object Storage Gen2 create bucket tests', () => { .should('be.enabled') .click(); - cy.contains('Bucket name is required.').should('be.visible'); - cy.findByLabelText('Bucket Name (required)').click(); + cy.contains('Label is required.').should('be.visible'); + cy.findByText('Label').click(); cy.focused().type(bucketLabel); - cy.contains('Bucket name is required.').should('not.exist'); + cy.contains('Label is required.').should('not.exist'); // confirms (mock) API error appears ui.buttonGroup @@ -771,7 +772,7 @@ describe('Object Storage Gen2 create bucket modal has disabled fields for restri cy.findByText(/You don't have permissions to create a Bucket./).should( 'be.visible' ); - cy.findByLabelText('Bucket Name (required)') + cy.findByLabelText(/Label.*/) .should('be.visible') .should('be.disabled'); ui.regionSelect.find().should('be.visible').should('be.disabled'); diff --git a/packages/manager/cypress/e2e/core/objectStorageGen2/bucket-details-gen2.spec.ts b/packages/manager/cypress/e2e/core/objectStorageGen2/bucket-details-gen2.spec.ts index ba58475cd86..a8bf39db270 100644 --- a/packages/manager/cypress/e2e/core/objectStorageGen2/bucket-details-gen2.spec.ts +++ b/packages/manager/cypress/e2e/core/objectStorageGen2/bucket-details-gen2.spec.ts @@ -1,19 +1,17 @@ -import { regionFactory } from '@linode/utilities'; import { mockGetAccount } from 'support/intercepts/account'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { - mockGetBucketAccess, mockGetBucketsForRegion, mockGetObjectStorageEndpoints, + mockGetBucketAccess, } from 'support/intercepts/object-storage'; - import { accountFactory, objectStorageBucketFactoryGen2, objectStorageEndpointsFactory, + regionFactory, } from 'src/factories'; - -import type { ACLType, ObjectStorageEndpointTypes } from '@linode/api-v4'; +import { ACLType, ObjectStorageEndpointTypes } from '@linode/api-v4'; describe('Object Storage Gen 2 bucket details tabs', () => { beforeEach(() => { diff --git a/packages/manager/cypress/e2e/core/objectStorageGen2/bucket-object-gen2.spec.ts b/packages/manager/cypress/e2e/core/objectStorageGen2/bucket-object-gen2.spec.ts index 6d10cf1b60a..b5f94c08487 100644 --- a/packages/manager/cypress/e2e/core/objectStorageGen2/bucket-object-gen2.spec.ts +++ b/packages/manager/cypress/e2e/core/objectStorageGen2/bucket-object-gen2.spec.ts @@ -1,7 +1,17 @@ -import { regionFactory } from '@linode/utilities'; import 'cypress-file-upload'; import { mockGetAccount } from 'support/intercepts/account'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { + accountFactory, + objectStorageBucketFactoryGen2, + objectStorageEndpointsFactory, + regionFactory, +} from 'src/factories'; +import { chooseRegion } from 'support/util/regions'; +import { mockGetRegions } from 'support/intercepts/regions'; +import { ObjectStorageEndpoint } from '@linode/api-v4'; +import { randomItem, randomLabel } from 'support/util/random'; +import { extendRegion } from 'support/util/regions'; import { mockCreateBucket, mockGetBucket, @@ -13,19 +23,7 @@ import { mockUploadBucketObject, mockUploadBucketObjectS3, } from 'support/intercepts/object-storage'; -import { mockGetRegions } from 'support/intercepts/regions'; import { ui } from 'support/ui'; -import { randomItem, randomLabel } from 'support/util/random'; -import { chooseRegion } from 'support/util/regions'; -import { extendRegion } from 'support/util/regions'; - -import { - accountFactory, - objectStorageBucketFactoryGen2, - objectStorageEndpointsFactory, -} from 'src/factories'; - -import type { ObjectStorageEndpoint } from '@linode/api-v4'; describe('Object Storage Gen2 bucket object tests', () => { beforeEach(() => { @@ -135,17 +133,17 @@ describe('Object Storage Gen2 bucket object tests', () => { const bucketLabel = randomLabel(); const bucketCluster = mockRegion.id; const mockBucket = objectStorageBucketFactoryGen2.build({ - endpoint_type: 'E0', label: bucketLabel, region: mockRegion.id, + endpoint_type: 'E0', s3_endpoint: undefined, }); - // mockGetBuckets([]).as('getBuckets'); + //mockGetBuckets([]).as('getBuckets'); mockCreateBucket({ - cors_enabled: true, - endpoint_type: 'E0', label: bucketLabel, + endpoint_type: 'E0', + cors_enabled: true, region: mockRegion.id, }).as('createBucket'); mockGetBucketsForRegion(mockRegion.id, [mockBucket]).as('getBuckets'); @@ -195,17 +193,17 @@ describe('Object Storage Gen2 bucket object tests', () => { const bucketLabel = randomLabel(); const bucketCluster = mockRegion.id; const mockBucket = objectStorageBucketFactoryGen2.build({ - endpoint_type: 'E1', label: bucketLabel, region: mockRegion.id, + endpoint_type: 'E1', s3_endpoint: 'us-sea-1.linodeobjects.com', }); - // mockGetBuckets([]).as('getBuckets'); + //mockGetBuckets([]).as('getBuckets'); mockCreateBucket({ - cors_enabled: true, - endpoint_type: 'E1', label: bucketLabel, + endpoint_type: 'E1', + cors_enabled: true, region: mockRegion.id, }).as('createBucket'); mockGetBucketsForRegion(mockRegion.id, [mockBucket]).as('getBuckets'); @@ -255,16 +253,16 @@ describe('Object Storage Gen2 bucket object tests', () => { const bucketLabel = randomLabel(); const bucketCluster = mockRegion.id; const mockBucket = objectStorageBucketFactoryGen2.build({ - endpoint_type: 'E2', label: bucketLabel, region: mockRegion.id, + endpoint_type: 'E2', s3_endpoint: undefined, }); mockCreateBucket({ - cors_enabled: true, - endpoint_type: 'E2', label: bucketLabel, + endpoint_type: 'E2', + cors_enabled: true, region: mockRegion.id, }).as('createBucket'); mockGetBucketsForRegion(mockRegion.id, [mockBucket]).as('getBuckets'); @@ -315,16 +313,16 @@ describe('Object Storage Gen2 bucket object tests', () => { const bucketLabel = randomLabel(); const bucketCluster = mockRegion.id; const mockBucket = objectStorageBucketFactoryGen2.build({ - endpoint_type: 'E3', label: bucketLabel, region: mockRegion.id, + endpoint_type: 'E3', s3_endpoint: undefined, }); mockCreateBucket({ - cors_enabled: true, - endpoint_type: 'E3', label: bucketLabel, + endpoint_type: 'E3', + cors_enabled: true, region: mockRegion.id, }).as('createBucket'); mockGetBucketsForRegion(mockRegion.id, [mockBucket]).as('getBuckets'); diff --git a/packages/manager/cypress/e2e/core/objectStorageMulticluster/access-keys-multicluster.spec.ts b/packages/manager/cypress/e2e/core/objectStorageMulticluster/access-keys-multicluster.spec.ts index 1b3db135ab7..790f12ff31e 100644 --- a/packages/manager/cypress/e2e/core/objectStorageMulticluster/access-keys-multicluster.spec.ts +++ b/packages/manager/cypress/e2e/core/objectStorageMulticluster/access-keys-multicluster.spec.ts @@ -1,28 +1,27 @@ -import { regionFactory } from '@linode/utilities'; -import { mockGetAccount } from 'support/intercepts/account'; +import { buildArray } from 'support/util/arrays'; +import { extendRegion } from 'support/util/regions'; +import { + accountFactory, + regionFactory, + objectStorageKeyFactory, + objectStorageBucketFactory, +} from 'src/factories'; +import { + randomString, + randomNumber, + randomLabel, + randomDomainName, +} from 'support/util/random'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { mockGetAccount } from 'support/intercepts/account'; +import { mockGetRegions } from 'support/intercepts/regions'; import { - mockCreateAccessKey, mockGetAccessKeys, + mockCreateAccessKey, mockGetBucketsForRegion, mockUpdateAccessKey, } from 'support/intercepts/object-storage'; -import { mockGetRegions } from 'support/intercepts/regions'; import { ui } from 'support/ui'; -import { buildArray } from 'support/util/arrays'; -import { - randomDomainName, - randomLabel, - randomNumber, - randomString, -} from 'support/util/random'; -import { extendRegion } from 'support/util/regions'; - -import { - accountFactory, - objectStorageBucketFactory, - objectStorageKeyFactory, -} from 'src/factories'; import type { ObjectStorageKeyBucketAccess } from '@linode/api-v4'; @@ -30,9 +29,9 @@ describe('Object Storage Multicluster access keys', () => { const mockRegionsObj = buildArray(3, () => { return extendRegion( regionFactory.build({ - capabilities: ['Object Storage'], id: `us-${randomString(5)}`, label: `mock-obj-region-${randomString(5)}`, + capabilities: ['Object Storage'], }) ); }); @@ -58,14 +57,14 @@ describe('Object Storage Multicluster access keys', () => { */ it('can create unlimited access keys with OBJ Multicluster', () => { const mockAccessKey = objectStorageKeyFactory.build({ - access_key: randomString(20), id: randomNumber(10000, 99999), label: randomLabel(), + access_key: randomString(20), + secret_key: randomString(39), regions: mockRegionsObj.map((mockObjRegion) => ({ id: mockObjRegion.id, s3_endpoint: randomDomainName(), })), - secret_key: randomString(39), }); mockGetAccessKeys([]); @@ -151,19 +150,29 @@ describe('Object Storage Multicluster access keys', () => { it('can create limited access keys with OBJ Multicluster', () => { const mockRegion = extendRegion( regionFactory.build({ - capabilities: ['Object Storage'], id: `us-${randomString(5)}`, label: `mock-obj-region-${randomString(5)}`, + capabilities: ['Object Storage'], }) ); const mockBuckets = objectStorageBucketFactory.buildList(2, { - cluster: undefined, region: mockRegion.id, + cluster: undefined, }); const mockAccessKey = objectStorageKeyFactory.build({ + id: randomNumber(10000, 99999), + label: randomLabel(), access_key: randomString(20), + secret_key: randomString(39), + regions: [ + { + id: mockRegion.id, + s3_endpoint: randomDomainName(), + }, + ], + limited: true, bucket_access: mockBuckets.map( (bucket): ObjectStorageKeyBucketAccess => ({ bucket_name: bucket.label, @@ -172,16 +181,6 @@ describe('Object Storage Multicluster access keys', () => { region: mockRegion.id, }) ), - id: randomNumber(10000, 99999), - label: randomLabel(), - limited: true, - regions: [ - { - id: mockRegion.id, - s3_endpoint: randomDomainName(), - }, - ], - secret_key: randomString(39), }); mockGetAccessKeys([]); @@ -287,33 +286,33 @@ describe('Object Storage Multicluster access keys', () => { it('can update access keys with OBJ Multicluster', () => { const mockInitialRegion = extendRegion( regionFactory.build({ - capabilities: ['Object Storage'], id: `us-${randomString(5)}`, label: `mock-obj-region-${randomString(5)}`, + capabilities: ['Object Storage'], }) ); const mockUpdatedRegion = extendRegion( regionFactory.build({ - capabilities: ['Object Storage'], id: `us-${randomString(5)}`, label: `mock-obj-region-${randomString(5)}`, + capabilities: ['Object Storage'], }) ); const mockRegions = [mockInitialRegion, mockUpdatedRegion]; const mockAccessKey = objectStorageKeyFactory.build({ - access_key: randomString(20), id: randomNumber(10000, 99999), label: randomLabel(), + access_key: randomString(20), + secret_key: randomString(39), regions: [ { id: mockInitialRegion.id, s3_endpoint: randomDomainName(), }, ], - secret_key: randomString(39), }); const mockUpdatedAccessKeyEndpoint = randomDomainName(); diff --git a/packages/manager/cypress/e2e/core/objectStorageMulticluster/bucket-create-multicluster.spec.ts b/packages/manager/cypress/e2e/core/objectStorageMulticluster/bucket-create-multicluster.spec.ts index d28f24324a9..a3ad4d53145 100644 --- a/packages/manager/cypress/e2e/core/objectStorageMulticluster/bucket-create-multicluster.spec.ts +++ b/packages/manager/cypress/e2e/core/objectStorageMulticluster/bucket-create-multicluster.spec.ts @@ -1,17 +1,19 @@ -import { regionFactory } from '@linode/utilities'; -import { mockGetAccount } from 'support/intercepts/account'; +import { extendRegion } from 'support/util/regions'; +import { + accountFactory, + regionFactory, + objectStorageBucketFactory, +} from 'src/factories'; +import { randomLabel, randomString } from 'support/util/random'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { mockGetAccount } from 'support/intercepts/account'; +import { mockGetRegions } from 'support/intercepts/regions'; import { mockCreateBucket, mockCreateBucketError, mockGetBuckets, } from 'support/intercepts/object-storage'; -import { mockGetRegions } from 'support/intercepts/regions'; import { ui } from 'support/ui'; -import { randomLabel, randomString } from 'support/util/random'; -import { extendRegion } from 'support/util/regions'; - -import { accountFactory, objectStorageBucketFactory } from 'src/factories'; describe('Object Storage Multicluster Bucket create', () => { /* @@ -27,9 +29,9 @@ describe('Object Storage Multicluster Bucket create', () => { const mockRegionWithObj = extendRegion( regionFactory.build({ - capabilities: ['Object Storage'], - id: `${randomString(2)}-${randomString(3)}`, label: randomLabel(), + id: `${randomString(2)}-${randomString(3)}`, + capabilities: ['Object Storage'], }) ); @@ -42,10 +44,10 @@ describe('Object Storage Multicluster Bucket create', () => { const mockRegions = [mockRegionWithObj, ...mockRegionsWithoutObj]; const mockBucket = objectStorageBucketFactory.build({ - cluster: undefined, label: randomLabel(), - objects: 0, region: mockRegionWithObj.id, + cluster: undefined, + objects: 0, }); mockGetAccount( @@ -65,14 +67,16 @@ describe('Object Storage Multicluster Bucket create', () => { cy.visitWithLogin('/object-storage'); cy.wait(['@getRegions', '@getBuckets']); - ui.button.findByTitle('Create Bucket').should('be.visible').click(); + ui.entityHeader.find().within(() => { + ui.button.findByTitle('Create Bucket').should('be.visible').click(); + }); ui.drawer .findByTitle('Create Bucket') .should('be.visible') .within(() => { // Enter label. - cy.findByLabelText('Bucket Name (required)').click(); + cy.contains('Label').click(); cy.focused().type(mockBucket.label); cy.log(`${mockRegionWithObj.label}`); cy.contains('Region').click(); @@ -120,7 +124,7 @@ describe('Object Storage Multicluster Bucket create', () => { // property in its payload when creating a bucket. cy.wait('@createBucket').then((xhr) => { const body = xhr.request.body; - expect(body.cluster).to.eq(undefined); + expect(body.cluster).to.be.undefined; expect(body.region).to.eq(mockRegionWithObj.id); }); diff --git a/packages/manager/cypress/e2e/core/objectStorageMulticluster/bucket-delete-multicluster.spec.ts b/packages/manager/cypress/e2e/core/objectStorageMulticluster/bucket-delete-multicluster.spec.ts index b6bb0f1dce4..669aea293c3 100644 --- a/packages/manager/cypress/e2e/core/objectStorageMulticluster/bucket-delete-multicluster.spec.ts +++ b/packages/manager/cypress/e2e/core/objectStorageMulticluster/bucket-delete-multicluster.spec.ts @@ -1,13 +1,12 @@ +import { randomLabel } from 'support/util/random'; +import { accountFactory, objectStorageBucketFactory } from 'src/factories'; import { mockGetAccount } from 'support/intercepts/account'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { - mockDeleteBucket, mockGetBuckets, + mockDeleteBucket, } from 'support/intercepts/object-storage'; import { ui } from 'support/ui'; -import { randomLabel } from 'support/util/random'; - -import { accountFactory, objectStorageBucketFactory } from 'src/factories'; describe('Object Storage Multicluster Bucket delete', () => { /* @@ -19,9 +18,9 @@ describe('Object Storage Multicluster Bucket delete', () => { const bucketLabel = randomLabel(); const bucketCluster = 'us-southeast-1'; const bucketMock = objectStorageBucketFactory.build({ + label: bucketLabel, cluster: bucketCluster, hostname: `${bucketLabel}.${bucketCluster}.linodeobjects.com`, - label: bucketLabel, objects: 0, }); diff --git a/packages/manager/cypress/e2e/core/objectStorageMulticluster/bucket-details-multicluster.spec.ts b/packages/manager/cypress/e2e/core/objectStorageMulticluster/bucket-details-multicluster.spec.ts index 253b54a4596..a5cc6e7158e 100644 --- a/packages/manager/cypress/e2e/core/objectStorageMulticluster/bucket-details-multicluster.spec.ts +++ b/packages/manager/cypress/e2e/core/objectStorageMulticluster/bucket-details-multicluster.spec.ts @@ -1,12 +1,14 @@ -import { regionFactory } from '@linode/utilities'; import { mockGetAccount } from 'support/intercepts/account'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; -import { mockGetBucket } from 'support/intercepts/object-storage'; -import { mockGetRegions } from 'support/intercepts/regions'; import { ui } from 'support/ui'; +import { + accountFactory, + objectStorageBucketFactory, + regionFactory, +} from 'src/factories'; import { randomLabel } from 'support/util/random'; - -import { accountFactory, objectStorageBucketFactory } from 'src/factories'; +import { mockGetBucket } from 'support/intercepts/object-storage'; +import { mockGetRegions } from 'support/intercepts/regions'; describe('Object Storage Multicluster Bucket Details Tabs', () => { beforeEach(() => { diff --git a/packages/manager/cypress/e2e/core/objectStorageMulticluster/object-storage-objects-multicluster.spec.ts b/packages/manager/cypress/e2e/core/objectStorageMulticluster/object-storage-objects-multicluster.spec.ts index c89f507d138..0281cfa2d35 100644 --- a/packages/manager/cypress/e2e/core/objectStorageMulticluster/object-storage-objects-multicluster.spec.ts +++ b/packages/manager/cypress/e2e/core/objectStorageMulticluster/object-storage-objects-multicluster.spec.ts @@ -1,13 +1,12 @@ -import { createBucket } from '@linode/api-v4'; import 'cypress-file-upload'; import { authenticate } from 'support/api/authentication'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; -import { interceptUploadBucketObjectS3 } from 'support/intercepts/object-storage'; -import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; import { randomLabel } from 'support/util/random'; - +import { ui } from 'support/ui'; import { createObjectStorageBucketFactoryGen1 } from 'src/factories'; +import { interceptUploadBucketObjectS3 } from 'support/intercepts/object-storage'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { createBucket } from '@linode/api-v4'; // Message shown on-screen when user navigates to an empty bucket. const emptyBucketMessage = 'This bucket is empty.'; @@ -48,14 +47,14 @@ const setUpBucketMulticluster = ( ) => { return createBucket( createObjectStorageBucketFactoryGen1.build({ - // to avoid 400 responses from the API. - cluster: undefined, - cors_enabled, label, + region: regionId, + cors_enabled, // API accepts either `cluster` or `region`, but not both. Our factory // populates both fields, so we have to manually set `cluster` to `undefined` - region: regionId, + // to avoid 400 responses from the API. + cluster: undefined, }) ); }; @@ -74,8 +73,8 @@ const assertStatusForUrlAtAlias = ( // An alias can resolve to anything. We're assuming the user passed a valid // alias which resolves to a string. cy.request({ - failOnStatusCode: false, url: url as string, + failOnStatusCode: false, }).then((response) => { expect(response.status).to.eq(expectedStatus); }); @@ -137,8 +136,8 @@ describe('Object Storage Multicluster objects', () => { const bucketFolderName = randomLabel(); const bucketFiles = [ - { name: '1.txt', path: 'object-storage-files/1.txt' }, - { name: '2.jpg', path: 'object-storage-files/2.jpg' }, + { path: 'object-storage-files/1.txt', name: '1.txt' }, + { path: 'object-storage-files/2.jpg', name: '2.jpg' }, ]; cy.defer( 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 67f44a6b5a1..827533a2bfe 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,26 +1,24 @@ -import { linodeFactory } from '@linode/utilities'; -import { mockGetAllImages } from 'support/intercepts/images'; -import { mockCreateLinode } from 'support/intercepts/linodes'; +import { ui } from 'support/ui'; import { interceptGetStackScripts, mockGetStackScript, mockGetStackScripts, } from 'support/intercepts/stackscripts'; -import { ui } from 'support/ui'; +import { mockCreateLinode } from 'support/intercepts/linodes'; import { getRandomOCAId } from 'support/util/one-click-apps'; import { randomLabel, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; - -import { imageFactory } from 'src/factories'; import { stackScriptFactory } from 'src/factories/stackscripts'; -import { getMarketplaceAppLabel } from 'src/features/Linodes/LinodeCreate/Tabs/Marketplace/utilities'; import { oneClickApps } from 'src/features/OneClickApps/oneClickApps'; +import { getMarketplaceAppLabel } from 'src/features/Linodes/LinodeCreate/Tabs/Marketplace/utilities'; import type { StackScript } from '@linode/api-v4'; +import { imageFactory, linodeFactory } from 'src/factories'; +import { mockGetAllImages } from 'support/intercepts/images'; describe('OneClick Apps (OCA)', () => { it('Lists all the OneClick Apps', () => { - cy.tag('method:e2e', 'env:marketplaceApps'); + cy.tag('method:e2e'); interceptGetStackScripts().as('getStackScripts'); cy.visitWithLogin(`/linodes/create?type=One-Click`); @@ -57,7 +55,7 @@ describe('OneClick Apps (OCA)', () => { }); it('Can view app details of a marketplace app', () => { - cy.tag('method:e2e', 'env:marketplaceApps'); + cy.tag('method:e2e'); interceptGetStackScripts().as('getStackScripts'); cy.visitWithLogin(`/linodes/create?type=One-Click`); @@ -119,39 +117,39 @@ describe('OneClick Apps (OCA)', () => { }), ]; const stackscript = stackScriptFactory.build({ - created: '2019-03-08T21:13:32', - deployments_active: 412, - deployments_total: 18854, - description: 'Minecraft OCA', id: getRandomOCAId(), - images: ['linode/debian11', 'linode/ubuntu24.04'], - is_public: true, + username: 'linode', + user_gravatar_id: '9d4d301385af69ceb7ad658aad09c142', label: 'E2E Test App', + description: 'Minecraft OCA', + ordinal: 10, logo_url: 'assets/Minecraft.svg', + images: ['linode/debian11', 'linode/ubuntu24.04'], + deployments_total: 18854, + deployments_active: 412, + is_public: true, mine: false, - ordinal: 10, + created: '2019-03-08T21:13:32', + updated: '2023-09-26T15:00:45', rev_note: 'remove maxplayers hard coded options [oca-707]', script: '#!/usr/bin/env bash\n', - updated: '2023-09-26T15:00:45', user_defined_fields: [ { - example: 'lgsmuser', + name: 'username', label: "The username for the Linode's non-root admin/SSH user(must be lowercase)", - name: 'username', + example: 'lgsmuser', }, { - example: 'S3cuReP@s$w0rd', - label: "The password for the Linode's non-root admin/SSH user", name: 'password', + label: "The password for the Linode's non-root admin/SSH user", + example: 'S3cuReP@s$w0rd', }, { - label: 'World Name', name: 'levelname', + label: 'World Name', }, ], - user_gravatar_id: '9d4d301385af69ceb7ad658aad09c142', - username: 'linode', }); const rootPassword = randomString(16); @@ -180,7 +178,7 @@ describe('OneClick Apps (OCA)', () => { cy.findByText('New apps').should('be.visible'); // Check that the app is listed and select it - // The app may be listed 2 or 3 times. + cy.get('[data-qa-selection-card="true"]').should('have.length', 2); cy.findAllByText(stackscript.label).first().should('be.visible').click(); }); @@ -249,8 +247,8 @@ describe('OneClick Apps (OCA)', () => { }); // leave test disabled by default - it.skip('Validate the summaries of all the OneClick Apps', () => { - cy.tag('method:e2e', 'env:marketplaceApps'); + xit('Validate the summaries of all the OneClick Apps', () => { + cy.tag('method:e2e'); interceptGetStackScripts().as('getStackScripts'); cy.visitWithLogin(`/linodes/create?type=One-Click`); diff --git a/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts b/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts index 271235aa07f..c14eda35f46 100644 --- a/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts +++ b/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts @@ -1,16 +1,15 @@ -import { profileFactory } from '@linode/utilities'; import { accountFactory, appTokenFactory, paymentMethodFactory, + profileFactory, } from '@src/factories'; import { accountUserFactory } from '@src/factories/accountUsers'; -import { grantsFactory } from '@src/factories/grants'; import { DateTime } from 'luxon'; import { interceptGetInvoices, - interceptGetPaymentMethods, interceptGetPayments, + interceptGetPaymentMethods, mockCreateChildAccountToken, mockCreateChildAccountTokenError, mockGetAccount, @@ -32,6 +31,7 @@ import { mockGetRegions } from 'support/intercepts/regions'; import { ui } from 'support/ui'; import { assertLocalStorageValue } from 'support/util/local-storage'; import { randomLabel, randomNumber, randomString } from 'support/util/random'; +import { grantsFactory } from '@src/factories/grants'; /** * Confirms expected username and company name are shown in user menu button and yields the button. @@ -73,13 +73,13 @@ const mockParentAccount = accountFactory.build({ }); const mockParentProfile = profileFactory.build({ - user_type: 'parent', username: randomLabel(), + user_type: 'parent', }); const mockParentUser = accountUserFactory.build({ - user_type: 'parent', username: mockParentProfile.username, + user_type: 'parent', }); const mockChildAccount = accountFactory.build({ @@ -92,25 +92,25 @@ const mockAlternateChildAccount = accountFactory.build({ }); const mockChildAccountProxyUser = accountUserFactory.build({ - user_type: 'proxy', username: mockParentProfile.username, + user_type: 'proxy', }); // Used for testing flows involving multiple children (e.g. switching child -> child). const mockAlternateChildAccountProxyUser = accountUserFactory.build({ - user_type: 'proxy', username: mockParentProfile.username, + user_type: 'proxy', }); const mockChildAccountProfile = profileFactory.build({ - user_type: 'proxy', username: mockChildAccountProxyUser.username, + user_type: 'proxy', }); // Used for testing flows involving multiple children (e.g. switching child -> child). const mockAlternateChildAccountProfile = profileFactory.build({ - user_type: 'proxy', username: mockAlternateChildAccountProxyUser.username, + user_type: 'proxy', }); const childAccountAccessGrantEnabled = grantsFactory.build({ @@ -122,26 +122,26 @@ const childAccountAccessGrantDisabled = grantsFactory.build({ }); const mockChildAccountToken = appTokenFactory.build({ + id: randomNumber(), created: DateTime.now().toISO(), expiry: DateTime.now().plus({ minutes: 15 }).toISO(), - id: randomNumber(), label: `${mockChildAccount.company}_proxy`, scopes: '*', - thumbnail_url: undefined, token: randomString(32), website: undefined, + thumbnail_url: undefined, }); // Used for testing flows involving multiple children (e.g. switching child -> child). const mockAlternateChildAccountToken = appTokenFactory.build({ + id: randomNumber(), created: DateTime.now().toISO(), expiry: DateTime.now().plus({ minutes: 15 }).toISO(), - id: randomNumber(), label: `${mockAlternateChildAccount.company}_proxy`, scopes: '*', - thumbnail_url: undefined, token: randomString(32), website: undefined, + thumbnail_url: undefined, }); const mockErrorMessage = 'An unknown error has occurred.'; @@ -400,10 +400,10 @@ describe('Parent/Child account switching', () => { // data set to mock values. cy.visitWithLogin('/account/billing', { localStorageOverrides: { + proxy_user: true, + 'authentication/parent_token/token': `Bearer ${mockParentToken}`, 'authentication/parent_token/expire': mockParentExpiration, 'authentication/parent_token/scopes': '*', - 'authentication/parent_token/token': `Bearer ${mockParentToken}`, - proxy_user: true, }, }); @@ -487,10 +487,10 @@ describe('Parent/Child account switching', () => { // data set to mock values. cy.visitWithLogin('/account/billing', { localStorageOverrides: { + proxy_user: true, + 'authentication/parent_token/token': `Bearer ${mockParentToken}`, 'authentication/parent_token/expire': mockParentExpiration, 'authentication/parent_token/scopes': '*', - 'authentication/parent_token/token': `Bearer ${mockParentToken}`, - proxy_user: true, }, }); diff --git a/packages/manager/cypress/e2e/core/parentChild/token-expiration.spec.ts b/packages/manager/cypress/e2e/core/parentChild/token-expiration.spec.ts index 18227d3bce4..2173815b7a2 100644 --- a/packages/manager/cypress/e2e/core/parentChild/token-expiration.spec.ts +++ b/packages/manager/cypress/e2e/core/parentChild/token-expiration.spec.ts @@ -1,28 +1,30 @@ -import { profileFactory } from '@linode/utilities'; -import { DateTime } from 'luxon'; +import { mockGetLinodes } from 'support/intercepts/linodes'; +import { + accountFactory, + accountUserFactory, + profileFactory, +} from 'src/factories'; +import { randomLabel, randomString } from 'support/util/random'; import { mockGetAccount, mockGetChildAccounts, } from 'support/intercepts/account'; -import { mockGetLinodes } from 'support/intercepts/linodes'; import { mockGetProfile } from 'support/intercepts/profile'; +import { DateTime } from 'luxon'; import { ui } from 'support/ui'; -import { randomLabel, randomString } from 'support/util/random'; - -import { accountFactory, accountUserFactory } from 'src/factories'; const mockChildAccount = accountFactory.build({ company: 'Partner Company', }); const mockChildAccountProxyUser = accountUserFactory.build({ - user_type: 'proxy', username: randomLabel(), + user_type: 'proxy', }); const mockChildAccountProxyProfile = profileFactory.build({ - user_type: 'proxy', username: mockChildAccountProxyUser.username, + user_type: 'proxy', }); describe('Parent/Child token expiration', () => { @@ -39,12 +41,12 @@ describe('Parent/Child token expiration', () => { // Mock local storage parent token expiry to have already passed. cy.visitWithLogin('/', { localStorageOverrides: { + proxy_user: true, + 'authentication/parent_token/token': `Bearer ${randomString(32)}`, 'authentication/parent_token/expire': DateTime.local() .minus({ minutes: 30 }) .toISO(), 'authentication/parent_token/scopes': '*', - 'authentication/parent_token/token': `Bearer ${randomString(32)}`, - proxy_user: true, }, }); @@ -75,6 +77,6 @@ describe('Parent/Child token expiration', () => { .click(); }); - cy.url().should('endWith', '/logout'); + cy.url().should('endWith', '/login'); }); }); diff --git a/packages/manager/cypress/e2e/core/parentChild/token-scopes.spec.ts b/packages/manager/cypress/e2e/core/parentChild/token-scopes.spec.ts index 5547bd1a262..845701b9c90 100644 --- a/packages/manager/cypress/e2e/core/parentChild/token-scopes.spec.ts +++ b/packages/manager/cypress/e2e/core/parentChild/token-scopes.spec.ts @@ -1,5 +1,8 @@ -import { profileFactory } from '@linode/utilities'; -import { accountFactory, appTokenFactory } from '@src/factories'; +import { + accountFactory, + appTokenFactory, + profileFactory, +} from '@src/factories'; import { accountUserFactory } from '@src/factories/accountUsers'; import { DateTime } from 'luxon'; import { @@ -21,13 +24,13 @@ const mockParentAccount = accountFactory.build({ }); const mockParentProfile = profileFactory.build({ - user_type: 'parent', username: randomLabel(), + user_type: 'parent', }); const mockParentUser = accountUserFactory.build({ - user_type: 'parent', username: mockParentProfile.username, + user_type: 'parent', }); const mockChildAccount = accountFactory.build({ @@ -35,14 +38,14 @@ const mockChildAccount = accountFactory.build({ }); const mockParentAccountToken = appTokenFactory.build({ + id: randomNumber(), created: DateTime.now().toISO(), expiry: DateTime.now().plus({ minutes: 15 }).toISO(), - id: randomNumber(), label: `${mockParentAccount.company}_proxy`, scopes: '*', - thumbnail_url: undefined, token: randomString(32), website: undefined, + thumbnail_url: undefined, }); describe('Token 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 aaef31b0af9..2484db5c322 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,39 +1,42 @@ -import { linodeFactory, regionFactory } from '@linode/utilities'; import { mockGetAccount } from 'support/intercepts/account'; +import { + accountFactory, + linodeFactory, + placementGroupFactory, +} from 'src/factories'; +import { regionFactory } from 'src/factories'; +import { ui } from 'support/ui/'; import { mockCreateLinode, mockGetLinodeDetails, } from 'support/intercepts/linodes'; +import { mockGetRegions } from 'support/intercepts/regions'; import { mockCreatePlacementGroup, mockGetPlacementGroups, } from 'support/intercepts/placement-groups'; -import { mockGetRegions } from 'support/intercepts/regions'; -import { ui } from 'support/ui/'; -import { linodeCreatePage } from 'support/ui/pages'; import { randomNumber, randomString } from 'support/util/random'; -import { extendRegion } from 'support/util/regions'; - -import { accountFactory, placementGroupFactory } from 'src/factories'; import { CANNOT_CHANGE_PLACEMENT_GROUP_POLICY_MESSAGE } from 'src/features/PlacementGroups/constants'; +import { linodeCreatePage } from 'support/ui/pages'; +import { extendRegion } from 'support/util/regions'; const mockAccount = accountFactory.build(); const mockNewarkRegion = extendRegion( regionFactory.build({ capabilities: ['Linodes', 'Placement Group'], - country: 'us', id: 'us-east', label: 'Newark, NJ', + country: 'us', }) ); const mockDallasRegion = extendRegion( regionFactory.build({ capabilities: ['Linodes'], - country: 'us', id: 'us-central', label: 'Dallas, TX', + country: 'us', }) ); @@ -106,11 +109,11 @@ describe('Linode create flow with Placement Group', () => { .click(); const mockPlacementGroup = placementGroupFactory.build({ - is_compliant: true, label: 'pg-1-us-east', - placement_group_policy: 'strict', - placement_group_type: 'anti_affinity:local', region: mockRegions[0].id, + placement_group_type: 'anti_affinity:local', + placement_group_policy: 'strict', + is_compliant: true, }); mockGetPlacementGroups([mockPlacementGroup]).as('getPlacementGroups'); @@ -170,22 +173,20 @@ describe('Linode create flow with Placement Group', () => { const linodeLabel = 'linode-with-placement-group'; const mockLinode = linodeFactory.build({ label: linodeLabel, + region: mockRegions[0].id, placement_group: { id: mockPlacementGroup.id, }, - region: mockRegions[0].id, }); // Confirm the Placement group assignment is accounted for in the summary. cy.findByText('Assigned to Placement Group') - .as('qaAssigned') - .scrollIntoView(); - cy.get('@qaAssigned').should('be.visible'); + .scrollIntoView() + .should('be.visible'); // Type in a label, password and submit the form. mockCreateLinode(mockLinode).as('createLinode'); - cy.get('#linode-label').clear(); - cy.focused().type('linode-with-placement-group'); + 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(); @@ -209,21 +210,21 @@ describe('Linode create flow with Placement Group', () => { */ it('can assign existing Placement Group during Linode Create flow', () => { const mockPlacementGroup = placementGroupFactory.build({ - is_compliant: true, label: 'pg-1-us-east', - placement_group_policy: 'strict', - placement_group_type: 'anti_affinity:local', region: mockRegions[0].id, + placement_group_type: 'anti_affinity:local', + placement_group_policy: 'strict', + is_compliant: true, }); const linodeLabel = 'linode-with-placement-group'; const mockLinode = linodeFactory.build({ id: randomNumber(), label: linodeLabel, + region: mockRegions[0].id, placement_group: { id: mockPlacementGroup.id, }, - region: mockRegions[0].id, }); mockGetPlacementGroups([mockPlacementGroup]).as('getPlacementGroups'); @@ -241,8 +242,9 @@ describe('Linode create flow with Placement Group', () => { // Confirm that mocked Placement Group is shown in the Autocomplete, and then select it. cy.findByText( `Placement Groups in ${mockNewarkRegion.label} (${mockNewarkRegion.id})` - ).click(); - cy.focused().type(`${mockPlacementGroup.label}`); + ) + .click() + .type(`${mockPlacementGroup.label}`); ui.autocompletePopper .findByTitle(mockPlacementGroup.label) .should('be.visible') @@ -250,9 +252,8 @@ describe('Linode create flow with Placement Group', () => { // Confirm the Placement group assignment is accounted for in the summary. cy.findByText('Assigned to Placement Group') - .as('qaAssigned') - .scrollIntoView(); - cy.get('@qaAssigned').should('be.visible'); + .scrollIntoView() + .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 8a42a655912..e114bf576de 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 @@ -1,15 +1,15 @@ -import { regionFactory } from '@linode/utilities'; 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, mockGetPlacementGroups, } from 'support/intercepts/placement-groups'; -import { mockGetRegions } from 'support/intercepts/regions'; -import { ui } from 'support/ui/'; import { randomLabel, randomNumber } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import { accountFactory, placementGroupFactory } from 'src/factories'; import { CANNOT_CHANGE_PLACEMENT_GROUP_POLICY_MESSAGE } from 'src/features/PlacementGroups/constants'; const mockAccount = accountFactory.build(); @@ -26,6 +26,9 @@ describe('Placement Group create flow', () => { */ it('can create Placement Group', () => { const mockRegions = regionFactory.buildList(5, { + placement_group_limits: { + maximum_pgs_per_customer: randomNumber(), + }, capabilities: [ 'Linodes', 'NodeBalancers', @@ -37,22 +40,19 @@ describe('Placement Group create flow', () => { 'Vlans', 'Premium Plans', ], - placement_group_limits: { - maximum_pgs_per_customer: randomNumber(), - }, }); const mockPlacementGroupRegion = chooseRegion({ - capabilities: ['Placement Group'], regions: mockRegions, + capabilities: ['Placement Group'], }); const mockPlacementGroup = placementGroupFactory.build({ - is_compliant: true, label: randomLabel(), - placement_group_policy: 'strict', - placement_group_type: 'anti_affinity:local', region: mockPlacementGroupRegion.id, + placement_group_type: 'anti_affinity:local', + placement_group_policy: 'strict', + is_compliant: true, }); const placementGroupLimitMessage = `Maximum placement groups in region: ${mockPlacementGroupRegion.placement_group_limits.maximum_pgs_per_customer}`; @@ -83,8 +83,9 @@ describe('Placement Group create flow', () => { // Enter label, select region, and submit form. cy.findByLabelText('Label').type(mockPlacementGroup.label); - cy.findByLabelText('Region').click(); - cy.focused().type(`${mockPlacementGroupRegion.label}{enter}`); + cy.findByLabelText('Region') + .click() + .type(`${mockPlacementGroupRegion.label}{enter}`); cy.findByText(placementGroupLimitMessage).should('be.visible'); cy.findByText(CANNOT_CHANGE_PLACEMENT_GROUP_POLICY_MESSAGE).should( diff --git a/packages/manager/cypress/e2e/core/placementGroups/delete-placement-groups.spec.ts b/packages/manager/cypress/e2e/core/placementGroups/delete-placement-groups.spec.ts index 1d7911d1280..f2e121e73f5 100644 --- a/packages/manager/cypress/e2e/core/placementGroups/delete-placement-groups.spec.ts +++ b/packages/manager/cypress/e2e/core/placementGroups/delete-placement-groups.spec.ts @@ -2,24 +2,26 @@ * @file Cypress integration tests for VM Placement Groups deletion flows. */ -import { linodeFactory } from '@linode/utilities'; import { mockGetAccount } from 'support/intercepts/account'; -import { mockGetLinodes } from 'support/intercepts/linodes'; import { mockDeletePlacementGroup, - mockDeletePlacementGroupError, mockGetPlacementGroup, mockGetPlacementGroups, mockUnassignPlacementGroupLinodes, + mockDeletePlacementGroupError, mockUnassignPlacementGroupLinodesError, } from 'support/intercepts/placement-groups'; -import { ui } from 'support/ui'; -import { buildArray } from 'support/util/arrays'; +import { + accountFactory, + linodeFactory, + placementGroupFactory, +} from 'src/factories'; +import { headers as emptyStatePageHeaders } from 'src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLandingEmptyStateData'; import { randomLabel, randomNumber } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; - -import { accountFactory, placementGroupFactory } from 'src/factories'; -import { headers as emptyStatePageHeaders } from 'src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLandingEmptyStateData'; +import { ui } from 'support/ui'; +import { buildArray } from 'support/util/arrays'; +import { mockGetLinodes } from 'support/intercepts/linodes'; // Mock an account with 'Placement Group' capability. const mockAccount = accountFactory.build(); @@ -54,10 +56,10 @@ describe('Placement Group deletion', () => { const mockPlacementGroupRegion = chooseRegion(); const mockPlacementGroup = placementGroupFactory.build({ id: randomNumber(), - is_compliant: true, label: randomLabel(), members: [], region: mockPlacementGroupRegion.id, + is_compliant: true, }); mockGetPlacementGroups([mockPlacementGroup]).as('getPlacementGroups'); @@ -141,8 +143,8 @@ describe('Placement Group deletion', () => { // Linodes that are assigned to the Placement Group being deleted. const mockPlacementGroupLinodes = buildArray(3, () => linodeFactory.build({ - id: randomNumber(), label: randomLabel(), + id: randomNumber(), region: mockPlacementGroupRegion.id, }) ); @@ -150,22 +152,22 @@ describe('Placement Group deletion', () => { // Placement Group that will be deleted. const mockPlacementGroup = placementGroupFactory.build({ id: randomNumber(), - is_compliant: true, label: randomLabel(), members: mockPlacementGroupLinodes.map((linode) => ({ - is_compliant: true, linode_id: linode.id, + is_compliant: true, })), region: mockPlacementGroupRegion.id, + is_compliant: true, }); // Second unrelated Placement Group to verify landing page content after deletion. const secondMockPlacementGroup = placementGroupFactory.build({ id: randomNumber(), - is_compliant: true, label: randomLabel(), members: [], region: mockPlacementGroupRegion.id, + is_compliant: true, }); mockGetLinodes(mockPlacementGroupLinodes).as('getLinodes'); @@ -315,10 +317,10 @@ describe('Placement Group deletion', () => { const mockPlacementGroupRegion = chooseRegion(); const mockPlacementGroup = placementGroupFactory.build({ id: randomNumber(), - is_compliant: true, label: randomLabel(), members: [], region: mockPlacementGroupRegion.id, + is_compliant: true, }); mockGetPlacementGroups([mockPlacementGroup]).as('getPlacementGroups'); @@ -417,8 +419,8 @@ describe('Placement Group deletion', () => { // Linodes that are assigned to the Placement Group being deleted. const mockPlacementGroupLinodes = buildArray(3, () => linodeFactory.build({ - id: randomNumber(), label: randomLabel(), + id: randomNumber(), region: mockPlacementGroupRegion.id, }) ); @@ -426,22 +428,22 @@ describe('Placement Group deletion', () => { // Placement Group that will be deleted. const mockPlacementGroup = placementGroupFactory.build({ id: randomNumber(), - is_compliant: true, label: randomLabel(), members: mockPlacementGroupLinodes.map((linode) => ({ - is_compliant: true, linode_id: linode.id, + is_compliant: true, })), region: mockPlacementGroupRegion.id, + is_compliant: true, }); // Second unrelated Placement Group to verify landing page content after deletion. const secondMockPlacementGroup = placementGroupFactory.build({ id: randomNumber(), - is_compliant: true, label: randomLabel(), members: [], region: mockPlacementGroupRegion.id, + is_compliant: true, }); mockGetLinodes(mockPlacementGroupLinodes).as('getLinodes'); diff --git a/packages/manager/cypress/e2e/core/placementGroups/placement-groups-landing-page.spec.ts b/packages/manager/cypress/e2e/core/placementGroups/placement-groups-landing-page.spec.ts index efe8709cf31..244f629f834 100644 --- a/packages/manager/cypress/e2e/core/placementGroups/placement-groups-landing-page.spec.ts +++ b/packages/manager/cypress/e2e/core/placementGroups/placement-groups-landing-page.spec.ts @@ -1,12 +1,14 @@ -import { linodeFactory } from '@linode/utilities'; -import { mockGetAccount } from 'support/intercepts/account'; -import { mockGetLinodes } from 'support/intercepts/linodes'; import { mockGetPlacementGroups } from 'support/intercepts/placement-groups'; import { ui } from 'support/ui'; +import { + accountFactory, + linodeFactory, + placementGroupFactory, +} from 'src/factories'; +import { mockGetAccount } from 'support/intercepts/account'; import { randomLabel, randomNumber } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; - -import { accountFactory, placementGroupFactory } from 'src/factories'; +import { mockGetLinodes } from 'support/intercepts/linodes'; const mockAccount = accountFactory.build(); @@ -47,31 +49,31 @@ describe('VM Placement landing page', () => { const mockPlacementGroupNoncompliantRegion = chooseRegion(); const mockPlacementGroupLinode = linodeFactory.build({ - id: randomNumber(), label: randomLabel(), + id: randomNumber(), region: mockPlacementGroupNoncompliantRegion.id, }); const mockPlacementGroupCompliant = placementGroupFactory.build({ id: randomNumber(), - is_compliant: true, label: randomLabel(), - members: [], - placement_group_policy: 'flexible', - placement_group_type: 'anti_affinity:local', region: mockPlacementGroupCompliantRegion.id, + placement_group_type: 'anti_affinity:local', + is_compliant: true, + placement_group_policy: 'flexible', + members: [], }); const mockPlacementGroupNoncompliant = placementGroupFactory.build({ id: randomNumber(), - is_compliant: false, label: randomLabel(), + region: mockPlacementGroupNoncompliantRegion.id, + placement_group_type: 'affinity:local', + is_compliant: false, + placement_group_policy: 'strict', members: [ - { is_compliant: false, linode_id: mockPlacementGroupLinode.id }, + { linode_id: mockPlacementGroupLinode.id, is_compliant: false }, ], - placement_group_policy: 'strict', - placement_group_type: 'affinity:local', - region: mockPlacementGroupNoncompliantRegion.id, }); const mockPlacementGroups = [ 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 d328c6444bb..2f25e62d576 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 @@ -1,4 +1,9 @@ -import { linodeFactory, regionFactory } from '@linode/utilities'; +import { + accountFactory, + linodeFactory, + placementGroupFactory, + regionFactory, +} from 'src/factories'; import { mockGetAccount } from 'support/intercepts/account'; import { mockGetLinodeDetails, @@ -17,9 +22,6 @@ 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 { accountFactory, placementGroupFactory } from 'src/factories'; - import type { Linode } from '@linode/api-v4'; const mockAccount = accountFactory.build(); @@ -69,10 +71,10 @@ describe('Placement Groups Linode assignment', () => { const mockLinode = mockLinodes[0]; const mockPlacementGroup = placementGroupFactory.build({ - is_compliant: true, label: randomLabel(), - members: [], region: mockPlacementGroupRegion.id, + members: [], + is_compliant: true, }); const mockPlacementGroupWithLinode = { @@ -188,22 +190,22 @@ describe('Placement Groups Linode assignment', () => { }); const mockPlacementGroup = placementGroupFactory.build({ - is_compliant: true, label: randomLabel(), members: [], - placement_group_policy: 'flexible', region: mockPlacementGroupRegion.id, + is_compliant: true, + placement_group_policy: 'flexible', }); const mockPlacementGroupAfterAssignment = { ...mockPlacementGroup, - is_compliant: false, members: [ { - is_compliant: false, linode_id: mockLinode.id, + is_compliant: false, }, ], + is_compliant: false, }; const complianceWarning = `Placement Group ${mockPlacementGroup.label} is non-compliant. We are working to resolve compliance issues so that you can continue assigning Linodes to this Placement Group.`; @@ -294,11 +296,11 @@ describe('Placement Groups Linode assignment', () => { }); const mockPlacementGroup = placementGroupFactory.build({ - is_compliant: true, label: randomLabel(), members: [], - placement_group_policy: 'strict', region: mockPlacementGroupRegion.id, + is_compliant: true, + placement_group_policy: 'strict', }); const complianceErrorMessage = `Assignment would break Placement Group's compliance, non compliant Linode IDs: [${mockLinode.id}]`; @@ -376,18 +378,18 @@ describe('Placement Groups Linode assignment', () => { const mockLinodeRemaining = mockLinodes[1]; const mockPlacementGroup = placementGroupFactory.build({ - is_compliant: true, label: randomLabel(), + region: mockPlacementGroupRegion.id, members: mockLinodes.map((linode: Linode) => ({ - is_compliant: true, linode_id: linode.id, + is_compliant: true, })), - region: mockPlacementGroupRegion.id, + is_compliant: true, }); const mockPlacementGroupAfterUnassignment = { ...mockPlacementGroup, - members: [{ is_compliant: true, linode_id: mockLinodeRemaining.id }], + members: [{ linode_id: mockLinodeRemaining.id, is_compliant: true }], }; mockGetRegions(mockRegions); diff --git a/packages/manager/cypress/e2e/core/placementGroups/placement-groups-navigation.spec.ts b/packages/manager/cypress/e2e/core/placementGroups/placement-groups-navigation.spec.ts index 084f1b6cf29..f863e7aa8cc 100644 --- a/packages/manager/cypress/e2e/core/placementGroups/placement-groups-navigation.spec.ts +++ b/packages/manager/cypress/e2e/core/placementGroups/placement-groups-navigation.spec.ts @@ -3,9 +3,8 @@ */ import { mockGetAccount } from 'support/intercepts/account'; -import { ui } from 'support/ui'; - import { accountFactory } from 'src/factories'; +import { ui } from 'support/ui'; const mockAccount = accountFactory.build(); diff --git a/packages/manager/cypress/e2e/core/placementGroups/update-placement-group-label.spec.ts b/packages/manager/cypress/e2e/core/placementGroups/update-placement-group-label.spec.ts index 2c45e3ddbd5..de6c903c887 100644 --- a/packages/manager/cypress/e2e/core/placementGroups/update-placement-group-label.spec.ts +++ b/packages/manager/cypress/e2e/core/placementGroups/update-placement-group-label.spec.ts @@ -2,18 +2,17 @@ * @file Integration tests for Placement Group update label flows. */ -import { mockGetAccount } from 'support/intercepts/account'; +import { randomLabel, randomNumber } from 'support/util/random'; import { mockGetPlacementGroup, mockGetPlacementGroups, mockUpdatePlacementGroup, mockUpdatePlacementGroupError, } from 'support/intercepts/placement-groups'; -import { ui } from 'support/ui'; -import { randomLabel, randomNumber } from 'support/util/random'; -import { chooseRegion } from 'support/util/regions'; - import { accountFactory, placementGroupFactory } from 'src/factories'; +import { mockGetAccount } from 'support/intercepts/account'; +import { chooseRegion } from 'support/util/regions'; +import { ui } from 'support/ui'; const mockAccount = accountFactory.build(); @@ -36,12 +35,12 @@ describe('Placement Group update label flow', () => { const mockPlacementGroup = placementGroupFactory.build({ id: randomNumber(), - is_compliant: true, label: randomLabel(), - members: [], - placement_group_policy: 'flexible', - placement_group_type: 'anti_affinity:local', region: mockPlacementGroupCompliantRegion.id, + placement_group_type: 'anti_affinity:local', + is_compliant: true, + placement_group_policy: 'flexible', + members: [], }); const mockPlacementGroupUpdated = { @@ -76,10 +75,8 @@ describe('Placement Group update label flow', () => { cy.findByText('Edit').should('be.visible'); cy.findByDisplayValue(mockPlacementGroup.label) .should('be.visible') - .click(); - cy.focused().type( - `{selectall}{backspace}${mockPlacementGroupUpdated.label}` - ); + .click() + .type(`{selectall}{backspace}${mockPlacementGroupUpdated.label}`); cy.findByText('Edit').should('be.visible').click(); @@ -105,12 +102,12 @@ describe('Placement Group update label flow', () => { const mockPlacementGroup = placementGroupFactory.build({ id: randomNumber(), - is_compliant: true, label: randomLabel(), - members: [], - placement_group_policy: 'flexible', - placement_group_type: 'anti_affinity:local', region: mockPlacementGroupCompliantRegion.id, + placement_group_type: 'anti_affinity:local', + is_compliant: true, + placement_group_policy: 'flexible', + members: [], }); const mockPlacementGroupUpdated = { @@ -143,10 +140,8 @@ describe('Placement Group update label flow', () => { cy.findByText('Edit').should('be.visible'); cy.findByDisplayValue(mockPlacementGroup.label) .should('be.visible') - .click(); - cy.focused().type( - `{selectall}{backspace}${mockPlacementGroupUpdated.label}` - ); + .click() + .type(`{selectall}{backspace}${mockPlacementGroupUpdated.label}`); cy.findByText('Edit').should('be.visible').click(); diff --git a/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts b/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts index a0e95b46b21..98992a90d5e 100644 --- a/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts +++ b/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts @@ -1,28 +1,26 @@ -import { createImage, getLinodeDisks, resizeLinodeDisk } from '@linode/api-v4'; -import { createLinodeRequestFactory } from '@linode/utilities'; import { authenticate } from 'support/api/authentication'; +import { + pollLinodeStatus, + pollImageStatus, + pollLinodeDiskSize, +} from 'support/util/polling'; +import { randomLabel, randomString, randomPhrase } from 'support/util/random'; import { interceptGetAccountAvailability } from 'support/intercepts/account'; -import { interceptGetAllImages } from 'support/intercepts/images'; -import { interceptCreateLinode } from 'support/intercepts/linodes'; import { interceptCreateStackScript, interceptGetStackScripts, } from 'support/intercepts/stackscripts'; +import { interceptCreateLinode } from 'support/intercepts/linodes'; import { ui } from 'support/ui'; +import { createLinodeRequestFactory } from 'src/factories'; +import { createImage, getLinodeDisks, resizeLinodeDisk } from '@linode/api-v4'; +import { chooseRegion, getRegionByLabel } from 'support/util/regions'; import { SimpleBackoffMethod } from 'support/util/backoff'; import { cleanUp } from 'support/util/cleanup'; import { createTestLinode } from 'support/util/linodes'; -import { - pollImageStatus, - pollLinodeDiskSize, - pollLinodeStatus, -} from 'support/util/polling'; -import { randomLabel, randomPhrase, randomString } from 'support/util/random'; -import { chooseRegion, getRegionByLabel } from 'support/util/regions'; - -import { getFilteredImagesForImageSelect } from 'src/components/ImageSelect/utilities'; - +import { interceptGetAllImages } from 'support/intercepts/images'; import type { Image } from '@linode/api-v4'; +import { getFilteredImagesForImageSelect } from 'src/components/ImageSelect/utilities'; // StackScript fixture paths. const stackscriptBasicPath = 'stackscripts/stackscript-basic.sh'; @@ -72,12 +70,14 @@ const fillOutStackscriptForm = ( // Fill out "StackScript Label", "Description", "Target Images", and "Script" fields. cy.findByLabelText(/^StackScript Label.*/) .should('be.visible') - .click(); - cy.focused().type(label); + .click() + .type(label); if (description) { - cy.findByLabelText('Description').should('be.visible').click(); - cy.focused().type(description); + cy.findByLabelText('Description') + .should('be.visible') + .click() + .type(description); } ui.autocomplete.findByLabel('Target Images').should('be.visible').click(); @@ -108,9 +108,11 @@ const fillOutLinodeForm = (label: string, regionName: string) => { .click(); ui.regionSelect.find().should('have.value', `${region.label} (${region.id})`); - cy.findByText('Linode Label').should('be.visible').click(); - cy.focused().type('{selectall}{backspace}'); - cy.focused().type(label); + cy.findByText('Linode Label') + .should('be.visible') + .click() + .type('{selectall}{backspace}') + .type(label); cy.findByText('Dedicated CPU').should('be.visible').click(); cy.get('[id="g6-dedicated-2"]').click(); @@ -131,11 +133,11 @@ const createLinodeAndImage = async () => { const resizedDiskSize = 2048; const linode = await createTestLinode( createLinodeRequestFactory.build({ - booted: false, label: randomLabel(), region: chooseRegion().id, root_pass: randomString(32), type: 'g6-nanode-1', + booted: false, }) ); @@ -265,12 +267,16 @@ describe('Create stackscripts', () => { // Fill out Linode creation form, confirm UDF fields behave as expected. fillOutLinodeForm(linodeLabel, linodeRegion.label); - cy.findByLabelText('Example Password').should('be.visible').click(); - cy.focused().type(randomString(32)); + cy.findByLabelText('Example Password') + .should('be.visible') + .click() + .type(randomString(32)); - cy.findByLabelText('Example Title').should('be.visible').click(); - cy.focused().type('{selectall}{backspace}'); - cy.focused().type(randomString(12)); + cy.findByLabelText('Example Title') + .should('be.visible') + .click() + .type('{selectall}{backspace}') + .type(randomString(12)); ui.button .findByTitle('Create Linode') @@ -361,16 +367,18 @@ describe('Create stackscripts', () => { filteredImageData?.forEach((imageSample: Image) => { const imageLabel = imageSample.label; cy.findAllByText(imageLabel, { exact: false }) - .as('qaImageLabel') .last() - .scrollIntoView(); - cy.get('@qaImageLabel').should('exist').should('be.visible'); + .scrollIntoView() + .should('exist') + .should('be.visible'); }); }); // Select private image. - cy.findByText(privateImage.label).as('qaPrivateImage').scrollIntoView(); - cy.get('@qaPrivateImage').should('be.visible').click(); + cy.findByText(privateImage.label) + .scrollIntoView() + .should('be.visible') + .click(); interceptCreateLinode().as('createLinode'); fillOutLinodeForm( diff --git a/packages/manager/cypress/e2e/core/stackscripts/delete-stackscripts.spec.ts b/packages/manager/cypress/e2e/core/stackscripts/delete-stackscripts.spec.ts index cd0aef158e3..4738d91e962 100644 --- a/packages/manager/cypress/e2e/core/stackscripts/delete-stackscripts.spec.ts +++ b/packages/manager/cypress/e2e/core/stackscripts/delete-stackscripts.spec.ts @@ -1,13 +1,11 @@ import { authenticate } from 'support/api/authentication'; +import { stackScriptFactory } from 'src/factories'; import { mockDeleteStackScript, - mockGetStackScript, mockGetStackScripts, } from 'support/intercepts/stackscripts'; import { ui } from 'support/ui'; -import { stackScriptFactory } from 'src/factories'; - authenticate(); describe('Delete stackscripts', () => { /* @@ -21,9 +19,6 @@ describe('Delete stackscripts', () => { is_public: false, }); mockGetStackScripts(stackScripts).as('getStackScripts'); - mockGetStackScript(stackScripts[0].id, stackScripts[0]).as( - 'getStackScript' - ); cy.visitWithLogin('/stackscripts/account'); cy.wait('@getStackScripts'); @@ -37,7 +32,6 @@ describe('Delete stackscripts', () => { .click(); }); ui.actionMenuItem.findByTitle('Delete').should('be.visible').click(); - cy.wait('@getStackScript'); ui.dialog .findByTitle(`Delete StackScript ${stackScripts[0].label}?`) .should('be.visible') @@ -63,9 +57,6 @@ describe('Delete stackscripts', () => { }); mockDeleteStackScript(stackScripts[0].id).as('deleteStackScript'); mockGetStackScripts([stackScripts[1]]).as('getUpdatedStackScripts'); - mockGetStackScript(stackScripts[1].id, stackScripts[1]).as( - 'getUpdatedStackScript' - ); ui.actionMenuItem.findByTitle('Delete').should('be.visible').click(); ui.dialog .findByTitle(`Delete StackScript ${stackScripts[0].label}?`) @@ -78,6 +69,7 @@ describe('Delete stackscripts', () => { }); cy.wait('@deleteStackScript'); cy.wait('@getUpdatedStackScripts'); + cy.findByText(stackScripts[0].label).should('not.exist'); // The "Automate Deployment with StackScripts!" welcome page appears when no StackScript exists. @@ -92,7 +84,6 @@ describe('Delete stackscripts', () => { mockDeleteStackScript(stackScripts[1].id).as('deleteStackScript'); mockGetStackScripts([]).as('getUpdatedStackScripts'); ui.actionMenuItem.findByTitle('Delete').should('be.visible').click(); - cy.wait('@getUpdatedStackScript'); ui.dialog .findByTitle(`Delete StackScript ${stackScripts[1].label}?`) .should('be.visible') 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 2407527532e..e87b60bb6f2 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 @@ -1,4 +1,9 @@ -import { getProfile } from '@linode/api-v4'; +import type { StackScript } from '@linode/api-v4'; +import { Profile, getProfile } from '@linode/api-v4'; + +import { stackScriptFactory } from 'src/factories'; +import { formatDate } from 'src/utilities/formatDate'; + import { authenticate } from 'support/api/authentication'; import { interceptCreateLinode } from 'support/intercepts/linodes'; import { mockGetUserPreferences } from 'support/intercepts/profile'; @@ -12,19 +17,16 @@ import { cleanUp } from 'support/util/cleanup'; import { randomLabel, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import { stackScriptFactory } from 'src/factories'; -import { formatDate } from 'src/utilities/formatDate'; - -import type { Profile, StackScript } from '@linode/api-v4'; - const mockStackScripts: StackScript[] = [ stackScriptFactory.build({ - created: '2019-05-23T16:21:41', - deployments_active: 238, - deployments_total: 4400, + id: 443929, + username: 'litespeed', + user_gravatar_id: 'f7360fb588b5f65d81ee1afe11b6c0ad', + label: 'OpenLiteSpeed-WordPress', description: 'Blazing-fast WordPress with LSCache, 300+ times faster than regular WordPress\n\nOpenLiteSpeed is the Open Source edition of LiteSpeed Web Server Enterprise and contains all of the essential features. OLS provides enormous scalability, and an accelerated hosting platform for WordPress. \n\nWhole process maybe take up to 10 minutes to finish. ', - id: 443929, + ordinal: 0, + logo_url: '', images: [ 'linode/centos7', 'linode/debian9', @@ -41,47 +43,45 @@ const mockStackScripts: StackScript[] = [ 'linode/almalinux9', 'linode/rocky9', ], + deployments_total: 4400, + deployments_active: 238, is_public: true, - label: 'OpenLiteSpeed-WordPress', - logo_url: '', mine: false, - ordinal: 0, + created: '2019-05-23T16:21:41', + updated: '2023-08-22T16:41:48', rev_note: 'add more OS', script: '#!/bin/bash\n### linode\n### Install OpenLiteSpeed and WordPress\nbash <( curl -sk https://raw.githubusercontent.com/litespeedtech/ls-cloud-image/master/Setup/wpimgsetup.sh )\n### Regenerate password for Web Admin, Database, setup Welcome Message\nbash <( curl -sk https://raw.githubusercontent.com/litespeedtech/ls-cloud-image/master/Cloud-init/per-instance.sh )\n### Reboot server\nreboot\n', - updated: '2023-08-22T16:41:48', user_defined_fields: [], - user_gravatar_id: 'f7360fb588b5f65d81ee1afe11b6c0ad', - username: 'litespeed', }), stackScriptFactory.build({ - created: '2017-02-07T02:28:49', - deployments_active: 13, - deployments_total: 35469, - description: 'Auto setup Squid Proxy Server on Ubuntu 16.04 LTS', id: 68166, - images: ['linode/ubuntu16.04lts'], - is_public: true, + username: 'serverok', + user_gravatar_id: '8c2562f63286df4f8aae5babe5920ade', label: 'Squid Proxy Server', + description: 'Auto setup Squid Proxy Server on Ubuntu 16.04 LTS', + ordinal: 0, logo_url: '', + images: ['linode/ubuntu16.04lts'], + deployments_total: 35469, + deployments_active: 13, + is_public: true, mine: false, - ordinal: 0, + created: '2017-02-07T02:28:49', + updated: '2023-08-07T02:34:15', rev_note: 'Initial import', script: '#!/bin/bash\n# \n# \n# Squid Proxy Server\n# Author: admin@serverok.in\n# Blog: https://www.serverok.in\n\n\n/usr/bin/apt update\n/usr/bin/apt -y install apache2-utils squid3\n\n/usr/bin/htpasswd -b -c /etc/squid/passwd $SQUID_USER $SQUID_PASSWORD\n\n/bin/rm -f /etc/squid/squid.conf\n/usr/bin/touch /etc/squid/blacklist.acl\n/usr/bin/wget --no-check-certificate -O /etc/squid/squid.conf https://raw.githubusercontent.com/hostonnet/squid-proxy-installer/master/squid.conf\n\n/sbin/iptables -I INPUT -p tcp --dport 3128 -j ACCEPT\n/sbin/iptables-save\n\nservice squid restart\nupdate-rc.d squid defaults', - updated: '2023-08-07T02:34:15', user_defined_fields: [ { - label: 'Proxy Username', name: 'squid_user', + label: 'Proxy Username', }, { - label: 'Proxy Password', name: 'squid_password', + label: 'Proxy Password', }, ], - user_gravatar_id: '8c2562f63286df4f8aae5babe5920ade', - username: 'serverok', }), ]; @@ -110,13 +110,13 @@ describe('Community Stackscripts integration tests', () => { cy.defer(getProfile, 'getting profile').then((profile: Profile) => { const dateFormatOptionsLanding = { - displayTime: false, timezone: profile.timezone, + displayTime: false, }; const dateFormatOptionsDetails = { - displayTime: true, timezone: profile.timezone, + displayTime: true, }; const updatedTimeLanding = formatDate( @@ -137,10 +137,9 @@ describe('Community Stackscripts integration tests', () => { // Search the corresponding community stack script mockGetStackScripts([stackScript]).as('getFilteredStackScripts'); - cy.findByPlaceholderText( - 'Search by Label, Username, or Description' - ).click(); - cy.focused().type(`${stackScript.label}{enter}`); + cy.findByPlaceholderText('Search by Label, Username, or Description') + .click() + .type(`${stackScript.label}{enter}`); cy.wait('@getFilteredStackScripts'); // Check filtered results @@ -190,7 +189,7 @@ describe('Community Stackscripts integration tests', () => { * - Confirms that pagination works as expected. */ it('pagination works with infinite scrolling', () => { - cy.tag('method:e2e', 'env:stackScripts'); + cy.tag('method:e2e'); interceptGetStackScripts().as('getStackScripts'); // Fetch all public Images to later use while filtering StackScripts. @@ -236,7 +235,7 @@ describe('Community Stackscripts integration tests', () => { * - Confirms that search can filter the expected results. */ it('search function filters results correctly', () => { - cy.tag('method:e2e', 'env:stackScripts'); + cy.tag('method:e2e'); const stackScript = mockStackScripts[0]; interceptGetStackScripts().as('getStackScripts'); @@ -252,10 +251,9 @@ describe('Community Stackscripts integration tests', () => { cy.get('tr').then((value) => { const rowCount = Cypress.$(value).length - 1; // Remove the table title row - cy.findByPlaceholderText( - 'Search by Label, Username, or Description' - ).click(); - cy.focused().type(`${stackScript.label}{enter}`); + cy.findByPlaceholderText('Search by Label, Username, or Description') + .click() + .type(`${stackScript.label}{enter}`); cy.get(`[data-qa-table-row="${stackScript.label}"]`).should('be.visible'); cy.get('tr').its('length').should('be.lt', rowCount); @@ -267,7 +265,6 @@ describe('Community Stackscripts integration tests', () => { * - Confirms that the deployment flow works. */ it('deploys a new linode as expected', () => { - cy.tag('method:e2e', 'env:stackScripts'); const stackScriptId = '37239'; const stackScriptName = 'setup-ipsec-vpn'; const sharedKey = randomString(); @@ -288,10 +285,9 @@ describe('Community Stackscripts integration tests', () => { cy.visitWithLogin('/stackscripts/community'); cy.wait(['@getStackScripts', '@getPreferences']); - cy.findByPlaceholderText( - 'Search by Label, Username, or Description' - ).click(); - cy.focused().type(`${stackScriptName}{enter}`); + cy.findByPlaceholderText('Search by Label, Username, or Description') + .click() + .type(`${stackScriptName}{enter}`); cy.get(`[data-qa-table-row="${stackScriptName}"]`) .should('be.visible') .within(() => { @@ -329,12 +325,18 @@ describe('Community Stackscripts integration tests', () => { ); // Input VPN information - cy.get('[id="ipsec-pre-shared-key"]').should('be.visible').click(); - cy.focused().type(`${sharedKey}{enter}`); - cy.get('[id="vpn-username"]').should('be.visible').click(); - cy.focused().type(`${vpnUser}{enter}`); - cy.get('[id="vpn-password"]').should('be.visible').click(); - cy.focused().type(`${vpnPassword}{enter}`); + cy.get('[id="ipsec-pre-shared-key"]') + .should('be.visible') + .click() + .type(`${sharedKey}{enter}`); + cy.get('[id="vpn-username"]') + .should('be.visible') + .click() + .type(`${vpnUser}{enter}`); + cy.get('[id="vpn-password"]') + .should('be.visible') + .click() + .type(`${vpnPassword}{enter}`); // Check each field should persist when moving onto another field cy.get('[id="ipsec-pre-shared-key"]').should('have.value', sharedKey); @@ -342,8 +344,10 @@ describe('Community Stackscripts integration tests', () => { cy.get('[id="vpn-password"]').should('have.value', vpnPassword); // Choose an image - cy.findByPlaceholderText('Choose an image').should('be.visible').click(); - cy.focused().type(image); + cy.findByPlaceholderText('Choose an image') + .should('be.visible') + .click() + .type(image); ui.autocompletePopper.findByTitle(image).should('be.visible').click(); cy.findByText(image).should('be.visible').click(); @@ -356,8 +360,7 @@ describe('Community Stackscripts integration tests', () => { .click(); // An error message shows up when no region is selected cy.contains('Region is required.').should('be.visible'); - ui.regionSelect.find().click(); - cy.focused().type(`${region.id}{enter}`); + ui.regionSelect.find().click().type(`${region.id}{enter}`); // Choose a plan ui.button @@ -367,9 +370,11 @@ describe('Community Stackscripts integration tests', () => { .click(); // Enter a label. - cy.findByText('Linode Label').should('be.visible').click(); - cy.focused().type('{selectAll}{backspace}'); - cy.focused().type(linodeLabel); + cy.findByText('Linode Label') + .should('be.visible') + .click() + .type('{selectAll}{backspace}') + .type(linodeLabel); // An error message shows up when no region is selected cy.contains('Plan is required.').should('be.visible'); @@ -381,8 +386,7 @@ describe('Community Stackscripts integration tests', () => { // Input root password // Weak or fair root password cannot rebuild the linode - cy.get('[id="root-password"]').clear(); - cy.focused().type(weakPassword); + cy.get('[id="root-password"]').clear().type(weakPassword); ui.button .findByTitle('Create Linode') .should('be.visible') @@ -393,8 +397,7 @@ describe('Community Stackscripts integration tests', () => { 'be.visible' ); - cy.get('[id="root-password"]').clear(); - cy.focused().type(fairPassword); + cy.get('[id="root-password"]').clear().type(fairPassword); ui.button .findByTitle('Create Linode') .should('be.visible') diff --git a/packages/manager/cypress/e2e/core/stackscripts/smoke-stackscripts-landing-page.spec.ts b/packages/manager/cypress/e2e/core/stackscripts/smoke-stackscripts-landing-page.spec.ts index 8c57536150b..ee132f35ea3 100644 --- a/packages/manager/cypress/e2e/core/stackscripts/smoke-stackscripts-landing-page.spec.ts +++ b/packages/manager/cypress/e2e/core/stackscripts/smoke-stackscripts-landing-page.spec.ts @@ -1,9 +1,8 @@ import { authenticate } from 'support/api/authentication'; +import { stackScriptFactory } from 'src/factories'; import { mockGetStackScripts } from 'support/intercepts/stackscripts'; import { ui } from 'support/ui'; -import { stackScriptFactory } from 'src/factories'; - authenticate(); describe('Display stackscripts', () => { /* diff --git a/packages/manager/cypress/e2e/core/stackscripts/update-stackscripts.spec.ts b/packages/manager/cypress/e2e/core/stackscripts/update-stackscripts.spec.ts index f3bee63a668..02b4274fc2a 100644 --- a/packages/manager/cypress/e2e/core/stackscripts/update-stackscripts.spec.ts +++ b/packages/manager/cypress/e2e/core/stackscripts/update-stackscripts.spec.ts @@ -1,17 +1,17 @@ -import { getImages } from '@linode/api-v4'; -import { stackScriptFactory } from '@src/factories'; import { authenticate } from 'support/api/authentication'; +import { randomLabel, randomPhrase } from 'support/util/random'; import { mockGetStackScript, - mockGetStackScripts, mockUpdateStackScript, mockUpdateStackScriptError, + mockGetStackScripts, } from 'support/intercepts/stackscripts'; import { ui } from 'support/ui'; +import { stackScriptFactory } from '@src/factories'; +import { getImages, StackScript } from '@linode/api-v4'; import { depaginate } from 'support/util/paginate'; -import { randomLabel, randomPhrase } from 'support/util/random'; -import type { Image, StackScript } from '@linode/api-v4'; +import type { Image } from '@linode/api-v4'; // StackScript fixture paths. const stackscriptNoShebangPath = 'stackscripts/stackscript-no-shebang.sh'; @@ -60,14 +60,16 @@ const fillOutStackscriptForm = ( // Fill out "StackScript Label", "Description", "Target Images", and "Script" fields. cy.findByLabelText(/^StackScript Label.*/) .should('be.visible') - .click(); - cy.focused().clear(); - cy.focused().type(label); + .click() + .clear() + .type(label); if (description) { - cy.findByLabelText('Description').should('be.visible').click(); - cy.focused().clear(); - cy.focused().type(description); + cy.findByLabelText('Description') + .should('be.visible') + .click() + .clear() + .type(description); } ui.autocomplete.findByLabel('Target Images').should('be.visible').click(); @@ -108,8 +110,8 @@ describe('Update stackscripts', () => { // Spread operator clones an object... { ...stackScripts[0], - description: stackscriptDesc, label: stackscriptLabel, + description: stackscriptDesc, }, { ...stackScripts[1] }, ]; @@ -210,9 +212,6 @@ describe('Update stackscripts', () => { is_public: false, }); mockGetStackScripts(stackScripts).as('getStackScripts'); - mockGetStackScript(stackScripts[0].id, stackScripts[0]).as( - 'getStackScript' - ); cy.visitWithLogin('/stackscripts/account'); cy.wait('@getStackScripts'); @@ -254,7 +253,6 @@ describe('Update stackscripts', () => { .findByTitle('Make StackScript Public') .should('be.visible') .click(); - cy.wait('@getStackScript'); const updatedStackScript = { ...stackScripts[0] }; updatedStackScript.is_public = true; mockUpdateStackScript(updatedStackScript.id, updatedStackScript).as( 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 17d552e1620..41c53259a98 100644 --- a/packages/manager/cypress/e2e/core/volumes/attach-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/attach-volume.spec.ts @@ -1,20 +1,18 @@ import { createVolume } from '@linode/api-v4/lib/volumes'; -import { createLinodeRequestFactory } from '@linode/utilities'; +import { Linode, Volume } from '@linode/api-v4'; +import { createLinodeRequestFactory } from 'src/factories/linodes'; +import { volumeRequestPayloadFactory } from 'src/factories/volume'; import { authenticate } from 'support/api/authentication'; -import { interceptGetLinodeConfigs } from 'support/intercepts/configs'; import { interceptAttachVolume, // interceptDetachVolume, } from 'support/intercepts/volumes'; +import { randomLabel, randomString } from 'support/util/random'; import { ui } from 'support/ui'; +import { chooseRegion } from 'support/util/regions'; +import { interceptGetLinodeConfigs } from 'support/intercepts/configs'; import { cleanUp } from 'support/util/cleanup'; import { createTestLinode } from 'support/util/linodes'; -import { randomLabel, randomString } from 'support/util/random'; -import { chooseRegion } from 'support/util/regions'; - -import { volumeRequestPayloadFactory } from 'src/factories/volume'; - -import type { Linode, Volume } from '@linode/api-v4'; // 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. @@ -68,10 +66,10 @@ describe('volume attach and detach flows', () => { }); const linodeRequest = createLinodeRequestFactory.build({ - booted: false, label: randomLabel(), region: commonRegion.id, root_pass: randomString(32), + booted: false, }); const entityPromise = Promise.all([ @@ -102,8 +100,10 @@ describe('volume attach and detach flows', () => { .click(); ui.drawer.findByTitle(`Attach Volume ${volume.label}`).within(() => { - cy.findByLabelText('Linode').should('be.visible').click(); - cy.focused().type(linode.label); + cy.findByLabelText('Linode') + .should('be.visible') + .click() + .type(linode.label); ui.autocompletePopper .findByTitle(linode.label) 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 8288734266a..146233cae6c 100644 --- a/packages/manager/cypress/e2e/core/volumes/clone-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/clone-volume.spec.ts @@ -1,13 +1,11 @@ +import { Volume } from '@linode/api-v4'; +import { volumeRequestPayloadFactory } from 'src/factories/volume'; import { authenticate } from 'support/api/authentication'; -import { createActiveVolume } from 'support/api/volumes'; import { interceptCloneVolume } from 'support/intercepts/volumes'; import { cleanUp } from 'support/util/cleanup'; import { randomLabel } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; - -import { volumeRequestPayloadFactory } from 'src/factories/volume'; - -import type { Volume } from '@linode/api-v4'; +import { createActiveVolume } from 'support/api/volumes'; // 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. @@ -64,8 +62,7 @@ describe('volume clone flow', () => { .closest('[data-qa-drawer="true"]') .should('be.visible') .within(() => { - cy.findByText('Label').click(); - cy.focused().type(cloneVolumeLabel); + cy.findByText('Label').click().type(cloneVolumeLabel); cy.get('[data-qa-buttons="true"]').within(() => { cy.findByText('Clone Volume').should('be.visible').click(); }); diff --git a/packages/manager/cypress/e2e/core/volumes/create-volume-encryption.spec.ts b/packages/manager/cypress/e2e/core/volumes/create-volume-encryption.spec.ts deleted file mode 100644 index bfd3af1a891..00000000000 --- a/packages/manager/cypress/e2e/core/volumes/create-volume-encryption.spec.ts +++ /dev/null @@ -1,331 +0,0 @@ -/** - * @file UI tests involving Volume creation with Block Storage Encryption functionality. - */ -import { linodeFactory, regionFactory } from '@linode/utilities'; -import { mockGetAccount } from 'support/intercepts/account'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; -import { - mockGetLinodeDetails, - mockGetLinodeDisks, - mockGetLinodeVolumes, - mockGetLinodes, -} from 'support/intercepts/linodes'; -import { mockGetRegions } from 'support/intercepts/regions'; -import { mockGetVolume, mockGetVolumes } from 'support/intercepts/volumes'; -import { ui } from 'support/ui'; -import { randomLabel } from 'support/util/random'; - -import { - accountFactory, - linodeDiskFactory, - volumeFactory, -} from 'src/factories'; - -import type { Linode } from '@linode/api-v4'; - -/** - * Notice text that is expected to appear upon attempting to attach encrypted Volume to Linode without BSE capability. - */ -const CLIENT_LIBRARY_UPDATE_COPY = - 'This Linode requires a client library update and will need to be rebooted prior to attaching an encrypted volume.'; - -describe('Volume creation with Block Storage Encryption', () => { - describe('Reboot notice', () => { - const mockAccount = accountFactory.build({ - capabilities: ['Linodes', 'Block Storage Encryption'], - }); - - const mockRegion = regionFactory.build({ - capabilities: ['Linodes', 'Block Storage Encryption', 'Block Storage'], - }); - - const mockLinodeWithoutCapability = linodeFactory.build({ - capabilities: [], - label: randomLabel(), - region: mockRegion.id, - }); - - const mockLinodeWithCapability: Linode = { - ...mockLinodeWithoutCapability, - capabilities: ['Block Storage Encryption'], - }; - - const mockVolumeEncrypted = volumeFactory.build({ - encryption: 'enabled', - label: randomLabel(), - region: mockRegion.id, - }); - - /* - * Tests that confirm that the Linode reboot notice appears when expected. - * - * Some Linodes lack the capability to support Block Storage Encryption. The - * capability can be added, however, by rebooting the affected Linode(s) to - * allow a client library update to take place that enables support for - * encryption. - * - * These tests confirm that users are informed of this requirement and - * are prevented from completing Volume create/attach flows when this - * requirement is not met. - */ - describe('Notice is shown when expected', () => { - beforeEach(() => { - mockAppendFeatureFlags({ - blockStorageEncryption: true, - }); - mockGetAccount(mockAccount); - mockGetRegions([mockRegion]); - mockGetLinodes([mockLinodeWithoutCapability]); - mockGetLinodeDetails( - mockLinodeWithoutCapability.id, - mockLinodeWithoutCapability - ); - mockGetLinodeVolumes(mockLinodeWithoutCapability.id, []); - mockGetLinodeDisks(mockLinodeWithoutCapability.id, [ - linodeDiskFactory.build(), - ]); - }); - - /* - * - Confirms notice appears when creating and attaching a new Volume via the Volume create page. - * - Confirms submit button is disabled while notice is present. - */ - it('shows notice on Volume create page when attaching new Volume to Linode without BSE', () => { - mockGetVolumes([]); - cy.visitWithLogin('/volumes/create'); - - // Select a region, then select a Linode that does not have the BSE capability. - ui.autocomplete.findByLabel('Region').type(mockRegion.label); - - ui.regionSelect.findItemByRegionId(mockRegion.id, [mockRegion]).click(); - - ui.autocomplete - .findByLabel('Linode') - .type(mockLinodeWithoutCapability.label); - - ui.autocompletePopper.find().within(() => { - cy.findByText(mockLinodeWithoutCapability.label).click(); - }); - - // Confirm that reboot notice is absent before clicking the "Encrypt Volume" checkbox. - cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('not.exist'); - - cy.findByText('Encrypt Volume').should('be.visible').click(); - - // Confirm that reboot notice appears after clicking the "Encrypt Volume" checkbox, - // and that Volume create submit button remains disabled. - cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('be.visible'); - - ui.button - .findByTitle('Create Volume') - .scrollIntoView() - .should('be.visible') - .should('be.disabled'); - }); - - /* - * - Confirms notice appears when attaching an existing Volume via the Linode details page. - * - Confirms submit button is disabled while notice is present. - */ - it('shows notice on Linode details page when attaching existing Volume to Linode without BSE', () => { - mockGetVolumes([mockVolumeEncrypted]); - mockGetVolume(mockVolumeEncrypted); - cy.visitWithLogin(`/linodes/${mockLinodeWithoutCapability.id}/storage`); - - ui.button - .findByTitle('Add Volume') - .should('be.visible') - .should('be.enabled') - .click(); - - ui.drawer - .findByTitle(`Create Volume for ${mockLinodeWithoutCapability.label}`) - .should('be.visible') - .within(() => { - cy.findByText('Attach Existing Volume') - .should('be.visible') - .click(); - - // Confirm that reboot notice is absent before Volume is selected. - cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('not.exist'); - - ui.autocomplete - .findByLabel('Volume') - .type(mockVolumeEncrypted.label); - - ui.autocompletePopper.find().within(() => { - cy.findByText(mockVolumeEncrypted.label) - .should('be.visible') - .click(); - }); - - // Confirm that selecting an encrypted Volume triggers reboot notice to appear. - cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('be.visible'); - ui.button - .findByTitle('Attach Volume') - .scrollIntoView() - .should('be.disabled'); - }); - }); - - /* - * - Confirms notice appears when creating and attaching a new Volume via the Linode details page. - * - Confirms submit button is disabled while notice is present. - */ - it('shows notice on Linode details page when creating new Volume and attaching to Linode without BSE', () => { - mockGetVolumes([]); - cy.visitWithLogin(`/linodes/${mockLinodeWithoutCapability.id}/storage`); - - ui.button - .findByTitle('Add Volume') - .should('be.visible') - .should('be.enabled') - .click(); - - ui.drawer - .findByTitle(`Create Volume for ${mockLinodeWithoutCapability.label}`) - .should('be.visible') - .within(() => { - cy.findByLabelText('Create and Attach Volume').should('be.checked'); - - // Confirm that reboot notice is absent before encryption is selected. - cy.findByLabelText('Encrypt Volume').should('not.be.checked'); - cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('not.exist'); - - // Click the "Encrypt Volume" checkbox and confirm that notice appears. - cy.findByText('Encrypt Volume').should('be.visible').click(); - - cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('be.visible'); - ui.button - .findByTitle('Create Volume') - .scrollIntoView() - .should('be.disabled'); - }); - }); - }); - - /* - * Tests that confirm that the Linode reboot notice is not shown when it shouldn't be. - * - * These tests confirm that users are not shown the Linode reboot notice when attaching - * encrypted Volumes to Linodes that already have the block storage encryption capability, - * and they are not prevented from attaching Volumes to Linodes in these cases. - */ - describe('Reboot notice is absent when expected', () => { - beforeEach(() => { - mockAppendFeatureFlags({ - blockStorageEncryption: true, - }); - mockGetAccount(mockAccount); - mockGetRegions([mockRegion]); - mockGetLinodes([mockLinodeWithCapability]); - mockGetLinodeDetails( - mockLinodeWithCapability.id, - mockLinodeWithCapability - ); - mockGetLinodeVolumes(mockLinodeWithCapability.id, []); - mockGetLinodeDisks(mockLinodeWithCapability.id, [ - linodeDiskFactory.build(), - ]); - }); - - /* - * - Confirms notice appears is absent when creating and attaching a new Volume via the Volume create page. - */ - it('does not show notice on Volume create page when attaching new Volume to Linode with BSE', () => { - mockGetVolumes([]); - cy.visitWithLogin('/volumes/create'); - - // Select a region, then select a Linode that has the BSE capability. - ui.autocomplete.findByLabel('Region').type(mockRegion.label); - - ui.regionSelect.findItemByRegionId(mockRegion.id, [mockRegion]).click(); - - ui.autocomplete - .findByLabel('Linode') - .type(mockLinodeWithCapability.label); - - ui.autocompletePopper.find().within(() => { - cy.findByText(mockLinodeWithCapability.label).click(); - }); - - cy.findByText('Encrypt Volume').should('be.visible').click(); - - // Confirm that reboot notice is absent after checking "Encrypt Volume", - // and the "Create Volume" button is enabled. - cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('not.exist'); - - ui.button.findByTitle('Create Volume').should('be.enabled'); - }); - - /* - * - Confirms notice is absent when attaching an existing Volume via the Linode details page. - */ - it('does not show notice on Linode details page when attaching existing Volume to Linode with BSE', () => { - mockGetVolumes([mockVolumeEncrypted]); - mockGetVolume(mockVolumeEncrypted); - cy.visitWithLogin(`/linodes/${mockLinodeWithCapability.id}/storage`); - - ui.button - .findByTitle('Add Volume') - .should('be.visible') - .should('be.enabled') - .click(); - - ui.drawer - .findByTitle(`Create Volume for ${mockLinodeWithCapability.label}`) - .should('be.visible') - .within(() => { - cy.findByText('Attach Existing Volume') - .should('be.visible') - .click(); - - ui.autocomplete - .findByLabel('Volume') - .type(mockVolumeEncrypted.label); - - ui.autocompletePopper.find().within(() => { - cy.findByText(mockVolumeEncrypted.label) - .should('be.visible') - .click(); - }); - - // Confirm that reboot notice is absent and submit button is enabled. - cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('not.exist'); - ui.button.findByTitle('Attach Volume').should('be.enabled'); - }); - }); - - /* - * - Confirms notice is absent when creating and attaching a new Volume via the Linode details page. - */ - it('does not show notice on Linode details page when creating new Volume and attaching to Linode with BSE', () => { - mockGetVolumes([]); - cy.visitWithLogin(`/linodes/${mockLinodeWithCapability.id}/storage`); - - ui.button - .findByTitle('Add Volume') - .should('be.visible') - .should('be.enabled') - .click(); - - ui.drawer - .findByTitle(`Create Volume for ${mockLinodeWithCapability.label}`) - .should('be.visible') - .within(() => { - cy.findByLabelText('Create and Attach Volume').should('be.checked'); - - // Confirm that reboot notice is absent before encryption is selected. - cy.findByLabelText('Encrypt Volume').should('not.be.checked'); - cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('not.exist'); - - // Click the "Encrypt Volume" checkbox and confirm that notice appears. - cy.findByText('Encrypt Volume').should('be.visible').click(); - - cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('not.exist'); - ui.button.findByTitle('Create Volume').should('be.enabled'); - }); - }); - }); - }); -}); diff --git a/packages/manager/cypress/e2e/core/volumes/create-volume.smoke.spec.ts b/packages/manager/cypress/e2e/core/volumes/create-volume.smoke.spec.ts index df637091da3..0ca5c8ce4e2 100644 --- a/packages/manager/cypress/e2e/core/volumes/create-volume.smoke.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/create-volume.smoke.spec.ts @@ -1,29 +1,32 @@ /* eslint-disable sonarjs/no-duplicate-string */ -import { linodeFactory } from '@linode/utilities'; -import { volumeFactory, volumeTypeFactory } from '@src/factories'; import { + volumeFactory, + linodeFactory, + volumeTypeFactory, +} from '@src/factories'; +import { + mockGetLinodes, mockGetLinodeDetails, - mockGetLinodeDisks, mockGetLinodeVolumes, - mockGetLinodes, } from 'support/intercepts/linodes'; import { mockCreateVolume, - mockDetachVolume, mockGetVolume, - mockGetVolumeTypes, - mockGetVolumeTypesError, mockGetVolumes, + mockDetachVolume, + mockGetVolumeTypesError, + mockGetVolumeTypes, } from 'support/intercepts/volumes'; -import { ui } from 'support/ui'; import { randomLabel, randomNumber } from 'support/util/random'; -import { chooseRegion } from 'support/util/regions'; +import { ui } from 'support/ui'; import { PRICES_RELOAD_ERROR_NOTICE_TEXT, UNKNOWN_PRICE, } from 'src/utilities/pricing/constants'; +const region = 'US, Newark, NJ'; + /** * Asserts that a volume is listed and has the expected config information. * @@ -35,7 +38,6 @@ import { */ const validateBasicVolume = ( volumeLabel: string, - regionLabel: string, attachedLinodeLabel?: string ) => { const attached = attachedLinodeLabel ?? 'Unattached'; @@ -51,7 +53,7 @@ const validateBasicVolume = ( cy.findByText(volumeLabel) .closest('tr') .within(() => { - cy.findByText(regionLabel).should('be.visible'); + cy.findByText(region).should('be.visible'); cy.findByText(attached).should('be.visible'); }); }; @@ -60,14 +62,14 @@ const validateBasicVolume = ( // This is a workaround for accounts that have volumes unrelated to tests. // @TODO Remove preference override when volumes are removed from test accounts. const preferenceOverrides = { - desktop_sidebar_open: false, - linodes_group_by_tag: false, linodes_view_style: 'list', + linodes_group_by_tag: false, + volumes_group_by_tag: false, + desktop_sidebar_open: false, sortKeys: { 'linodes-landing': { order: 'asc', orderBy: 'label' }, volume: { order: 'desc', orderBy: 'label' }, }, - volumes_group_by_tag: false, }; // Local storage override to force volume table to list up to 100 items. @@ -79,11 +81,7 @@ const localStorageOverrides = { describe('volumes', () => { it('creates a volume without linode from volumes page', () => { - const mockRegion = chooseRegion({ capabilities: ['Block Storage'] }); - const mockVolume = volumeFactory.build({ - label: randomLabel(), - region: mockRegion.id, - }); + const mockVolume = volumeFactory.build({ label: randomLabel() }); const mockVolumeTypes = volumeTypeFactory.buildList(1); mockGetVolumes([]).as('getVolumes'); @@ -92,8 +90,8 @@ describe('volumes', () => { mockGetVolumeTypes(mockVolumeTypes).as('getVolumeTypes'); cy.visitWithLogin('/volumes', { - localStorageOverrides, preferenceOverrides, + localStorageOverrides, }); ui.button.findByTitle('Create Volume').should('be.visible').click(); @@ -105,23 +103,21 @@ describe('volumes', () => { ui.button.findByTitle('Create Volume').should('be.visible').click(); cy.findByText('Label is required.').should('be.visible'); - cy.findByLabelText('Label', { exact: false }).should('be.visible').click(); - cy.focused().type(mockVolume.label); + cy.findByLabelText('Label', { exact: false }) + .should('be.visible') + .click() + .type(mockVolume.label); ui.button.findByTitle('Create Volume').should('be.visible').click(); cy.findByText('Must provide a region or a Linode ID.').should('be.visible'); - ui.regionSelect.find().click().type(`${mockRegion.label}`); - ui.regionSelect - .findItemByRegionId(mockRegion.id) - .should('be.visible') - .click(); + ui.regionSelect.find().click().type('newark{enter}'); mockGetVolumes([mockVolume]).as('getVolumes'); ui.button.findByTitle('Create Volume').should('be.visible').click(); cy.wait(['@createVolume', '@getVolume', '@getVolumes']); - validateBasicVolume(mockVolume.label, mockRegion.label); + validateBasicVolume(mockVolume.label); ui.actionMenu .findByTitle(`Action menu for Volume ${mockVolume.label}`) @@ -132,27 +128,23 @@ describe('volumes', () => { }); it('creates volume from linode details', () => { - const mockRegion = chooseRegion({ capabilities: ['Block Storage'] }); const mockLinode = linodeFactory.build({ - id: randomNumber(), label: randomLabel(), - region: mockRegion.id, + id: randomNumber(), }); const newVolume = volumeFactory.build({ - label: randomLabel(), linode_id: mockLinode.id, - region: mockRegion.id, + label: randomLabel(), }); mockCreateVolume(newVolume).as('createVolume'); mockGetLinodes([mockLinode]).as('getLinodes'); mockGetLinodeDetails(mockLinode.id, mockLinode).as('getLinodeDetail'); - mockGetLinodeDisks(mockLinode.id, []); mockGetLinodeVolumes(mockLinode.id, []).as('getVolumes'); cy.visitWithLogin('/linodes', { - localStorageOverrides, preferenceOverrides, + localStorageOverrides, }); // Visit a Linode's details page. @@ -174,8 +166,7 @@ describe('volumes', () => { .should('be.visible') .within(() => { cy.findByText('Create and Attach Volume').should('be.visible').click(); - cy.get('[data-qa-volume-label]').click(); - cy.focused().type(newVolume.label); + cy.get('[data-qa-volume-label]').click().type(newVolume.label); ui.button.findByTitle('Create Volume').should('be.visible').click(); }); @@ -195,24 +186,19 @@ describe('volumes', () => { }); it('detaches attached volume', () => { - const mockRegion = chooseRegion({ capabilities: ['Block Storage'] }); - const mockLinode = linodeFactory.build({ - label: randomLabel(), - region: mockRegion.id, - }); + const mockLinode = linodeFactory.build({ label: randomLabel() }); const mockAttachedVolume = volumeFactory.build({ label: randomLabel(), linode_id: mockLinode.id, linode_label: mockLinode.label, - region: mockRegion.id, }); mockDetachVolume(mockAttachedVolume.id).as('detachVolume'); mockGetVolumes([mockAttachedVolume]).as('getAttachedVolumes'); mockGetVolume(mockAttachedVolume).as('getVolume'); cy.visitWithLogin('/volumes', { - localStorageOverrides, preferenceOverrides, + localStorageOverrides, }); cy.wait('@getAttachedVolumes'); @@ -232,8 +218,10 @@ describe('volumes', () => { .findByTitle(`Detach Volume ${mockAttachedVolume.label}?`) .should('be.visible') .within(() => { - cy.findByLabelText('Volume Label').should('be.visible').click(); - cy.focused().type(mockAttachedVolume.label); + cy.findByLabelText('Volume Label') + .should('be.visible') + .click() + .type(mockAttachedVolume.label); ui.button .findByTitle('Detach') @@ -247,11 +235,7 @@ describe('volumes', () => { }); it('does not allow creation of a volume with invalid pricing from volumes landing', () => { - const mockRegion = chooseRegion({ capabilities: ['Block Storage'] }); - const mockVolume = volumeFactory.build({ - label: randomLabel(), - region: mockRegion.id, - }); + const mockVolume = volumeFactory.build({ label: randomLabel() }); mockGetVolumes([]).as('getVolumes'); mockCreateVolume(mockVolume).as('createVolume'); @@ -259,19 +243,15 @@ describe('volumes', () => { mockGetVolumeTypesError().as('getVolumeTypesError'); cy.visitWithLogin('/volumes', { - localStorageOverrides, preferenceOverrides, + localStorageOverrides, }); ui.button.findByTitle('Create Volume').should('be.visible').click(); cy.url().should('endWith', 'volumes/create'); - ui.regionSelect.find().click().type(mockRegion.label); - ui.regionSelect - .findItemByRegionId(mockRegion.id) - .should('be.visible') - .click(); + ui.regionSelect.find().click().type('newark{enter}'); cy.wait(['@getVolumeTypesError']); @@ -286,28 +266,24 @@ describe('volumes', () => { }); it('does not allow creation of a volume with invalid pricing from linode details', () => { - const mockRegion = chooseRegion({ capabilities: ['Block Storage'] }); const mockLinode = linodeFactory.build({ - id: randomNumber(), label: randomLabel(), - region: mockRegion.id, + id: randomNumber(), }); const newVolume = volumeFactory.build({ label: randomLabel(), - region: mockRegion.id, }); mockCreateVolume(newVolume).as('createVolume'); mockGetLinodes([mockLinode]).as('getLinodes'); mockGetLinodeDetails(mockLinode.id, mockLinode).as('getLinodeDetail'); - mockGetLinodeDisks(mockLinode.id, []); mockGetLinodeVolumes(mockLinode.id, []).as('getVolumes'); // Mock an error response to the /types endpoint so prices cannot be calculated. mockGetVolumeTypesError().as('getVolumeTypesError'); cy.visitWithLogin('/linodes', { - localStorageOverrides, preferenceOverrides, + localStorageOverrides, }); // Visit a Linode's details page. 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 df55d1fbab8..5f2f2f6931b 100644 --- a/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts @@ -1,20 +1,28 @@ -import { createLinodeRequestFactory, profileFactory } from '@linode/utilities'; -import { accountUserFactory, grantsFactory } from '@src/factories'; -import { authenticate } from 'support/api/authentication'; -import { entityTag } from 'support/constants/cypress'; -import { mockGetUser } from 'support/intercepts/account'; +import type { Linode, Region } from '@linode/api-v4'; +import { createTestLinode } from 'support/util/linodes'; import { - mockGetProfile, - mockGetProfileGrants, -} from 'support/intercepts/profile'; -import { interceptCreateVolume } from 'support/intercepts/volumes'; -import { ui } from 'support/ui'; + createLinodeRequestFactory, + linodeFactory, +} from 'src/factories/linodes'; +import { authenticate } from 'support/api/authentication'; import { cleanUp } from 'support/util/cleanup'; -import { createTestLinode } from 'support/util/linodes'; -import { randomLabel, randomNumber, randomString } from 'support/util/random'; +import { + interceptCreateVolume, + mockGetVolume, + mockGetVolumes, +} from 'support/intercepts/volumes'; +import { randomNumber, randomString, randomLabel } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; - -import type { Linode } from '@linode/api-v4'; +import { ui } from 'support/ui'; +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'; +import { entityTag } from 'support/constants/cypress'; // 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. @@ -23,6 +31,18 @@ const pageSizeOverride = { PAGE_SIZE: 100, }; +const mockRegions: Region[] = [ + regionFactory.build({ + capabilities: ['Linodes', 'Block Storage', 'Block Storage Encryption'], + id: 'us-east', + label: 'Newark, NJ', + site_type: 'core', + }), +]; + +const CLIENT_LIBRARY_UPDATE_COPY = + 'This Linode requires a client library update and will need to be rebooted prior to attaching an encrypted volume.'; + authenticate(); describe('volume create flow', () => { before(() => { @@ -40,9 +60,9 @@ describe('volume create flow', () => { const region = chooseRegion(); const volume = { label: randomLabel(), + size: `${randomNumber(10, 250)}`, region: region.id, regionLabel: region.label, - size: `${randomNumber(10, 250)}`, }; interceptCreateVolume().as('createVolume'); @@ -52,12 +72,9 @@ describe('volume create flow', () => { }); // Fill out and submit volume create form. - cy.contains('Label').click(); - cy.focused().type(volume.label); - cy.findByLabelText('Tags').click(); - cy.focused().type(entityTag); - cy.contains('Size').click(); - cy.focused().type(`{selectall}{backspace}${volume.size}`); + cy.contains('Label').click().type(volume.label); + cy.findByLabelText('Tags').click().type(entityTag); + cy.contains('Size').click().type(`{selectall}{backspace}${volume.size}`); ui.regionSelect.find().click().type(`${volume.region}{enter}`); cy.findByText('Create Volume').click(); @@ -88,17 +105,17 @@ describe('volume create flow', () => { const region = chooseRegion(); const linodeRequest = createLinodeRequestFactory.build({ - booted: false, label: randomLabel(), region: region.id, root_pass: randomString(16), + booted: false, }); const volume = { label: randomLabel(), + size: `${randomNumber(10, 250)}`, region: region.id, regionLabel: region.label, - size: `${randomNumber(10, 250)}`, }; cy.defer(() => createTestLinode(linodeRequest), 'creating Linode').then( @@ -110,20 +127,26 @@ describe('volume create flow', () => { }); // Fill out and submit volume create form. - cy.contains('Label').click(); - cy.focused().type(volume.label); - cy.contains('Size').click(); - cy.focused().type(`{selectall}{backspace}${volume.size}`); + cy.contains('Label').click().type(volume.label); + cy.contains('Size') + .click() + .type(`{selectall}{backspace}${volume.size}`); ui.regionSelect.find().click().type(`${volume.region}{enter}`); - cy.findByLabelText('Linode').should('be.visible').click(); - cy.focused().type(linode.label); + cy.findByLabelText('Linode') + .should('be.visible') + .click() + .type(linode.label); ui.autocompletePopper .findByTitle(linode.label) .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'); @@ -153,6 +176,199 @@ 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 + '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', () => { + // 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 + 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 volume = volumeFactory.build({ + region: mockRegions[0].id, + encryption: 'enabled', + }); + + 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) => { + mockGetVolumes([volume]).as('getVolumes'); + mockGetVolume(volume); + + cy.visitWithLogin(`/linodes/${linode.id}/storage`); + cy.wait(['@getFeatureFlags', '@getAccount']); + + // 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 + cy.findByPlaceholderText('Select a Volume') + .should('be.visible') + .click() + .type(`${volume.label}{downarrow}{enter}`); + ui.autocompletePopper + .findByTitle(volume.label) + .should('be.visible') + .click(); + + cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('be.visible'); + ui.button + .findByTitle('Attach Volume') + .should('be.visible') + .should('be.disabled'); + } + ); + }); + /* * - Creates a volume from the 'Storage' details page of an existing Linode. * - Confirms that volume is listed correctly on Linode 'Storage' details page. @@ -161,10 +377,10 @@ describe('volume create flow', () => { it('creates a volume from an existing Linode', () => { cy.tag('method:e2e'); const linodeRequest = createLinodeRequestFactory.build({ - booted: false, label: randomLabel(), - region: chooseRegion().id, root_pass: randomString(16), + region: chooseRegion().id, + booted: false, }); cy.defer(() => createTestLinode(linodeRequest), 'creating Linode').then( @@ -185,8 +401,7 @@ describe('volume create flow', () => { 'be.visible' ); cy.contains('Create and Attach Volume').click(); - cy.contains('Label').click(); - cy.focused().type(volume.label); + cy.contains('Label').click().type(volume.label); cy.contains('Size').type(`{selectall}{backspace}${volume.size}`); cy.findByText('Create Volume').click(); }); @@ -218,59 +433,4 @@ describe('volume create flow', () => { } ); }); - - it('does not allow creation of a volume for restricted users from volume create page', () => { - // Mock setup for user profile, account user, and user grants with restricted permissions, - // simulating a default user without the ability to add Linodes. - const mockProfile = profileFactory.build({ - restricted: true, - username: randomLabel(), - }); - - const mockUser = accountUserFactory.build({ - restricted: true, - user_type: 'default', - username: mockProfile.username, - }); - - const mockGrants = grantsFactory.build({ - global: { - add_volumes: false, - }, - }); - - mockGetProfile(mockProfile); - mockGetProfileGrants(mockGrants); - mockGetUser(mockUser); - - cy.visitWithLogin('/volumes/create', { - localStorageOverrides: pageSizeOverride, - }); - - // Confirm that a notice should be shown informing the user they do not have permission to create a Linode. - cy.findByText( - "You don't have permissions to create this Volume. Please contact your account administrator to request the necessary permissions." - ).should('be.visible'); - - // Confirm that the "Label" field should be disabled. - cy.get('[id="label"]').should('be.visible').should('be.disabled'); - - // Confirm that the "Tags" field should be disabled. - cy.findByLabelText('Tags').should('be.visible').should('be.disabled'); - - // Confirm that the "Region" field should be disabled. - ui.regionSelect.find().should('be.visible').should('be.disabled'); - - // Confirm that the "Linode" field should be disabled. - cy.findByLabelText('Linode').should('be.visible').should('be.disabled'); - - // Confirm that the "Config" field should be disabled. - cy.findByLabelText('Config').should('be.visible').should('be.disabled'); - - // Confirm that the "Size" field should be disabled. - cy.get('[id="size"]').should('be.visible').should('be.disabled'); - - // Confirm that the "Create Volume" button is disabled. - cy.findByText('Create Volume').should('be.visible').should('be.disabled'); - }); }); 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 11792d1ee35..be6d7f8dec1 100644 --- a/packages/manager/cypress/e2e/core/volumes/delete-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/delete-volume.spec.ts @@ -1,17 +1,14 @@ -import { createVolume } from '@linode/api-v4/lib/volumes'; +import { createVolume, VolumeRequestPayload } from '@linode/api-v4/lib/volumes'; +import { Volume } from '@linode/api-v4'; +import { volumeRequestPayloadFactory } from 'src/factories/volume'; import { authenticate } from 'support/api/authentication'; import { interceptDeleteVolume } from 'support/intercepts/volumes'; +import { randomLabel } from 'support/util/random'; import { ui } from 'support/ui'; -import { SimpleBackoffMethod } from 'support/util/backoff'; +import { chooseRegion } from 'support/util/regions'; import { cleanUp } from 'support/util/cleanup'; +import { SimpleBackoffMethod } from 'support/util/backoff'; import { pollVolumeStatus } from 'support/util/polling'; -import { randomLabel } from 'support/util/random'; -import { chooseRegion } from 'support/util/regions'; - -import { volumeRequestPayloadFactory } from 'src/factories/volume'; - -import type { Volume } from '@linode/api-v4'; -import type { VolumeRequestPayload } from '@linode/api-v4/lib/volumes'; // 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. @@ -93,8 +90,10 @@ describe('volume delete flow', () => { .findByTitle(`Delete Volume ${volume.label}?`) .should('be.visible') .within(() => { - cy.findByLabelText('Volume Label').should('be.visible').click(); - cy.focused().type(volume.label); + cy.findByLabelText('Volume Label') + .should('be.visible') + .click() + .type(volume.label); ui.buttonGroup .findButtonByTitle('Delete') diff --git a/packages/manager/cypress/e2e/core/volumes/landing-page-empty-state.spec.ts b/packages/manager/cypress/e2e/core/volumes/landing-page-empty-state.spec.ts index 75c7ab29e33..1d254586082 100644 --- a/packages/manager/cypress/e2e/core/volumes/landing-page-empty-state.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/landing-page-empty-state.spec.ts @@ -1,5 +1,5 @@ -import { mockGetVolumes } from 'support/intercepts/volumes'; import { ui } from 'support/ui'; +import { mockGetVolumes } from 'support/intercepts/volumes'; describe('confirms Volumes landing page empty state is shown when no Volumes exist', () => { /* 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 a34ca3cc7ec..74b9955ea76 100644 --- a/packages/manager/cypress/e2e/core/volumes/resize-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/resize-volume.spec.ts @@ -1,4 +1,7 @@ +import type { VolumeRequestPayload } from '@linode/api-v4'; import { createVolume } from '@linode/api-v4'; +import { Volume } from '@linode/api-v4'; +import { volumeRequestPayloadFactory } from 'src/factories/volume'; import { authenticate } from 'support/api/authentication'; import { interceptResizeVolume } from 'support/intercepts/volumes'; import { SimpleBackoffMethod } from 'support/util/backoff'; @@ -7,11 +10,6 @@ import { pollVolumeStatus } from 'support/util/polling'; import { randomLabel, randomNumber } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import { volumeRequestPayloadFactory } from 'src/factories/volume'; - -import type { VolumeRequestPayload } from '@linode/api-v4'; -import type { Volume } from '@linode/api-v4'; - // 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. // @TODO Remove local storage override when stuck volumes are removed from test accounts. @@ -83,8 +81,9 @@ describe('volume resize flow', () => { cy.get('[data-qa-drawer="true"]') .should('be.visible') .within(() => { - cy.findByText('Size').click(); - cy.focused().type(`{selectall}{backspace}${newSize}`); + cy.findByText('Size') + .click() + .type(`{selectall}{backspace}${newSize}`); cy.get('[data-qa-buttons="true"]').within(() => { cy.findByText('Resize Volume').should('be.visible').click(); }); 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 34cfd8678b8..fbf120ce824 100644 --- a/packages/manager/cypress/e2e/core/volumes/search-volumes.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/search-volumes.spec.ts @@ -1,11 +1,9 @@ import { createVolume } from '@linode/api-v4/lib/volumes'; -import { authenticate } from 'support/api/authentication'; +import { Volume } from '@linode/api-v4'; import { ui } from 'support/ui'; -import { cleanUp } from 'support/util/cleanup'; +import { authenticate } from 'support/api/authentication'; import { randomLabel } from 'support/util/random'; -import { chooseRegion } from 'support/util/regions'; - -import type { Volume } from '@linode/api-v4'; +import { cleanUp } from 'support/util/cleanup'; authenticate(); describe('Search Volumes', () => { @@ -21,16 +19,15 @@ describe('Search Volumes', () => { */ it('creates two volumes and make sure they show up in the table and are searchable', () => { const createTwoVolumes = async (): Promise<[Volume, Volume]> => { - const volumeRegion = chooseRegion(); return Promise.all([ createVolume({ label: randomLabel(), - region: volumeRegion.id, + region: 'us-east', size: 10, }), createVolume({ label: randomLabel(), - region: volumeRegion.id, + region: 'us-east', size: 10, }), ]); @@ -46,8 +43,8 @@ describe('Search Volumes', () => { // Search for the first volume by label, confirm it's the only one shown. cy.findByPlaceholderText('Search Volumes').type(volume1.label); - cy.findByText(volume1.label).should('be.visible'); - cy.findByText(volume2.label).should('not.exist'); + expect(cy.findByText(volume1.label).should('be.visible')); + expect(cy.findByText(volume2.label).should('not.exist')); // Clear search, confirm both volumes are shown. cy.findByTestId('clear-volumes-search').click(); 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 9b1b021897c..25b5e128405 100644 --- a/packages/manager/cypress/e2e/core/volumes/update-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/update-volume.spec.ts @@ -1,13 +1,12 @@ +import { Volume } from '@linode/api-v4'; + +import { volumeRequestPayloadFactory } from 'src/factories/volume'; import { authenticate } from 'support/api/authentication'; -import { createActiveVolume } from 'support/api/volumes'; -import { ui } from 'support/ui'; -import { cleanUp } from 'support/util/cleanup'; import { randomLabel } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; - -import { volumeRequestPayloadFactory } from 'src/factories/volume'; - -import type { Volume } from '@linode/api-v4'; +import { cleanUp } from 'support/util/cleanup'; +import { ui } from 'support/ui'; +import { createActiveVolume } from 'support/api/volumes'; authenticate(); describe('volume update flow', () => { @@ -56,8 +55,10 @@ describe('volume update flow', () => { // Enter new label, click "Save Changes". cy.get('[data-qa-drawer="true"]').within(() => { cy.findByText('Edit Volume').should('be.visible'); - cy.findByDisplayValue(volume.label).should('be.visible').click(); - cy.focused().type(`{selectall}{backspace}${newLabel}`); + cy.findByDisplayValue(volume.label) + .should('be.visible') + .click() + .type(`{selectall}{backspace}${newLabel}`); cy.findByText('Save Changes').should('be.visible').click(); }); @@ -122,8 +123,8 @@ describe('volume update flow', () => { cy.findByPlaceholderText('Type to choose or create a tag.') .should('be.visible') - .click(); - cy.focused().type(`${newTags.join('{enter}')}{enter}`); + .click() + .type(`${newTags.join('{enter}')}{enter}`); cy.findByText('Save Changes').should('be.visible').click(); }); diff --git a/packages/manager/cypress/e2e/core/volumes/upgrade-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/upgrade-volume.spec.ts index f6ff9e71a2d..27f5e379b84 100644 --- a/packages/manager/cypress/e2e/core/volumes/upgrade-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/upgrade-volume.spec.ts @@ -1,6 +1,6 @@ -import { linodeFactory } from '@linode/utilities'; import { eventFactory, + linodeFactory, notificationFactory, volumeFactory, } from '@src/factories'; @@ -11,23 +11,19 @@ import { mockGetLinodeVolumes, } from 'support/intercepts/linodes'; import { - mockGetVolume, - mockGetVolumes, mockMigrateVolumes, + mockGetVolumes, + mockGetVolume, } from 'support/intercepts/volumes'; import { ui } from 'support/ui'; -import { chooseRegion } from 'support/util/regions'; describe('volume upgrade/migration', () => { it('can upgrade an unattached volume to NVMe', () => { - const mockRegion = chooseRegion({ capabilities: ['Block Storage'] }); - const volume = volumeFactory.build({ - region: mockRegion.id, - }); + const volume = volumeFactory.build(); const migrationScheduledNotification = notificationFactory.build({ - entity: { id: volume.id, type: 'volume' }, type: 'volume_migration_scheduled', + entity: { type: 'volume', id: volume.id }, }); mockGetVolumes([volume]).as('getVolumes'); @@ -47,8 +43,8 @@ describe('volume upgrade/migration', () => { .click(); const migrationImminentNotification = notificationFactory.build({ - entity: { id: volume.id, type: 'volume' }, type: 'volume_migration_imminent', + entity: { type: 'volume', id: volume.id }, }); mockGetNotifications([migrationImminentNotification]).as( 'getNotifications' @@ -70,8 +66,8 @@ describe('volume upgrade/migration', () => { const mockStartedMigrationEvent = eventFactory.build({ action: 'volume_migrate', entity: { id: volume.id, type: 'volume' }, - percent_complete: percentage, status: 'started', + percent_complete: percentage, }); mockGetEvents([mockStartedMigrationEvent]).as('getEvents'); @@ -83,7 +79,7 @@ describe('volume upgrade/migration', () => { const mockFinishedMigrationEvent = eventFactory.build({ action: 'volume_migrate', - entity: { id: volume.id, label: volume.label, type: 'volume' }, + entity: { id: volume.id, type: 'volume', label: volume.label }, status: 'finished', }); @@ -100,25 +96,20 @@ describe('volume upgrade/migration', () => { }); it('can upgrade an attached volume from the volumes landing page', () => { - const mockRegion = chooseRegion({ capabilities: ['Block Storage'] }); - const linode = linodeFactory.build({ - region: mockRegion.id, - }); + const linode = linodeFactory.build(); const volume = volumeFactory.build({ linode_id: linode.id, linode_label: linode.label, - region: mockRegion.id, }); const migrationScheduledNotification = notificationFactory.build({ - entity: { id: volume.id, type: 'volume' }, type: 'volume_migration_scheduled', + entity: { type: 'volume', id: volume.id }, }); mockGetVolumes([volume]).as('getVolumes'); mockMigrateVolumes().as('migrateVolumes'); mockGetLinodeDetails(linode.id, linode).as('getLinode'); - mockGetLinodeVolumes(linode.id, [volume]); mockGetLinodeDisks(linode.id, []); mockGetNotifications([migrationScheduledNotification]).as( 'getNotifications' @@ -139,8 +130,8 @@ describe('volume upgrade/migration', () => { cy.wait(['@getLinode', '@getLinodeVolumes']); const migrationImminentNotification = notificationFactory.build({ - entity: { id: volume.id, type: 'volume' }, type: 'volume_migration_imminent', + entity: { type: 'volume', id: volume.id }, }); mockGetNotifications([migrationImminentNotification]).as( 'getNotifications' @@ -151,6 +142,7 @@ describe('volume upgrade/migration', () => { `A Volume attached to Linode ${linode.label} will be upgraded to high-performance NVMe Block Storage.`, { exact: false } ).should('be.visible'); + ui.button .findByTitle('Enter Upgrade Queue') .should('be.visible') @@ -165,8 +157,8 @@ describe('volume upgrade/migration', () => { const mockStartedMigrationEvent = eventFactory.build({ action: 'volume_migrate', entity: { id: volume.id, type: 'volume' }, - percent_complete: percentage, status: 'started', + percent_complete: percentage, }); mockGetEvents([mockStartedMigrationEvent]).as('getEvents'); @@ -178,7 +170,7 @@ describe('volume upgrade/migration', () => { const mockFinishedMigrationEvent = eventFactory.build({ action: 'volume_migrate', - entity: { id: volume.id, label: volume.label, type: 'volume' }, + entity: { id: volume.id, type: 'volume', label: volume.label }, status: 'finished', }); @@ -195,19 +187,15 @@ describe('volume upgrade/migration', () => { }); it('can upgrade an attached volume from the linode details page', () => { - const mockRegion = chooseRegion({ capabilities: ['Block Storage'] }); - const linode = linodeFactory.build({ - region: mockRegion.id, - }); + const linode = linodeFactory.build(); const volume = volumeFactory.build({ linode_id: linode.id, linode_label: linode.label, - region: mockRegion.id, }); const migrationScheduledNotification = notificationFactory.build({ - entity: { id: volume.id, type: 'volume' }, type: 'volume_migration_scheduled', + entity: { type: 'volume', id: volume.id }, }); mockMigrateVolumes().as('migrateVolumes'); @@ -229,8 +217,8 @@ describe('volume upgrade/migration', () => { .click(); const migrationImminentNotification = notificationFactory.build({ - entity: { id: volume.id, type: 'volume' }, type: 'volume_migration_imminent', + entity: { type: 'volume', id: volume.id }, }); mockGetNotifications([migrationImminentNotification]).as( 'getNotifications' @@ -256,8 +244,8 @@ describe('volume upgrade/migration', () => { const mockStartedMigrationEvent = eventFactory.build({ action: 'volume_migrate', entity: { id: volume.id, type: 'volume' }, - percent_complete: percentage, status: 'started', + percent_complete: percentage, }); mockGetEvents([mockStartedMigrationEvent]).as('getEvents'); @@ -269,7 +257,7 @@ describe('volume upgrade/migration', () => { const mockFinishedMigrationEvent = eventFactory.build({ action: 'volume_migrate', - entity: { id: volume.id, label: volume.label, type: 'volume' }, + entity: { id: volume.id, type: 'volume', label: volume.label }, status: 'finished', }); diff --git a/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts b/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts index a355f77a294..31c9522f730 100644 --- a/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts +++ b/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts @@ -2,28 +2,30 @@ * @file Integration tests for VPC create flow. */ -import { linodeFactory, regionFactory } from '@linode/utilities'; -import { subnetFactory, vpcFactory } from '@src/factories'; +import type { Subnet, VPC } from '@linode/api-v4'; +import { + vpcFactory, + subnetFactory, + linodeFactory, + regionFactory, +} from '@src/factories'; import { mockGetRegions } from 'support/intercepts/regions'; import { - mockCreateVPC, mockCreateVPCError, + mockCreateVPC, mockGetSubnets, } from 'support/intercepts/vpc'; -import { ui } from 'support/ui'; -import { buildArray } from 'support/util/arrays'; import { - randomIp, randomLabel, - randomNumber, randomPhrase, + randomIp, + randomNumber, randomString, } from 'support/util/random'; -import { extendRegion } from 'support/util/regions'; - +import { ui } from 'support/ui'; +import { buildArray } from 'support/util/arrays'; import { getUniqueLinodesFromSubnets } from 'src/features/VPCs/utils'; - -import type { Subnet, VPC } from '@linode/api-v4'; +import { extendRegion } from 'support/util/regions'; /** * Gets the "Add another Subnet" section with the given index. @@ -51,9 +53,9 @@ describe('VPC create flow', () => { const mockSubnets: Subnet[] = buildArray(3, (index: number) => { return subnetFactory.build({ + label: randomLabel(), id: randomNumber(10000, 99999), ipv4: `${randomIp()}/${randomNumber(0, 32)}`, - label: randomLabel(), linodes: linodeFactory.buildList(index + 1), }); }); @@ -62,10 +64,10 @@ describe('VPC create flow', () => { const mockInvalidIpRange = `${randomIp()}/${randomNumber(33, 100)}`; const mockVpc: VPC = vpcFactory.build({ - description: randomPhrase(), id: randomNumber(10000, 99999), label: randomLabel(), region: mockVPCRegion.id, + description: randomPhrase(), subnets: mockSubnets, }); @@ -79,25 +81,29 @@ describe('VPC create flow', () => { cy.visitWithLogin('/vpcs/create'); cy.wait('@getRegions'); - ui.regionSelect.find().click(); - cy.focused().type(`${mockVPCRegion.label}{enter}`); + ui.regionSelect.find().click().type(`${mockVPCRegion.label}{enter}`); - cy.findByText('VPC Label').should('be.visible').click(); - cy.focused().type(mockVpc.label); + cy.findByText('VPC Label').should('be.visible').click().type(mockVpc.label); - cy.findByText('Description').should('be.visible').click(); - cy.focused().type(mockVpc.description); + cy.findByText('Description') + .should('be.visible') + .click() + .type(mockVpc.description); // Fill out the first Subnet. // Insert an invalid empty IP range to confirm client side validation. getSubnetNodeSection(0) .should('be.visible') .within(() => { - cy.findByText('Subnet Label').should('be.visible').click(); - cy.focused().type(mockSubnets[0].label); + cy.findByText('Subnet Label') + .should('be.visible') + .click() + .type(mockSubnets[0].label); - cy.findByText('Subnet IP Address Range').should('be.visible').click(); - cy.focused().type(`{selectAll}{backspace}`); + cy.findByText('Subnet IP Address Range') + .should('be.visible') + .click() + .type(`{selectAll}{backspace}`); }); ui.button @@ -109,9 +115,11 @@ describe('VPC create flow', () => { cy.findByText(ipValidationErrorMessage1).should('be.visible'); // Enter a random non-IP address string to further test client side validation. - cy.findByText('Subnet IP Address Range').should('be.visible').click(); - cy.focused().type(`{selectAll}{backspace}`); - cy.focused().type(randomString(18)); + cy.findByText('Subnet IP Address Range') + .should('be.visible') + .click() + .type(`{selectAll}{backspace}`) + .type(randomString(18)); ui.button .findByTitle('Create VPC') @@ -122,9 +130,11 @@ describe('VPC create flow', () => { cy.findByText(ipValidationErrorMessage2).should('be.visible'); // Enter a valid IP address with an invalid network prefix to further test client side validation. - cy.findByText('Subnet IP Address Range').should('be.visible').click(); - cy.focused().type(`{selectAll}{backspace}`); - cy.focused().type(mockInvalidIpRange); + cy.findByText('Subnet IP Address Range') + .should('be.visible') + .click() + .type(`{selectAll}{backspace}`) + .type(mockInvalidIpRange); ui.button .findByTitle('Create VPC') @@ -135,9 +145,11 @@ describe('VPC create flow', () => { cy.findByText(ipValidationErrorMessage2).should('be.visible'); // Replace invalid IP address range with valid range. - cy.findByText('Subnet IP Address Range').should('be.visible').click(); - cy.focused().type(`{selectAll}{backspace}`); - cy.focused().type(mockSubnets[0].ipv4!); + cy.findByText('Subnet IP Address Range') + .should('be.visible') + .click() + .type(`{selectAll}{backspace}`) + .type(mockSubnets[0].ipv4!); // Add another subnet that we will remove later. ui.button @@ -151,9 +163,11 @@ describe('VPC create flow', () => { getSubnetNodeSection(1) .should('be.visible') .within(() => { - cy.findByText('Subnet IP Address Range').should('be.visible').click(); - cy.focused().type(`{selectAll}{backspace}`); - cy.focused().type(mockSubnetToDelete.ipv4!); + cy.findByText('Subnet IP Address Range') + .should('be.visible') + .click() + .type(`{selectAll}{backspace}`) + .type(mockSubnetToDelete.ipv4!); }); ui.button @@ -192,12 +206,16 @@ describe('VPC create flow', () => { getSubnetNodeSection(index + 1) .should('be.visible') .within(() => { - cy.findByText('Subnet Label').should('be.visible').click(); - cy.focused().type(mockSubnet.label); - - cy.findByText('Subnet IP Address Range').should('be.visible').click(); - cy.focused().type(`{selectAll}{backspace}`); - cy.focused().type(`${randomIp()}/${randomNumber(0, 32)}`); + cy.findByText('Subnet Label') + .should('be.visible') + .click() + .type(mockSubnet.label); + + cy.findByText('Subnet IP Address Range') + .should('be.visible') + .click() + .type(`{selectAll}{backspace}`) + .type(`${randomIp()}/${randomNumber(0, 32)}`); }); }); @@ -263,10 +281,10 @@ describe('VPC create flow', () => { ); const mockVpc: VPC = vpcFactory.build({ - description: randomPhrase(), id: randomNumber(10000, 99999), label: randomLabel(), region: mockVPCRegion.id, + description: randomPhrase(), subnets: [], }); @@ -279,8 +297,7 @@ describe('VPC create flow', () => { ui.regionSelect.find().click().type(`${mockVPCRegion.label}{enter}`); - cy.findByText('VPC Label').should('be.visible').click(); - cy.focused().type(mockVpc.label); + cy.findByText('VPC Label').should('be.visible').click().type(mockVpc.label); // Remove the subnet. getSubnetNodeSection(0) diff --git a/packages/manager/cypress/e2e/core/vpc/vpc-details-page.spec.ts b/packages/manager/cypress/e2e/core/vpc/vpc-details-page.spec.ts index 812427afbaf..1ee95a8652c 100644 --- a/packages/manager/cypress/e2e/core/vpc/vpc-details-page.spec.ts +++ b/packages/manager/cypress/e2e/core/vpc/vpc-details-page.spec.ts @@ -1,29 +1,29 @@ import { - linodeConfigInterfaceFactory, - linodeConfigInterfaceFactoryWithVPC, - linodeFactory, -} from '@linode/utilities'; -import { linodeConfigFactory, subnetFactory, vpcFactory } from '@src/factories'; -import { mockGetLinodeConfig } from 'support/intercepts/configs'; -import { mockGetLinodeDetails } from 'support/intercepts/linodes'; -import { + mockGetVPC, + mockGetVPCs, + mockDeleteVPC, + mockUpdateVPC, mockCreateSubnet, mockDeleteSubnet, - mockDeleteVPC, mockEditSubnet, - mockGetSubnet, mockGetSubnets, - mockGetVPC, - mockGetVPCs, - mockUpdateVPC, } from 'support/intercepts/vpc'; -import { ui } from 'support/ui'; +import { mockGetLinodeConfigs } from 'support/intercepts/configs'; +import { mockGetLinodeDetails } from 'support/intercepts/linodes'; +import { + linodeFactory, + linodeConfigFactory, + LinodeConfigInterfaceFactoryWithVPC, + subnetFactory, + vpcFactory, + LinodeConfigInterfaceFactory, +} from '@src/factories'; import { randomLabel, randomNumber, randomPhrase } from 'support/util/random'; -import { chooseRegion, getRegionById } from 'support/util/regions'; - -import { WARNING_ICON_UNRECOMMENDED_CONFIG } from 'src/features/VPCs/constants'; - +import { chooseRegion } from 'support/util/regions'; import type { VPC } from '@linode/api-v4'; +import { getRegionById } from 'support/util/regions'; +import { ui } from 'support/ui'; +import { WARNING_ICON_UNRECOMMENDED_CONFIG } from 'src/features/VPCs/constants'; describe('VPC details page', () => { /** @@ -36,13 +36,12 @@ describe('VPC details page', () => { const mockVPC: VPC = vpcFactory.build({ id: randomNumber(), label: randomLabel(), - region: chooseRegion().id, }); const mockVPCUpdated = { ...mockVPC, - description: randomPhrase(), label: randomLabel(), + description: randomPhrase(), }; const vpcRegion = getRegionById(mockVPC.region); @@ -69,13 +68,17 @@ describe('VPC details page', () => { .findByTitle('Edit VPC') .should('be.visible') .within(() => { - cy.findByLabelText('Label').should('be.visible').click(); - cy.focused().clear(); - cy.focused().type(mockVPCUpdated.label); + cy.findByLabelText('Label') + .should('be.visible') + .click() + .clear() + .type(mockVPCUpdated.label); - cy.findByLabelText('Description').should('be.visible').click(); - cy.focused().clear(); - cy.focused().type(mockVPCUpdated.description); + cy.findByLabelText('Description') + .should('be.visible') + .click() + .clear() + .type(mockVPCUpdated.description); ui.button .findByTitle('Save') @@ -99,8 +102,10 @@ describe('VPC details page', () => { .findByTitle(`Delete VPC ${mockVPCUpdated.label}`) .should('be.visible') .within(() => { - cy.findByLabelText('VPC Label').should('be.visible').click(); - cy.focused().type(mockVPCUpdated.label); + cy.findByLabelText('VPC Label') + .should('be.visible') + .click() + .type(mockVPCUpdated.label); ui.button .findByTitle('Delete') @@ -134,7 +139,6 @@ describe('VPC details page', () => { const mockVPC = vpcFactory.build({ id: randomNumber(), label: randomLabel(), - region: chooseRegion().id, }); const mockVPCAfterSubnetCreation = vpcFactory.build({ @@ -163,8 +167,10 @@ describe('VPC details page', () => { .findByTitle('Create Subnet') .should('be.visible') .within(() => { - cy.findByText('Subnet Label').should('be.visible').click(); - cy.focused().type(mockSubnet.label); + cy.findByText('Subnet Label') + .should('be.visible') + .click() + .type(mockSubnet.label); cy.findByTestId('create-subnet-drawer-button') .should('be.visible') @@ -178,7 +184,6 @@ describe('VPC details page', () => { cy.findByText(mockVPC.label).should('be.visible'); cy.findByText('Subnets (1)').should('be.visible'); cy.findByText(mockSubnet.label).should('be.visible'); - mockGetSubnet(mockVPC.id, mockSubnet.id, mockSubnet); // edit a subnet const mockEditedSubnet = subnetFactory.build({ @@ -203,15 +208,16 @@ describe('VPC details page', () => { .should('be.visible') .click(); ui.actionMenuItem.findByTitle('Edit').should('be.visible').click(); - mockGetSubnet(mockVPC.id, mockEditedSubnet.id, mockEditedSubnet); ui.drawer .findByTitle('Edit Subnet') .should('be.visible') .within(() => { - cy.findByLabelText('Label').should('be.visible').click(); - cy.focused().clear(); - cy.focused().type(mockEditedSubnet.label); + cy.findByLabelText('Label') + .should('be.visible') + .click() + .clear() + .type(mockEditedSubnet.label); cy.findByLabelText('Subnet IP Address Range') .should('be.visible') @@ -250,8 +256,10 @@ describe('VPC details page', () => { .findByTitle(`Delete Subnet ${mockEditedSubnet.label}`) .should('be.visible') .within(() => { - cy.findByLabelText('Subnet Label').should('be.visible').click(); - cy.focused().type(mockEditedSubnet.label); + cy.findByLabelText('Subnet Label') + .should('be.visible') + .click() + .type(mockEditedSubnet.label); ui.button .findByTitle('Delete') @@ -277,7 +285,6 @@ describe('VPC details page', () => { const linodeRegion = chooseRegion({ capabilities: ['VPCs'] }); const mockInterfaceId = randomNumber(); - const mockConfigId = randomNumber(); const mockLinode = linodeFactory.build({ id: randomNumber(), label: randomLabel(), @@ -286,20 +293,14 @@ describe('VPC details page', () => { const mockSubnet = subnetFactory.build({ id: randomNumber(), - ipv4: '10.0.0.0/24', label: randomLabel(), linodes: [ { id: mockLinode.id, - interfaces: [ - { - active: true, - config_id: mockConfigId, - id: mockInterfaceId, - }, - ], + interfaces: [{ id: mockInterfaceId, active: true }], }, ], + ipv4: '10.0.0.0/24', }); const mockVPC = vpcFactory.build({ @@ -309,31 +310,27 @@ describe('VPC details page', () => { subnets: [mockSubnet], }); - const mockInterface = linodeConfigInterfaceFactoryWithVPC.build({ - active: true, - id: mockInterfaceId, - primary: true, - subnet_id: mockSubnet.id, + const mockInterface = LinodeConfigInterfaceFactoryWithVPC.build({ vpc_id: mockVPC.id, + subnet_id: mockSubnet.id, + primary: true, + active: true, }); const mockLinodeConfig = linodeConfigFactory.build({ - id: mockConfigId, interfaces: [mockInterface], }); mockGetVPC(mockVPC).as('getVPC'); mockGetSubnets(mockVPC.id, [mockSubnet]).as('getSubnets'); mockGetLinodeDetails(mockLinode.id, mockLinode).as('getLinode'); - mockGetLinodeConfig({ - config: mockLinodeConfig, - configId: mockLinodeConfig.id, - linodeId: mockLinode.id, - }).as('getLinodeConfig'); + mockGetLinodeConfigs(mockLinode.id, [mockLinodeConfig]).as( + 'getLinodeConfigs' + ); cy.visitWithLogin(`/vpcs/${mockVPC.id}`); cy.findByLabelText(`expand ${mockSubnet.label} row`).click(); - cy.wait('@getLinodeConfig'); + cy.wait('@getLinodeConfigs'); cy.findByTestId(WARNING_ICON_UNRECOMMENDED_CONFIG).should('not.exist'); }); @@ -344,7 +341,6 @@ describe('VPC details page', () => { const linodeRegion = chooseRegion({ capabilities: ['VPCs'] }); const mockInterfaceId = randomNumber(); - const mockConfigId = randomNumber(); const mockLinode = linodeFactory.build({ id: randomNumber(), label: randomLabel(), @@ -353,20 +349,14 @@ describe('VPC details page', () => { const mockSubnet = subnetFactory.build({ id: randomNumber(), - ipv4: '10.0.0.0/24', label: randomLabel(), linodes: [ { id: mockLinode.id, - interfaces: [ - { - active: true, - config_id: mockConfigId, - id: mockInterfaceId, - }, - ], + interfaces: [{ id: mockInterfaceId, active: true }], }, ], + ipv4: '10.0.0.0/24', }); const mockVPC = vpcFactory.build({ @@ -376,31 +366,28 @@ describe('VPC details page', () => { subnets: [mockSubnet], }); - const mockInterface = linodeConfigInterfaceFactoryWithVPC.build({ - active: true, + const mockInterface = LinodeConfigInterfaceFactoryWithVPC.build({ id: mockInterfaceId, - primary: false, - subnet_id: mockSubnet.id, vpc_id: mockVPC.id, + subnet_id: mockSubnet.id, + primary: false, + active: true, }); const mockLinodeConfig = linodeConfigFactory.build({ - id: mockConfigId, interfaces: [mockInterface], }); mockGetVPC(mockVPC).as('getVPC'); mockGetSubnets(mockVPC.id, [mockSubnet]).as('getSubnets'); mockGetLinodeDetails(mockLinode.id, mockLinode).as('getLinode'); - mockGetLinodeConfig({ - config: mockLinodeConfig, - configId: mockLinodeConfig.id, - linodeId: mockLinode.id, - }).as('getLinodeConfig'); + mockGetLinodeConfigs(mockLinode.id, [mockLinodeConfig]).as( + 'getLinodeConfigs' + ); cy.visitWithLogin(`/vpcs/${mockVPC.id}`); cy.findByLabelText(`expand ${mockSubnet.label} row`).click(); - cy.wait('@getLinodeConfig'); + cy.wait('@getLinodeConfigs'); cy.findByTestId(WARNING_ICON_UNRECOMMENDED_CONFIG).should('not.exist'); }); @@ -411,7 +398,6 @@ describe('VPC details page', () => { const linodeRegion = chooseRegion({ capabilities: ['VPCs'] }); const mockInterfaceId = randomNumber(); - const mockConfigId = randomNumber(); const mockLinode = linodeFactory.build({ id: randomNumber(), label: randomLabel(), @@ -420,20 +406,14 @@ describe('VPC details page', () => { const mockSubnet = subnetFactory.build({ id: randomNumber(), - ipv4: '10.0.0.0/24', label: randomLabel(), linodes: [ { id: mockLinode.id, - interfaces: [ - { - active: true, - config_id: mockConfigId, - id: mockInterfaceId, - }, - ], + interfaces: [{ id: mockInterfaceId, active: true }], }, ], + ipv4: '10.0.0.0/24', }); const mockVPC = vpcFactory.build({ @@ -443,37 +423,34 @@ describe('VPC details page', () => { subnets: [mockSubnet], }); - const mockPrimaryInterface = linodeConfigInterfaceFactory.build({ - active: false, + const mockPrimaryInterface = LinodeConfigInterfaceFactory.build({ primary: true, + active: false, purpose: 'public', }); - const mockInterface = linodeConfigInterfaceFactoryWithVPC.build({ - active: true, + const mockInterface = LinodeConfigInterfaceFactoryWithVPC.build({ id: mockInterfaceId, - primary: false, - subnet_id: mockSubnet.id, vpc_id: mockVPC.id, + subnet_id: mockSubnet.id, + primary: false, + active: true, }); const mockLinodeConfig = linodeConfigFactory.build({ - id: mockConfigId, interfaces: [mockInterface, mockPrimaryInterface], }); mockGetVPC(mockVPC).as('getVPC'); mockGetSubnets(mockVPC.id, [mockSubnet]).as('getSubnets'); mockGetLinodeDetails(mockLinode.id, mockLinode).as('getLinode'); - mockGetLinodeConfig({ - config: mockLinodeConfig, - configId: mockLinodeConfig.id, - linodeId: mockLinode.id, - }).as('getLinodeConfig'); + mockGetLinodeConfigs(mockLinode.id, [mockLinodeConfig]).as( + 'getLinodeConfigs' + ); cy.visitWithLogin(`/vpcs/${mockVPC.id}`); cy.findByLabelText(`expand ${mockSubnet.label} row`).click(); - cy.wait('@getLinodeConfig'); + cy.wait('@getLinodeConfigs'); cy.findByTestId(WARNING_ICON_UNRECOMMENDED_CONFIG).should('exist'); }); }); diff --git a/packages/manager/cypress/e2e/core/vpc/vpc-landing-page.spec.ts b/packages/manager/cypress/e2e/core/vpc/vpc-landing-page.spec.ts index c2e921f6f20..e2863a1a22d 100644 --- a/packages/manager/cypress/e2e/core/vpc/vpc-landing-page.spec.ts +++ b/packages/manager/cypress/e2e/core/vpc/vpc-landing-page.spec.ts @@ -1,16 +1,14 @@ -import { subnetFactory, vpcFactory } from '@src/factories'; import { - MOCK_DELETE_VPC_ERROR, + mockGetVPCs, mockDeleteVPC, mockDeleteVPCError, - mockGetVPC, - mockGetVPCs, mockUpdateVPC, + MOCK_DELETE_VPC_ERROR, } from 'support/intercepts/vpc'; +import { subnetFactory, vpcFactory } from '@src/factories'; import { ui } from 'support/ui'; import { randomLabel, randomPhrase } from 'support/util/random'; import { chooseRegion, getRegionById } from 'support/util/regions'; - import { VPC_LABEL } from 'src/features/VPCs/constants'; // TODO Remove feature flag mocks when feature flag is removed from codebase. @@ -19,9 +17,7 @@ describe('VPC landing page', () => { * - Confirms that VPCs are listed on the VPC landing page. */ it('lists VPC instances', () => { - const mockVPCs = vpcFactory.buildList(5, { - region: chooseRegion().id, - }); + const mockVPCs = vpcFactory.buildList(5); mockGetVPCs(mockVPCs).as('getVPCs'); cy.visitWithLogin('/vpcs'); @@ -83,25 +79,24 @@ describe('VPC landing page', () => { it('can update and delete VPCs from VPC landing page', () => { const mockVPCs = [ vpcFactory.build({ - description: randomPhrase(), label: randomLabel(), region: chooseRegion().id, + description: randomPhrase(), }), vpcFactory.build({ - description: randomPhrase(), label: randomLabel(), region: chooseRegion().id, + description: randomPhrase(), }), ]; const mockUpdatedVPC = { ...mockVPCs[1], - description: randomPhrase(), label: randomLabel(), + description: randomPhrase(), }; mockGetVPCs([mockVPCs[1]]).as('getVPCs'); - mockGetVPC(mockVPCs[1]).as('getVPC'); mockUpdateVPC(mockVPCs[1].id, mockUpdatedVPC).as('updateVPC'); cy.visitWithLogin('/vpcs'); @@ -124,14 +119,14 @@ describe('VPC landing page', () => { cy.findByLabelText('Label') .should('be.visible') .should('have.value', mockVPCs[1].label) - .clear(); - cy.focused().type(mockUpdatedVPC.label); + .clear() + .type(mockUpdatedVPC.label); cy.findByLabelText('Description') .should('be.visible') .should('have.value', mockVPCs[1].description) - .clear(); - cy.focused().type(mockUpdatedVPC.description); + .clear() + .type(mockUpdatedVPC.description); // TODO Add interactions/assertions for region selection once feature is available. ui.button @@ -167,7 +162,6 @@ describe('VPC landing page', () => { // Delete VPCs Flow mockGetVPCs(mockVPCs).as('getVPCs'); - mockGetVPC(mockVPCs[0]).as('getVPC'); mockDeleteVPC(mockVPCs[0].id).as('deleteVPC'); cy.visitWithLogin('/vpcs'); @@ -189,8 +183,10 @@ describe('VPC landing page', () => { .findByTitle(`Delete VPC ${mockVPCs[0].label}`) .should('be.visible') .within(() => { - cy.findByLabelText('VPC Label').should('be.visible').click(); - cy.focused().type(mockVPCs[0].label); + cy.findByLabelText('VPC Label') + .should('be.visible') + .click() + .type(mockVPCs[0].label); ui.button .findByTitle('Delete') @@ -221,8 +217,10 @@ describe('VPC landing page', () => { .findByTitle(`Delete VPC ${mockVPCs[1].label}`) .should('be.visible') .within(() => { - cy.findByLabelText('VPC Label').should('be.visible').click(); - cy.focused().type(mockVPCs[1].label); + cy.findByLabelText('VPC Label') + .should('be.visible') + .click() + .type(mockVPCs[1].label); ui.button .findByTitle('Delete') @@ -257,7 +255,6 @@ describe('VPC landing page', () => { ]; mockGetVPCs(mockVPCs).as('getVPCs'); - mockGetVPC(mockVPCs[0]).as('getVPC'); mockDeleteVPCError(mockVPCs[0].id).as('deleteVPCError'); cy.visitWithLogin('/vpcs'); @@ -280,8 +277,10 @@ describe('VPC landing page', () => { .findByTitle(`Delete VPC ${mockVPCs[0].label}`) .should('be.visible') .within(() => { - cy.findByLabelText('VPC Label').should('be.visible').click(); - cy.focused().type(mockVPCs[0].label); + cy.findByLabelText('VPC Label') + .should('be.visible') + .click() + .type(mockVPCs[0].label); ui.button .findByTitle('Delete') @@ -306,7 +305,6 @@ describe('VPC landing page', () => { .click(); }); - mockGetVPC(mockVPCs[1]).as('getVPC'); cy.findByText(mockVPCs[1].label) .should('be.visible') .closest('tr') diff --git a/packages/manager/cypress/e2e/core/vpc/vpc-linodes-update.spec.ts b/packages/manager/cypress/e2e/core/vpc/vpc-linodes-update.spec.ts index baee7a5de3f..4ad53ea2022 100644 --- a/packages/manager/cypress/e2e/core/vpc/vpc-linodes-update.spec.ts +++ b/packages/manager/cypress/e2e/core/vpc/vpc-linodes-update.spec.ts @@ -3,31 +3,31 @@ */ import { - linodeConfigInterfaceFactoryWithVPC, - linodeFactory, -} from '@linode/utilities'; -import { linodeConfigFactory, subnetFactory, vpcFactory } from '@src/factories'; + mockGetSubnets, + mockCreateSubnet, + mockGetVPC, + mockGetVPCs, +} from 'support/intercepts/vpc'; import { - vpcAssignLinodeRebootNotice, - vpcUnassignLinodeRebootNotice, -} from 'support/constants/vpc'; + subnetFactory, + vpcFactory, + linodeFactory, + linodeConfigFactory, + LinodeConfigInterfaceFactoryWithVPC, +} from '@src/factories'; +import { ui } from 'support/ui'; +import { randomNumber, randomLabel } from 'support/util/random'; +import { mockGetLinodes } from 'support/intercepts/linodes'; import { mockCreateLinodeConfigInterfaces, - mockDeleteLinodeConfigInterface, mockGetLinodeConfigs, + mockDeleteLinodeConfigInterface, } from 'support/intercepts/configs'; -import { mockGetLinodes } from 'support/intercepts/linodes'; import { - mockCreateSubnet, - mockGetSubnet, - mockGetSubnets, - mockGetVPC, - mockGetVPCs, -} from 'support/intercepts/vpc'; -import { ui } from 'support/ui'; -import { randomLabel, randomNumber } from 'support/util/random'; - -import type { Config, Linode, VPC } from '@linode/api-v4'; + vpcAssignLinodeRebootNotice, + vpcUnassignLinodeRebootNotice, +} from 'support/constants/vpc'; +import { VPC, Linode, Config } from '@linode/api-v4'; describe('VPC assign/unassign flows', () => { let mockVPCs: VPC[]; @@ -100,8 +100,10 @@ describe('VPC assign/unassign flows', () => { .findByTitle('Create Subnet') .should('be.visible') .within(() => { - cy.findByText('Subnet Label').should('be.visible').click(); - cy.focused().type(mockSubnet.label); + cy.findByText('Subnet Label') + .should('be.visible') + .click() + .type(mockSubnet.label); cy.findByTestId('create-subnet-drawer-button') .should('be.visible') @@ -111,8 +113,6 @@ describe('VPC assign/unassign flows', () => { cy.wait(['@createSubnet', '@getVPC', '@getSubnets', '@getLinodes']); - mockGetSubnet(mockVPC.id, mockSubnet.id, mockSubnet); - // confirm that newly created subnet should now appear on VPC's detail page cy.findByText(mockVPC.label).should('be.visible'); cy.findByText('Subnets (1)').should('be.visible'); @@ -144,9 +144,11 @@ describe('VPC assign/unassign flows', () => { mockGetLinodeConfigs(mockLinode.id, [mockConfig]).as( 'getLinodeConfigs' ); - cy.findByLabelText('Linode').should('be.visible').click(); - cy.focused().type(mockLinode.label); - cy.focused().should('have.value', mockLinode.label); + cy.findByLabelText('Linode') + .should('be.visible') + .click() + .type(mockLinode.label) + .should('have.value', mockLinode.label); ui.autocompletePopper .findByTitle(mockLinode.label) @@ -209,9 +211,9 @@ describe('VPC assign/unassign flows', () => { subnets: [mockSubnet], }); - const vpcInterface = linodeConfigInterfaceFactoryWithVPC.build({ - subnet_id: mockSubnet.id, + const vpcInterface = LinodeConfigInterfaceFactoryWithVPC.build({ vpc_id: mockVPC.id, + subnet_id: mockSubnet.id, }); const mockLinodeConfig = linodeConfigFactory.build({ interfaces: [vpcInterface], @@ -234,8 +236,6 @@ describe('VPC assign/unassign flows', () => { cy.findByText('Subnets (1)').should('be.visible'); cy.findByText(mockSubnet.label).should('be.visible'); - mockGetSubnet(mockVPC.id, mockSubnet.id, mockSubnet); - // unassign a linode to the subnet ui.actionMenu .findByTitle(`Action menu for Subnet ${mockSubnet.label}`) @@ -266,8 +266,10 @@ describe('VPC assign/unassign flows', () => { 'getLinodeConfigs' ); - cy.findByLabelText('Linodes').should('be.visible').click(); - cy.focused().type(mockLinode.label); + cy.findByLabelText('Linodes') + .should('be.visible') + .click() + .type(mockLinode.label); ui.autocompletePopper .findByTitle(mockLinode.label) @@ -288,8 +290,10 @@ describe('VPC assign/unassign flows', () => { mockGetLinodeConfigs(mockSecondLinode.id, [mockLinodeConfig]).as( 'getLinodeConfigs' ); - cy.findByText('Linodes').should('be.visible').click(); - cy.focused().type(mockSecondLinode.label); + cy.findByText('Linodes') + .should('be.visible') + .click() + .type(mockSecondLinode.label); cy.findByText(mockSecondLinode.label).should('be.visible').click(); cy.wait('@getLinodeConfigs'); diff --git a/packages/manager/cypress/e2e/core/vpc/vpc-navigation.spec.ts b/packages/manager/cypress/e2e/core/vpc/vpc-navigation.spec.ts index 41eabbada0f..38ecad7a0d9 100644 --- a/packages/manager/cypress/e2e/core/vpc/vpc-navigation.spec.ts +++ b/packages/manager/cypress/e2e/core/vpc/vpc-navigation.spec.ts @@ -2,8 +2,8 @@ * @file Integration tests for VPC navigation. */ -import { mockGetUserPreferences } from 'support/intercepts/profile'; import { ui } from 'support/ui'; +import { mockGetUserPreferences } from 'support/intercepts/profile'; describe('VPC navigation', () => { /* diff --git a/packages/manager/cypress/support/api/common.ts b/packages/manager/cypress/support/api/common.ts index 69e9fe4cb6c..e9b88d819df 100644 --- a/packages/manager/cypress/support/api/common.ts +++ b/packages/manager/cypress/support/api/common.ts @@ -12,8 +12,12 @@ export const apiCheckErrors = ( if (resp.body && resp.body.ERRORARRAY && resp.body.ERRORARRAY.length > 0) { errs = resp.body.ERRORARRAY; } - if (failOnError && !!errs) { - throw new Error('API error!'); + if (failOnError) { + if (errs) { + expect((errs[0] as any).ERRORMESSAGE).not.to.be.exist; + } else { + expect(!!errs).to.be.false; + } } return errs; }; diff --git a/packages/manager/cypress/support/api/linodes.ts b/packages/manager/cypress/support/api/linodes.ts index 65e7a30bcc2..9fbe30f3302 100644 --- a/packages/manager/cypress/support/api/linodes.ts +++ b/packages/manager/cypress/support/api/linodes.ts @@ -1,5 +1,5 @@ import { Linode, deleteLinode, getLinodes } from '@linode/api-v4'; -import { linodeFactory } from '@linode/utilities'; +import { linodeFactory } from '@src/factories'; import { makeResourcePage } from '@src/mocks/serverHandlers'; import { pageSize } from 'support/constants/api'; import { depaginate } from 'support/util/paginate'; diff --git a/packages/manager/cypress/support/api/nodebalancers.ts b/packages/manager/cypress/support/api/nodebalancers.ts index d8c55b8922c..37ff6c420d2 100644 --- a/packages/manager/cypress/support/api/nodebalancers.ts +++ b/packages/manager/cypress/support/api/nodebalancers.ts @@ -1,5 +1,4 @@ import { deleteNodeBalancer, getNodeBalancers } from '@linode/api-v4'; -import { nodeBalancerFactory } from '@linode/utilities'; import { oauthToken, pageSize } from 'support/constants/api'; import { entityTag } from 'support/constants/cypress'; import { depaginate } from 'support/util/paginate'; @@ -7,7 +6,7 @@ import { randomLabel } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; import { isTestLabel } from './common'; - +import { nodeBalancerFactory } from 'src/factories'; import type { NodeBalancer } from '@linode/api-v4'; export const makeNodeBalCreateReq = (nodeBal: NodeBalancer) => { diff --git a/packages/manager/cypress/support/component/setup.tsx b/packages/manager/cypress/support/component/setup.tsx index 87fdadf9db1..18ff5ac6909 100644 --- a/packages/manager/cypress/support/component/setup.tsx +++ b/packages/manager/cypress/support/component/setup.tsx @@ -13,7 +13,7 @@ // https://on.cypress.io/configuration // *********************************************************** -import { queryClientFactory } from '@linode/queries'; +import { queryClientFactory } from '@src/queries/base'; import { QueryClientProvider } from '@tanstack/react-query'; import { RouterProvider, @@ -35,7 +35,7 @@ import { LinodeThemeWrapper } from 'src/LinodeThemeWrapper'; import { storeFactory } from 'src/store'; import type { ThemeName } from '@linode/ui'; -import type { AnyRoute, AnyRouter } from '@tanstack/react-router'; +import type { AnyRouter } from '@tanstack/react-router'; import type { Flags } from 'src/featureFlags'; /** @@ -48,8 +48,7 @@ export const mountWithTheme = ( jsx: React.ReactNode, theme: ThemeName = 'light', flags: Partial = {}, - useTanstackRouter: boolean = false, - routeTree?: (parentRoute: AnyRoute) => AnyRoute[] + useTanstackRouter: boolean = false ) => { const queryClient = queryClientFactory(); const store = storeFactory(); @@ -60,13 +59,10 @@ export const mountWithTheme = ( path: '/', }); const router: AnyRouter = createRouter({ - defaultNotFoundComponent: () =>
Not Found
, history: createMemoryHistory({ initialEntries: ['/'], }), - routeTree: routeTree - ? rootRoute.addChildren([indexRoute, ...routeTree(indexRoute)]) - : rootRoute.addChildren([indexRoute]), + routeTree: rootRoute.addChildren([indexRoute]), }); return mount( diff --git a/packages/manager/cypress/support/constants/alert.ts b/packages/manager/cypress/support/constants/alert.ts index 45190a28e47..2783c154f5c 100644 --- a/packages/manager/cypress/support/constants/alert.ts +++ b/packages/manager/cypress/support/constants/alert.ts @@ -41,6 +41,4 @@ export const aggregationTypeMap: Record = { export const statusMap: Record = { disabled: 'Disabled', enabled: 'Enabled', - failed: 'Failed', - 'in progress': 'In Progress', }; diff --git a/packages/manager/cypress/support/constants/databases.ts b/packages/manager/cypress/support/constants/databases.ts index ca64379cf9c..e463f331750 100644 --- a/packages/manager/cypress/support/constants/databases.ts +++ b/packages/manager/cypress/support/constants/databases.ts @@ -9,7 +9,7 @@ import { randomLabel } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; import { databaseEngineFactory, databaseTypeFactory } from '@src/factories'; -export interface DatabaseClusterConfiguration { +export interface databaseClusterConfiguration { clusterSize: ClusterSize; dbType: Engine; engine: string; @@ -325,7 +325,7 @@ export const mockDatabaseNodeTypes: DatabaseType[] = [ ]; // Array of database cluster configurations for which to test creation. -export const databaseConfigurations: DatabaseClusterConfiguration[] = [ +export const databaseConfigurations: databaseClusterConfiguration[] = [ { clusterSize: 1, dbType: 'mysql', @@ -344,6 +344,16 @@ export const databaseConfigurations: DatabaseClusterConfiguration[] = [ region: chooseRegion({ capabilities: ['Managed Databases'] }), version: '5', }, + // { + // label: randomLabel(), + // linodeType: 'g6-dedicated-16', + // clusterSize: 1, + // dbType: 'mongodb', + // regionTypeahead: 'Atlanta', + // region: 'us-southeast', + // engine: 'MongoDB', + // version: '4', + // }, { clusterSize: 3, dbType: 'postgresql', @@ -355,7 +365,7 @@ export const databaseConfigurations: DatabaseClusterConfiguration[] = [ }, ]; -export const databaseConfigurationsResize: DatabaseClusterConfiguration[] = [ +export const databaseConfigurationsResize: databaseClusterConfiguration[] = [ { clusterSize: 3, dbType: 'mysql', diff --git a/packages/manager/cypress/support/constants/dc-specific-pricing.ts b/packages/manager/cypress/support/constants/dc-specific-pricing.ts index 9c8ea564874..584fee1378f 100644 --- a/packages/manager/cypress/support/constants/dc-specific-pricing.ts +++ b/packages/manager/cypress/support/constants/dc-specific-pricing.ts @@ -2,7 +2,7 @@ * @file Constants related to DC-specific pricing. */ -import { linodeTypeFactory } from '@linode/utilities'; +import { linodeTypeFactory } from '@src/factories'; import type { LkePlanDescription } from 'support/api/lke'; diff --git a/packages/manager/cypress/support/e2e.ts b/packages/manager/cypress/support/e2e.ts index 0c614affa44..5996d3d71aa 100644 --- a/packages/manager/cypress/support/e2e.ts +++ b/packages/manager/cypress/support/e2e.ts @@ -14,8 +14,6 @@ // *********************************************************** import '@testing-library/cypress/add-commands'; -// reporter needs to register for events in order to attach media to test results in html report -import 'cypress-mochawesome-reporter/register'; // Cypress command and assertion setup. import chaiString from 'chai-string'; import 'cypress-axe'; diff --git a/packages/manager/cypress/support/intercepts/cloudpulse.ts b/packages/manager/cypress/support/intercepts/cloudpulse.ts index 0dc4df75750..8bcbbe22d65 100644 --- a/packages/manager/cypress/support/intercepts/cloudpulse.ts +++ b/packages/manager/cypress/support/intercepts/cloudpulse.ts @@ -14,6 +14,7 @@ import { makeResponse } from 'support/util/response'; import type { Alert, CloudPulseMetricsResponse, + CreateAlertDefinitionPayload, Dashboard, MetricDefinition, NotificationChannel, @@ -354,12 +355,12 @@ export const mockGetAlertChannels = ( export const mockCreateAlertDefinition = ( serviceType: string, - alert: Alert + createAlertRequest: CreateAlertDefinitionPayload ): Cypress.Chainable => { return cy.intercept( 'POST', apiMatcher(`/monitor/services/${serviceType}/alert-definitions`), - makeResponse(alert) + paginateResponse(createAlertRequest) ); }; /** @@ -391,128 +392,3 @@ export const mockUpdateAlertDefinitions = ( makeResponse(alert) ); }; -/** - * Mocks the API response for retrieving alert definitions in the monitoring service. - * This function intercepts a `GET` request to fetch a list of alert definitions for a specific - * service type and returns a mock response, simulating the behavior of the real API. - * - * The mock response allows the test to simulate the scenario where the system is retrieving - * the alert definitions without actually calling the backend API. - * - * @param {string} serviceType - The type of service (e.g., "dbaas", "web") for which the alert - * definitions are being retrieved. This value is part of the URL in the request. - * @param {Alert[]} alert - An array of `Alert` objects that will be returned as the mock response. - * These objects represent the alert definitions being fetched. - * - * @returns {Cypress.Chainable} - A Cypress chainable object that represents the intercepted - * `GET` request and the mock response, allowing subsequent - * Cypress commands to be chained. - */ - -export const mockGetAlertDefinition = ( - serviceType: string, - alert: Alert[] -): Cypress.Chainable => { - return cy.intercept( - 'GET', - apiMatcher(`/monitor/services/${serviceType}/alert-definitions`), - paginateResponse(alert) - ); -}; - -/** - * Mocks the API response for adding an entity to an alert definition in the monitoring service. - * This function intercepts a 'POST' request to associate a specific entity with an alert definition - * and returns a mock response, simulating the behavior of the real API. - * - * The mock response simulates the scenario where the system is successfully associating the - * entity with the alert definition without actually calling the backend API. - * - * @param {string} serviceType - The type of service (e.g., "dbaas", "Linocde") where the alert - * definition is being added. This value is part of the URL in the request. - * @param {string} entityId - The unique identifier of the entity being associated with the alert - * definition. This ID is part of the URL in the request. - * @param {Object} data - The data object containing the `alert-definition-id` being added to the entity. - * This object contains the alert definition that will be associated with the entity. - * - * @returns {Cypress.Chainable} - A Cypress chainable object that represents the intercepted - * 'POST' request and the mock response, allowing subsequent - * Cypress commands to be chained. - */ - -export const mockAddEntityToAlert = ( - serviceType: string, - entityId: string, - data: { 'alert-definition-id': number } -): Cypress.Chainable => { - return cy.intercept( - 'POST', - apiMatcher( - `/monitor/service/${serviceType}/entity/${entityId}/alert-definition` - ), - paginateResponse(data) - ); -}; - -/** - * Mocks the API response for adding an entity to an alert definition in the monitoring service. - * This function intercepts a 'DELETE' request to associate a specific entity with an alert definition - * and returns a mock response, simulating the behavior of the real API. - * - * The mock response simulates the scenario where the system is successfully associating the - * entity with the alert definition without actually calling the backend API. - * - * @param {string} serviceType - The type of service (e.g., "dbaas", "Linode") where the alert - * definition is being added. This value is part of the URL in the request. - * @param {string} entityId - The unique identifier of the entity being associated with the alert - * definition. This ID is part of the URL in the request. - * - * @param {number} alertId - The unique identifier of the alert definition from which the entity - * is being removed. This ID is part of the URL in the request. - * - * - * @returns {Cypress.Chainable} - A Cypress chainable object that represents the intercepted - * 'DELETE' request and the mock response, allowing subsequent - * Cypress commands to be chained. - */ - -export const mockDeleteEntityFromAlert = ( - serviceType: string, - entityId: string, - id: number -): Cypress.Chainable => { - return cy.intercept( - 'DELETE', - apiMatcher( - `/monitor/service/${serviceType}/entity/${entityId}/alert-definition/${id}` - ), - { - statusCode: 200, - } - ); -}; - -/** - * Intercepts and mocks – Indicates enabling/disabling alerts with error handling. - * - * @param {string} serviceType - The type of service for which the alert definition belongs. - * @param {number} id - The unique identifier of the alert definition. - * @param {string} errorMessage - The error message to be returned in the response. - * @param {number} [status=500] - The HTTP status code for the error response (default: 500). - * @returns {Cypress.Chainable} - A Cypress intercept that simulates a failed API request. - * - * This function is used in Cypress tests to simulate a failed API call when updating - * alert definitions. It intercepts `PUT` requests to the specified API endpoint and - * returns a mock error response. - */ -export const mockUpdateAlertDefinitionsError = ( - serviceType: string, - id: number, - errorMessage: string -): Cypress.Chainable => { - return cy.intercept( - 'PUT', - apiMatcher(`/monitor/services/${serviceType}/alert-definitions/${id}`), - makeErrorResponse(errorMessage, 500) - ); -}; diff --git a/packages/manager/cypress/support/intercepts/configs.ts b/packages/manager/cypress/support/intercepts/configs.ts index 1671b9227e0..3a6396050e9 100644 --- a/packages/manager/cypress/support/intercepts/configs.ts +++ b/packages/manager/cypress/support/intercepts/configs.ts @@ -4,10 +4,9 @@ import { apiMatcher } from 'support/util/intercepts'; import { paginateResponse } from 'support/util/paginate'; +import { Config } from '@linode/api-v4'; import { makeResponse } from 'support/util/response'; -import type { Config, Interface } from '@linode/api-v4'; - /** * Intercepts GET request to fetch all configs for a given linode. * @@ -103,7 +102,7 @@ export const mockDeleteLinodeConfigInterface = ( * Mocks GET request to retrieve Linode configs. * * @param linodeId - ID of Linode for mocked request. - * @param configs - a list of Linode configs with which to mocked response. + * @param configs - a list of Linode configswith which to mocked response. * * @returns Cypress chainable. */ @@ -118,54 +117,6 @@ export const mockGetLinodeConfigs = ( ); }; -/** - * Mocks GET request to retrieve a Linode Config - * - * @param linodeId - ID of Linode for mocked request. - * @param configId - ID of Config for mocked request. - * @param config - the config with which to mocked response. - * - * @returns Cypress chainable. - */ -export const mockGetLinodeConfig = (inputs: { - config: Config; - configId: number; - linodeId: number; -}): Cypress.Chainable => { - const { config, configId, linodeId } = inputs; - return cy.intercept( - 'GET', - apiMatcher(`linode/instances/${linodeId}/configs/${configId}`), - config - ); -}; - -/** - * Mocks GET request to retrieve an interface of a Linode config - * - * @param linodeId - ID of Linode for mocked request. - * @param configId - ID of Config for mocked request. - * @param linodeId - ID of Config Interface for mocked request. - * @param configInterface - the interface with which to mocked response. - * - * @returns Cypress chainable. - */ -export const mockGetLinodeConfigInterface = (inputs: { - configId: number; - configInterface: Interface; - interfaceId: number; - linodeId: number; -}): Cypress.Chainable => { - const { configId, configInterface, interfaceId, linodeId } = inputs; - return cy.intercept( - 'GET', - apiMatcher( - `linode/instances/${linodeId}/configs/${configId}/interfaces/${interfaceId}` - ), - configInterface - ); -}; - /** * Mocks PUT request to update a linode config. * diff --git a/packages/manager/cypress/support/intercepts/firewalls.ts b/packages/manager/cypress/support/intercepts/firewalls.ts index 490c1fb11bc..6ffe43ada71 100644 --- a/packages/manager/cypress/support/intercepts/firewalls.ts +++ b/packages/manager/cypress/support/intercepts/firewalls.ts @@ -37,29 +37,6 @@ export const mockGetFirewalls = ( ); }; -/** - * Intercepts GET request to fetch a Linode Interface's Firewalls - * - * @param linodeId - The ID of the Linode - * @param interfaceId - The ID of the Linode Interface - * @param firewalls - The Firewalls assigned to the LinodeInterface - * - * @returns Cypress chainable. - */ -export const mockGetLinodeInterfaceFirewalls = ( - linodeId: number, - interfaceId: number, - firewalls: Firewall[] -): Cypress.Chainable => { - return cy.intercept( - 'GET', - apiMatcher( - `linode/instances/${linodeId}/interfaces/${interfaceId}/firewalls` - ), - paginateResponse(firewalls) - ); -}; - /** * Intercepts POST request to create a Firewall and mocks response. * diff --git a/packages/manager/cypress/support/intercepts/linodes.ts b/packages/manager/cypress/support/intercepts/linodes.ts index 163dbb07136..2d806f28c6f 100644 --- a/packages/manager/cypress/support/intercepts/linodes.ts +++ b/packages/manager/cypress/support/intercepts/linodes.ts @@ -13,8 +13,6 @@ import type { Firewall, Kernel, Linode, - LinodeInterface, - LinodeInterfaces, LinodeIPsResponse, LinodeType, Volume, @@ -119,7 +117,7 @@ export const interceptGetLinodes = (): Cypress.Chainable => { export const mockGetLinodes = (linodes: Linode[]): Cypress.Chainable => { return cy.intercept( 'GET', - apiMatcher('linode/instances*'), + apiMatcher('linode/instances/**'), paginateResponse(linodes) ); }; @@ -191,25 +189,6 @@ export const interceptRebuildLinode = ( ); }; -/** - * Intercepts POST request to rebuild a Linode and mocks the response. - * - * @param linodeId - ID of Linode for intercepted request. - * @param linode - Linode for the mocked response - * - * @returns Cypress chainable. - */ -export const mockRebuildLinode = ( - linodeId: number, - linode: Linode -): Cypress.Chainable => { - return cy.intercept( - 'POST', - apiMatcher(`linode/instances/${linodeId}/rebuild`), - makeResponse(linode) - ); -}; - /** * Intercepts POST request to rebuild a Linode and mocks an error response. * @@ -643,41 +622,3 @@ export const interceptCancelLinodeBackups = ( apiMatcher(`linode/instances/${linodeId}/backups/cancel`) ); }; - -/** - * Mocks GET request to get a Linode's Interfaces. - * - * @param linodeId - ID of Linode to get interfaces associated with it - * @param interfaces - the mocked Linode interfaces - * - * @returns Cypress Chainable. - */ -export const mockGetLinodeInterfaces = ( - linodeId: number, - interfaces: LinodeInterfaces -): Cypress.Chainable => { - return cy.intercept( - 'GET', - apiMatcher(`linode/instances/${linodeId}/interfaces`), - interfaces - ); -}; - -/** - * Intercepts POST request to create a Linode Interface. - * - * @param linodeId - the Linodes ID to add the interface to. - * @param linodeInterface - a mock linode interface object. - * - * @returns Cypress chainable. - */ -export const mockCreateLinodeInterface = ( - linodeId: number, - linodeInterface: LinodeInterface -): Cypress.Chainable => { - return cy.intercept( - 'POST', - apiMatcher(`linode/instances/${linodeId}/interfaces`), - makeResponse(linodeInterface) - ); -}; diff --git a/packages/manager/cypress/support/intercepts/vpc.ts b/packages/manager/cypress/support/intercepts/vpc.ts index d2c179b8520..362b263076c 100644 --- a/packages/manager/cypress/support/intercepts/vpc.ts +++ b/packages/manager/cypress/support/intercepts/vpc.ts @@ -132,27 +132,6 @@ export const mockGetSubnets = ( ); }; -/** - * Intercepts GET request to get a specific subnet and mocks response. - * - * @param vpcId - ID of VPC for which to mock response. - * @param subnetId - ID of subnet for which to mock response. - * @param subnet - Subnet for which to mock response - * - * @returns Cypress chainable. - */ -export const mockGetSubnet = ( - vpcId: number, - subnetId: number, - subnet: Subnet -): Cypress.Chainable => { - return cy.intercept( - 'GET', - apiMatcher(`vpcs/${vpcId}/subnets/${subnetId}`), - subnet - ); -}; - /** * Intercepts DELETE request to delete a subnet of a VPC and mocks response * diff --git a/packages/manager/cypress/support/plugins/configure-multi-reporters.ts b/packages/manager/cypress/support/plugins/configure-multi-reporters.ts deleted file mode 100644 index 262f50fc223..00000000000 --- a/packages/manager/cypress/support/plugins/configure-multi-reporters.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { CypressPlugin } from './plugin'; -// The name of the environment variable to read when checking report configuration. -const envVarJunit = 'CY_TEST_JUNIT_REPORT'; -const envVarHtml = 'CY_TEST_HTML_REPORT'; - -/** - * Configure multiple reporters to be used by Cypress - * Multireporter uses between 0 and 2 reporters (junit, html) - * and for either core or component directory - * - * @returns Cypress configuration object. - */ -export const configureMultiReporters: CypressPlugin = (_on, config) => { - const arrReporters = []; - if (config.env[envVarJunit]) { - console.log('Junit reporting configuration added.'); - arrReporters.push('mocha-junit-reporter'); - } - if (config.env[envVarHtml]) { - console.log('Html reporting configuration added.'); - arrReporters.push('cypress-mochawesome-reporter'); - } - if (arrReporters.length > 0) { - config.reporter = 'cypress-multi-reporters'; - if (!config.reporterOptions) { - config.reporterOptions = {}; - } - config.reporterOptions.reporterEnabled = arrReporters.join(', '); - } else { - console.log('No reporters configured.'); - } - return config; -}; diff --git a/packages/manager/cypress/support/plugins/configure-test-suite.ts b/packages/manager/cypress/support/plugins/configure-test-suite.ts new file mode 100644 index 00000000000..0e431ee07a9 --- /dev/null +++ b/packages/manager/cypress/support/plugins/configure-test-suite.ts @@ -0,0 +1,38 @@ +import { CypressPlugin } from './plugin'; + +// The name of the environment variable to read when checking suite configuration. +const envVarName = 'CY_TEST_SUITE'; + +/** + * Overrides the Cypress test suite according to `CY_TEST_SUITE` environment variable. + * + * If `CY_TEST_SUITE` is undefined or invalid, the 'core' test suite will be run + * by default. + * + * The resolved test suite name can be read by tests and other plugins via + * `Cypress.env('cypress_test_suite')`. + * + * @returns Cypress configuration object. + */ +export const configureTestSuite: CypressPlugin = (_on, config) => { + const suiteName = (() => { + switch (config.env[envVarName]) { + case 'synthetic': + return 'synthetic'; + + case 'core': + default: + if (!!config.env[envVarName] && config.env[envVarName] !== 'core') { + const desiredSuite = config.env[envVarName]; + console.warn( + `Unknown test suite '${desiredSuite}'. Running 'core' test suite instead.` + ); + } + return 'core'; + } + })(); + + config.env['cypress_test_suite'] = suiteName; + config.specPattern = `cypress/e2e/${suiteName}/**/*.spec.{ts,tsx}`; + return config; +}; diff --git a/packages/manager/cypress/support/plugins/html-report.ts b/packages/manager/cypress/support/plugins/html-report.ts deleted file mode 100644 index ebcd7a528c2..00000000000 --- a/packages/manager/cypress/support/plugins/html-report.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { CypressPlugin } from './plugin'; -import cypressReporterLib from 'cypress-mochawesome-reporter/lib'; -const { beforeRunHook, afterRunHook } = cypressReporterLib; - -// The name of the environment variable to read when checking report configuration. -const envVarName = 'CY_TEST_HTML_REPORT'; - -/** - * @returns Cypress configuration object. - */ -export const enableHtmlReport: CypressPlugin = async function (on, config) { - if (!!config.env[envVarName]) { - if (!config.reporterOptions) { - config.reporterOptions = {}; - } - config.reporterOptions.cypressMochawesomeReporterReporterOptions = { - reportPageTitle: 'Cloud Manager E2e Test Results', - inlineAssets: true, - embeddedScreenshots: true, - videoOnFailOnly: true, - charts: true, - quiet: true, - }; - on('before:run', async (results) => { - await beforeRunHook(results); - }); - - on('after:run', async () => { - await afterRunHook(); - }); - } - return config; -}; diff --git a/packages/manager/cypress/support/plugins/junit-report.ts b/packages/manager/cypress/support/plugins/junit-report.ts index e2d1b3393ef..33117a64823 100644 --- a/packages/manager/cypress/support/plugins/junit-report.ts +++ b/packages/manager/cypress/support/plugins/junit-report.ts @@ -8,37 +8,42 @@ 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. + * * @returns Cypress configuration object. */ -export const enableJunitE2eReport: CypressPlugin = (_on, config) => { - const testSuiteName = 'core'; - return getCommonJunitConfig(testSuiteName, config); -}; +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'; -/** - * @returns Cypress configuration object. - */ -export const enableJunitComponentReport: CypressPlugin = (_on, config) => { - const testSuiteName = 'component'; - return getCommonJunitConfig(testSuiteName, config); -}; + 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'; -const getCommonJunitConfig = ( - testSuite: string, - config: Cypress.PluginConfigOptions -) => { - if (!!config.env[envVarName]) { - if (!config.reporterOptions) { - config.reporterOptions = {}; + // 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 ? '→' : ' ', + }; } - const testSuiteName = `${capitalize(testSuite)} Test Suite`; - config.reporterOptions.mochaJunitReporterReporterOptions = { - mochaFile: 'cypress/results/test-results-[hash].xml', - rootSuiteTitle: 'Cloud Manager Cypress Tests', - testsuitesTitle: testSuiteName, - jenkinsMode: true, - suiteTitleSeparatedBy: '→', - }; - } - return config; + return config; + }; }; diff --git a/packages/manager/cypress/support/setup/defer-command.ts b/packages/manager/cypress/support/setup/defer-command.ts index 1b09f4ab5db..bb695ef47a0 100644 --- a/packages/manager/cypress/support/setup/defer-command.ts +++ b/packages/manager/cypress/support/setup/defer-command.ts @@ -220,9 +220,6 @@ Cypress.Commands.add( return result; }; - return cy.wrap, T>(wrapPromise(), { - timeout: timeoutLength, - ...wrapOptions, - }); + return cy.wrap, T>(wrapPromise(), wrapOptions); } ); diff --git a/packages/manager/cypress/support/setup/test-tagging.ts b/packages/manager/cypress/support/setup/test-tagging.ts index 0d3b0931943..5dbd48c8405 100644 --- a/packages/manager/cypress/support/setup/test-tagging.ts +++ b/packages/manager/cypress/support/setup/test-tagging.ts @@ -19,7 +19,7 @@ const query = Cypress.env('CY_TEST_TAGS') ?? ''; */ Cypress.on('test:before:run', (_test: Test, _runnable: Runnable) => { /* - * Looks for the first command that does not belong in a hook and evaluates tags. + * Looks for the first command that does not belong in a hook and evalutes tags. * * Waiting for the first command to begin executing ensures that test context * is set up and that tags have been assigned to the test. @@ -27,16 +27,9 @@ Cypress.on('test:before:run', (_test: Test, _runnable: Runnable) => { const commandHandler = () => { const context = cy.state('ctx'); if (context && context.test?.type !== 'hook') { - const tags = context?.tags ? [...context.tags] : []; - - // Remove tags from context now that we've read them and can evaluate them. - // This prevents tags from persisting between tests in certain situations. - if (tags.length) { - context.tags = []; - } + const tags = context?.tags ?? []; if (!evaluateQuery(query, tags)) { - // eslint-disable-next-line sonarjs/no-skipped-tests context.skip(); } diff --git a/packages/manager/cypress/support/ui/common.ts b/packages/manager/cypress/support/ui/common.ts index 77c2debe55c..2dc2ac382d1 100644 --- a/packages/manager/cypress/support/ui/common.ts +++ b/packages/manager/cypress/support/ui/common.ts @@ -15,11 +15,7 @@ export const waitForAppLoad = (path = '/', withLogin = true) => { 'getNotifications' ); - if (withLogin) { - cy.visitWithLogin(path); - } else { - cy.visit(path); - } + withLogin ? cy.visitWithLogin(path) : cy.visit(path); cy.wait([ '@getAccount', '@getAccountSettings', diff --git a/packages/manager/cypress/support/util/components.ts b/packages/manager/cypress/support/util/components.ts index 607c3c019c3..7eca479968c 100644 --- a/packages/manager/cypress/support/util/components.ts +++ b/packages/manager/cypress/support/util/components.ts @@ -3,9 +3,9 @@ */ import type { ThemeName } from '@linode/ui'; -import type { AnyRoute } from '@tanstack/react-router'; import type { MountReturn } from 'cypress/react'; import type { Flags } from 'src/featureFlags'; + /** * Array of themes for which to test components. */ @@ -49,18 +49,11 @@ export const componentTests = ( componentName: string, callback: (mountCommand: MountCommand) => void, options: { - routeTree?: (parentRoute: AnyRoute) => AnyRoute[]; useTanstackRouter?: boolean; } = {} ) => { const mountCommand = (jsx: React.ReactNode, flags?: Flags) => - cy.mountWithTheme( - jsx, - defaultTheme, - flags, - options.useTanstackRouter, - options.routeTree - ); + cy.mountWithTheme(jsx, defaultTheme, flags, options.useTanstackRouter); describe(`${componentName} component tests`, () => { callback(mountCommand); }); @@ -78,23 +71,11 @@ export const componentTests = ( * * @param callback - Test scope callback. */ -export const visualTests = ( - callback: (mountCommand: MountCommand) => void, - options: { - routeTree?: (parentRoute: AnyRoute) => AnyRoute[]; - useTanstackRouter?: boolean; - } = {} -) => { +export const visualTests = (callback: (mountCommand: MountCommand) => void) => { describe('Visual tests', () => { componentThemes.forEach((themeName: ThemeName) => { const mountCommand = (jsx: React.ReactNode, flags?: any) => - cy.mountWithTheme( - jsx, - themeName, - flags, - options.useTanstackRouter, - options.routeTree - ); + cy.mountWithTheme(jsx, themeName, flags); describe(`${capitalize(themeName)} theme`, () => { callback(mountCommand); }); diff --git a/packages/manager/cypress/support/util/csv.ts b/packages/manager/cypress/support/util/csv.ts index c4cc6581f24..16c1388729b 100644 --- a/packages/manager/cypress/support/util/csv.ts +++ b/packages/manager/cypress/support/util/csv.ts @@ -17,7 +17,6 @@ export function parseCsv(csvContent: string): any[] { // Extract the headers from the first line and remove any quotes const headers = lines[0] .split(',') - // eslint-disable-next-line sonarjs/anchor-precedence .map((header) => header.trim().replace(/^"|"$/g, '')); // Map the remaining lines to objects using the headers @@ -31,9 +30,7 @@ export function parseCsv(csvContent: string): any[] { // - Removes the enclosing double quotes from quoted values // - Replaces any escaped double quotes within quoted values with a single double quote const values = line - // eslint-disable-next-line sonarjs/slow-regex .match(/("([^"]|"")*"|[^",\s]+)(?=\s*,|\s*$)/g) - // eslint-disable-next-line sonarjs/anchor-precedence ?.map((value) => value.trim().replace(/^"|"$/g, '').replace(/""/g, '"')); // Create an object to represent the row diff --git a/packages/manager/cypress/support/util/cypress-mochawesome-reporter.d.ts b/packages/manager/cypress/support/util/cypress-mochawesome-reporter.d.ts deleted file mode 100644 index 383f99e586b..00000000000 --- a/packages/manager/cypress/support/util/cypress-mochawesome-reporter.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'cypress-mochawesome-reporter/lib'; diff --git a/packages/manager/cypress/support/util/linodes.ts b/packages/manager/cypress/support/util/linodes.ts index c5c64f68535..d6b07f2c131 100644 --- a/packages/manager/cypress/support/util/linodes.ts +++ b/packages/manager/cypress/support/util/linodes.ts @@ -1,5 +1,5 @@ import { createLinode, getLinodeConfigs } from '@linode/api-v4'; -import { createLinodeRequestFactory } from '@linode/utilities'; +import { createLinodeRequestFactory } from '@src/factories'; import { findOrCreateDependencyFirewall } from 'support/api/firewalls'; import { findOrCreateDependencyVlan } from 'support/api/vlans'; import { pageSize } from 'support/constants/api'; @@ -8,8 +8,6 @@ import { pollLinodeDiskStatuses, pollLinodeStatus } from 'support/util/polling'; import { randomLabel, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; - import { depaginate } from './paginate'; import type { @@ -79,7 +77,7 @@ export const defaultCreateTestLinodeOptions = { * @returns Promise that resolves to the created Linode. */ export const createTestLinode = async ( - createRequestPayload?: null | Partial, + createRequestPayload?: Partial | null, options?: Partial ): Promise => { const resolvedOptions = { @@ -92,32 +90,29 @@ export const createTestLinode = async ( regionId = chooseRegion().id; } - const securityMethodPayload: Partial = - await (async () => { - switch (resolvedOptions.securityMethod) { - case 'firewall': - const firewall = await findOrCreateDependencyFirewall(); - return { - firewall_id: firewall.id, - }; - - case 'powered_off': - return { - booted: false, - }; - - case 'vlan_no_internet': - const vlanConfig = linodeVlanNoInternetConfig; - const vlanLabel = await findOrCreateDependencyVlan(regionId); - vlanConfig[0].label = vlanLabel; - return { - interfaces: vlanConfig, - }; - - default: - return {}; - } - })(); + const securityMethodPayload: Partial = await (async () => { + switch (resolvedOptions.securityMethod) { + case 'firewall': + default: + const firewall = await findOrCreateDependencyFirewall(); + return { + firewall_id: firewall.id, + }; + + case 'vlan_no_internet': + const vlanConfig = linodeVlanNoInternetConfig; + const vlanLabel = await findOrCreateDependencyVlan(regionId); + vlanConfig[0].label = vlanLabel; + return { + interfaces: vlanConfig, + }; + + case 'powered_off': + return { + booted: false, + }; + } + })(); const resolvedCreatePayload = { ...createLinodeRequestFactory.build({ @@ -152,7 +147,7 @@ export const createTestLinode = async ( ); } - // eslint-disable-next-line @linode/cloud-manager/no-createLinode + // eslint-disable-next-line const linode = await createLinode(resolvedCreatePayload); // Wait for disks to become available if `waitForDisks` option is set. @@ -189,7 +184,6 @@ export const createTestLinode = async ( }, message: `Create Linode '${linode.label}' (ID ${linode.id})`, name: 'createTestLinode', - timeout: LINODE_CREATE_TIMEOUT, }); return { diff --git a/packages/manager/cypress/support/util/lke.ts b/packages/manager/cypress/support/util/lke.ts index 63005b76c96..e1e65848532 100644 --- a/packages/manager/cypress/support/util/lke.ts +++ b/packages/manager/cypress/support/util/lke.ts @@ -1,4 +1,4 @@ -import { sortByVersion } from '@linode/utilities'; +import { sortByVersion } from 'src/utilities/sort-by'; /** * Returns the string of the highest semantic version. diff --git a/packages/manager/cypress/support/util/regions.ts b/packages/manager/cypress/support/util/regions.ts index d48677e5c05..888fd547c46 100644 --- a/packages/manager/cypress/support/util/regions.ts +++ b/packages/manager/cypress/support/util/regions.ts @@ -1,7 +1,6 @@ -import { getNewRegionLabel } from '@linode/utilities'; import { randomItem } from 'support/util/random'; - import { buildArray, shuffleArray } from './arrays'; +import { getNewRegionLabel } from 'src/components/RegionSelect/RegionSelect.utils'; import type { Capabilities, Region } from '@linode/api-v4'; @@ -17,7 +16,7 @@ import type { Capabilities, Region } from '@linode/api-v4'; * the `apiLabel` property. * * @see {@link https://github.com/linode/manager/pull/10740|Cloud Manager PR #10740} - * @see {@link packages/queries/src/regions/regions.ts (@linode/queries)} + * @see {@link src/queries/regions/regions.ts} */ export interface ExtendedRegion extends Region { /** Region label as defined by API v4. */ @@ -32,7 +31,7 @@ export interface ExtendedRegion extends Region { * @returns `true` if `region` is an `ExtendedRegion` instance, `false` otherwise. */ export const isExtendedRegion = ( - region: ExtendedRegion | Region + region: Region | ExtendedRegion ): region is ExtendedRegion => { if ('apiLabel' in region) { return true; @@ -51,13 +50,13 @@ export const isExtendedRegion = ( * @returns `ExtendedRegion` object for `region`. */ export const extendRegion = ( - region: ExtendedRegion | Region + region: Region | ExtendedRegion ): ExtendedRegion => { if (!isExtendedRegion(region)) { return { ...region, - apiLabel: region.label, label: getNewRegionLabel(region), + apiLabel: region.label, }; } return region; @@ -74,14 +73,14 @@ export const getRegionFromExtendedRegion = ( extendedRegion: ExtendedRegion ): Region => { return { - capabilities: extendedRegion.capabilities, - country: extendedRegion.country, id: extendedRegion.id, label: extendedRegion.apiLabel, + country: extendedRegion.country, + capabilities: extendedRegion.capabilities, placement_group_limits: extendedRegion.placement_group_limits, + status: extendedRegion.status, resolvers: extendedRegion.resolvers, site_type: extendedRegion.site_type, - status: extendedRegion.status, }; }; @@ -100,36 +99,6 @@ const disallowedRegionIds = [ // Washington, DC 'us-iad', - - // Atlanta, GA - 'us-southeast', - - // Dallas, TX - 'us-central', - - // Frankfurt, DE - 'eu-central', - - // Fremont, CA - 'us-west', - - // London, GB - 'eu-west', - - // Mumbai, IN - 'ap-west', - - // Newark, NJ - 'us-east', - - // Singapore, SG - 'ap-south', - - // Sydney, AU - 'ap-southeast', - - // Toronto, CA - 'ca-central', ]; /** @@ -236,11 +205,6 @@ interface ChooseRegionOptions { * Regions from which to choose. If unspecified, Regions exposed by the API will be used. */ regions?: Region[]; - - /** - * Array of region IDs to exclude from results, in addition to `disallowedRegionIds` regions. - */ - exclude?: string[]; } /** @@ -294,10 +258,6 @@ const resolveSearchRegions = ( ): Region[] => { const requiredCapabilities = options?.capabilities ?? []; const overrideRegion = getOverrideRegion(); - const allDisallowedRegionIds = [ - ...disallowedRegionIds, - ...(options?.exclude ?? []), - ]; // If the user has specified an override region for this run, it takes precedent // over any other specified criteria. @@ -312,7 +272,7 @@ const resolveSearchRegions = ( )}` ); } - if (allDisallowedRegionIds.includes(overrideRegion.id)) { + if (disallowedRegionIds.includes(overrideRegion.id)) { throw new Error( `Override region ${overrideRegion.id} (${overrideRegion.label}) is disallowed for testing due to capacity limitations.` ); @@ -323,7 +283,7 @@ const resolveSearchRegions = ( const capableRegions = regionsWithCapabilities( options?.regions ?? regions, requiredCapabilities - ).filter((region: Region) => !allDisallowedRegionIds.includes(region.id)); + ).filter((region: Region) => !disallowedRegionIds.includes(region.id)); if (!capableRegions.length) { throw new Error( diff --git a/packages/manager/cypress/support/util/tag.ts b/packages/manager/cypress/support/util/tag.ts index 203291be8f0..9be33d52a39 100644 --- a/packages/manager/cypress/support/util/tag.ts +++ b/packages/manager/cypress/support/util/tag.ts @@ -7,13 +7,6 @@ const queryRegex = /(?:-|\+)?([^\s]+)/g; * Allowed test tags. */ export type TestTag = - // Environment-related tags. - // Used to identify tests where certain environment-specific features are required. - | 'env:marketplaceApps' - | 'env:multipleRegions' - | 'env:premiumPlans' - | 'env:stackScripts' - // Feature-related tags. // Used to identify tests which deal with a certain feature or features. | 'feat:linodes' diff --git a/packages/manager/eslint.config.js b/packages/manager/eslint.config.js deleted file mode 100644 index b40b8f71f66..00000000000 --- a/packages/manager/eslint.config.js +++ /dev/null @@ -1,449 +0,0 @@ -import js from '@eslint/js'; -import eslint from '@eslint/js'; -import linodeRules from '@linode/eslint-plugin-cloud-manager/dist/index.js'; -import * as tsParser from '@typescript-eslint/parser'; -import eslintConfigPrettier from 'eslint-config-prettier'; -import pluginCypress from 'eslint-plugin-cypress/flat'; -import jsxA11y from 'eslint-plugin-jsx-a11y'; -import perfectionist from 'eslint-plugin-perfectionist'; -import prettier from 'eslint-plugin-prettier'; -import react from 'eslint-plugin-react'; -import reactHooks from 'eslint-plugin-react-hooks'; -import sonarjs from 'eslint-plugin-sonarjs'; -import testingLibrary from 'eslint-plugin-testing-library'; -import xss from 'eslint-plugin-xss'; -import { defineConfig } from 'eslint/config'; -import globals from 'globals'; -import tseslint from 'typescript-eslint'; - -export const baseConfig = [ - // 1. Ignores - { - ignores: [ - '**/node_modules/*', - '**/build/*', - '**/dist/*', - '**/lib/*', - '**/storybook-static/*', - '**/.storybook/*', - '**/public/*', - ], - }, - - // 2. TypeScript configuration - { - files: ['**/*.{js,ts,tsx}'], - languageOptions: { - globals: { - ...globals.browser, - }, - parser: tsParser, - parserOptions: { - ecmaFeatures: { jsx: true }, - ecmaVersion: 2020, - }, - }, - }, - - // 3. Recommended configs - eslint.configs.recommended, - js.configs.recommended, - jsxA11y.flatConfigs.recommended, - perfectionist.configs['recommended-natural'], - pluginCypress.configs.recommended, - react.configs.flat.recommended, - reactHooks.configs['recommended-latest'], - sonarjs.configs.recommended, - tseslint.configs.recommended, - - // 4. Base rules - { - files: ['**/*.{js,ts,tsx}'], - rules: { - 'array-callback-return': 'error', - 'comma-dangle': 'off', - curly: 'warn', - eqeqeq: 'warn', - 'no-await-in-loop': 'error', - 'no-bitwise': 'error', - 'no-caller': 'error', - 'no-case-declarations': 'warn', - 'no-console': 'error', - 'no-empty': 'warn', - 'no-eval': 'error', - 'no-extra-boolean-cast': 'warn', - 'no-invalid-this': 'off', - 'no-loop-func': 'error', - 'no-mixed-requires': 'warn', - 'no-multiple-empty-lines': 'error', - 'no-new-wrappers': 'error', - 'no-restricted-imports': [ - 'error', - 'rxjs', - '@mui/core', - '@mui/system', - '@mui/icons-material', - { - importNames: ['Typography'], - message: - 'Please use Typography component from @linode/ui instead of @mui/material', - name: '@mui/material', - }, - { - importNames: ['Link'], - message: - 'Please use the Link component from src/components/Link instead of react-router-dom', - name: 'react-router-dom', - }, - ], - 'no-restricted-syntax': [ - 'error', - { - message: - "The 'data-test-id' attribute is not allowed; use 'data-testid' instead.", - selector: "JSXAttribute[name.name='data-test-id']", - }, - ], - 'no-throw-literal': 'warn', - 'no-trailing-spaces': 'warn', - 'no-undef-init': 'off', - 'no-unused-expressions': 'warn', - 'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], - 'no-useless-escape': 'warn', - 'object-shorthand': 'warn', - 'sort-keys': 'off', - 'spaced-comment': 'warn', - }, - }, - - // 5. React and React Hooks - { - files: ['**/*.{ts,tsx}'], - plugins: { - react, - }, - rules: { - 'react-hooks/exhaustive-deps': 'warn', - 'react-hooks/rules-of-hooks': 'error', - 'react/display-name': 'off', - 'react/jsx-no-bind': 'off', - 'react/jsx-no-script-url': 'error', - 'react/jsx-no-useless-fragment': 'warn', - 'react/no-unescaped-entities': 'warn', - 'react/prop-types': 'off', - 'react/self-closing-comp': 'warn', - }, - }, - - // 6. TypeScript-specific - { - files: ['**/*.{ts,tsx}'], - rules: { - '@typescript-eslint/consistent-type-imports': 'warn', - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/interface-name-prefix': 'off', - '@typescript-eslint/naming-convention': [ - 'warn', - { - format: ['camelCase', 'UPPER_CASE', 'PascalCase'], - leadingUnderscore: 'allow', - selector: 'variable', - trailingUnderscore: 'allow', - }, - { - format: null, - modifiers: ['destructured'], - selector: 'variable', - }, - { - format: ['camelCase', 'PascalCase'], - selector: 'function', - }, - { - format: ['camelCase'], - leadingUnderscore: 'allow', - selector: 'parameter', - }, - { - format: ['PascalCase'], - selector: 'typeLike', - }, - ], - '@typescript-eslint/no-empty-interface': 'warn', - '@typescript-eslint/no-empty-object-type': 'warn', - '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/no-inferrable-types': 'off', - '@typescript-eslint/no-namespace': 'warn', - '@typescript-eslint/no-non-null-assertion': 'off', - '@typescript-eslint/no-unused-vars': 'off', - '@typescript-eslint/no-use-before-define': 'off', - }, - }, - - // 7. XSS - { - files: ['**/*.{js,ts,tsx}'], - plugins: { - xss, - }, - }, - - // 8. SonarJS - { - files: ['**/*.{js,ts,tsx}'], - rules: { - 'sonarjs/arrow-function-convention': 'off', - 'sonarjs/cognitive-complexity': 'off', - 'sonarjs/duplicates-in-character-class': 'warn', - 'sonarjs/no-clear-text-protocols': 'off', - 'sonarjs/no-commented-code': 'warn', - 'sonarjs/no-duplicate-string': 'warn', - 'sonarjs/no-identical-functions': 'warn', - 'sonarjs/no-ignored-exceptions': 'warn', - 'sonarjs/no-nested-conditional': 'off', - 'sonarjs/no-nested-functions': 'warn', - 'sonarjs/no-redundant-jump': 'warn', - 'sonarjs/no-small-switch': 'warn', - 'sonarjs/prefer-immediate-return': 'warn', - 'sonarjs/prefer-single-boolean-return': 'off', - 'sonarjs/redundant-type-aliases': 'warn', - 'sonarjs/todo-tag': 'warn', - 'sonarjs/single-character-alternation': 'warn', - 'sonarjs/no-duplicate-in-composite': 'warn', - 'sonarjs/no-nested-template-literals': 'off', - 'sonarjs/public-static-readonly': 'warn', - 'sonarjs/concise-regex': 'warn', - 'sonarjs/use-type-alias': 'warn', - }, - }, - - // 9. JSX A11y - { - files: ['**/*.{jsx,tsx}'], - rules: { - 'jsx-a11y/aria-role': [ - 'error', - { - allowedInvalidRoles: [ - 'graphics-document', - 'graphics-object', - 'graphics-symbol', - ], - }, - ], - }, - }, - - // 10. Perfectionist - { - files: ['**/*.{js,ts,tsx}'], - rules: { - 'perfectionist/sort-array-includes': 'warn', - 'perfectionist/sort-classes': 'warn', - 'perfectionist/sort-enums': 'warn', - 'perfectionist/sort-exports': 'warn', - 'perfectionist/sort-heritage-clauses': 'off', - 'perfectionist/sort-imports': [ - 'warn', - { - customGroups: { - type: { - react: ['^react$', '^react-.+'], - src: ['^src$'], - }, - value: { - react: ['^react$', '^react-.+'], - src: ['^src$', '^src/.+'], - }, - }, - groups: [ - ['react', 'builtin', 'external'], - ['src', 'internal'], - ['parent', 'sibling', 'index'], - 'object', - 'unknown', - [ - 'type', - 'internal-type', - 'parent-type', - 'sibling-type', - 'index-type', - ], - ], - newlinesBetween: 'always', - }, - ], - 'perfectionist/sort-interfaces': 'warn', - 'perfectionist/sort-intersection-types': 'off', - 'perfectionist/sort-jsx-props': 'warn', - 'perfectionist/sort-modules': 'off', - 'perfectionist/sort-named-exports': 'warn', - 'perfectionist/sort-named-imports': 'warn', - 'perfectionist/sort-object-types': 'warn', - 'perfectionist/sort-objects': 'off', - 'perfectionist/sort-sets': 'off', - 'perfectionist/sort-switch-case': 'warn', - 'perfectionist/sort-union-types': 'warn', - }, - }, - - // 11. Cloud Manager - { - files: ['**/*.{js,ts,tsx}'], - plugins: { - '@linode/cloud-manager': linodeRules, - }, - rules: { - '@linode/cloud-manager/no-custom-fontWeight': 'warn', - '@linode/cloud-manager/deprecate-formik': 'warn', - '@linode/cloud-manager/no-createLinode': 'off', - '@linode/cloud-manager/no-mui-theme-spacing': 'warn', - }, - }, - - // 12. Unit tests, factories, mocks & stories - { - files: [ - '**/*.test.{js,ts,tsx}', - '**/*.stories.{js,ts,tsx}', - '**/factories/**/*.{js,ts,tsx}', - '**/__data__/**/*.{js,ts,tsx}', - '**/mocks/**/*.{js,ts,tsx}', - ], - plugins: { - 'testing-library': testingLibrary, - }, - rules: { - '@typescript-eslint/no-empty-object-type': 'off', - 'no-useless-escape': 'off', - 'no-empty-pattern': 'off', - ...Object.fromEntries( - Object.keys(testingLibrary.rules).map((rule) => { - // Special case for consistent-data-testid which needs config - if (rule === 'consistent-data-testid') { - return [ - `testing-library/${rule}`, - [ - 'warn', - { - testIdAttribute: 'data-testid', - testIdPattern: '^[a-z-]+$', - }, - ], - ]; - } - // All other rules just get set to warn - return [`testing-library/${rule}`, 'warn']; - }) - ), - // This will make all sonar rules warnings. - // It is a good idea to keep them as such so that we don't introduce new issues that could trigger dependabot or security scripts. - ...Object.fromEntries( - Object.keys(sonarjs.rules).map((rule) => [`sonarjs/${rule}`, 'warn']) - ), - 'sonarjs/arrow-function-convention': 'off', - 'sonarjs/enforce-trailing-comma': 'off', - 'sonarjs/file-header': 'off', - 'sonarjs/no-implicit-dependencies': 'off', - 'sonarjs/no-reference-error': 'off', - 'sonarjs/no-wildcard-import': 'off', - 'sonarjs/no-hardcoded-ip': 'off', - 'sonarjs/pseudo-random': 'off', - }, - }, - - // 13. Cypress - { - files: ['**/cypress/**/*.{js,ts,tsx}'], - rules: { - 'no-console': 'off', - 'sonarjs/pseudo-random': 'off', - 'sonarjs/no-hardcoded-ip': 'off', - '@linode/cloud-manager/no-createLinode': 'error', - }, - }, - - // 14. Tanstack Router (temporary) - { - files: [ - // for each new features added to the migration router, add its directory here - 'src/features/Betas/**/*', - 'src/features/Domains/**/*', - 'src/features/Firewalls/**/*', - 'src/features/Images/**/*', - 'src/features/Longview/**/*', - 'src/features/NodeBalancers/**/*', - 'src/features/ObjectStorage/**/*', - 'src/features/PlacementGroups/**/*', - 'src/features/StackScripts/**/*', - 'src/features/Volumes/**/*', - 'src/features/VPCs/**/*', - ], - rules: { - 'no-restricted-imports': [ - // This needs to remain an error however trying to link to a feature that is not yet migrated will break the router - // For those cases react-router-dom history.push is still needed - // using `eslint-disable-next-line no-restricted-imports` can help bypass those imports - 'error', - { - paths: [ - { - importNames: [ - // intentionally not including in this list as this will be updated last globally - 'useNavigate', - 'useParams', - 'useLocation', - 'useHistory', - 'useRouteMatch', - 'matchPath', - 'MemoryRouter', - 'Route', - 'RouteProps', - 'Switch', - 'Redirect', - 'RouteComponentProps', - 'withRouter', - ], - message: - 'Please use routing utilities intended for @tanstack/react-router.', - name: 'react-router-dom', - }, - { - importNames: ['TabLinkList'], - message: - 'Please use the TanStackTabLinkList component for components being migrated to TanStack Router.', - name: 'src/components/Tabs/TabLinkList', - }, - { - importNames: ['OrderBy', 'default'], - message: - 'Please use useOrderV2 hook for components being migrated to TanStack Router.', - name: 'src/components/OrderBy', - }, - { - importNames: ['Prompt'], - message: - 'Please use the TanStack useBlocker hook for components/features being migrated to TanStack Router.', - name: 'src/components/Prompt/Prompt', - }, - ], - }, - ], - }, - }, - - // 15. Prettier (coming last as recommended) - { - files: ['**/*.{js,ts,tsx}'], - plugins: { - prettier, - }, - rules: { - ...eslintConfigPrettier.rules, - 'prettier/prettier': 'warn', - }, - }, -]; - -export default defineConfig(baseConfig); diff --git a/packages/manager/package.json b/packages/manager/package.json index 388c67e8726..fa249919723 100644 --- a/packages/manager/package.json +++ b/packages/manager/package.json @@ -2,7 +2,7 @@ "name": "linode-manager", "author": "Linode", "description": "The Linode Manager website", - "version": "1.139.0", + "version": "1.137.2", "private": true, "type": "module", "bugs": { @@ -25,19 +25,17 @@ "@hookform/resolvers": "3.9.1", "@linode/api-v4": "workspace:*", "@linode/design-language-system": "^4.0.0", - "@linode/queries": "workspace:*", "@linode/search": "workspace:*", - "@linode/shared": "workspace:*", "@linode/ui": "workspace:*", - "@linode/utilities": "workspace:*", "@linode/validation": "workspace:*", + "@linode/utilities": "workspace:*", "@lukemorales/query-key-factory": "^1.3.4", "@mui/icons-material": "^6.4.5", "@mui/material": "^6.4.5", "@mui/utils": "^6.4.3", "@mui/x-date-pickers": "^7.27.0", "@paypal/react-paypal-js": "^7.8.3", - "@reach/tabs": "^0.18.0", + "@reach/tabs": "^0.10.5", "@sentry/react": "^7.119.1", "@shikijs/langs": "^3.1.0", "@shikijs/themes": "^3.1.0", @@ -46,7 +44,7 @@ "@tanstack/react-router": "^1.111.11", "@xterm/xterm": "^5.5.0", "algoliasearch": "^4.14.3", - "axios": "~1.8.3", + "axios": "~1.7.4", "braintree-web": "^3.92.2", "chart.js": "~2.9.4", "copy-to-clipboard": "^3.0.8", @@ -58,7 +56,8 @@ "he": "^1.2.0", "immer": "^9.0.6", "ipaddr.js": "^1.9.1", - "jspdf": "^3.0.1", + "js-sha256": "^0.11.0", + "jspdf": "^3.0.0", "jspdf-autotable": "^5.0.2", "launchdarkly-react-client-sdk": "3.0.10", "libphonenumber-js": "^1.10.6", @@ -120,26 +119,25 @@ "prettier --write", "eslint --ext .js,.ts,.tsx --quiet" ], - "*.{ts,tsx}": [ - "sh -c 'pnpm typecheck'" - ] + "*.{ts,tsx}": ["sh -c 'pnpm typecheck'"] }, "devDependencies": { "@4tw/cypress-drag-drop": "^2.3.0", - "@storybook/addon-a11y": "^8.6.7", - "@storybook/addon-actions": "^8.6.7", - "@storybook/addon-controls": "^8.6.7", - "@storybook/addon-docs": "^8.6.7", - "@storybook/addon-mdx-gfm": "^8.6.7", - "@storybook/addon-measure": "^8.6.7", - "@storybook/addon-storysource": "^8.6.7", - "@storybook/addon-viewport": "^8.6.7", - "@storybook/blocks": "^8.6.7", - "@storybook/manager-api": "^8.6.7", - "@storybook/preview-api": "^8.6.7", - "@storybook/react": "^8.6.7", - "@storybook/react-vite": "^8.6.7", - "@storybook/theming": "^8.6.7", + "@linode/eslint-plugin-cloud-manager": "^0.0.7", + "@storybook/addon-a11y": "^8.4.7", + "@storybook/addon-actions": "^8.4.7", + "@storybook/addon-controls": "^8.4.7", + "@storybook/addon-docs": "^8.4.7", + "@storybook/addon-mdx-gfm": "^8.4.7", + "@storybook/addon-measure": "^8.4.7", + "@storybook/addon-storysource": "^8.4.7", + "@storybook/addon-viewport": "^8.4.7", + "@storybook/blocks": "^8.4.7", + "@storybook/manager-api": "^8.4.7", + "@storybook/preview-api": "^8.4.7", + "@storybook/react": "^8.4.7", + "@storybook/react-vite": "^8.4.7", + "@storybook/theming": "^8.4.7", "@swc/core": "^1.10.9", "@testing-library/cypress": "^10.0.3", "@testing-library/dom": "^10.1.0", @@ -150,7 +148,6 @@ "@types/chai-string": "^1.4.5", "@types/chart.js": "^2.9.21", "@types/css-mediaquery": "^0.1.1", - "@types/eslint-plugin-jsx-a11y": "^6.10.0", "@types/he": "^1.1.0", "@types/history": "4", "@types/jspdf": "^1.3.3", @@ -172,6 +169,8 @@ "@types/throttle-debounce": "^1.0.0", "@types/uuid": "^3.4.3", "@types/zxcvbn": "^4.4.0", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", "@vitejs/plugin-react-swc": "^3.7.2", "@vitest/coverage-v8": "^3.0.7", "axe-core": "^4.10.2", @@ -181,24 +180,35 @@ "cypress": "14.0.1", "cypress-axe": "^1.6.0", "cypress-file-upload": "^5.0.8", - "cypress-mochawesome-reporter": "^3.8.2", - "cypress-multi-reporters": "^2.0.5", - "cypress-on-fix": "^1.1.0", "cypress-real-events": "^1.14.0", "cypress-vite": "^1.6.0", "dotenv": "^16.0.3", + "eslint": "^7.1.0", + "eslint-config-prettier": "~8.1.0", + "eslint-plugin-cypress": "^2.11.3", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-perfectionist": "^1.4.0", + "eslint-plugin-prettier": "~3.3.1", + "eslint-plugin-ramda": "^2.5.1", + "eslint-plugin-react": "^7.19.0", + "eslint-plugin-react-hooks": "^3.0.0", + "eslint-plugin-react-refresh": "0.4.13", + "eslint-plugin-scanjs-rules": "^0.2.1", + "eslint-plugin-sonarjs": "^0.5.0", + "eslint-plugin-testing-library": "^3.1.2", + "eslint-plugin-xss": "^0.1.10", "factory.ts": "^0.5.1", "glob": "^10.3.1", - "globals": "^16.0.0", "history": "4", "jsdom": "^24.1.1", "mocha-junit-reporter": "^2.2.1", "msw": "^2.2.3", "pdfreader": "^3.0.7", + "prettier": "~2.2.1", "redux-mock-store": "^1.5.3", - "storybook": "^8.6.7", + "storybook": "^8.4.7", "storybook-dark-mode": "4.0.1", - "vite": "^6.2.5", + "vite": "^6.1.1", "vite-plugin-svgr": "^3.2.0" }, "browserslist": [ diff --git a/packages/manager/src/MainContent.tsx b/packages/manager/src/MainContent.tsx index 3f5644439cd..ebe5c399f1d 100644 --- a/packages/manager/src/MainContent.tsx +++ b/packages/manager/src/MainContent.tsx @@ -1,9 +1,3 @@ -import { - useAccountSettings, - useMutatePreferences, - usePreferences, - useProfile, -} from '@linode/queries'; import { Box } from '@linode/ui'; import { useMediaQuery } from '@mui/material'; import Grid from '@mui/material/Grid2'; @@ -30,6 +24,10 @@ import { useNotificationContext, } from 'src/features/NotificationCenter/NotificationCenterContext'; import { TopMenu } from 'src/features/TopMenu/TopMenu'; +import { + useMutatePreferences, + usePreferences, +} from 'src/queries/profile/preferences'; import { useIsPageScrollable } from './components/PrimaryNav/utils'; import { ENABLE_MAINTENANCE_MODE } from './constants'; @@ -41,6 +39,8 @@ import { useIsDatabasesEnabled } from './features/Databases/utilities'; import { useIsIAMEnabled } from './features/IAM/Shared/utilities'; import { TOPMENU_HEIGHT } from './features/TopMenu/constants'; import { useGlobalErrors } from './hooks/useGlobalErrors'; +import { useAccountSettings } from './queries/account/settings'; +import { useProfile } from './queries/profile/profile'; import { migrationRouter } from './routes'; import type { Theme } from '@mui/material/styles'; @@ -123,11 +123,18 @@ const Kubernetes = React.lazy(() => default: module.Kubernetes, })) ); +const ObjectStorage = React.lazy(() => import('src/features/ObjectStorage')); const Profile = React.lazy(() => import('src/features/Profile/Profile').then((module) => ({ default: module.Profile, })) ); +const NodeBalancers = React.lazy( + () => import('src/features/NodeBalancers/NodeBalancers') +); +const StackScripts = React.lazy( + () => import('src/features/StackScripts/StackScripts') +); const SupportTickets = React.lazy( () => import('src/features/Support/SupportTickets') ); @@ -156,21 +163,12 @@ const AccountActivationLanding = React.lazy( () => import('src/components/AccountActivation/AccountActivationLanding') ); const Databases = React.lazy(() => import('src/features/Databases')); +const VPC = React.lazy(() => import('src/features/VPCs')); -const CloudPulseMetrics = React.lazy(() => - import('src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding').then( - (module) => ({ - default: module.CloudPulseDashboardLanding, - }) - ) -); - -const CloudPulseAlerts = React.lazy(() => - import('src/features/CloudPulse/Alerts/AlertsLanding/AlertsLanding').then( - (module) => ({ - default: module.AlertsLanding, - }) - ) +const CloudPulse = React.lazy(() => + import('src/features/CloudPulse/CloudPulseLanding').then((module) => ({ + default: module.CloudPulseLanding, + })) ); const IAM = React.lazy(() => @@ -297,8 +295,8 @@ export const MainContent = () => { isNarrowViewport ? '100%' : isPageScrollable - ? '100vh' - : `calc(100vh - ${TOPMENU_HEIGHT}px)` + ? '100vh' + : `calc(100vh - ${TOPMENU_HEIGHT}px)` } position="sticky" top={0} @@ -319,9 +317,9 @@ export const MainContent = () => { marginLeft: isNarrowViewport ? 0 : desktopMenuIsOpen || - (desktopMenuIsOpen && desktopMenuIsOpen === true) - ? SIDEBAR_COLLAPSED_WIDTH - : SIDEBAR_WIDTH, + (desktopMenuIsOpen && desktopMenuIsOpen === true) + ? SIDEBAR_COLLAPSED_WIDTH + : SIDEBAR_WIDTH, }} > @@ -363,7 +361,19 @@ export const MainContent = () => { }> + + + {isIAMEnabled && ( @@ -376,17 +386,9 @@ export const MainContent = () => { {isDatabasesEnabled && ( )} + {isACLPEnabled && ( - - )} - {isACLPEnabled && ( - + )} {/** We don't want to break any bookmarks. This can probably be removed eventually. */} diff --git a/packages/manager/src/Root.tsx b/packages/manager/src/Root.tsx index 2527a0f1a12..1e8202ef083 100644 --- a/packages/manager/src/Root.tsx +++ b/packages/manager/src/Root.tsx @@ -26,14 +26,14 @@ import { TopMenu } from 'src/features/TopMenu/TopMenu'; import { useMutatePreferences, usePreferences, - useProfile, -} from '@linode/queries'; +} from 'src/queries/profile/preferences'; import { ENABLE_MAINTENANCE_MODE } from './constants'; import { complianceUpdateContext } from './context/complianceUpdateContext'; import { sessionExpirationContext } from './context/sessionExpirationContext'; import { switchAccountSessionContext } from './context/switchAccountSessionContext'; import { useGlobalErrors } from './hooks/useGlobalErrors'; +import { useProfile } from './queries/profile/profile'; import { useStyles } from './Root.styles'; export const Root = () => { diff --git a/packages/manager/src/Router.tsx b/packages/manager/src/Router.tsx index f7fcb6f48ed..a8e12fd54f3 100644 --- a/packages/manager/src/Router.tsx +++ b/packages/manager/src/Router.tsx @@ -1,4 +1,3 @@ -import { useAccountSettings } from '@linode/queries'; import { QueryClient } from '@tanstack/react-query'; import { RouterProvider } from '@tanstack/react-router'; import * as React from 'react'; @@ -8,6 +7,7 @@ import { useGlobalErrors } from 'src/hooks/useGlobalErrors'; import { useIsACLPEnabled } from './features/CloudPulse/Utils/utils'; import { useIsDatabasesEnabled } from './features/Databases/utilities'; import { useIsPlacementGroupsEnabled } from './features/PlacementGroups/utils'; +import { useAccountSettings } from './queries/account/settings'; import { router } from './routes'; export const Router = () => { diff --git a/packages/utilities/src/__data__/regionsData.ts b/packages/manager/src/__data__/regionsData.ts similarity index 100% rename from packages/utilities/src/__data__/regionsData.ts rename to packages/manager/src/__data__/regionsData.ts diff --git a/packages/manager/src/__data__/searchResults.ts b/packages/manager/src/__data__/searchResults.ts index 9dc532e6b69..408f349a060 100644 --- a/packages/manager/src/__data__/searchResults.ts +++ b/packages/manager/src/__data__/searchResults.ts @@ -42,7 +42,7 @@ export const searchbarResult1 = { searchText: 'result', tags: [], }, - entityType: 'linode' as const, + entityType: 'linode' as any, label: 'result1', value: '111111', }; @@ -57,7 +57,6 @@ export const searchbarResult2 = { searchText: 'result', tags: ['tag1', 'tag2'], }, - entityType: 'nodebalancer' as const, label: 'result2', value: '222222', }; diff --git a/packages/manager/src/assets/icons/chevron-up.svg b/packages/manager/src/assets/icons/chevron-up.svg new file mode 100644 index 00000000000..79281e16a34 --- /dev/null +++ b/packages/manager/src/assets/icons/chevron-up.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/manager/src/assets/icons/close.svg b/packages/manager/src/assets/icons/close.svg deleted file mode 100644 index 96dca5a4443..00000000000 --- a/packages/manager/src/assets/icons/close.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/manager/src/assets/icons/refresh.svg b/packages/manager/src/assets/icons/refresh.svg index 118f068f75c..4864b6402c3 100644 --- a/packages/manager/src/assets/icons/refresh.svg +++ b/packages/manager/src/assets/icons/refresh.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/manager/src/assets/icons/swapSmall.svg b/packages/manager/src/assets/icons/swapSmall.svg index 3d69d430869..6711e50df3b 100644 --- a/packages/manager/src/assets/icons/swapSmall.svg +++ b/packages/manager/src/assets/icons/swapSmall.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/manager/src/assets/icons/zoomin.svg b/packages/manager/src/assets/icons/zoomin.svg index 389ea95c5da..1a9d0e6873d 100644 --- a/packages/manager/src/assets/icons/zoomin.svg +++ b/packages/manager/src/assets/icons/zoomin.svg @@ -1,18 +1,18 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + diff --git a/packages/manager/src/assets/icons/zoomout.svg b/packages/manager/src/assets/icons/zoomout.svg index 30f229b8083..173bcb9b058 100644 --- a/packages/manager/src/assets/icons/zoomout.svg +++ b/packages/manager/src/assets/icons/zoomout.svg @@ -1,10 +1,10 @@ - - - - - - - - - + + + + + + + + + diff --git a/packages/manager/src/components/AbuseTicketBanner/AbuseTicketBanner.tsx b/packages/manager/src/components/AbuseTicketBanner/AbuseTicketBanner.tsx index 40e1067eb5f..d9d10a0becb 100644 --- a/packages/manager/src/components/AbuseTicketBanner/AbuseTicketBanner.tsx +++ b/packages/manager/src/components/AbuseTicketBanner/AbuseTicketBanner.tsx @@ -1,12 +1,11 @@ -import { useNotificationsQuery } from '@linode/queries'; import { Typography } from '@linode/ui'; import Grid from '@mui/material/Grid2'; import { DateTime } from 'luxon'; import * as React from 'react'; -import { useLocation } from 'react-router-dom'; +import { Link, useLocation } from 'react-router-dom'; import { DismissibleBanner } from 'src/components/DismissibleBanner/DismissibleBanner'; -import { Link } from 'src/components/Link'; +import { useNotificationsQuery } from 'src/queries/account/notifications'; import { getAbuseTickets } from 'src/store/selectors/getAbuseTicket'; const preferenceKey = 'abuse-tickets'; diff --git a/packages/manager/src/components/AccessPanel/UserSSHKeyPanel.test.tsx b/packages/manager/src/components/AccessPanel/UserSSHKeyPanel.test.tsx index 6cc350e05aa..d0885e58d39 100644 --- a/packages/manager/src/components/AccessPanel/UserSSHKeyPanel.test.tsx +++ b/packages/manager/src/components/AccessPanel/UserSSHKeyPanel.test.tsx @@ -1,8 +1,8 @@ -import { profileFactory, sshKeyFactory } from '@linode/utilities'; import { waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; +import { profileFactory, sshKeyFactory } from 'src/factories'; import { accountUserFactory } from 'src/factories/accountUsers'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; @@ -22,7 +22,7 @@ describe('UserSSHKeyPanel', () => { return HttpResponse.json(makeResourcePage([])); }), http.get('*/account/users', () => { - return HttpResponse.json(makeResourcePage([]), { status: 401 }); + return HttpResponse.json(makeResourcePage([])); }) ); const { queryByTestId } = renderWithTheme( @@ -46,7 +46,7 @@ describe('UserSSHKeyPanel', () => { return HttpResponse.json(makeResourcePage(sshKeys)); }), http.get('*/account/users', () => { - return HttpResponse.json(makeResourcePage([]), { status: 401 }); + return HttpResponse.json(makeResourcePage([])); }) ); const { getByText } = renderWithTheme( diff --git a/packages/manager/src/components/AccessPanel/UserSSHKeyPanel.tsx b/packages/manager/src/components/AccessPanel/UserSSHKeyPanel.tsx index c745a8bbb0e..5f02af4faee 100644 --- a/packages/manager/src/components/AccessPanel/UserSSHKeyPanel.tsx +++ b/packages/manager/src/components/AccessPanel/UserSSHKeyPanel.tsx @@ -1,6 +1,4 @@ -import { useAccountUsers, useProfile, useSSHKeysQuery } from '@linode/queries'; import { Box, Button, Checkbox, Typography } from '@linode/ui'; -import { truncateAndJoinList } from '@linode/utilities'; import { useTheme } from '@mui/material/styles'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; @@ -14,6 +12,9 @@ import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty'; import { TableRowError } from 'src/components/TableRowError/TableRowError'; import { CreateSSHKeyDrawer } from 'src/features/Profile/SSHKeys/CreateSSHKeyDrawer'; import { usePagination } from 'src/hooks/usePagination'; +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 { PaginationFooter } from '../PaginationFooter/PaginationFooter'; diff --git a/packages/ui/src/components/ActionsPanel/ActionsPanel.stories.tsx b/packages/manager/src/components/ActionsPanel/ActionsPanel.stories.tsx similarity index 77% rename from packages/ui/src/components/ActionsPanel/ActionsPanel.stories.tsx rename to packages/manager/src/components/ActionsPanel/ActionsPanel.stories.tsx index e61587d7868..6b9163beb31 100644 --- a/packages/ui/src/components/ActionsPanel/ActionsPanel.stories.tsx +++ b/packages/manager/src/components/ActionsPanel/ActionsPanel.stories.tsx @@ -1,9 +1,7 @@ +import { Meta, StoryObj } from '@storybook/react'; import React from 'react'; -import { ActionsPanel } from './ActionsPanel'; - -import type { ActionPanelProps } from './ActionsPanel'; -import type { Meta, StoryObj } from '@storybook/react'; +import { ActionPanelProps, ActionsPanel } from './ActionsPanel'; const meta: Meta = { component: ActionsPanel, diff --git a/packages/ui/src/components/ActionsPanel/ActionsPanel.test.tsx b/packages/manager/src/components/ActionsPanel/ActionsPanel.test.tsx similarity index 95% rename from packages/ui/src/components/ActionsPanel/ActionsPanel.test.tsx rename to packages/manager/src/components/ActionsPanel/ActionsPanel.test.tsx index 61b67ace112..9cbc228dc1c 100644 --- a/packages/ui/src/components/ActionsPanel/ActionsPanel.test.tsx +++ b/packages/manager/src/components/ActionsPanel/ActionsPanel.test.tsx @@ -1,9 +1,9 @@ import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { describe, expect, it, vi } from 'vitest'; -import { renderWithTheme } from '../../utilities/testHelpers'; +import { renderWithTheme } from 'src/utilities/testHelpers'; + import { ActionsPanel } from './ActionsPanel'; describe('ActionsPanel', () => { diff --git a/packages/ui/src/components/ActionsPanel/ActionsPanel.tsx b/packages/manager/src/components/ActionsPanel/ActionsPanel.tsx similarity index 92% rename from packages/ui/src/components/ActionsPanel/ActionsPanel.tsx rename to packages/manager/src/components/ActionsPanel/ActionsPanel.tsx index 5c703543cd7..eddee90c914 100644 --- a/packages/ui/src/components/ActionsPanel/ActionsPanel.tsx +++ b/packages/manager/src/components/ActionsPanel/ActionsPanel.tsx @@ -1,13 +1,9 @@ +import { Box, Button, omittedProps } from '@linode/ui'; import { styled } from '@mui/material/styles'; import * as React from 'react'; import { useStyles } from 'tss-react/mui'; -import { omittedProps } from '../../utilities/omittedProps'; -import { Box } from '../Box'; -import { Button } from '../Button'; - -import type { BoxProps } from '../Box'; -import type { ButtonProps } from '../Button'; +import type { BoxProps, ButtonProps } from '@linode/ui'; export interface ActionButtonsProps extends ButtonProps { 'data-node-idx'?: number; diff --git a/packages/manager/src/components/AkamaiBanner/AkamaiBanner.tsx b/packages/manager/src/components/AkamaiBanner/AkamaiBanner.tsx index fcf66cbb97f..58835a51557 100644 --- a/packages/manager/src/components/AkamaiBanner/AkamaiBanner.tsx +++ b/packages/manager/src/components/AkamaiBanner/AkamaiBanner.tsx @@ -1,10 +1,10 @@ import { Box, Stack, Typography } from '@linode/ui'; -import { replaceNewlinesWithLineBreaks } from '@linode/utilities'; import { useMediaQuery, useTheme } from '@mui/material'; import * as React from 'react'; import { Link } from 'src/components/Link'; import { useFlags } from 'src/hooks/useFlags'; +import { replaceNewlinesWithLineBreaks } from 'src/utilities/replaceNewlinesWithLineBreaks'; import { StyledAkamaiLogo, diff --git a/packages/manager/src/components/AreaChart/AreaChart.stories.tsx b/packages/manager/src/components/AreaChart/AreaChart.stories.tsx index 96b881a94fc..bd9f8fcf682 100644 --- a/packages/manager/src/components/AreaChart/AreaChart.stories.tsx +++ b/packages/manager/src/components/AreaChart/AreaChart.stories.tsx @@ -1,7 +1,7 @@ -import { getMetrics } from '@linode/utilities'; import React from 'react'; import { tooltipValueFormatter } from 'src/components/AreaChart/utils'; +import { getMetrics } from 'src/utilities/statMetrics'; import { AreaChart } from './AreaChart'; import { customLegendData, timeData } from './utils'; diff --git a/packages/manager/src/components/AreaChart/AreaChart.test.tsx b/packages/manager/src/components/AreaChart/AreaChart.test.tsx index 99184b881b0..fe8b4c28ab1 100644 --- a/packages/manager/src/components/AreaChart/AreaChart.test.tsx +++ b/packages/manager/src/components/AreaChart/AreaChart.test.tsx @@ -1,4 +1,4 @@ -import { waitFor } from '@testing-library/react'; +/* eslint-disable @typescript-eslint/no-empty-function */ import * as React from 'react'; import { renderWithTheme } from 'src/utilities/testHelpers'; @@ -32,13 +32,10 @@ class ResizeObserver { describe('AreaChart', () => { window.ResizeObserver = ResizeObserver; - it('renders an AreaChart', async () => { + it('renders an AreaChart', () => { const { container } = renderWithTheme(); - await waitFor(() => { - expect( - container.querySelector('[class*="recharts-responsive-container"]') - ).toBeVisible(); - }); + expect(container.querySelector('recharts-responsive-container')) + .toBeVisible; }); }); diff --git a/packages/manager/src/components/AreaChart/AreaChart.tsx b/packages/manager/src/components/AreaChart/AreaChart.tsx index dd1ba65a1db..7eea892a55e 100644 --- a/packages/manager/src/components/AreaChart/AreaChart.tsx +++ b/packages/manager/src/components/AreaChart/AreaChart.tsx @@ -226,7 +226,7 @@ export const AreaChart = (props: AreaChartProps) => { {item.dataKey} - + {tooltipValueFormatter(item.value, unit)}
@@ -267,11 +267,7 @@ export const AreaChart = (props: AreaChartProps) => { return ( <> - + <_AreaChart aria-label={ariaLabel} data={data} margin={margin}> { vertical={false} /> { scale="time" stroke={theme.color.label} tickFormatter={xAxisTickFormatter} - ticks={ - xAxisTickCount - ? generate12HourTicks(data, timezone, xAxisTickCount) - : [] - } type="number" /> } contentStyle={{ color: theme.tokens.color.Neutrals[70], }} @@ -308,6 +303,7 @@ export const AreaChart = (props: AreaChartProps) => { color: theme.tokens.color.Neutrals[70], font: theme.font.bold, }} + content={} /> {showLegend && !legendRows && ( { {value} )} - iconType="square" onClick={({ dataKey }) => { if (dataKey) { handleLegendClick(dataKey as string); } }} + iconType="square" wrapperStyle={legendStyles} /> )} @@ -337,7 +333,7 @@ export const AreaChart = (props: AreaChartProps) => { dataKey={dataKey} dot={{ r: showDot ? dotRadius : 0 }} fill={color} - fillOpacity={variant === 'line' ? 0 : (fillOpacity ?? 1)} + fillOpacity={variant === 'line' ? 0 : fillOpacity ?? 1} hide={activeSeries.includes(dataKey)} isAnimationActive={false} key={dataKey} diff --git a/packages/manager/src/components/AreaChart/utils.test.ts b/packages/manager/src/components/AreaChart/utils.test.ts index ae75f0fe653..86dbe8533b3 100644 --- a/packages/manager/src/components/AreaChart/utils.test.ts +++ b/packages/manager/src/components/AreaChart/utils.test.ts @@ -1,4 +1,4 @@ -import { determinePower } from '@linode/utilities'; +import { determinePower } from 'src/utilities/unitConversions'; import { generate12HourTicks, @@ -9,7 +9,7 @@ import { } from './utils'; import type { DataSet } from './AreaChart'; -import type { StorageSymbol } from '@linode/utilities'; +import type { StorageSymbol } from 'src/utilities/unitConversions'; const timestamp = 1704204000000; diff --git a/packages/manager/src/components/AreaChart/utils.ts b/packages/manager/src/components/AreaChart/utils.ts index 610e2023512..0bbcb638a0b 100644 --- a/packages/manager/src/components/AreaChart/utils.ts +++ b/packages/manager/src/components/AreaChart/utils.ts @@ -1,6 +1,7 @@ -import { roundTo } from '@linode/utilities'; import { DateTime } from 'luxon'; +import { roundTo } from 'src/utilities/roundTo'; + import type { DataSet } from './AreaChart'; import type { LinodeNetworkTimeData } from './types'; diff --git a/packages/manager/src/components/Avatar/Avatar.test.tsx b/packages/manager/src/components/Avatar/Avatar.test.tsx index 2ef7c70fd7d..abc5606b557 100644 --- a/packages/manager/src/components/Avatar/Avatar.test.tsx +++ b/packages/manager/src/components/Avatar/Avatar.test.tsx @@ -1,6 +1,6 @@ -import { profileFactory } from '@linode/utilities'; import * as React from 'react'; +import { profileFactory } from 'src/factories/profile'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { Avatar } from './Avatar'; @@ -13,8 +13,8 @@ const queryMocks = vi.hoisted(() => ({ useProfile: vi.fn().mockReturnValue({}), })); -vi.mock('@linode/queries', async () => { - const actual = await vi.importActual('@linode/queries'); +vi.mock('src/queries/profile/profile', async () => { + const actual = await vi.importActual('src/queries/profile/profile'); return { ...actual, useProfile: queryMocks.useProfile, diff --git a/packages/manager/src/components/Avatar/Avatar.tsx b/packages/manager/src/components/Avatar/Avatar.tsx index 2f316f85378..fae766d2aa8 100644 --- a/packages/manager/src/components/Avatar/Avatar.tsx +++ b/packages/manager/src/components/Avatar/Avatar.tsx @@ -4,7 +4,8 @@ import { default as _Avatar } from '@mui/material/Avatar'; import * as React from 'react'; import AkamaiWave from 'src/assets/logo/akamai-wave.svg'; -import { usePreferences, useProfile } from '@linode/queries'; +import { usePreferences } from 'src/queries/profile/preferences'; +import { useProfile } from 'src/queries/profile/profile'; import type { SxProps, Theme } from '@mui/material'; diff --git a/packages/manager/src/components/BackupStatus/BackupStatus.tsx b/packages/manager/src/components/BackupStatus/BackupStatus.tsx index 567f84f01c4..2bb8ae12dff 100644 --- a/packages/manager/src/components/BackupStatus/BackupStatus.tsx +++ b/packages/manager/src/components/BackupStatus/BackupStatus.tsx @@ -1,6 +1,7 @@ import { Tooltip, TooltipIcon, Typography } from '@linode/ui'; import Backup from '@mui/icons-material/Backup'; import * as React from 'react'; +import { Link as RouterLink } from 'react-router-dom'; import { makeStyles } from 'tss-react/mui'; import { DateTimeDisplay } from 'src/components/DateTimeDisplay'; @@ -84,7 +85,7 @@ const BackupStatus = (props: Props) => { return (
- { > Scheduled - +
); @@ -122,7 +123,7 @@ const BackupStatus = (props: Props) => { return (
- { Never - +
); diff --git a/packages/ui/src/components/BetaChip/BetaChip.stories.tsx b/packages/manager/src/components/BetaChip/BetaChip.stories.tsx similarity index 92% rename from packages/ui/src/components/BetaChip/BetaChip.stories.tsx rename to packages/manager/src/components/BetaChip/BetaChip.stories.tsx index 047795c144b..3cc3a4ea934 100644 --- a/packages/ui/src/components/BetaChip/BetaChip.stories.tsx +++ b/packages/manager/src/components/BetaChip/BetaChip.stories.tsx @@ -10,7 +10,7 @@ export const Default: StoryObj = { }; const meta: Meta = { - args: {}, + args: { color: 'default' }, component: BetaChip, title: 'Foundations/Chip/BetaChip', }; diff --git a/packages/manager/src/components/BetaChip/BetaChip.test.tsx b/packages/manager/src/components/BetaChip/BetaChip.test.tsx new file mode 100644 index 00000000000..39d28178640 --- /dev/null +++ b/packages/manager/src/components/BetaChip/BetaChip.test.tsx @@ -0,0 +1,32 @@ +import { fireEvent } from '@testing-library/react'; +import React from 'react'; + +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { BetaChip } from './BetaChip'; + +describe('BetaChip', () => { + it('renders with default color', () => { + const { getByTestId } = renderWithTheme(); + const betaChip = getByTestId('betaChip'); + expect(betaChip).toBeInTheDocument(); + expect(betaChip).toHaveStyle('background-color: rgba(0, 0, 0, 0.08)'); + }); + + it('renders with primary color', () => { + const { getByTestId } = renderWithTheme(); + const betaChip = getByTestId('betaChip'); + expect(betaChip).toBeInTheDocument(); + expect(betaChip).toHaveStyle('background-color: rgb(16, 138, 214)'); + }); + + it('triggers an onClick callback', () => { + const onClickMock = vi.fn(); + const { getByTestId } = renderWithTheme( + + ); + const betaChip = getByTestId('betaChip'); + fireEvent.click(betaChip); + expect(onClickMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/manager/src/components/BetaChip/BetaChip.tsx b/packages/manager/src/components/BetaChip/BetaChip.tsx new file mode 100644 index 00000000000..d5d7f589133 --- /dev/null +++ b/packages/manager/src/components/BetaChip/BetaChip.tsx @@ -0,0 +1,63 @@ +import { Chip } from '@linode/ui'; +import { styled } from '@mui/material/styles'; +import * as React from 'react'; + +import type { ChipProps } from '@linode/ui'; + +export interface BetaChipProps + extends Omit< + ChipProps, + | 'avatar' + | 'clickable' + | 'deleteIcon' + | 'disabled' + | 'icon' + | 'label' + | 'onDelete' + | 'outlineColor' + | 'size' + | 'variant' + > { + /** + * The color of the chip. + * default renders a gray chip, primary renders a blue chip. + */ + color?: 'default' | 'primary'; +} + +/** + * ## Usage + * + * Beta chips label features that are not yet part of Cloud Manager's core supported functionality.
+ * **Example:** A beta chip may appear in the [primary navigation](https://github.com/linode/manager/pull/8104#issuecomment-1309334374), + * breadcrumbs, [banners](/docs/components-notifications-dismissible-banners--beta-banners), tabs, and/or plain text to designate beta functionality.
+ * **Visual style:** bold, capitalized text; reduced height, letter spacing, and font size; solid color background. + * + */ +export const BetaChip = (props: BetaChipProps) => { + const { color } = props; + + return ( + + ); +}; + +const StyledBetaChip = styled(Chip, { + label: 'StyledBetaChip', +})(({ theme }) => ({ + '& .MuiChip-label': { + padding: 0, + }, + font: theme.font.bold, + fontSize: '0.625rem', + height: 16, + letterSpacing: '.25px', + marginLeft: theme.spacing(), + padding: theme.spacing(0.5), + textTransform: 'uppercase', +})); diff --git a/packages/manager/src/components/Breadcrumb/Breadcrumb.stories.tsx b/packages/manager/src/components/Breadcrumb/Breadcrumb.stories.tsx index 4edad9d654b..a50d3a4206f 100644 --- a/packages/manager/src/components/Breadcrumb/Breadcrumb.stories.tsx +++ b/packages/manager/src/components/Breadcrumb/Breadcrumb.stories.tsx @@ -1,57 +1,12 @@ import { action } from '@storybook/addon-actions'; import { Meta, StoryObj } from '@storybook/react'; import React from 'react'; -import { Chip } from '@linode/ui'; import { Breadcrumb } from './Breadcrumb'; -const withBadgeCrumbs = [ - { - position: 3, - label: ( - <> - test - - - - - ), - }, -]; - -const noBadgeCrumbs = [ - { - position: 3, - label: test, - }, -]; - const meta: Meta = { component: Breadcrumb, title: 'Foundations/Breadcrumb', - argTypes: { - crumbOverrides: { - options: ['With Badge', 'No Badge'], - mapping: { - 'With Badge': withBadgeCrumbs, - 'No Badge': noBadgeCrumbs, - }, - control: { - type: 'radio', - labels: { - 'With Badge': 'Show Beta Badge', - 'No Badge': 'Hide Beta Badge', - }, - }, - defaultValue: 'No Badge', - }, - }, }; type Story = StoryObj; @@ -65,7 +20,6 @@ export const Default: Story = { onEdit: async () => action('onEdit'), }, pathname: '/linodes/9872893679817/test/lastcrumb', - crumbOverrides: noBadgeCrumbs, }, render: (args) => , }; diff --git a/packages/manager/src/components/Breadcrumb/Crumbs.styles.tsx b/packages/manager/src/components/Breadcrumb/Crumbs.styles.tsx index 8536c2dffd8..241f6705b90 100644 --- a/packages/manager/src/components/Breadcrumb/Crumbs.styles.tsx +++ b/packages/manager/src/components/Breadcrumb/Crumbs.styles.tsx @@ -3,24 +3,23 @@ import { styled } from '@mui/material'; export const StyledTypography = styled(Typography, { label: 'StyledTypography', -})(({ theme }) => ({ +})(({}) => ({ '&:hover': { textDecoration: 'underline', }, - fontSize: '1rem', + fontSize: '1.125rem', lineHeight: 'normal', textTransform: 'capitalize', whiteSpace: 'nowrap', - color: theme.tokens.component.Breadcrumb.LastItem.Text, })); export const StyledSlashTypography = styled(Typography, { label: 'StyledSlashTypography', })(({ theme }) => ({ color: theme.textColors.tableHeader, - fontSize: 16, - marginLeft: 4, - marginRight: 4, + fontSize: 20, + marginLeft: 2, + marginRight: 2, })); export const StyledDiv = styled('div', { label: 'StyledDiv' })({ diff --git a/packages/manager/src/components/Breadcrumb/Crumbs.tsx b/packages/manager/src/components/Breadcrumb/Crumbs.tsx index bd8e6585f87..06a0957d235 100644 --- a/packages/manager/src/components/Breadcrumb/Crumbs.tsx +++ b/packages/manager/src/components/Breadcrumb/Crumbs.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; -// eslint-disable-next-line no-restricted-imports -import { Link } from 'react-router-dom'; +import { Link, LinkProps } from 'react-router-dom'; import { StyledDiv, @@ -9,12 +8,10 @@ import { } from './Crumbs.styles'; import { FinalCrumb } from './FinalCrumb'; import { FinalCrumbPrefix } from './FinalCrumbPrefix'; - import type { EditableProps, LabelProps } from './types'; -import type { LinkProps } from 'react-router-dom'; export interface CrumbOverridesProps { - label?: string | React.ReactNode; + label?: string; linkTo?: LinkProps['to']; noCap?: boolean; position: number; diff --git a/packages/manager/src/components/Breadcrumb/FinalCrumb.styles.tsx b/packages/manager/src/components/Breadcrumb/FinalCrumb.styles.tsx index 0fd695b3110..af43289b40f 100644 --- a/packages/manager/src/components/Breadcrumb/FinalCrumb.styles.tsx +++ b/packages/manager/src/components/Breadcrumb/FinalCrumb.styles.tsx @@ -14,16 +14,16 @@ export const StyledEditableText = styled(EditableText, { '& > div': { width: 250, }, + marginLeft: `-${theme.spacing()}`, })); export const StyledH1Header = styled(H1Header, { label: 'StyledH1Header' })( ({ theme }) => ({ - color: theme.tokens.component.Breadcrumb.Normal.Text.Default, - fontSize: '1rem', - paddingLeft: 0, + color: theme.textColors.tableStatic, + fontSize: '1.125rem', textTransform: 'capitalize', [theme.breakpoints.up('lg')]: { - fontSize: '1rem', + fontSize: '1.125rem', }, }) ); diff --git a/packages/manager/src/components/Breadcrumb/FinalCrumb.tsx b/packages/manager/src/components/Breadcrumb/FinalCrumb.tsx index f5f011ff4fd..729f1222230 100644 --- a/packages/manager/src/components/Breadcrumb/FinalCrumb.tsx +++ b/packages/manager/src/components/Breadcrumb/FinalCrumb.tsx @@ -38,7 +38,6 @@ export const FinalCrumb = React.memo((props: Props) => { disabledBreadcrumbEditButton={disabledBreadcrumbEditButton} errorText={onEditHandlers.errorText} handleAnalyticsEvent={onEditHandlers.handleAnalyticsEvent} - isBreadcrumb onCancel={onEditHandlers.onCancel} onEdit={onEditHandlers.onEdit} text={onEditHandlers.editableTextTitle} diff --git a/packages/manager/src/components/ConfirmationDialog/ConfirmationDialog.stories.tsx b/packages/manager/src/components/ConfirmationDialog/ConfirmationDialog.stories.tsx index caa5772f732..122c6cd56c3 100644 --- a/packages/manager/src/components/ConfirmationDialog/ConfirmationDialog.stories.tsx +++ b/packages/manager/src/components/ConfirmationDialog/ConfirmationDialog.stories.tsx @@ -1,7 +1,7 @@ -import { ActionsPanel } from '@linode/ui'; import { action } from '@storybook/addon-actions'; import React from 'react'; +import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import type { Meta, StoryObj } from '@storybook/react'; @@ -63,7 +63,7 @@ export const Default: Story = { ), }; -export const ConfirmationDialogError: Story = { +export const Error: Story = { args: { error: 'There was an error somewhere in the process.', }, diff --git a/packages/manager/src/components/ConfirmationDialog/ConfirmationDialog.test.tsx b/packages/manager/src/components/ConfirmationDialog/ConfirmationDialog.test.tsx index d814a944edf..f42b2eafa45 100644 --- a/packages/manager/src/components/ConfirmationDialog/ConfirmationDialog.test.tsx +++ b/packages/manager/src/components/ConfirmationDialog/ConfirmationDialog.test.tsx @@ -1,9 +1,9 @@ -import { ActionsPanel } from '@linode/ui'; import { fireEvent } from '@testing-library/react'; import * as React from 'react'; import { renderWithTheme } from 'src/utilities/testHelpers'; +import { ActionsPanel } from '../ActionsPanel/ActionsPanel'; import { ConfirmationDialog } from './ConfirmationDialog'; import type { ConfirmationDialogProps } from './ConfirmationDialog'; diff --git a/packages/manager/src/components/ConfirmationDialog/ConfirmationDialog.tsx b/packages/manager/src/components/ConfirmationDialog/ConfirmationDialog.tsx index 7fd5211fe9b..41449a61be9 100644 --- a/packages/manager/src/components/ConfirmationDialog/ConfirmationDialog.tsx +++ b/packages/manager/src/components/ConfirmationDialog/ConfirmationDialog.tsx @@ -29,7 +29,12 @@ export const ConfirmationDialog = React.forwardRef< return ( {children} - + {actions && typeof actions === 'function' ? actions(dialogProps) : actions} diff --git a/packages/manager/src/components/CopyableTextField/CopyableTextField.test.tsx b/packages/manager/src/components/CopyableTextField/CopyableTextField.test.tsx index a37bed88cca..e065cb36c48 100644 --- a/packages/manager/src/components/CopyableTextField/CopyableTextField.test.tsx +++ b/packages/manager/src/components/CopyableTextField/CopyableTextField.test.tsx @@ -8,8 +8,7 @@ import { CopyableTextField } from './CopyableTextField'; import type { CopyableTextFieldProps } from './CopyableTextField'; -vi.mock('@linode/utilities', async () => ({ - ...(await vi.importActual('@linode/utilities')), +vi.mock('@linode/utilities', () => ({ downloadFile: vi.fn(), })); diff --git a/packages/manager/src/components/DatePicker/DateTimePicker.tsx b/packages/manager/src/components/DatePicker/DateTimePicker.tsx index d46e281df1a..f572a866437 100644 --- a/packages/manager/src/components/DatePicker/DateTimePicker.tsx +++ b/packages/manager/src/components/DatePicker/DateTimePicker.tsx @@ -1,4 +1,4 @@ -import { ActionsPanel, InputAdornment, TextField } from '@linode/ui'; +import { InputAdornment, TextField } from '@linode/ui'; import { Divider } from '@linode/ui'; import { Box } from '@linode/ui'; import CalendarTodayIcon from '@mui/icons-material/CalendarToday'; @@ -10,6 +10,7 @@ import { TimePicker } from '@mui/x-date-pickers/TimePicker'; import React, { useEffect, useState } from 'react'; import { timezones } from 'src/assets/timezones/timezones'; +import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { TimeZoneSelect } from './TimeZoneSelect'; diff --git a/packages/manager/src/components/DateTimeDisplay/DateTimeDisplay.test.tsx b/packages/manager/src/components/DateTimeDisplay/DateTimeDisplay.test.tsx index 25b977b5876..86c393f0c56 100644 --- a/packages/manager/src/components/DateTimeDisplay/DateTimeDisplay.test.tsx +++ b/packages/manager/src/components/DateTimeDisplay/DateTimeDisplay.test.tsx @@ -4,17 +4,8 @@ import * as React from 'react'; import { ISO_DATETIME_NO_TZ_FORMAT } from 'src/constants'; import { renderWithTheme } from 'src/utilities/testHelpers'; -import { DateTimeDisplay } from './DateTimeDisplay'; - -import type { DateTimeDisplayProps } from './DateTimeDisplay'; - -vi.mock('@linode/utilities', async () => { - const actual = await vi.importActual('@linode/utilities'); - return { - ...actual, - getUserTimezone: vi.fn().mockReturnValue('utc'), - }; -}); +import { DateTimeDisplay, DateTimeDisplayProps } from './DateTimeDisplay'; +vi.mock('../../utilities/getUserTimezone'); const APIDate = '2018-07-20T04:23:17'; diff --git a/packages/manager/src/components/DateTimeDisplay/DateTimeDisplay.tsx b/packages/manager/src/components/DateTimeDisplay/DateTimeDisplay.tsx index c2fdfaeee14..812bff2f499 100644 --- a/packages/manager/src/components/DateTimeDisplay/DateTimeDisplay.tsx +++ b/packages/manager/src/components/DateTimeDisplay/DateTimeDisplay.tsx @@ -1,7 +1,7 @@ -import { useProfile } from '@linode/queries'; import { Typography } from '@linode/ui'; import * as React from 'react'; +import { useProfile } from 'src/queries/profile/profile'; import { formatDate } from 'src/utilities/formatDate'; import type { TimeInterval } from 'src/utilities/formatDate'; diff --git a/packages/manager/src/components/DebouncedSearchTextField/DebouncedSearchTextField.tsx b/packages/manager/src/components/DebouncedSearchTextField/DebouncedSearchTextField.tsx index 2bf259bcc1f..2c044a0d631 100644 --- a/packages/manager/src/components/DebouncedSearchTextField/DebouncedSearchTextField.tsx +++ b/packages/manager/src/components/DebouncedSearchTextField/DebouncedSearchTextField.tsx @@ -4,18 +4,15 @@ import { InputAdornment, TextField, } from '@linode/ui'; +import Clear from '@mui/icons-material/Clear'; +import Search from '@mui/icons-material/Search'; +import { styled } from '@mui/material/styles'; import * as React from 'react'; import { debounce } from 'throttle-debounce'; -import Close from 'src/assets/icons/close.svg'; -import Search from 'src/assets/icons/search.svg'; - -import type { InputProps, TextFieldProps } from '@linode/ui'; +import type { TextFieldProps } from '@linode/ui'; export interface DebouncedSearchProps extends TextFieldProps { - /** - * Class name to apply to the component. - */ className?: string; /** * Whether to show a clear button at the end of the input. @@ -26,18 +23,9 @@ export interface DebouncedSearchProps extends TextFieldProps { * @default 400 */ debounceTime?: number; - /** - * Default value of the input. - */ defaultValue?: string; - /** - * Whether to hide the label. - */ hideLabel?: boolean; - /** - * Custom props to apply to the input element. - */ - inputSlotProps?: InputProps; + /** * Determines if the textbox is currently searching for inputted query */ @@ -46,25 +34,19 @@ export interface DebouncedSearchProps extends TextFieldProps { * Function to perform when searching for query */ onSearch: (query: string) => void; - /** - * Placeholder text for the input. - */ placeholder?: string; - /** - * Value of the input. - */ value: string; } export const DebouncedSearchTextField = React.memo( (props: DebouncedSearchProps) => { const { + InputProps, className, clearable, debounceTime, defaultValue, hideLabel, - inputSlotProps, isSearching, label, onSearch, @@ -94,35 +76,38 @@ export const DebouncedSearchTextField = React.memo( return ( - {isSearching && } - {clearable && Boolean(textFieldValue) && ( - { - setTextFieldValue(''); - onSearch(''); - }} - sx={{ - padding: 0, - }} - aria-label="Clear" - size="small" - > - - - )} - - ), - startAdornment: ( - - - - ), - ...inputSlotProps, - }, + InputProps={{ + endAdornment: isSearching ? ( + + + + ) : ( + clearable && + textFieldValue && ( + { + setTextFieldValue(''); + onSearch(''); + }} + aria-label="Clear" + size="small" + > + ({ + '&&': { + color: theme.color.grey1, + }, + })} + /> + + ) + ), + startAdornment: ( + + + + ), + ...InputProps, }} className={className} data-qa-debounced-search @@ -137,3 +122,9 @@ export const DebouncedSearchTextField = React.memo( ); } ); + +const StyledSearchIcon = styled(Search)(({ theme }) => ({ + '&&, &&:hover': { + color: theme.color.grey1, + }, +})); diff --git a/packages/manager/src/components/DeletionDialog/DeletionDialog.stories.tsx b/packages/manager/src/components/DeletionDialog/DeletionDialog.stories.tsx index 016ea3debf3..278748ecc43 100644 --- a/packages/manager/src/components/DeletionDialog/DeletionDialog.stories.tsx +++ b/packages/manager/src/components/DeletionDialog/DeletionDialog.stories.tsx @@ -62,14 +62,14 @@ export const Default: Story = { render: (args) => {args.children}, }; -export const DeletionDialogError: Story = { +export const Error: Story = { args: { error: 'There was an error deleting this Linode.', }, render: (args) => {args.children}, }; -export const DeletionDialogLoading: Story = { +export const Loading: Story = { args: { loading: true, }, diff --git a/packages/manager/src/components/DeletionDialog/DeletionDialog.test.tsx b/packages/manager/src/components/DeletionDialog/DeletionDialog.test.tsx index d86ca80e94b..5f7928afa03 100644 --- a/packages/manager/src/components/DeletionDialog/DeletionDialog.test.tsx +++ b/packages/manager/src/components/DeletionDialog/DeletionDialog.test.tsx @@ -6,7 +6,7 @@ import { renderWithTheme } from 'src/utilities/testHelpers'; import { DeletionDialog } from './DeletionDialog'; import type { DeletionDialogProps } from './DeletionDialog'; -import type { ManagerPreferences } from '@linode/utilities'; +import type { ManagerPreferences } from 'src/types/ManagerPreferences'; const preference: ManagerPreferences['type_to_confirm'] = true; @@ -14,8 +14,8 @@ const queryMocks = vi.hoisted(() => ({ usePreferences: vi.fn().mockReturnValue({}), })); -vi.mock('@linode/queries', async () => { - const actual = await vi.importActual('@linode/queries'); +vi.mock('src/queries/profile/preferences', async () => { + const actual = await vi.importActual('src/queries/profile/preferences'); return { ...actual, usePreferences: queryMocks.usePreferences, diff --git a/packages/manager/src/components/DeletionDialog/DeletionDialog.tsx b/packages/manager/src/components/DeletionDialog/DeletionDialog.tsx index 7a553e87e41..efed45049e3 100644 --- a/packages/manager/src/components/DeletionDialog/DeletionDialog.tsx +++ b/packages/manager/src/components/DeletionDialog/DeletionDialog.tsx @@ -1,12 +1,13 @@ -import { ActionsPanel, Notice, Typography } from '@linode/ui'; +import { Notice, Typography } from '@linode/ui'; import { capitalize } from '@linode/utilities'; import { useTheme } from '@mui/material/styles'; import * as React from 'react'; +import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import { TypeToConfirm } from 'src/components/TypeToConfirm/TypeToConfirm'; import { titlecase } from 'src/features/Linodes/presentation'; -import { usePreferences } from '@linode/queries'; +import { usePreferences } from 'src/queries/profile/preferences'; import type { DialogProps } from '@linode/ui'; diff --git a/packages/manager/src/components/DocumentTitle/DocumentTitle.tsx b/packages/manager/src/components/DocumentTitle/DocumentTitle.tsx index 546d75b8186..10d87b7afd8 100644 --- a/packages/manager/src/components/DocumentTitle/DocumentTitle.tsx +++ b/packages/manager/src/components/DocumentTitle/DocumentTitle.tsx @@ -6,10 +6,11 @@ the Linodes landing page. More context: https://github.com/linode/manager/pull/9 */ -import { usePrevious } from '@linode/utilities'; import { reverse } from 'ramda'; import * as React from 'react'; +import { usePrevious } from 'src/hooks/usePrevious'; + interface DocumentTitleSegmentsContext { appendSegment: (segment: string) => void; removeSegment: (segment: string) => void; diff --git a/packages/ui/src/components/Drawer/Drawer.stories.tsx b/packages/manager/src/components/Drawer.stories.tsx similarity index 68% rename from packages/ui/src/components/Drawer/Drawer.stories.tsx rename to packages/manager/src/components/Drawer.stories.tsx index 39467b9f243..6ee56017130 100644 --- a/packages/ui/src/components/Drawer/Drawer.stories.tsx +++ b/packages/manager/src/components/Drawer.stories.tsx @@ -1,13 +1,9 @@ -import ErrorOutline from '@mui/icons-material/ErrorOutline'; -import { useTheme } from '@mui/material/styles'; +import { Button, TextField, Typography } from '@linode/ui'; import { action } from '@storybook/addon-actions'; import { useArgs } from '@storybook/preview-api'; import React from 'react'; -import { ActionsPanel } from '../ActionsPanel'; -import { Button } from '../Button'; -import { TextField } from '../TextField'; -import { Typography } from '../Typography'; +import { ActionsPanel } from './ActionsPanel/ActionsPanel'; import { Drawer } from './Drawer'; import type { Meta, StoryObj } from '@storybook/react'; @@ -136,79 +132,4 @@ export const Fetching: Story = { }, }; -const NotFound = (_props: React.PropsWithChildren<{ className?: string }>) => { - const theme = useTheme(); - - const sxIcon = { - color: theme.color.black, - height: 130, - marginBottom: theme.spacing(4), - width: 130, - }; - - return ( -
- - Not Found -
- ); -}; - -export const WithNotFoundComponent: Story = { - args: { - NotFoundComponent: NotFound, - error: 'Not Found', - onClose: action('onClose'), - open: false, - title: 'My Drawer', - }, - render: (args) => { - const DrawerExampleWrapper = () => { - const [open, setOpen] = React.useState(args.open); - - return ( - <> - - setOpen(false)} open={open} /> - - ); - }; - return ; - }, -}; - -export const WithoutNotFoundComponent: Story = { - args: { - error: 'Some other Error', - onClose: action('onClose'), - open: false, - title: 'My Drawer', - }, - render: (args) => { - const DrawerExampleWrapper = () => { - const [open, setOpen] = React.useState(args.open); - - return ( - <> - - setOpen(false)} open={open} /> - - ); - }; - return ; - }, -}; - export default meta; diff --git a/packages/ui/src/components/Drawer/Drawer.test.tsx b/packages/manager/src/components/Drawer.test.tsx similarity index 93% rename from packages/ui/src/components/Drawer/Drawer.test.tsx rename to packages/manager/src/components/Drawer.test.tsx index 549785782dd..9d0245b43ec 100644 --- a/packages/ui/src/components/Drawer/Drawer.test.tsx +++ b/packages/manager/src/components/Drawer.test.tsx @@ -1,9 +1,9 @@ +import { Button } from '@linode/ui'; import { fireEvent, waitFor } from '@testing-library/react'; import * as React from 'react'; -import { describe, expect, it, vi } from 'vitest'; -import { renderWithTheme } from '../../utilities/testHelpers'; -import { Button } from '../Button'; +import { renderWithTheme } from 'src/utilities/testHelpers'; + import { Drawer } from './Drawer'; import type { DrawerProps } from './Drawer'; diff --git a/packages/ui/src/components/Drawer/Drawer.tsx b/packages/manager/src/components/Drawer.tsx similarity index 66% rename from packages/ui/src/components/Drawer/Drawer.tsx rename to packages/manager/src/components/Drawer.tsx index 1469973bdba..2aa1c8ab5d3 100644 --- a/packages/ui/src/components/Drawer/Drawer.tsx +++ b/packages/manager/src/components/Drawer.tsx @@ -1,25 +1,24 @@ +import { + Box, + CircleProgress, + ErrorState, + IconButton, + Typography, + convertForAria, +} from '@linode/ui'; import Close from '@mui/icons-material/Close'; import _Drawer from '@mui/material/Drawer'; import Grid from '@mui/material/Grid2'; -import { useTheme } from '@mui/material/styles'; import * as React from 'react'; +import { makeStyles } from 'tss-react/mui'; -import { convertForAria } from '../../utilities/stringUtils'; -import { Box } from '../Box'; -import { CircleProgress } from '../CircleProgress'; -import { ErrorState } from '../ErrorState'; -import { IconButton } from '../IconButton'; -import { Typography } from '../Typography'; +import { NotFound } from './NotFound'; +import type { APIError } from '@linode/api-v4'; import type { DrawerProps as _DrawerProps } from '@mui/material/Drawer'; +import type { Theme } from '@mui/material/styles'; -// simplified APIError interface for use in this file (api-v4 is not a dependency of ui) -interface APIError { - field?: string; - reason: string; -} - -interface BaseProps extends _DrawerProps { +export interface DrawerProps extends _DrawerProps { error?: APIError[] | null | string; /** * Whether the drawer is fetching the entity's data. @@ -38,14 +37,6 @@ interface BaseProps extends _DrawerProps { wide?: boolean; } -interface PropsWithNotFound extends BaseProps { - NotFoundComponent?: React.ComponentType< - React.PropsWithChildren<{ className?: string }> - >; -} - -export type DrawerProps = PropsWithNotFound; - /** * ## Overview * - Drawers are essentially modal dialogs that appear on the right of the screen rather than the center. @@ -59,52 +50,19 @@ export type DrawerProps = PropsWithNotFound; */ export const Drawer = React.forwardRef( (props: DrawerProps, ref) => { + const { classes, cx } = useStyles(); const { - NotFoundComponent, children, error, isFetching, onClose, open, - sx, title, wide, ...rest } = props; const titleID = convertForAria(title); - const theme = useTheme(); - - const sxDrawer = { - '& .MuiDrawer-paper': { - padding: theme.spacing(4), - [theme.breakpoints.down('sm')]: { - padding: theme.spacing(2), - }, - ...(wide - ? { - maxWidth: 700, - width: '100%', - } - : { - [theme.breakpoints.down('sm')]: { - maxWidth: 445, - width: '100%', - }, - width: 480, - }), - }, - '& .actionPanel': { - display: 'flex', - justifyContent: 'flex-end', - marginTop: theme.spacing(1), - }, - '& .selectionCard': { - flexBasis: '100%', - maxWidth: '100%', - }, - }; - // Store the last valid children and title in refs // This is to prevent flashes of content during the drawer's closing transition, // and its content becomes potentially undefined @@ -118,15 +76,17 @@ export const Drawer = React.forwardRef( return ( <_Drawer + classes={{ + paper: cx(classes.common, { + [classes.default]: !wide, + [classes.wide]: wide, + }), + }} onClose={(_, reason) => { if (onClose && reason !== 'backdropClick') { onClose({}, 'escapeKeyDown'); } }} - sx={{ - ...sxDrawer, - ...sx, - }} anchor="right" open={open} ref={ref} @@ -137,24 +97,19 @@ export const Drawer = React.forwardRef( role="dialog" > ({ - '&&': { - marginBottom: theme.spacing(2), - }, + className={classes.drawerHeader} + container + wrap="nowrap" + sx={{ alignItems: 'flex-start', justifyContent: 'space-between', position: 'relative', - })} - container - wrap="nowrap" + }} > {isFetching ? null : ( ({ - marginRight: theme.spacing(2), - wordBreak: 'break-word', - })} + className={classes.title} data-qa-drawer-title={title} data-testid="drawer-title" id={titleID} @@ -182,8 +137,8 @@ export const Drawer = React.forwardRef( {error ? ( - error === 'Not Found' && NotFoundComponent ? ( - + error === 'Not Found' ? ( + ) : ( ( ); } ); + +const useStyles = makeStyles()((theme: Theme) => ({ + button: { + '& :hover, & :focus': { + backgroundColor: theme.palette.primary.main, + color: theme.tokens.color.Neutrals.White, + }, + '& > span': { + padding: 2, + }, + minHeight: 'auto', + minWidth: 'auto', + padding: 0, + }, + common: { + '& .actionPanel': { + display: 'flex', + justifyContent: 'flex-end', + marginTop: theme.spacing(), + }, + '& .selectionCard': { + flexBasis: '100%', + maxWidth: '100%', + }, + padding: theme.spacing(4), + [theme.breakpoints.down('sm')]: { + padding: theme.spacing(2), + }, + }, + default: { + [theme.breakpoints.down('sm')]: { + maxWidth: 445, + width: '100%', + }, + width: 480, + }, + drawerHeader: { + '&&': { + marginBottom: theme.spacing(2), + }, + }, + title: { + marginRight: theme.spacing(4), + wordBreak: 'break-word', + }, + wide: { + maxWidth: 700, + width: '100%', + }, +})); diff --git a/packages/manager/src/components/EmptyLandingPageResources/ResourcesLinksTypes.ts b/packages/manager/src/components/EmptyLandingPageResources/ResourcesLinksTypes.ts index 2129b44b6e7..c94d2a6f6a1 100644 --- a/packages/manager/src/components/EmptyLandingPageResources/ResourcesLinksTypes.ts +++ b/packages/manager/src/components/EmptyLandingPageResources/ResourcesLinksTypes.ts @@ -4,7 +4,7 @@ interface ResourcesLink { to: string; } -export interface LinkAnalyticsEvent { +export interface linkAnalyticsEvent { action: string; category: string; } @@ -17,7 +17,7 @@ export interface ResourcesHeaders { } export interface ResourcesLinks { - linkAnalyticsEvent: LinkAnalyticsEvent; + linkAnalyticsEvent: linkAnalyticsEvent; links: ResourcesLink[]; onClick?: () => void; } diff --git a/packages/manager/src/components/EmptyLandingPageResources/ResourcesSection.tsx b/packages/manager/src/components/EmptyLandingPageResources/ResourcesSection.tsx index f7d72ddad85..4c2107f9d54 100644 --- a/packages/manager/src/components/EmptyLandingPageResources/ResourcesSection.tsx +++ b/packages/manager/src/components/EmptyLandingPageResources/ResourcesSection.tsx @@ -19,7 +19,7 @@ import { import type { ResourcesHeaders, ResourcesLinkSection, - LinkAnalyticsEvent, + linkAnalyticsEvent, } from 'src/components/EmptyLandingPageResources/ResourcesLinksTypes'; interface ButtonProps { @@ -62,7 +62,7 @@ interface ResourcesSectionProps { /** * The event data to be sent when the call to action is clicked */ - linkAnalyticsEvent: LinkAnalyticsEvent; + linkAnalyticsEvent: linkAnalyticsEvent; /** * If true, the transfer display will be shown at the bottom * */ @@ -82,14 +82,14 @@ interface ResourcesSectionProps { const GuideLinks = ( guides: ResourcesLinkSection, - linkAnalyticsEvent: LinkAnalyticsEvent + linkAnalyticsEvent: linkAnalyticsEvent ) => ( ); const YoutubeLinks = ( youtube: ResourcesLinkSection, - linkAnalyticsEvent: LinkAnalyticsEvent + linkAnalyticsEvent: linkAnalyticsEvent ) => ( ({ border: 'none', }, border: 'none', - borderRadius: theme.tokens.alias.Radius.Default, + borderRadius: theme.tokens.borderRadius.None, height: 34, minHeight: 'fit-content', minWidth: 30, diff --git a/packages/manager/src/components/EntityDetail/EntityDetail.stories.tsx b/packages/manager/src/components/EntityDetail/EntityDetail.stories.tsx index 37b71485bba..478b59d667d 100644 --- a/packages/manager/src/components/EntityDetail/EntityDetail.stories.tsx +++ b/packages/manager/src/components/EntityDetail/EntityDetail.stories.tsx @@ -1,7 +1,7 @@ -import { linodeFactory } from '@linode/utilities'; import { action } from '@storybook/addon-actions'; import * as React from 'react'; +import { linodeFactory } from 'src/factories/linodes'; import { LinodeEntityDetail } from 'src/features/Linodes/LinodeEntityDetail'; import { EntityDetail } from './EntityDetail'; diff --git a/packages/manager/src/components/ErrorMessage.tsx b/packages/manager/src/components/ErrorMessage.tsx index 53f3c8e78af..aae52c9770d 100644 --- a/packages/manager/src/components/ErrorMessage.tsx +++ b/packages/manager/src/components/ErrorMessage.tsx @@ -39,5 +39,5 @@ export const ErrorMessage = (props: Props) => { return ; } - return {message}; + return {message}; }; diff --git a/packages/manager/src/components/GaugePercent/GaugePercent.tsx b/packages/manager/src/components/GaugePercent/GaugePercent.tsx index 449e4845aa2..76e95cc0451 100644 --- a/packages/manager/src/components/GaugePercent/GaugePercent.tsx +++ b/packages/manager/src/components/GaugePercent/GaugePercent.tsx @@ -106,7 +106,6 @@ export const GaugePercent = React.memo((props: GaugePercentProps) => { // we use a reference to access it. // https://dev.to/vcanales/using-chart-js-in-a-function-component-with-react-hooks-246l if (graphRef.current) { - // eslint-disable-next-line sonarjs/constructor-for-side-effects new Chart(graphRef.current.getContext('2d'), { data: { datasets: graphDatasets, diff --git a/packages/manager/src/components/GenerateFirewallDialog/GenerateFirewallDialog.tsx b/packages/manager/src/components/GenerateFirewallDialog/GenerateFirewallDialog.tsx index b6378219940..49fb74774dd 100644 --- a/packages/manager/src/components/GenerateFirewallDialog/GenerateFirewallDialog.tsx +++ b/packages/manager/src/components/GenerateFirewallDialog/GenerateFirewallDialog.tsx @@ -1,8 +1,8 @@ import { Button, Dialog, Notice, Stack, Typography } from '@linode/ui'; -import { replaceNewlinesWithLineBreaks } from '@linode/utilities'; import React from 'react'; import { useFlags } from 'src/hooks/useFlags'; +import { replaceNewlinesWithLineBreaks } from 'src/utilities/replaceNewlinesWithLineBreaks'; import { ErrorMessage } from '../ErrorMessage'; import { LinearProgress } from '../LinearProgress'; diff --git a/packages/manager/src/components/GenerateFirewallDialog/useCreateFirewallFromTemplate.ts b/packages/manager/src/components/GenerateFirewallDialog/useCreateFirewallFromTemplate.ts index ffd698de55a..7d866302135 100644 --- a/packages/manager/src/components/GenerateFirewallDialog/useCreateFirewallFromTemplate.ts +++ b/packages/manager/src/components/GenerateFirewallDialog/useCreateFirewallFromTemplate.ts @@ -1,6 +1,7 @@ import { useQueryClient } from '@tanstack/react-query'; -import { firewallQueries, useCreateFirewall } from '@linode/queries'; +import { firewallQueries } from 'src/queries/firewalls'; +import { useCreateFirewall } from 'src/queries/firewalls'; import type { DialogState } from './GenerateFirewallDialog'; import type { diff --git a/packages/manager/src/components/IPSelect/IPSelect.tsx b/packages/manager/src/components/IPSelect/IPSelect.tsx index 8c712690d1b..c65c091fa05 100644 --- a/packages/manager/src/components/IPSelect/IPSelect.tsx +++ b/packages/manager/src/components/IPSelect/IPSelect.tsx @@ -1,7 +1,7 @@ import { Autocomplete } from '@linode/ui'; import * as React from 'react'; -import { useLinodeQuery } from '@linode/queries'; +import { useLinodeQuery } from 'src/queries/linodes/linodes'; interface Option { label: string; diff --git a/packages/manager/src/components/IconTextLink/IconTextLink.tsx b/packages/manager/src/components/IconTextLink/IconTextLink.tsx index 2cd7b0f3079..a56a352e89a 100644 --- a/packages/manager/src/components/IconTextLink/IconTextLink.tsx +++ b/packages/manager/src/components/IconTextLink/IconTextLink.tsx @@ -1,9 +1,8 @@ import { Button } from '@linode/ui'; import * as React from 'react'; +import { Link } from 'react-router-dom'; import { makeStyles } from 'tss-react/mui'; -import { Link } from 'src/components/Link'; - import type { Theme } from '@mui/material/styles'; import type { SvgIcon } from 'src/components/SvgIcon'; @@ -31,7 +30,7 @@ const useStyles = makeStyles()((theme: Theme) => ({ }, root: { alignItems: 'center', - borderRadius: theme.tokens.alias.Radius.Default, + borderRadius: theme.tokens.borderRadius.None, display: 'flex', gap: theme.spacing(2), padding: theme.spacing(0.5), diff --git a/packages/manager/src/components/ImageSelect/ImageOption.tsx b/packages/manager/src/components/ImageSelect/ImageOption.tsx index 87d920afd6d..7616ab5af15 100644 --- a/packages/manager/src/components/ImageSelect/ImageOption.tsx +++ b/packages/manager/src/components/ImageSelect/ImageOption.tsx @@ -1,21 +1,22 @@ -import { ListItemOption, Stack, Tooltip, Typography } from '@linode/ui'; +import { Stack, Tooltip, Typography } from '@linode/ui'; import React from 'react'; import CloudInitIcon from 'src/assets/icons/cloud-init.svg'; +import { ListItemOption } from 'src/components/ListItemOption'; import { useFlags } from 'src/hooks/useFlags'; import { OSIcon } from '../OSIcon'; import { isImageDeprecated } from './utilities'; import type { Image } from '@linode/api-v4'; -import type { ListItemOptionProps } from '@linode/ui'; +import type { ListItemProps } from 'src/components/ListItemOption'; export const ImageOption = ({ disabledOptions, item, props, selected, -}: ListItemOptionProps) => { +}: ListItemProps) => { const flags = useFlags(); return ( diff --git a/packages/manager/src/components/ImageSelect/ImageSelect.tsx b/packages/manager/src/components/ImageSelect/ImageSelect.tsx index 139019aa04f..440e9f64130 100644 --- a/packages/manager/src/components/ImageSelect/ImageSelect.tsx +++ b/packages/manager/src/components/ImageSelect/ImageSelect.tsx @@ -1,11 +1,4 @@ -import { - Autocomplete, - Box, - InputAdornment, - Notice, - Stack, - Typography, -} from '@linode/ui'; +import { Autocomplete, Box, Notice, Stack, Typography } from '@linode/ui'; import { DateTime } from 'luxon'; import React, { useMemo } from 'react'; @@ -185,15 +178,15 @@ export const ImageSelect = (props: Props) => { InputProps: { startAdornment: !multiple && value && !Array.isArray(value) ? ( - - - + ) : null, }, }} diff --git a/packages/manager/src/components/ImageSelect/utilities.ts b/packages/manager/src/components/ImageSelect/utilities.ts index e18797f7500..dcb1babfb1e 100644 --- a/packages/manager/src/components/ImageSelect/utilities.ts +++ b/packages/manager/src/components/ImageSelect/utilities.ts @@ -4,7 +4,7 @@ import { MAX_MONTHS_EOL_FILTER } from 'src/constants'; import type { ImageSelectVariant } from './ImageSelect'; import type { Image, RegionSite } from '@linode/api-v4'; -import type { DisableItemOption } from '@linode/ui'; +import type { DisableItemOption } from 'src/components/ListItemOption'; /** * Given a Image Select "variant", this PR returns an diff --git a/packages/manager/src/components/InlineMenuAction/InlineMenuAction.tsx b/packages/manager/src/components/InlineMenuAction/InlineMenuAction.tsx index 1a64c04487b..bf85e77865a 100644 --- a/packages/manager/src/components/InlineMenuAction/InlineMenuAction.tsx +++ b/packages/manager/src/components/InlineMenuAction/InlineMenuAction.tsx @@ -2,8 +2,7 @@ import { StyledActionButton } from '@linode/ui'; import { styled } from '@mui/material/styles'; import * as React from 'react'; - -import { Link } from 'src/components/Link'; +import { Link } from 'react-router-dom'; interface InlineMenuActionProps { /** Required action text */ diff --git a/packages/manager/src/components/LineGraph/LineGraph.stories.tsx b/packages/manager/src/components/LineGraph/LineGraph.stories.tsx index 8be41bc9d22..0d596ef46de 100644 --- a/packages/manager/src/components/LineGraph/LineGraph.stories.tsx +++ b/packages/manager/src/components/LineGraph/LineGraph.stories.tsx @@ -1,7 +1,7 @@ -import { formatPercentage, getMetrics } from '@linode/utilities'; import * as React from 'react'; import { LineGraph } from 'src/components/LineGraph/LineGraph'; +import { formatPercentage, getMetrics } from 'src/utilities/statMetrics'; import type { DataSet, LineGraphProps } from './LineGraph'; import type { Meta, StoryObj } from '@storybook/react'; diff --git a/packages/manager/src/components/LineGraph/LineGraph.tsx b/packages/manager/src/components/LineGraph/LineGraph.tsx index f85dc6150ac..3e635a15720 100644 --- a/packages/manager/src/components/LineGraph/LineGraph.tsx +++ b/packages/manager/src/components/LineGraph/LineGraph.tsx @@ -2,7 +2,6 @@ * ONLY USED IN LONGVIEW * Delete when Lonview is sunsetted, along with AccessibleGraphData */ -import { roundTo } from '@linode/utilities'; import { useTheme } from '@mui/material/styles'; import useMediaQuery from '@mui/material/useMediaQuery'; import { Chart } from 'chart.js'; @@ -16,6 +15,7 @@ import { TableCell } from 'src/components/TableCell'; import { TableHead } from 'src/components/TableHead'; import { TableRow } from 'src/components/TableRow'; import { setUpCharts } from 'src/utilities/charts'; +import { roundTo } from 'src/utilities/roundTo'; import AccessibleGraphData from './AccessibleGraphData'; import { @@ -26,7 +26,6 @@ import { StyledWrapper, } from './LineGraph.styles'; -import type { Metrics } from '@linode/utilities'; import type { Theme } from '@mui/material/styles'; import type { ChartData, @@ -35,6 +34,7 @@ import type { ChartTooltipItem, ChartXAxe, } from 'chart.js'; +import type { Metrics } from 'src/utilities/statMetrics'; setUpCharts(); diff --git a/packages/manager/src/components/LineGraph/MetricsDisplay.test.tsx b/packages/manager/src/components/LineGraph/MetricsDisplay.test.tsx index aa7ffe27e57..9157f758a38 100644 --- a/packages/manager/src/components/LineGraph/MetricsDisplay.test.tsx +++ b/packages/manager/src/components/LineGraph/MetricsDisplay.test.tsx @@ -1,8 +1,8 @@ -import { formatPercentage } from '@linode/utilities'; import { screen } from '@testing-library/react'; import * as React from 'react'; import { MetricsDisplay } from 'src/components/LineGraph/MetricsDisplay'; +import { formatPercentage } from 'src/utilities/statMetrics'; import { renderWithTheme } from 'src/utilities/testHelpers'; describe('CPUMetrics', () => { diff --git a/packages/manager/src/components/LineGraph/MetricsDisplay.tsx b/packages/manager/src/components/LineGraph/MetricsDisplay.tsx index c579fd7784c..53dc61a932f 100644 --- a/packages/manager/src/components/LineGraph/MetricsDisplay.tsx +++ b/packages/manager/src/components/LineGraph/MetricsDisplay.tsx @@ -9,7 +9,7 @@ import { TableRow } from 'src/components/TableRow'; import { StyledLegend } from './MetricsDisplay.styles'; -import type { Metrics } from '@linode/utilities'; +import type { Metrics } from 'src/utilities/statMetrics'; const ROW_HEADERS = ['Max', 'Avg', 'Last'] as const; diff --git a/packages/manager/src/components/Link.tsx b/packages/manager/src/components/Link.tsx index 73c16ce0648..dc2f428d806 100644 --- a/packages/manager/src/components/Link.tsx +++ b/packages/manager/src/components/Link.tsx @@ -6,7 +6,6 @@ import { opensInNewTab, } from '@linode/utilities'; // `link.ts` utils from @linode/utilities import * as React from 'react'; -// eslint-disable-next-line no-restricted-imports import { Link as RouterLink } from 'react-router-dom'; import ExternalLinkIcon from 'src/assets/icons/external-link.svg'; diff --git a/packages/ui/src/components/ListItemOption/ListItemOption.tsx b/packages/manager/src/components/ListItemOption.tsx similarity index 92% rename from packages/ui/src/components/ListItemOption/ListItemOption.tsx rename to packages/manager/src/components/ListItemOption.tsx index 0074342f05c..14333d4d7e6 100644 --- a/packages/ui/src/components/ListItemOption/ListItemOption.tsx +++ b/packages/manager/src/components/ListItemOption.tsx @@ -1,15 +1,11 @@ +import { Box, ListItem, SelectedIcon, Tooltip } from '@linode/ui'; import { styled } from '@mui/material/styles'; import { visuallyHidden } from '@mui/utils'; import React from 'react'; -import { Box } from '../Box'; -import { ListItem } from '../ListItem'; -import { SelectedIcon } from '../Autocomplete'; -import { Tooltip } from '../Tooltip'; - import type { ListItemComponentsPropsOverrides } from '@mui/material/ListItem'; -export interface ListItemOptionProps { +export interface ListItemProps { children?: React.ReactNode; disabledOptions?: DisableItemOption; item: T & { id: number | string }; @@ -38,7 +34,7 @@ export const ListItemOption = ({ maxHeight, props, selected, -}: ListItemOptionProps) => { +}: ListItemProps) => { const { className, onClick, ...rest } = props; const isItemOptionDisabled = Boolean(disabledOptions); const itemOptionDisabledReason = disabledOptions?.reason; diff --git a/packages/manager/src/components/MainContentBanner.test.tsx b/packages/manager/src/components/MainContentBanner.test.tsx index f5531e4e2d0..7d5b723919f 100644 --- a/packages/manager/src/components/MainContentBanner.test.tsx +++ b/packages/manager/src/components/MainContentBanner.test.tsx @@ -7,7 +7,7 @@ import { renderWithTheme } from 'src/utilities/testHelpers'; import { MainContentBanner } from './MainContentBanner'; -import type { ManagerPreferences } from '@linode/utilities'; +import type { ManagerPreferences } from 'src/types/ManagerPreferences'; describe('MainContentBanner', () => { const mainContentBanner = { diff --git a/packages/manager/src/components/MainContentBanner.tsx b/packages/manager/src/components/MainContentBanner.tsx index ca5d787a2c2..239389e033c 100644 --- a/packages/manager/src/components/MainContentBanner.tsx +++ b/packages/manager/src/components/MainContentBanner.tsx @@ -5,7 +5,10 @@ import * as React from 'react'; import { Link } from 'src/components/Link'; import { useFlags } from 'src/hooks/useFlags'; -import { useMutatePreferences, usePreferences } from '@linode/queries'; +import { + useMutatePreferences, + usePreferences, +} from 'src/queries/profile/preferences'; export const MainContentBanner = React.memo(() => { // Uncomment this to test this banner: diff --git a/packages/manager/src/components/MaintenanceBanner/MaintenanceBanner.tsx b/packages/manager/src/components/MaintenanceBanner/MaintenanceBanner.tsx index 1463ab86584..402536b26f5 100644 --- a/packages/manager/src/components/MaintenanceBanner/MaintenanceBanner.tsx +++ b/packages/manager/src/components/MaintenanceBanner/MaintenanceBanner.tsx @@ -1,9 +1,10 @@ -import { useAllAccountMaintenanceQuery, useProfile } from '@linode/queries'; import { Notice, Typography } from '@linode/ui'; import * as React from 'react'; +import { Link } from 'react-router-dom'; -import { Link } from 'src/components/Link'; import { PENDING_MAINTENANCE_FILTER } from 'src/features/Account/Maintenance/utilities'; +import { useAllAccountMaintenanceQuery } from 'src/queries/account/maintenance'; +import { useProfile } from 'src/queries/profile/profile'; import { formatDate } from 'src/utilities/formatDate'; import { isPast } from 'src/utilities/isPast'; diff --git a/packages/manager/src/components/Markdown/__snapshots__/Markdown.test.tsx.snap b/packages/manager/src/components/Markdown/__snapshots__/Markdown.test.tsx.snap index 3b2c384c680..b8d31f626b6 100644 --- a/packages/manager/src/components/Markdown/__snapshots__/Markdown.test.tsx.snap +++ b/packages/manager/src/components/Markdown/__snapshots__/Markdown.test.tsx.snap @@ -3,7 +3,7 @@ exports[`Markdown component > should highlight text consistently 1`] = `

Some markdown diff --git a/packages/manager/src/components/MaskableText/MaskableText.test.tsx b/packages/manager/src/components/MaskableText/MaskableText.test.tsx index 6bd0d5c999b..38c101a9230 100644 --- a/packages/manager/src/components/MaskableText/MaskableText.test.tsx +++ b/packages/manager/src/components/MaskableText/MaskableText.test.tsx @@ -6,7 +6,7 @@ import { renderWithTheme } from 'src/utilities/testHelpers'; import { MaskableText } from './MaskableText'; import type { MaskableTextProps } from './MaskableText'; -import type { ManagerPreferences } from '@linode/utilities'; +import type { ManagerPreferences } from 'src/types/ManagerPreferences'; describe('MaskableText', () => { const maskedText = '•••••••••••'; @@ -24,8 +24,8 @@ describe('MaskableText', () => { usePreferences: vi.fn().mockReturnValue({}), })); - vi.mock('@linode/queries', async () => { - const actual = await vi.importActual('@linode/queries'); + vi.mock('src/queries/profile/preferences', async () => { + const actual = await vi.importActual('src/queries/profile/preferences'); return { ...actual, usePreferences: queryMocks.usePreferences, diff --git a/packages/manager/src/components/MaskableText/MaskableText.tsx b/packages/manager/src/components/MaskableText/MaskableText.tsx index 06c8edadcd6..0512d8ef9bb 100644 --- a/packages/manager/src/components/MaskableText/MaskableText.tsx +++ b/packages/manager/src/components/MaskableText/MaskableText.tsx @@ -1,7 +1,8 @@ -import { Stack, VisibilityTooltip, Typography } from '@linode/ui'; +import { Stack, VisibilityTooltip } from '@linode/ui'; +import { Typography } from '@linode/ui'; import * as React from 'react'; -import { usePreferences } from '@linode/queries'; +import { usePreferences } from 'src/queries/profile/preferences'; import { createMaskedText } from 'src/utilities/createMaskedText'; import type { SxProps, Theme } from '@mui/material'; diff --git a/packages/manager/src/components/OrderBy.test.tsx b/packages/manager/src/components/OrderBy.test.tsx index 12fafd2b019..4d3e189c65a 100644 --- a/packages/manager/src/components/OrderBy.test.tsx +++ b/packages/manager/src/components/OrderBy.test.tsx @@ -9,7 +9,7 @@ import { sortData, } from './OrderBy'; -import type { ManagerPreferences } from '@linode/utilities'; +import type { ManagerPreferences } from 'src/types/ManagerPreferences'; const a = { age: 43, diff --git a/packages/manager/src/components/OrderBy.tsx b/packages/manager/src/components/OrderBy.tsx index ee9df07fc61..5b20be11259 100644 --- a/packages/manager/src/components/OrderBy.tsx +++ b/packages/manager/src/components/OrderBy.tsx @@ -1,22 +1,25 @@ -import { useMutatePreferences, usePreferences } from '@linode/queries'; -import { - getQueryParamsFromQueryString, - pathOr, - sortByArrayLength, - sortByNumber, - sortByString, - splitAt, - usePrevious, -} from '@linode/utilities'; +import { getQueryParamsFromQueryString, pathOr } from '@linode/utilities'; import { DateTime } from 'luxon'; import { equals, sort } from 'ramda'; import * as React from 'react'; import { useHistory, useLocation } from 'react-router-dom'; import { debounce } from 'throttle-debounce'; -import { sortByUTFDate } from 'src/utilities/sortByUTFDate'; +import { usePrevious } from 'src/hooks/usePrevious'; +import { + useMutatePreferences, + usePreferences, +} from 'src/queries/profile/preferences'; +import { + sortByArrayLength, + sortByNumber, + sortByString, + sortByUTFDate, +} from 'src/utilities/sort-by'; +import { splitAt } from 'src/utilities/splitAt'; -import type { ManagerPreferences, Order } from '@linode/utilities'; +import type { Order } from 'src/hooks/useOrder'; +import type { ManagerPreferences } from 'src/types/ManagerPreferences'; export interface OrderByProps extends State { data: T[]; diff --git a/packages/manager/src/components/Paginate.ts b/packages/manager/src/components/Paginate.ts index dca5395cbbe..866916e725e 100644 --- a/packages/manager/src/components/Paginate.ts +++ b/packages/manager/src/components/Paginate.ts @@ -1,8 +1,8 @@ import { clamp } from '@linode/ui'; -import { scrollTo } from '@linode/utilities'; import { slice } from 'ramda'; import * as React from 'react'; +import scrollTo from 'src/utilities/scrollTo'; import { storage } from 'src/utilities/storage'; export const createDisplayPage = (page: number, pageSize: number) => ( @@ -49,6 +49,29 @@ interface Props { } export default class Paginate extends React.Component, State> { + render() { + let view: (data: T[]) => T[]; + // update view based on page url + if (this.props.updatePageUrl) { + view = createDisplayPage(this.props.page || 1, this.state.pageSize); + } + // update view based on state + else { + view = createDisplayPage(this.state.page, this.state.pageSize); + } + + const props = { + ...this.props, + ...this.state, + count: this.props.data.length, + data: view(this.props.data), + handlePageChange: this.handlePageChange, + handlePageSizeChange: this.handlePageSizeChange, + }; + + return this.props.children(props); + } + handlePageChange = (page: number) => { if (this.props.shouldScroll ?? true) { const { scrollToRef } = this.props; @@ -74,27 +97,4 @@ export default class Paginate extends React.Component, State> { page: this.props.page || 1, pageSize: this.props.pageSize || storage.pageSize.get() || 25, }; - - render() { - let view: (data: T[]) => T[]; - // update view based on page url - if (this.props.updatePageUrl) { - view = createDisplayPage(this.props.page || 1, this.state.pageSize); - } - // update view based on state - else { - view = createDisplayPage(this.state.page, this.state.pageSize); - } - - const props = { - ...this.props, - ...this.state, - count: this.props.data.length, - data: view(this.props.data), - handlePageChange: this.handlePageChange, - handlePageSizeChange: this.handlePageSizeChange, - }; - - return this.props.children(props); - } } diff --git a/packages/manager/src/components/PaginationFooter/PaginationFooter.constants.ts b/packages/manager/src/components/PaginationFooter/PaginationFooter.constants.ts deleted file mode 100644 index c00e67a64ff..00000000000 --- a/packages/manager/src/components/PaginationFooter/PaginationFooter.constants.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const MIN_PAGE_SIZE = 25; - -export const PAGE_SIZES = [MIN_PAGE_SIZE, 50, 75, 100, Infinity]; diff --git a/packages/manager/src/components/PaginationFooter/PaginationFooter.test.tsx b/packages/manager/src/components/PaginationFooter/PaginationFooter.test.tsx index 055bc14998e..b0b2b88554d 100644 --- a/packages/manager/src/components/PaginationFooter/PaginationFooter.test.tsx +++ b/packages/manager/src/components/PaginationFooter/PaginationFooter.test.tsx @@ -1,5 +1,7 @@ -import { PAGE_SIZES } from './PaginationFooter.constants'; -import { getMinimumPageSizeForNumberOfItems } from './PaginationFooter.utils'; +import { + PAGE_SIZES, + getMinimumPageSizeForNumberOfItems, +} from './PaginationFooter'; describe('getMinimumPageSizeForNumberOfItems', () => { it('should return the minimum page size needed to display a given number of items', () => { diff --git a/packages/manager/src/components/PaginationFooter/PaginationFooter.tsx b/packages/manager/src/components/PaginationFooter/PaginationFooter.tsx index e86876ea9fc..5c5702d275f 100644 --- a/packages/manager/src/components/PaginationFooter/PaginationFooter.tsx +++ b/packages/manager/src/components/PaginationFooter/PaginationFooter.tsx @@ -1,12 +1,15 @@ -import { Box, Select } from '@linode/ui'; -import { useTheme } from '@mui/material/styles'; +import { Box, TextField } from '@linode/ui'; +import { styled, useTheme } from '@mui/material/styles'; import * as React from 'react'; +import { MenuItem } from 'src/components/MenuItem'; + import { PaginationControls } from '../PaginationControls/PaginationControls'; -import { MIN_PAGE_SIZE, PAGE_SIZES } from './PaginationFooter.constants'; import type { SxProps } from '@mui/material/styles'; +export const MIN_PAGE_SIZE = 25; + export interface PaginationProps { count: number; eventCategory?: string; @@ -22,6 +25,8 @@ interface Props extends PaginationProps { handleSizeChange: (pageSize: number) => void; } +export const PAGE_SIZES = [MIN_PAGE_SIZE, 50, 75, 100, Infinity]; + const baseOptions = [ { label: 'Show 25', value: PAGE_SIZES[0] }, { label: 'Show 50', value: PAGE_SIZES[1] }, @@ -64,7 +69,7 @@ export const PaginationFooter = (props: Props) => { { )} {!fixedSize ? ( -