diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..a6f1887020 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,25 @@ +version: 2 +updates: + + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + labels: ["component/dependencies"] + + # Maintain dependencies for dashboard, shell, and packages + - package-ecosystem: "npm" + directories: + - "/" + - "/shell" + - "/creators/extension" + - "/creators/extension/*" + - "/docusaurus" + - "/storybook" + - "/pkg/*" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + labels: ["component/dependencies"] diff --git a/.github/workflows/add-issue-labels.yaml b/.github/workflows/add-issue-labels.yaml index 8db01a51cc..3f0370a7d4 100644 --- a/.github/workflows/add-issue-labels.yaml +++ b/.github/workflows/add-issue-labels.yaml @@ -9,7 +9,7 @@ jobs: permissions: issues: write steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Use Node.js uses: actions/setup-node@v4 with: diff --git a/.github/workflows/build-and-upload.yaml b/.github/workflows/build-and-upload.yaml index 434ca3bef7..782d61bcfc 100644 --- a/.github/workflows/build-and-upload.yaml +++ b/.github/workflows/build-and-upload.yaml @@ -22,7 +22,7 @@ jobs: name: Build & Upload Hosted runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 1 @@ -69,7 +69,7 @@ jobs: name: Build & Upload Embedded runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 1 diff --git a/.github/workflows/build-extension-charts.yml b/.github/workflows/build-extension-charts.yml index 1e7720bb28..d4e2fd6460 100644 --- a/.github/workflows/build-extension-charts.yml +++ b/.github/workflows/build-extension-charts.yml @@ -103,7 +103,7 @@ jobs: - name: Upload charts artifact if: github.ref_type == 'tag' || (github.ref == 'refs/heads/main' && github.event_name != 'pull_request') || inputs.is_test == 'true' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: charts path: tmp @@ -132,7 +132,7 @@ jobs: git config user.email 'github-actions[bot]@users.noreply.github.com' - name: Download build artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: charts diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index b8ba454680..20b93e7417 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -9,7 +9,7 @@ jobs: unit-test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 1 @@ -19,7 +19,7 @@ jobs: i18n: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 1 @@ -29,7 +29,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 1 diff --git a/.github/workflows/check-plugins.yaml b/.github/workflows/check-plugins.yaml index a8fbb48be6..de3f416eff 100644 --- a/.github/workflows/check-plugins.yaml +++ b/.github/workflows/check-plugins.yaml @@ -26,7 +26,7 @@ jobs: shell: bash - name: Upload files - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() with: name: test diff --git a/.github/workflows/docusaurus.yaml b/.github/workflows/docusaurus.yaml index 38056fe031..8992a5d71d 100644 --- a/.github/workflows/docusaurus.yaml +++ b/.github/workflows/docusaurus.yaml @@ -12,7 +12,7 @@ jobs: name: Build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' diff --git a/.github/workflows/extensions-compatibility-tests.yaml b/.github/workflows/extensions-compatibility-tests.yaml index e8a2e50ce9..104a96303f 100644 --- a/.github/workflows/extensions-compatibility-tests.yaml +++ b/.github/workflows/extensions-compatibility-tests.yaml @@ -30,7 +30,7 @@ jobs: ] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 1 - name: Setup env @@ -60,11 +60,17 @@ jobs: # Upload to sorry cypress in case of failure - name: Upload screenshots - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: ${{ failure() }} with: - name: ${{github.run_number}}-${{github.run_attempt}}-extensions-compatibility-tests-screenshots-${{ matrix.role.tag }}+${{ matrix.features[0] }} + name: ${{github.run_number}}-${{github.run_attempt}}-extensions-compatibility-tests-screenshots-${{ matrix.role.tag }}+${{ matrix.features[0] }}-${{ matrix.rancherEnv }} path: cypress/screenshots + + - name: Merge Artifacts + uses: actions/upload-artifact/merge@v4 + with: + name: ${{github.run_number}}-${{github.run_attempt}}-extensions-compatibility-tests-screenshots-${{ matrix.role.tag }}+${{ matrix.features[0] }} + pattern: ${{github.run_number}}-${{github.run_attempt}}-extensions-compatibility-tests-screenshots-* # Slack message with outcome - success - name: Slack message in workflow success @@ -88,4 +94,4 @@ jobs: curl -X POST \ -H "Content-type: application/json; charset=utf-8" \ --data '{"name": "${{ matrix.rancherEnv[0] }} - image:${{ matrix.rancherEnv[1] }} - ${{ matrix.features[1] }}", "status": "Workflow failed! 🆘", "workflow_run": ${{ github.run_id }} }' \ - $SLACK_URL \ No newline at end of file + $SLACK_URL diff --git a/.github/workflows/pr-gh-project.yaml b/.github/workflows/pr-gh-project.yaml index dcb875c3da..1e140d2734 100644 --- a/.github/workflows/pr-gh-project.yaml +++ b/.github/workflows/pr-gh-project.yaml @@ -12,7 +12,7 @@ jobs: id-token: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Use Node.js uses: actions/setup-node@v4 with: diff --git a/.github/workflows/release-rancher-components.yml b/.github/workflows/release-rancher-components.yml index 3d2061544d..4a56c15bdc 100644 --- a/.github/workflows/release-rancher-components.yml +++ b/.github/workflows/release-rancher-components.yml @@ -12,7 +12,7 @@ jobs: contents: read packages: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 persist-credentials: false diff --git a/.github/workflows/release-shell-pkg.yaml b/.github/workflows/release-shell-pkg.yaml index 1e41425078..3ab06bb95b 100644 --- a/.github/workflows/release-shell-pkg.yaml +++ b/.github/workflows/release-shell-pkg.yaml @@ -33,7 +33,7 @@ jobs: steps: - if: inputs.is_test == 'true' && inputs.test_branch != '' name: Checkout (test flow) - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: persist-credentials: false ref: ${{ inputs.test_branch }} @@ -41,7 +41,7 @@ jobs: - if: inputs.is_test != 'true' name: Checkout (normal flow) - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 persist-credentials: false diff --git a/.github/workflows/test-extension-workflows-master.yml b/.github/workflows/test-extension-workflows-master.yml index ef44059f04..6f8756083f 100644 --- a/.github/workflows/test-extension-workflows-master.yml +++ b/.github/workflows/test-extension-workflows-master.yml @@ -30,7 +30,7 @@ jobs: SHELL_TAG: ${{ steps.retrieve-data.outputs.SHELL_TAG }} CREATORS_TAG: ${{ steps.retrieve-data.outputs.CREATORS_TAG }} ECI_TAG: ${{ steps.retrieve-data.outputs.ECI_TAG }} - EXTENSIONS_TAG: ${{ steps.retrieve-data.outputs.EXTENSIONS_TAG }} + EXTENSION_TAG: ${{ steps.retrieve-data.outputs.EXTENSION_TAG }} CURR_JOB_ID: ${{ github.job }} tags-job-status: ${{ job.status }} steps: @@ -53,7 +53,7 @@ jobs: pages: write with: target_branch: 'gh-pages' - tagged_release: ${{ needs.retrieve-tags-master.outputs.EXTENSIONS_TAG }} + tagged_release: ${{ needs.retrieve-tags-master.outputs.EXTENSION_TAG }} is_test: 'true' test_ext_repo: 'ui-plugin-examples' test_ext_branch: 'main' diff --git a/.github/workflows/test-extension-workflows-release-2.8.yml b/.github/workflows/test-extension-workflows-release-2.8.yml index 212d7027c9..eff00910e5 100644 --- a/.github/workflows/test-extension-workflows-release-2.8.yml +++ b/.github/workflows/test-extension-workflows-release-2.8.yml @@ -17,7 +17,7 @@ jobs: SHELL_TAG: ${{ steps.retrieve-data.outputs.SHELL_TAG }} CREATORS_TAG: ${{ steps.retrieve-data.outputs.CREATORS_TAG }} ECI_TAG: ${{ steps.retrieve-data.outputs.ECI_TAG }} - EXTENSIONS_TAG: ${{ steps.retrieve-data.outputs.EXTENSIONS_TAG }} + EXTENSION_TAG: ${{ steps.retrieve-data.outputs.EXTENSION_TAG }} CURR_JOB_ID: ${{ github.job }} tags-job-status: ${{ job.status }} steps: @@ -40,7 +40,7 @@ jobs: pages: write with: target_branch: 'gh-pages' - tagged_release: ${{ needs.retrieve-tags-release-2-dot-8.outputs.EXTENSIONS_TAG }} + tagged_release: ${{ needs.retrieve-tags-release-2-dot-8.outputs.EXTENSION_TAG }} is_test: 'true' test_ext_repo: 'elemental-ui' test_ext_branch: 'release-2.8.x' diff --git a/.github/workflows/test-extension-workflows-release-2.9.yml b/.github/workflows/test-extension-workflows-release-2.9.yml index e7b5ebcb50..f6d742f4cf 100644 --- a/.github/workflows/test-extension-workflows-release-2.9.yml +++ b/.github/workflows/test-extension-workflows-release-2.9.yml @@ -17,7 +17,7 @@ jobs: SHELL_TAG: ${{ steps.retrieve-data.outputs.SHELL_TAG }} CREATORS_TAG: ${{ steps.retrieve-data.outputs.CREATORS_TAG }} ECI_TAG: ${{ steps.retrieve-data.outputs.ECI_TAG }} - EXTENSIONS_TAG: ${{ steps.retrieve-data.outputs.EXTENSIONS_TAG }} + EXTENSION_TAG: ${{ steps.retrieve-data.outputs.EXTENSION_TAG }} CURR_JOB_ID: ${{ github.job }} tags-job-status: ${{ job.status }} steps: @@ -40,7 +40,7 @@ jobs: pages: write with: target_branch: 'gh-pages' - tagged_release: ${{ needs.retrieve-tags-release-2-dot-9.outputs.EXTENSIONS_TAG }} + tagged_release: ${{ needs.retrieve-tags-release-2-dot-9.outputs.EXTENSION_TAG }} is_test: 'true' test_ext_repo: 'elemental-ui' test_ext_branch: 'release-2.9.x' diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 646866f407..3cb20fca92 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -22,6 +22,8 @@ env: TEST_BASE_URL: https://127.0.0.1/dashboard API: https://127.0.0.1 TEST_PROJECT_ID: rancher-dashboard + TEST_DISABLE_DASHBOARD: ${{ vars.TEST_DISABLE_DASHBOARD }} # This is required to get it from the project configuration + TEST_DISABLE_DASHBOARD_LABEL: "${{ contains(github.event.pull_request.labels.*.name, 'ci/skip-e2e-cypress-dashboard')}}" CYPRESS_API_URL: http://139.59.134.103:1234/ TEST_RUN_ID: ${{github.run_number}}-${{github.run_attempt}}-${{github.event.pull_request.title || github.event.head_commit.message}} # Build the dashboard to use in tests. When set to false it will grab `latest` from CDN (useful for running e2e tests quickly) diff --git a/.github/workflows/valid-pr-description.yaml b/.github/workflows/valid-pr-description.yaml index 72e7ff85b7..c69cf30967 100644 --- a/.github/workflows/valid-pr-description.yaml +++ b/.github/workflows/valid-pr-description.yaml @@ -11,7 +11,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Validate checklist has been completed env: diff --git a/README.md b/README.md index 20576a67cd..d490a5f284 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ We welcome external contributions - please refer to the internal documentation a License ======= -Copyright (c) 2014-2025 [Rancher Labs, Inc.](http://rancher.com) +Copyright (c) 2014-2025 [SUSE](https://www.suse.com) Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/creators/extension/app/files/.github/workflows/build-extension-catalog.yml b/creators/extension/app/files/.github/workflows/build-extension-catalog.yml index 61400830ef..1c2bb812a9 100644 --- a/creators/extension/app/files/.github/workflows/build-extension-catalog.yml +++ b/creators/extension/app/files/.github/workflows/build-extension-catalog.yml @@ -20,5 +20,6 @@ jobs: with: registry_target: ghcr.io registry_user: ${{ github.actor }} + tagged_release: ${{ github.ref_name }} secrets: registry_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/creators/extension/package.json b/creators/extension/package.json index 84c149a28f..9fc4d23e7f 100644 --- a/creators/extension/package.json +++ b/creators/extension/package.json @@ -1,7 +1,7 @@ { "name": "@rancher/create-extension", "description": "Rancher UI Extension generator", - "version": "3.0.6", + "version": "3.0.7", "license": "Apache-2.0", "author": "SUSE", "packageManager": "yarn@4.5.0", diff --git a/cypress/e2e/blueprints/nav/fake-cluster.ts b/cypress/e2e/blueprints/nav/fake-cluster.ts index 8391e69528..de9a85f57b 100644 --- a/cypress/e2e/blueprints/nav/fake-cluster.ts +++ b/cypress/e2e/blueprints/nav/fake-cluster.ts @@ -2474,7 +2474,7 @@ function generateFakeNavClusterData(provClusterId = 'some-prov-cluster-id', mgmt } export function generateFakeClusterDataAndIntercepts(fakeProvClusterId = 'some-prov-cluster-id', fakeMgmtClusterId = 'some-mgmt-cluster-id', addEditClusterCapabilities = false): {} { - const longClusterDescription = 'this-is-some-really-really-really-really-really-really-long-decription'; + const longClusterDescription = 'this-is-some-really-really-really-really-really-really-long-description'; const fakeNavClusterData = generateFakeNavClusterData(fakeProvClusterId, fakeMgmtClusterId, addEditClusterCapabilities); // add cluster to fleet clusters for testing https://github.com/rancher/dashboard/issues/9984 diff --git a/cypress/e2e/po/components/addon-config.po.ts b/cypress/e2e/po/components/addon-config.po.ts new file mode 100644 index 0000000000..fc9c4760fa --- /dev/null +++ b/cypress/e2e/po/components/addon-config.po.ts @@ -0,0 +1,12 @@ +import ComponentPo from '@/cypress/e2e/po/components/component.po'; +import YamlEditorPo from '~/cypress/e2e/po/components/yaml-editor.po'; + +export default class AddonConfigPo extends ComponentPo { + constructor(selector = '.dashboard-root') { + super(selector); + } + + yamlEditor() :YamlEditorPo { + return new YamlEditorPo(this.self().find('[data-testid="addon-yaml-editor"]')); + } +} diff --git a/cypress/e2e/po/components/card.po.ts b/cypress/e2e/po/components/card.po.ts index 050e545c9b..c456dfd6c3 100644 --- a/cypress/e2e/po/components/card.po.ts +++ b/cypress/e2e/po/components/card.po.ts @@ -14,6 +14,10 @@ export default class CardPo extends ComponentPo { return this.self().get('[data-testid="card-body-slot"]'); } + getError(): CypressChainable { + return this.self().get('[data-testid="card-body-slot"] > .text-error'); + } + getActionButton(): CypressChainable { return this.self().get('[data-testid="card-actions-slot"]'); } diff --git a/cypress/e2e/po/components/create-edit-view.po.ts b/cypress/e2e/po/components/create-edit-view.po.ts index 169e8a6ffb..000e880694 100644 --- a/cypress/e2e/po/components/create-edit-view.po.ts +++ b/cypress/e2e/po/components/create-edit-view.po.ts @@ -17,4 +17,12 @@ export default class CreateEditViewPo extends ComponentPo { nextPage() { return new AsyncButtonPo(this.self().find('.cru-resource-footer .role-primary')).click(); } + + saveButtonPo() :AsyncButtonPo { + return new AsyncButtonPo(this.self().find('.cru-resource-footer .role-primary')); + } + + editAsYaml() { + return new AsyncButtonPo(this.self().find('[data-testid="form-yaml"]')).click(); + } } diff --git a/cypress/e2e/po/components/header.po.ts b/cypress/e2e/po/components/header.po.ts index 1915f0e4d7..1888f5ebdd 100644 --- a/cypress/e2e/po/components/header.po.ts +++ b/cypress/e2e/po/components/header.po.ts @@ -28,6 +28,12 @@ export class HeaderPo extends ComponentPo { return wsFilter.clickOptionWithLabel(name); } + checkCurrentWorkspace(name: string) { + const wsFilter = new WorkspaceSwitcherPo(); + + return wsFilter.checkOptionSelected(name); + } + importYamlHeaderAction() { return this.self().find('[data-testid="header-action-import-yaml"]'); } diff --git a/cypress/e2e/po/edit/fleet/fleet.cattle.io.clustergroup.po.ts b/cypress/e2e/po/edit/fleet/fleet.cattle.io.clustergroup.po.ts index e792fdcb7f..6b5aa989f8 100644 --- a/cypress/e2e/po/edit/fleet/fleet.cattle.io.clustergroup.po.ts +++ b/cypress/e2e/po/edit/fleet/fleet.cattle.io.clustergroup.po.ts @@ -1,27 +1,35 @@ import PagePo from '@/cypress/e2e/po/pages/page.po'; import AsyncButtonPo from '@/cypress/e2e/po/components/async-button.po'; -import CodeMirrorPo from '@/cypress/e2e/po/components/code-mirror.po'; - +import ResourceDetailPo from '@/cypress/e2e/po/edit/resource-detail.po'; +import NameNsDescription from '@/cypress/e2e/po/components/name-ns-description.po'; export default class FleetClusterGroupsCreateEditPo extends PagePo { - private static createPath(clusterId: string, id?: string ) { - const root = `/c/${ clusterId }/explorer/storage.k8s.io.storageclass/create`; + private static createPath(clusterId: string, workspace?: string, id?: string ) { + const root = `/c/${ clusterId }/fleet/fleet.cattle.io.clustergroup`; - return id ? `${ root }/${ id }` : `${ root }/create`; + return id ? `${ root }/${ workspace }/${ id }` : `${ root }/create`; } static goTo(path: string): Cypress.Chainable { throw new Error('invalid'); } - constructor(clusterId = '_', id?: string) { - super(FleetClusterGroupsCreateEditPo.createPath(clusterId, id)); + constructor(clusterId = '_', workspace?: string, id?: string) { + super(FleetClusterGroupsCreateEditPo.createPath(clusterId, workspace, id)); } - editAsYaml() { - return new AsyncButtonPo('[data-testid="form-yaml"]', this.self()); + title() { + return this.self().get('.title .primaryheader h1'); } - yamlEditor(): CodeMirrorPo { - return CodeMirrorPo.bySelector(this.self(), '[data-testid="yaml-editor-code-mirror"]'); + nameNsDescription() { + return new NameNsDescription(this.self()); + } + + saveCreateForm(): ResourceDetailPo { + return new ResourceDetailPo(this.self()); + } + + saveButton() { + return new AsyncButtonPo('[data-testid="form-save"]', this.self()); } } diff --git a/cypress/e2e/po/edit/fleet/gitrepo-edit.po.ts b/cypress/e2e/po/edit/fleet/gitrepo-edit.po.ts new file mode 100644 index 0000000000..93bf3c2b20 --- /dev/null +++ b/cypress/e2e/po/edit/fleet/gitrepo-edit.po.ts @@ -0,0 +1,65 @@ +import PagePo from '@/cypress/e2e/po/pages/page.po'; +import ArrayListPo from '@/cypress/e2e/po/components/array-list.po'; +import CreateEditViewPo from '@/cypress/e2e/po/components/create-edit-view.po'; +import LabeledInputPo from '@/cypress/e2e/po/components/labeled-input.po'; +import LabeledSelectPo from '@/cypress/e2e/po/components/labeled-select.po'; +import SelectOrCreateAuthPo from '@/cypress/e2e/po/components/select-or-create-auth.po'; +import NameNsDescription from '@/cypress/e2e/po/components/name-ns-description.po'; + +export class GitRepoEditPo extends PagePo { + private static createPath(fleetWorkspace: string, gitRepoName: string) { + return `/c/_/fleet/fleet.cattle.io.gitrepo/${ fleetWorkspace }/${ gitRepoName }`; + } + + static goTo(path: string): Cypress.Chainable { + throw new Error('invalid'); + } + + constructor(fleetWorkspace: string, gitRepoName: string) { + super(GitRepoEditPo.createPath(fleetWorkspace, gitRepoName)); + } + + title() { + return this.self().get('.title .primaryheader h1'); + } + + nameNsDescription() { + return new NameNsDescription(this.self()); + } + + setBranchName(branch = 'dashboard-e2e-basic') { + return LabeledInputPo.byLabel(this.self(), 'Branch').set(branch); + } + + setGitRepoUrl(url: string) { + return LabeledInputPo.byLabel(this.self(), 'Repository URL').set(url); + } + + setHelmRepoURLRegex(regexStr = 'https://charts.rancher.io/*') { + return LabeledInputPo.bySelector(this.self(), '[data-testid="gitrepo-helm-repo-url-regex"]').set(regexStr); + } + + setGitRepoPath(path: string, index = 0) { + return this.gitRepoPaths().setValueAtIndex(path, index); + } + + targetCluster(): LabeledSelectPo { + return new LabeledSelectPo('[data-testid="fleet-gitrepo-target-cluster"]'); + } + + footer() { + return new CreateEditViewPo(this.self()); + } + + gitRepoPaths() { + return new ArrayListPo('[data-testid="gitRepo-paths"]'); + } + + authSelectOrCreate(selector: string) { + return new SelectOrCreateAuthPo(selector); + } + + helmAuthSelectOrCreate() { + return this.authSelectOrCreate('[data-testid="gitrepo-helm-auth"]'); + } +} diff --git a/cypress/e2e/po/edit/provisioning.cattle.io.cluster/create/cluster-create-rke2-custom.po.ts b/cypress/e2e/po/edit/provisioning.cattle.io.cluster/create/cluster-create-rke2-custom.po.ts index ff4685a09d..6690cffd94 100644 --- a/cypress/e2e/po/edit/provisioning.cattle.io.cluster/create/cluster-create-rke2-custom.po.ts +++ b/cypress/e2e/po/edit/provisioning.cattle.io.cluster/create/cluster-create-rke2-custom.po.ts @@ -4,6 +4,7 @@ import ClusterManagerCreatePagePo from '@/cypress/e2e/po/edit/provisioning.cattl import TabbedPo from '@/cypress/e2e/po/components/tabbed.po'; import RegistriesTabPo from '@/cypress/e2e/po/components/registries-tab.po'; import NetworkTabPo from '@/cypress/e2e/po/components/network-tab.po'; +import AddonConfigPo from '@/cypress/e2e/po/components/addon-config.po'; /** * Create page for an RKE2 custom cluster @@ -44,4 +45,8 @@ export default class ClusterManagerCreateRke2CustomPagePo extends ClusterManager network(): NetworkTabPo { return new NetworkTabPo(); } + + calicoAddonConfig(): AddonConfigPo { + return new AddonConfigPo(); + } } diff --git a/cypress/e2e/po/pages/explorer/cluster-dashboard.po.ts b/cypress/e2e/po/pages/explorer/cluster-dashboard.po.ts index 6eb0c38a2e..e3e6ad73d7 100644 --- a/cypress/e2e/po/pages/explorer/cluster-dashboard.po.ts +++ b/cypress/e2e/po/pages/explorer/cluster-dashboard.po.ts @@ -57,7 +57,7 @@ export default class ClusterDashboardPagePo extends PagePo { } fullEventsLink() { - return cy.get('.events-table-link').contains('Full events list'); + return cy.get('[data-testid="events-link"]').contains('Full events list'); } fullSecretsList() { diff --git a/cypress/e2e/po/pages/extensions.po.ts b/cypress/e2e/po/pages/extensions.po.ts index 0390b02453..ec2706bbf9 100644 --- a/cypress/e2e/po/pages/extensions.po.ts +++ b/cypress/e2e/po/pages/extensions.po.ts @@ -7,7 +7,7 @@ import RepositoriesPagePo from '@/cypress/e2e/po/pages/chart-repositories.po'; import BannersPo from '@/cypress/e2e/po/components/banners.po'; import ChartRepositoriesCreateEditPo from '@/cypress/e2e/po/edit/chart-repositories.po'; import AppClusterRepoEditPo from '@/cypress/e2e/po/edit/catalog.cattle.io.clusterrepo.po'; -import { LONG_TIMEOUT_OPT } from '@/cypress/support/utils/timeouts'; +import { LONG_TIMEOUT_OPT, MEDIUM_TIMEOUT_OPT } from '@/cypress/support/utils/timeouts'; export default class ExtensionsPagePo extends PagePo { static url = '/c/local/uiplugins' @@ -34,7 +34,7 @@ export default class ExtensionsPagePo extends PagePo { return this.title().should('contain', 'Extensions'); } - loading() { + loading(options: any) { return this.self().get('.data-loading'); } @@ -50,12 +50,16 @@ export default class ExtensionsPagePo extends PagePo { * @returns {Cypress.Chainable} */ addExtensionsRepository(repo: string, branch: string, name: string): Cypress.Chainable { + cy.intercept('GET', '/v1/catalog.cattle.io.clusterrepos?exclude=metadata.managedFields').as('getRepos'); + // we should be on the extensions page - this.waitForPage(); + this.waitForPage(null, 'available'); + this.loading(MEDIUM_TIMEOUT_OPT).should('not.exist'); // go to app repos this.extensionMenuToggle(); this.manageReposClick(); + cy.wait('@getRepos').its('response.statusCode').should('eq', 200); // create a new clusterrepo const appRepoList = new RepositoriesPagePo('local', 'apps'); @@ -82,6 +86,8 @@ export default class ExtensionsPagePo extends PagePo { appRepoList.waitForPage(); appRepoList.list().state(name).should('contain', 'Active'); + + return cy.wrap(appRepoList.list()); } /** diff --git a/cypress/e2e/po/pages/fleet/fleet-dashboard.po.ts b/cypress/e2e/po/pages/fleet/fleet-dashboard.po.ts index be82a80b99..e2a3699d08 100644 --- a/cypress/e2e/po/pages/fleet/fleet-dashboard.po.ts +++ b/cypress/e2e/po/pages/fleet/fleet-dashboard.po.ts @@ -2,6 +2,7 @@ import PagePo from '@/cypress/e2e/po/pages/page.po'; import ResourceTablePo from '@/cypress/e2e/po/components/resource-table.po'; import BurgerMenuPo from '@/cypress/e2e/po/side-bars/burger-side-menu.po'; import { LONG_TIMEOUT_OPT } from '@/cypress/support/utils/timeouts'; +import BaseResourceList from '@/cypress/e2e/po/lists/base-resource-list.po'; export class FleetDashboardPagePo extends PagePo { static url: string; @@ -44,4 +45,20 @@ export class FleetDashboardPagePo extends PagePo { sortableTable(tableName = 'fleet-local') { return this.resourceTable(tableName).sortableTable(); } + + goToGitRepoListLink(name: 'fleet-local' | 'fleet-default') { + return this.self().find(`[data-testid="collapsible-card-${ name }"] h2 span` ); + } + + list() { + return new BaseResourceList('[data-testid="sortable-table-list-container"]'); + } + + fleetDashboardEmptyState() { + return this.self().get('.fleet-empty-dashboard'); + } + + getStartedButton() { + return this.self().get('.btn').contains('Get started'); + } } diff --git a/cypress/e2e/po/pages/fleet/fleet.cattle.io.clustergroup.po.ts b/cypress/e2e/po/pages/fleet/fleet.cattle.io.clustergroup.po.ts index adad51dd08..332bac73b5 100644 --- a/cypress/e2e/po/pages/fleet/fleet.cattle.io.clustergroup.po.ts +++ b/cypress/e2e/po/pages/fleet/fleet.cattle.io.clustergroup.po.ts @@ -5,14 +5,16 @@ import ResourceTablePo from '@/cypress/e2e/po/components/resource-table.po'; import FleetClusterGroupsList from '@/cypress/e2e/po/lists/fleet/fleet.cattle.io.clustergroup'; import FleetClusterGroupsCreateEditPo from '@/cypress/e2e/po/edit/fleet/fleet.cattle.io.clustergroup.po'; export class FleetClusterGroupsListPagePo extends PagePo { - static url = `/c/_/fleet/fleet.cattle.io.clustergroup` - - constructor() { - super(FleetClusterGroupsListPagePo.url); + private static createPath(clusterId: string) { + return `/c/${ clusterId }/fleet/fleet.cattle.io.clustergroup`; } - goTo() { - return cy.visit(FleetClusterGroupsListPagePo.url); + static goTo(clusterId: string): Cypress.Chainable { + return super.goTo(FleetClusterGroupsListPagePo.createPath(clusterId)); + } + + constructor(private clusterId = '_') { + super(FleetClusterGroupsListPagePo.createPath(clusterId)); } static navTo() { @@ -44,7 +46,7 @@ export class FleetClusterGroupsListPagePo extends PagePo { return this.self().find('[data-testid="masthead-create"]').click(); } - createFleetClusterGroupsForm(id? : string): FleetClusterGroupsCreateEditPo { - return new FleetClusterGroupsCreateEditPo(id); + createFleetClusterGroupsForm(workspace?: string, id? : string): FleetClusterGroupsCreateEditPo { + return new FleetClusterGroupsCreateEditPo(this.clusterId, workspace, id); } } diff --git a/cypress/e2e/po/pages/global-settings/feature-flags.po.ts b/cypress/e2e/po/pages/global-settings/feature-flags.po.ts index 9ba6893bdd..d04893e475 100644 --- a/cypress/e2e/po/pages/global-settings/feature-flags.po.ts +++ b/cypress/e2e/po/pages/global-settings/feature-flags.po.ts @@ -54,6 +54,17 @@ export class FeatureFlagsPagePo extends RootClusterPage { return card.getBody().contains(label); } + /** + * Get card body error + * @param error + * @returns + */ + cardActionError(error: string): CypressChainable { + const card = new CardPo(); + + return card.getError().contains(error); + } + /** * Click action button * @param label Activate or Deactivate diff --git a/cypress/e2e/tests/navigation/side-nav/main-side-menu.spec.ts b/cypress/e2e/tests/navigation/side-nav/main-side-menu.spec.ts index 560a43f17f..ffc0ca298a 100644 --- a/cypress/e2e/tests/navigation/side-nav/main-side-menu.spec.ts +++ b/cypress/e2e/tests/navigation/side-nav/main-side-menu.spec.ts @@ -4,7 +4,7 @@ import PagePo from '@/cypress/e2e/po/pages/page.po'; import ProductNavPo from '@/cypress/e2e/po/side-bars/product-side-nav.po'; import { generateFakeClusterDataAndIntercepts } from '@/cypress/e2e/blueprints/nav/fake-cluster'; -const longClusterDescription = 'this-is-some-really-really-really-really-really-really-long-decription'; +const longClusterDescription = 'this-is-some-really-really-really-really-really-really-long-description'; const fakeProvClusterId = 'some-fake-cluster-id'; const fakeMgmtClusterId = 'some-fake-mgmt-id'; diff --git a/cypress/e2e/tests/pages/explorer/apps/charts.spec.ts b/cypress/e2e/tests/pages/explorer/apps/charts.spec.ts index d9cc8daf6e..20e89ef0ef 100644 --- a/cypress/e2e/tests/pages/explorer/apps/charts.spec.ts +++ b/cypress/e2e/tests/pages/explorer/apps/charts.spec.ts @@ -6,6 +6,8 @@ const chartsPage = new ChartsPage(); describe('Apps/Charts', { tags: ['@explorer', '@adminUser'] }, () => { beforeEach(() => { + cy.intercept('GET', '/v1/catalog.cattle.io.clusterrepos/**').as('fetchChartData'); + cy.login(); chartsPage.goTo(); chartsPage.waitForPage(); @@ -94,6 +96,9 @@ describe('Apps/Charts', { tags: ['@explorer', '@adminUser'] }, () => { it('should call fetch when route query changes with valid parameters', () => { const chartName = 'Logging'; + cy.wait('@fetchChartData'); + cy.get('@fetchChartData.all').should('have.length.at.least', 3); + chartsPage.getChartByName(chartName) .should('exist') .scrollIntoView() @@ -105,16 +110,19 @@ describe('Apps/Charts', { tags: ['@explorer', '@adminUser'] }, () => { chartPage.waitForPage(); // Set up intercept for the network request triggered by $fetch - cy.intercept('GET', '**/v1/catalog.cattle.io.clusterrepos/**').as('fetchChartData'); + cy.intercept('GET', '**/v1/catalog.cattle.io.clusterrepos/**').as('fetchChartDataAfterSelect'); chartPage.selectVersion('103.1.1+up4.4.0'); - cy.wait('@fetchChartData').its('response.statusCode').should('eq', 200); + cy.wait('@fetchChartDataAfterSelect').its('response.statusCode').should('eq', 200); }); it('should not call fetch when navigating back to charts page', () => { const chartName = 'Logging'; + cy.wait('@fetchChartData'); + cy.get('@fetchChartData.all').should('have.length.at.least', 3); + chartsPage.getChartByName(chartName) .should('exist') .scrollIntoView() diff --git a/cypress/e2e/tests/pages/explorer/apps/repositories.spec.ts b/cypress/e2e/tests/pages/explorer/apps/repositories.spec.ts index b8e7042396..54fcebbd33 100644 --- a/cypress/e2e/tests/pages/explorer/apps/repositories.spec.ts +++ b/cypress/e2e/tests/pages/explorer/apps/repositories.spec.ts @@ -172,7 +172,7 @@ describe('Apps', () => { cy.intercept('GET', '/v1/catalog.cattle.io.clusterrepos/rancher-charts?*').as('rancherCharts1'); // Nav to a summary page for a specific chart - ChartsPage.navTo(clusterId); + chartsPage.goTo(); chartsPage.chartsFilterCategoriesSelect().toggle(); chartsPage.chartsFilterCategoriesSelect().clickOptionWithLabel('All Categories'); chartsPage.chartsFilterReposSelect().toggle(); diff --git a/cypress/e2e/tests/pages/explorer/dashboard/cluster-dashboard.spec.ts b/cypress/e2e/tests/pages/explorer/dashboard/cluster-dashboard.spec.ts index 739bb22bff..5ea576f367 100644 --- a/cypress/e2e/tests/pages/explorer/dashboard/cluster-dashboard.spec.ts +++ b/cypress/e2e/tests/pages/explorer/dashboard/cluster-dashboard.spec.ts @@ -48,7 +48,7 @@ describe('Cluster Dashboard', { testIsolation: 'off', tags: ['@explorer', '@admi clusterDashboard.waitForPage(undefined, 'cluster-events'); - // check if burguer menu nav is highlighted correctly for local cluster + // check if burger menu nav is highlighted correctly for local cluster BurgerMenuPo.checkIfClusterMenuLinkIsHighlighted('local'); }); @@ -257,48 +257,47 @@ describe('Cluster Dashboard', { testIsolation: 'off', tags: ['@explorer', '@admi }); it('can view events table empty if no events', { tags: ['@vai', '@adminUser'] }, () => { - cy.visit(clusterDashboard.urlPath(), { - onBeforeLoad(win) { - cy.stub(win.console, 'error').as('consoleError'); - cy.stub(win.console, 'warn').as('consoleWarn'); - }, - }); - eventsNoDataset(); clusterDashboard.goTo(); - cy.get('@consoleError').should('not.be.called'); // See error lot - cy.get('@consoleWarn').should('not.be.called'); // See warning log (there will be some....) - cy.wait('@eventsNoData'); clusterDashboard.waitForPage(undefined, 'cluster-events'); clusterDashboard.eventsList().resourceTable().sortableTable().checkRowCount(true, 1); - const expectedHeaders = ['Reason', 'Object', 'Message', 'Name', 'Date']; + let expectedHeaders = ['Reason', 'Object', 'Message', 'Name', 'Date']; - clusterDashboard.eventsList().resourceTable().sortableTable().tableHeaderRow() - .within('.table-header-container .content') - .each((el, i) => { - expect(el.text().trim()).to.eq(expectedHeaders[i]); - }); + cy.isVaiCacheEnabled().then((isVaiCacheEnabled) => { + if (isVaiCacheEnabled) { + expectedHeaders = ['Reason', 'Object', 'Message', 'Name', 'First Seen', 'Last Seen', 'Count']; + } - clusterDashboard.fullEventsLink().click(); - cy.wait('@eventsNoData'); - const events = new EventsPagePo('local'); + clusterDashboard.eventsList().resourceTable().sortableTable().tableHeaderRow() + .self() + .scrollIntoView(); + clusterDashboard.eventsList().resourceTable().sortableTable().tableHeaderRow() + .within('.table-header-container .content') + .each((el, i) => { + expect(el.text().trim()).to.eq(expectedHeaders[i]); + }); - events.waitForPage(); + clusterDashboard.fullEventsLink().click(); + cy.wait('@eventsNoData'); + const events = new EventsPagePo('local'); - events.eventslist().resourceTable().sortableTable().checkRowCount(true, 1); + events.waitForPage(); - const expectedFullHeaders = ['State', 'Last Seen', 'Type', 'Reason', 'Object', - 'Subobject', 'Source', 'Message', 'First Seen', 'Count', 'Name', 'Namespace']; + events.eventslist().resourceTable().sortableTable().checkRowCount(true, 1); - events.eventslist().resourceTable().sortableTable().tableHeaderRow() - .within('.table-header-container .content') - .each((el, i) => { - expect(el.text().trim()).to.eq(expectedFullHeaders[i]); - }); + const expectedFullHeaders = ['State', 'Last Seen', 'Type', 'Reason', 'Object', + 'Subobject', 'Source', 'Message', 'First Seen', 'Count', 'Name', 'Namespace']; + + events.eventslist().resourceTable().sortableTable().tableHeaderRow() + .within('.table-header-container .content') + .each((el, i) => { + expect(el.text().trim()).to.eq(expectedFullHeaders[i]); + }); + }); }); describe('Cluster dashboard with limited permissions', () => { diff --git a/cypress/e2e/tests/pages/explorer/dashboard/events.spec.ts b/cypress/e2e/tests/pages/explorer/dashboard/events.spec.ts index 913405b771..4bac54da0c 100644 --- a/cypress/e2e/tests/pages/explorer/dashboard/events.spec.ts +++ b/cypress/e2e/tests/pages/explorer/dashboard/events.spec.ts @@ -7,6 +7,27 @@ import SortableTablePo from '@/cypress/e2e/po/components/sortable-table.po'; const cluster = 'local'; const clusterDashboard = new ClusterDashboardPagePo(cluster); const events = new EventsPagePo(cluster); +const pageSize = 10; +// Should be enough to create at least 3 pages of events +const podCount = 15; + +const countHelper = { + setupCount: (vaiCacheEnabled: boolean, initialCount: number) => { + if (vaiCacheEnabled) { + cy.intercept('GET', '/v1/events?*').as('getCount'); + } else { + cy.wrap(initialCount).as('count'); + } + }, + handleCount: (vaiCacheEnabled) => { + if (vaiCacheEnabled) { + cy.wait('@getCount').then((interception) => { + cy.wrap(interception.response.body.count).as('count'); + }); + } + }, + getCount: () => cy.get('@count').then((count) => count as any as number), +}; describe('Events', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] }, () => { before(() => { @@ -20,7 +41,12 @@ describe('Events', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] }, let nsName2: string; before('set up', () => { - cy.updateNamespaceFilter(cluster, 'none', '{\"local\":[]}'); + cy.tableRowsPerPageAndPreferences(pageSize, { + clusterName: cluster, + groupBy: 'none', + namespaceFilter: '{\"local\":[]}', + allNamespaces: 'true', + }); cy.createE2EResourceName('ns1').then((ns1) => { nsName1 = ns1; @@ -30,7 +56,7 @@ describe('Events', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] }, // create pods let i = 0; - while (i < 125) { + while (i < podCount) { const podName = Cypress._.uniqueId(Date.now().toString()); cy.createPod(nsName1, podName, 'nginx:latest', false, { createNameOptions: { prefixContext: true } }).then((resp) => { @@ -52,6 +78,9 @@ describe('Events', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] }, uniquePod = resp.body.metadata.name; }); }); + + // I'm loathed to do this, but the events created from the pods need to settle before we start + cy.wait(20000); // eslint-disable-line cypress/no-unnecessary-waiting }); it('pagination is visible and user is able to navigate through events data', () => { @@ -61,73 +90,113 @@ describe('Events', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] }, EventsPagePo.navTo(); events.waitForPage(); - cy.getRancherResource('v1', 'events').then((resp: Cypress.Response) => { - // Why 500? there's a hardcoded figure to stops ui from storing more than 500 events ... - const count = resp.body.count < 500 ? resp.body.count : 500; + let vaiCacheEnabled = false; - // Test break down if less than 400... - expect(count).to.be.greaterThan(400); + cy.isVaiCacheEnabled() + .then((isVaiCacheEnabled) => { + vaiCacheEnabled = isVaiCacheEnabled; - // pagination is visible - events.sortableTable().pagination().checkVisible(); + return cy.getRancherResource('v1', 'events'); + }) + .then((resp: Cypress.Response) => { + let initialCount = resp.body.count; - const loadingPo = new LoadingPo('.title .resource-loading-indicator'); + if (!vaiCacheEnabled && resp.body.count > 500) { + // Why 500? there's a hardcoded figure to stops ui from storing more than 500 events ... + initialCount = 500; + } - loadingPo.checkNotExists(); + // Test break down if less than 3 pages... + expect(initialCount).to.be.greaterThan(3 * pageSize); - // basic checks on navigation buttons - events.sortableTable().pagination().beginningButton().isDisabled(); - events.sortableTable().pagination().leftButton().isDisabled(); - events.sortableTable().pagination().rightButton().isEnabled(); - events.sortableTable().pagination().endButton().isEnabled(); + // pagination is visible + events.sortableTable().pagination().checkVisible(); - // check text before navigation - events.sortableTable().pagination().paginationText().then((el) => { - expect(el.trim()).to.eq(`1 - 100 of ${ count } Events`); + const loadingPo = new LoadingPo('.title .resource-loading-indicator'); + + loadingPo.checkNotExists(); + + // basic checks on navigation buttons + events.sortableTable().pagination().beginningButton().isDisabled(); + events.sortableTable().pagination().leftButton().isDisabled(); + events.sortableTable().pagination().rightButton().isEnabled(); + events.sortableTable().pagination().endButton().isEnabled(); + + // check text before navigation + events.sortableTable().pagination().self().scrollIntoView(); + events.sortableTable().pagination().paginationText().then((el) => { + expect(el.trim()).to.eq(`1 - ${ pageSize } of ${ initialCount } Events`); + }); + + // navigate to next page - right button + countHelper.setupCount(vaiCacheEnabled, initialCount); + events.sortableTable().pagination().rightButton().click(); + countHelper.handleCount(vaiCacheEnabled); + + // check text and buttons after navigation + events.sortableTable().pagination().self().scrollIntoView(); + countHelper.getCount().then((count) => { + return events.sortableTable().pagination().paginationText().then((el) => { + expect(el.trim()).to.eq(`${ pageSize + 1 } - ${ 2 * pageSize } of ${ count } Events`); + }); + }); + events.sortableTable().pagination().beginningButton().isEnabled(); + events.sortableTable().pagination().leftButton().isEnabled(); + + // navigate to first page - left button + countHelper.setupCount(vaiCacheEnabled, initialCount); + events.sortableTable().pagination().leftButton().click(); + countHelper.handleCount(vaiCacheEnabled); + + // check text and buttons after navigation + events.sortableTable().pagination().self().scrollIntoView(); + countHelper.getCount().then((count) => { + return events.sortableTable().pagination().paginationText().then((el) => { + expect(el.trim()).to.eq(`1 - ${ pageSize } of ${ count } Events`); + }); + }); + + events.sortableTable().pagination().beginningButton().isDisabled(); + events.sortableTable().pagination().leftButton().isDisabled(); + + // navigate to last page - end button + countHelper.setupCount(vaiCacheEnabled, initialCount); + events.sortableTable().pagination().endButton().scrollIntoView() + .click(); + countHelper.handleCount(vaiCacheEnabled); + + // check text after navigation + events.sortableTable().pagination().self().scrollIntoView(); + countHelper.getCount().then((count) => { + return events.sortableTable().pagination().paginationText().then((el) => { + let pages = Math.floor(count / pageSize); + + if (count % pageSize === 0) { + pages--; + } + const from = (pages * pageSize) + 1; + const to = count; + + expect(el.trim()).to.eq(`${ from } - ${ to } of ${ to } Events`); + }); + }); + + // navigate to first page - beginning button + countHelper.setupCount(vaiCacheEnabled, initialCount); + events.sortableTable().pagination().beginningButton().click(); + countHelper.handleCount(vaiCacheEnabled); + + // check text and buttons after navigation + events.sortableTable().pagination().self().scrollIntoView(); + countHelper.getCount().then((count) => { + events.sortableTable().pagination().paginationText().then((el) => { + expect(el.trim()).to.eq(`1 - ${ pageSize } of ${ count } Events`); + }); + }); + + events.sortableTable().pagination().beginningButton().isDisabled(); + events.sortableTable().pagination().leftButton().isDisabled(); }); - - // navigate to next page - right button - events.sortableTable().pagination().rightButton().click(); - - // check text and buttons after navigation - events.sortableTable().pagination().paginationText().then((el) => { - expect(el.trim()).to.eq(`101 - 200 of ${ count } Events`); - }); - events.sortableTable().pagination().beginningButton().isEnabled(); - events.sortableTable().pagination().leftButton().isEnabled(); - - // navigate to first page - left button - events.sortableTable().pagination().leftButton().click(); - - // check text and buttons after navigation - events.sortableTable().pagination().paginationText().then((el) => { - expect(el.trim()).to.eq(`1 - 100 of ${ count } Events`); - }); - events.sortableTable().pagination().beginningButton().isDisabled(); - events.sortableTable().pagination().leftButton().isDisabled(); - - // navigate to last page - end button - events.sortableTable().pagination().endButton().scrollIntoView() - .click(); - - // check row count on last page - events.sortableTable().checkRowCount(false, 100); - - // check text after navigation - events.sortableTable().pagination().paginationText().then((el) => { - expect(el.trim()).to.eq(`401 - ${ count } of ${ count } Events`); - }); - - // navigate to first page - beginning button - events.sortableTable().pagination().beginningButton().click(); - - // check text and buttons after navigation - events.sortableTable().pagination().paginationText().then((el) => { - expect(el.trim()).to.eq(`1 - 100 of ${ count } Events`); - }); - events.sortableTable().pagination().beginningButton().isDisabled(); - events.sortableTable().pagination().leftButton().isDisabled(); - }); }); it('filter events', () => { @@ -138,7 +207,7 @@ describe('Events', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] }, events.sortableTable().checkVisible(); events.sortableTable().checkLoadingIndicatorNotVisible(); - events.sortableTable().checkRowCount(false, 100); + events.sortableTable().checkRowCount(false, pageSize); // filter by namespace events.sortableTable().filter(nsName2); @@ -201,7 +270,12 @@ describe('Events', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] }, }); after('clean up', () => { - cy.updateNamespaceFilter(cluster, 'none', '{"local":["all://user"]}'); + cy.tableRowsPerPageAndPreferences(100, { + clusterName: cluster, + groupBy: 'none', + namespaceFilter: '{"local":["all://user"]}', + allNamespaces: 'false', + }); // delete namespace (this will also delete all pods in it) cy.deleteRancherResource('v1', 'namespaces', nsName1); diff --git a/cypress/e2e/tests/pages/explorer/service-discovery/horizontal-pod-autoscalers.spec.ts b/cypress/e2e/tests/pages/explorer/service-discovery/horizontal-pod-autoscalers.spec.ts index 2fd6984c29..d7ba6706b1 100644 --- a/cypress/e2e/tests/pages/explorer/service-discovery/horizontal-pod-autoscalers.spec.ts +++ b/cypress/e2e/tests/pages/explorer/service-discovery/horizontal-pod-autoscalers.spec.ts @@ -19,8 +19,11 @@ describe('HorizontalPodAutoscalers', { testIsolation: 'off', tags: ['@explorer', horizontalPodAutoscalersPage.waitForPage(); cy.wait('@horizontalpodautoscalerNoData'); - const expectedHeaders = ['State', 'Name', 'Workload', 'Minimum Replicas', 'Maximum Replicas', 'Current Replicas', 'Age']; + const expectedHeaders = ['State', 'Name', 'Namespace', 'Workload', 'Minimum Replicas', 'Maximum Replicas', 'Current Replicas', 'Age']; + horizontalPodAutoscalersPage.list().resourceTable().sortableTable().tableHeaderRow() + .self() + .scrollIntoView(); horizontalPodAutoscalersPage.list().resourceTable().sortableTable().tableHeaderRow() .get('.table-header-container .content') .each((el, i) => { @@ -39,7 +42,7 @@ describe('HorizontalPodAutoscalers', { testIsolation: 'off', tags: ['@explorer', horizontalPodAutoscalersPage.header().selectNamespaceFilterOption('All Namespaces'); // check table headers are visible - const expectedHeaders = ['State', 'Name', 'Workload', 'Minimum Replicas', 'Maximum Replicas', 'Current Replicas', 'Age']; + const expectedHeaders = ['State', 'Name', 'Namespace', 'Workload', 'Minimum Replicas', 'Maximum Replicas', 'Current Replicas', 'Age']; horizontalPodAutoscalersPage.list().resourceTable().sortableTable().tableHeaderRow() .get('.table-header-container .content') @@ -65,7 +68,7 @@ describe('HorizontalPodAutoscalers', { testIsolation: 'off', tags: ['@explorer', horizontalPodAutoscalersPage.list().resourceTable().sortableTable().groupByButtons(1) .click(); - // check table headers are visible + // check table headers are visible (minus namespace given we're now grouped by it) const expectedHeaders = ['State', 'Name', 'Workload', 'Minimum Replicas', 'Maximum Replicas', 'Current Replicas', 'Age']; horizontalPodAutoscalersPage.list().resourceTable().sortableTable().tableHeaderRow() diff --git a/cypress/e2e/tests/pages/extensions/extensions.spec.ts b/cypress/e2e/tests/pages/extensions/extensions.spec.ts index 6b8c4e01a2..f208c05fc6 100644 --- a/cypress/e2e/tests/pages/extensions/extensions.spec.ts +++ b/cypress/e2e/tests/pages/extensions/extensions.spec.ts @@ -8,6 +8,7 @@ import { NamespaceFilterPo } from '@/cypress/e2e/po/components/namespace-filter. const namespaceFilter = new NamespaceFilterPo(); const cluster = 'local'; +let removeExtensions = false; const DISABLED_CACHE_EXTENSION_NAME = 'large-extension'; // const DISABLED_CACHE_EXTENSION_MENU_LABEL = 'Large-extension'; @@ -16,6 +17,7 @@ const UNAUTHENTICATED_EXTENSION_NAME = 'uk-locale'; const EXTENSION_NAME = 'clock'; const UI_PLUGINS_PARTNERS_REPO_URL = 'https://github.com/rancher/partner-extensions'; const UI_PLUGINS_PARTNERS_REPO_NAME = 'partner-extensions'; +const GIT_REPO_NAME = 'rancher-plugin-examples'; describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => { beforeEach(() => { @@ -69,7 +71,9 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => { extensionsPo.extensionTabInstalledClick(); // Avoid nav guard failures that probably auto move user to this tab // install the rancher plugin examples - extensionsPo.addExtensionsRepository('https://github.com/rancher/ui-plugin-examples', 'main', 'rancher-plugin-examples'); + extensionsPo.addExtensionsRepository('https://github.com/rancher/ui-plugin-examples', 'main', GIT_REPO_NAME).then(() => { + removeExtensions = true; + }); }); it('has the correct title for Prime users and should display banner on main extensions screen EVEN IF setting is empty string', () => { @@ -253,9 +257,11 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => { }); it('Should install an extension', () => { + cy.intercept('POST', `/v1/catalog.cattle.io.clusterrepos/${ GIT_REPO_NAME }?action=install`).as('installExtension'); const extensionsPo = new ExtensionsPagePo(); extensionsPo.goTo(); + extensionsPo.waitForPage(); extensionsPo.extensionTabAvailableClick(); extensionsPo.waitForPage(null, 'available'); @@ -267,6 +273,7 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => { // select version and click install extensionsPo.installModalSelectVersionClick(2); extensionsPo.installModalInstallClick(); + cy.wait('@installExtension').its('response.statusCode').should('eq', 201); // let's check the extension reload banner and reload the page extensionsPo.extensionReloadBanner().should('be.visible'); @@ -297,6 +304,7 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => { }); it('Should update an extension version', () => { + cy.intercept('POST', `/v1/catalog.cattle.io.clusterrepos/${ GIT_REPO_NAME }?action=upgrade`).as('upgradeExtension'); const extensionsPo = new ExtensionsPagePo(); extensionsPo.goTo(); @@ -308,6 +316,7 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => { // click on update button on card extensionsPo.extensionCardUpdateClick(EXTENSION_NAME); extensionsPo.installModalInstallClick(); + cy.wait('@upgradeExtension').its('response.statusCode').should('eq', 201); // let's check the extension reload banner and reload the page extensionsPo.extensionReloadBanner().should('be.visible'); @@ -515,4 +524,10 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => { extensionsPo.extensionCardClick(DISABLED_CACHE_EXTENSION_NAME); extensionsPo.extensionDetailsTitle().should('contain', DISABLED_CACHE_EXTENSION_NAME); }); + + after(() => { + if ( removeExtensions ) { + cy.deleteRancherResource('v1', 'catalog.cattle.io.clusterrepos', GIT_REPO_NAME); + } + }); }); diff --git a/cypress/e2e/tests/pages/extensions/kubewarden.spec.ts b/cypress/e2e/tests/pages/extensions/kubewarden.spec.ts index 73eb234ce6..a39d9b7a14 100644 --- a/cypress/e2e/tests/pages/extensions/kubewarden.spec.ts +++ b/cypress/e2e/tests/pages/extensions/kubewarden.spec.ts @@ -6,8 +6,10 @@ import KubewardenExtensionPo from '@/cypress/e2e/po/pages/extensions/kubewarden. import { catchTargetPageException } from '@/cypress/support/utils/exception-utils'; const extensionName = 'kubewarden'; +const gitRepoName = 'rancher-extensions'; +let removeExtensions = false; -describe('Kubewarden Extension', { tags: ['@extensions-temp-excluded', '@adminUser'] }, () => { +describe('Kubewarden Extension', { tags: ['@extensions', '@adminUser'] }, () => { before(() => { catchTargetPageException('Navigation cancelled'); cy.login(); @@ -18,7 +20,9 @@ describe('Kubewarden Extension', { tags: ['@extensions-temp-excluded', '@adminUs extensionsPo.waitForPage(); // install the ui-plugin-charts repo - extensionsPo.addExtensionsRepository('https://github.com/rancher/ui-plugin-charts', 'main', 'rancher-extensions'); + extensionsPo.addExtensionsRepository('https://github.com/rancher/ui-plugin-charts', 'main', gitRepoName).then(() => { + removeExtensions = true; + }); }); beforeEach(() => { @@ -32,6 +36,7 @@ describe('Kubewarden Extension', { tags: ['@extensions-temp-excluded', '@adminUs extensionsPo.waitForPage(); extensionsPo.extensionTabAvailableClick(); + extensionsPo.waitForPage(null, 'available'); // click on install button on card extensionsPo.extensionCardInstallClick(extensionName); @@ -110,4 +115,10 @@ describe('Kubewarden Extension', { tags: ['@extensions-temp-excluded', '@adminUs extensionsPo.extensionCardClick(extensionName); extensionsPo.extensionDetailsTitle().should('contain', extensionName); }); + + after(() => { + if ( removeExtensions ) { + cy.deleteRancherResource('v1', 'catalog.cattle.io.clusterrepos', gitRepoName); + } + }); }); diff --git a/cypress/e2e/tests/pages/fleet/cluster-groups.spec.ts b/cypress/e2e/tests/pages/fleet/cluster-groups.spec.ts index a6dcd9f381..6f185ee44c 100644 --- a/cypress/e2e/tests/pages/fleet/cluster-groups.spec.ts +++ b/cypress/e2e/tests/pages/fleet/cluster-groups.spec.ts @@ -1,63 +1,147 @@ import { FleetClusterGroupsListPagePo } from '@/cypress/e2e/po/pages/fleet/fleet.cattle.io.clustergroup.po'; import FleetClusterGroupDetailsPo from '@/cypress/e2e/po/detail/fleet/fleet.cattle.io.clustergroup.po'; import { HeaderPo } from '@/cypress/e2e/po/components/header.po'; +import PromptRemove from '@/cypress/e2e/po/prompts/promptRemove.po'; describe('Cluster Groups', { testIsolation: 'off', tags: ['@fleet', '@adminUser'] }, () => { const fleetClusterGroups = new FleetClusterGroupsListPagePo(); const headerPo = new HeaderPo(); + const localWorkspace = 'fleet-local'; + let clusterGroupName; + let removeClusterGroups = false; + const clusterGroupsToDelete = []; - describe('List', { tags: ['@vai', '@adminUser'] }, () => { - before(() => { - cy.login(); - }); - - it('check table headers are available in list and details view', () => { - const groupName = 'default'; - const workspace = 'fleet-local'; - - FleetClusterGroupsListPagePo.navTo(); - fleetClusterGroups.waitForPage(); - headerPo.selectWorkspace(workspace); - fleetClusterGroups.clusterGroupsList().rowWithName(groupName).checkVisible(); - - // check table headers - const expectedHeaders = ['State', 'Name', 'Clusters Ready', 'Resources', 'Age']; - - fleetClusterGroups.clusterGroupsList().resourceTable().sortableTable().tableHeaderRow() - .within('.table-header-container .content') - .each((el, i) => { - expect(el.text().trim()).to.eq(expectedHeaders[i]); - }); - - // go to fleet cluster details - fleetClusterGroups.goToDetailsPage(groupName); - - const fleetClusterGroupDetailsPage = new FleetClusterGroupDetailsPo(workspace, groupName); - - fleetClusterGroupDetailsPage.waitForPage(null, 'clusters'); - - // check table headers - const expectedHeadersDetailsView = ['State', 'Name', 'Bundles Ready', 'Repos Ready', 'Resources', 'Last Seen', 'Age']; - - fleetClusterGroupDetailsPage.clusterList().resourceTable().sortableTable() - .tableHeaderRow() - .within('.table-header-container .content') - .each((el, i) => { - expect(el.text().trim()).to.eq(expectedHeadersDetailsView[i]); - }); + before(() => { + cy.login(); + cy.createE2EResourceName('cluster-group').then((name) => { + clusterGroupName = name; }); }); - describe('Edit', { tags: ['@vai', '@adminUser'] }, () => { - before(() => { - cy.login(); - }); - it('can open "Edit as YAML"', () => { - FleetClusterGroupsListPagePo.navTo(); - fleetClusterGroups.waitForPage(); - fleetClusterGroups.clickCreate(); - fleetClusterGroups.createFleetClusterGroupsForm().editAsYaml().click(); - fleetClusterGroups.createFleetClusterGroupsForm().yamlEditor().checkExists(); - }); + it('can create cluster group', () => { + FleetClusterGroupsListPagePo.navTo(); + fleetClusterGroups.waitForPage(); + headerPo.selectWorkspace(localWorkspace); + fleetClusterGroups.clickCreate(); + fleetClusterGroups.createFleetClusterGroupsForm().waitForPage(); + + fleetClusterGroups.createFleetClusterGroupsForm().nameNsDescription().name().set(clusterGroupName); + fleetClusterGroups.createFleetClusterGroupsForm().saveCreateForm().cruResource().saveOrCreate() + .click() + .then(() => { + removeClusterGroups = true; + clusterGroupsToDelete.push(`${ localWorkspace }/${ clusterGroupName }`); + }); + + fleetClusterGroups.waitForPage(); + fleetClusterGroups.clusterGroupsList().details(clusterGroupName, 1).should('be.visible'); + }); + + it('can edit a cluster group', () => { + FleetClusterGroupsListPagePo.navTo(); + fleetClusterGroups.waitForPage(); + headerPo.selectWorkspace(localWorkspace); + fleetClusterGroups.clusterGroupsList().actionMenu(clusterGroupName).getMenuItem('Edit Config').click(); + fleetClusterGroups.createFleetClusterGroupsForm(localWorkspace, clusterGroupName).waitForPage('mode=edit'); + fleetClusterGroups.createFleetClusterGroupsForm().nameNsDescription().description().set(`${ clusterGroupName }-fleet-desc`); + fleetClusterGroups.createFleetClusterGroupsForm().saveCreateForm().cruResource().saveAndWaitForRequests('PUT', `v1/fleet.cattle.io.clustergroups/${ localWorkspace }/${ clusterGroupName }`) + .then(({ response }) => { + expect(response?.statusCode).to.eq(200); + expect(response?.body.metadata).to.have.property('name', clusterGroupName); + expect(response?.body.metadata.annotations).to.have.property('field.cattle.io/description', `${ clusterGroupName }-fleet-desc`); + }); + fleetClusterGroups.waitForPage(); + }); + + it('can clone a cluster group', () => { + FleetClusterGroupsListPagePo.navTo(); + fleetClusterGroups.waitForPage(); + headerPo.selectWorkspace(localWorkspace); + fleetClusterGroups.clusterGroupsList().actionMenu(clusterGroupName).getMenuItem('Clone').click(); + fleetClusterGroups.createFleetClusterGroupsForm(localWorkspace, clusterGroupName).waitForPage('mode=clone'); + fleetClusterGroups.createFleetClusterGroupsForm().nameNsDescription().name().set(`clone-${ clusterGroupName }`); + fleetClusterGroups.createFleetClusterGroupsForm().nameNsDescription().description().set(`${ clusterGroupName }-fleet-desc`); + fleetClusterGroups.createFleetClusterGroupsForm().saveCreateForm().cruResource().saveAndWaitForRequests('POST', 'v1/fleet.cattle.io.clustergroups') + .then(({ response }) => { + expect(response?.statusCode).to.eq(201); + removeClusterGroups = true; + clusterGroupsToDelete.push(`${ localWorkspace }/clone-${ clusterGroupName }`); + expect(response?.body.metadata).to.have.property('name', `clone-${ clusterGroupName }`); + expect(response?.body.metadata.annotations).to.have.property('field.cattle.io/description', `${ clusterGroupName }-fleet-desc`); + }); + fleetClusterGroups.waitForPage(); + fleetClusterGroups.clusterGroupsList().details(`clone-${ clusterGroupName }`, 1).should('be.visible'); + }); + + it('can delete cluster group', () => { + FleetClusterGroupsListPagePo.navTo(); + fleetClusterGroups.waitForPage(); + headerPo.selectWorkspace(localWorkspace); + fleetClusterGroups.clusterGroupsList().actionMenu(clusterGroupName).getMenuItem('Delete').click(); + fleetClusterGroups.clusterGroupsList().resourceTable().sortableTable().rowNames('.col-link-detail') + .then((rows: any) => { + const promptRemove = new PromptRemove(); + + cy.intercept('DELETE', `v1/fleet.cattle.io.clustergroups/${ localWorkspace }/clone-${ clusterGroupName }`).as('deleteClusterGroup'); + + promptRemove.remove(); + cy.wait('@deleteClusterGroup'); + fleetClusterGroups.waitForPage(); + fleetClusterGroups.clusterGroupsList().resourceTable().sortableTable().checkRowCount(false, rows.length - 1); + fleetClusterGroups.clusterGroupsList().resourceTable().sortableTable().rowNames('.col-link-detail') + .should('not.contain', `clone-${ clusterGroupName }`); + }); + }); + + // testing https://github.com/rancher/dashboard/issues/11687 + it('can open "Edit as YAML"', () => { + FleetClusterGroupsListPagePo.navTo(); + fleetClusterGroups.waitForPage(); + fleetClusterGroups.clickCreate(); + fleetClusterGroups.createFleetClusterGroupsForm().saveCreateForm().createEditView().editAsYaml(); + fleetClusterGroups.createFleetClusterGroupsForm().saveCreateForm().resourceYaml().codeMirror() + .checkExists(); + }); + + it('check table headers are available in list and details view', { tags: ['@vai', '@adminUser'] }, () => { + const groupName = 'default'; + + FleetClusterGroupsListPagePo.navTo(); + fleetClusterGroups.waitForPage(); + headerPo.selectWorkspace(localWorkspace); + fleetClusterGroups.clusterGroupsList().rowWithName(groupName).checkVisible(); + + // check table headers + const expectedHeaders = ['State', 'Name', 'Clusters Ready', 'Resources', 'Age']; + + fleetClusterGroups.clusterGroupsList().resourceTable().sortableTable().tableHeaderRow() + .within('.table-header-container .content') + .each((el, i) => { + expect(el.text().trim()).to.eq(expectedHeaders[i]); + }); + + // go to fleet cluster details + fleetClusterGroups.goToDetailsPage(groupName); + + const fleetClusterGroupDetailsPage = new FleetClusterGroupDetailsPo(localWorkspace, groupName); + + fleetClusterGroupDetailsPage.waitForPage(null, 'clusters'); + + // check table headers + const expectedHeadersDetailsView = ['State', 'Name', 'Bundles Ready', 'Repos Ready', 'Resources', 'Last Seen', 'Age']; + + fleetClusterGroupDetailsPage.clusterList().resourceTable().sortableTable() + .tableHeaderRow() + .within('.table-header-container .content') + .each((el, i) => { + expect(el.text().trim()).to.eq(expectedHeadersDetailsView[i]); + }); + }); + + after(() => { + if (removeClusterGroups) { + // delete gitrepo + clusterGroupsToDelete.forEach((r) => cy.deleteRancherResource('v1', 'fleet.cattle.io.clustergroups', r, false)); + } }); }); diff --git a/cypress/e2e/tests/pages/fleet/dashboard.spec.ts b/cypress/e2e/tests/pages/fleet/dashboard.spec.ts index 8e8401a176..5cd98f6040 100644 --- a/cypress/e2e/tests/pages/fleet/dashboard.spec.ts +++ b/cypress/e2e/tests/pages/fleet/dashboard.spec.ts @@ -1,65 +1,216 @@ import { FleetDashboardPagePo } from '@/cypress/e2e/po/pages/fleet/fleet-dashboard.po'; -// import { GitRepoCreatePo } from '@/cypress/e2e/po/pages/fleet/gitrepo-create.po'; -// import BurgerMenuPo from '@/cypress/e2e/po/side-bars/burger-side-menu.po'; -// import { LONG_TIMEOUT_OPT } from '@/cypress/support/utils/timeouts'; +import FleetGitRepoDetailsPo from '@/cypress/e2e/po/detail/fleet/fleet.cattle.io.gitrepo.po'; +import { GitRepoCreatePo } from '@/cypress/e2e/po/pages/fleet/gitrepo-create.po'; +import { GitRepoEditPo } from '@/cypress/e2e/po/edit/fleet/gitrepo-edit.po'; +import BurgerMenuPo from '@/cypress/e2e/po/side-bars/burger-side-menu.po'; +import { LONG_TIMEOUT_OPT, MEDIUM_TIMEOUT_OPT } from '@/cypress/support/utils/timeouts'; +import { gitRepoTargetAllClustersRequest } from '@/cypress/e2e/blueprints/fleet/gitrepos'; +import { HeaderPo } from '@/cypress/e2e/po/components/header.po'; +import { MenuActions } from '@/cypress/support/types/menu-actions'; +import * as path from 'path'; +import * as jsyaml from 'js-yaml'; +import { FleetGitRepoListPagePo } from '@/cypress/e2e/po/pages/fleet/fleet.cattle.io.gitrepo.po'; +const downloadsFolder = Cypress.config('downloadsFolder'); -describe('Fleet Dashboard', { tags: ['@fleet', '@adminUser'] }, () => { - let fleetDashboardPage: FleetDashboardPagePo; - // const repoName = 'fleet-e2e-test-dashboard'; +describe('Fleet Dashboard', { tags: ['@fleet', '@adminUser', '@jenkins'] }, () => { + const fleetDashboardPage = new FleetDashboardPagePo('_'); + const gitRepoCreatePage = new GitRepoCreatePo('_'); + const headerPo = new HeaderPo(); - // Note - The 'describe` previously had `.only`, which ironically meant this was not tested in our CI (probably something to so with grep tags) - // Enabling the test results results in consistent failures (bundle does not become ready). For the short term comment these out + let repoName; + const gitRepoUrl = 'https://github.com/rancher/fleet-test-data'; + const branch = 'master'; + const paths = 'qa-test-apps/nginx-app'; + const localWorkspace = 'fleet-local'; + let removeGitRepo = false; + const reposToDelete = []; beforeEach(() => { cy.login(); - fleetDashboardPage = new FleetDashboardPagePo('_'); - fleetDashboardPage.goTo(); + cy.createE2EResourceName('git-repo').then((name) => { + repoName = name; + }); }); it('has the correct title', () => { - cy.get('.fleet-empty-dashboard').should('be.visible'); + fleetDashboardPage.goTo(); + fleetDashboardPage.waitForPage(); + + fleetDashboardPage.fleetDashboardEmptyState().should('be.visible'); cy.title().should('eq', 'Rancher - Continuous Delivery - Dashboard'); }); - // before(() => { - // cy.login(); + it('Get Started button takes you to the correct page', () => { + fleetDashboardPage.goTo(); + fleetDashboardPage.waitForPage(); - // const gitRepoCreatePage = new GitRepoCreatePo('_'); + fleetDashboardPage.fleetDashboardEmptyState().should('be.visible'); + fleetDashboardPage.getStartedButton().click(); + gitRepoCreatePage.waitForPage(); + gitRepoCreatePage.title().contains('Git Repo: Create').should('be.visible'); + }); - // gitRepoCreatePage.goTo(); + it('Should display cluster status', () => { + // create gitrepo + cy.createRancherResource('v1', 'fleet.cattle.io.gitrepos', gitRepoTargetAllClustersRequest(localWorkspace, repoName, gitRepoUrl, branch, paths)).then(() => { + removeGitRepo = true; + reposToDelete.push(`fleet-local/${ repoName }`); + }); - // gitRepoCreatePage.setRepoName(repoName); - // gitRepoCreatePage.selectWorkspace('fleet-local'); - // gitRepoCreatePage.setGitRepoUrl('https://github.com/rancher/fleet-test-data.git'); - // gitRepoCreatePage.setBranchName(); - // // NB - This step is here because DOM may not be ready - // gitRepoCreatePage.goToNext(); - // gitRepoCreatePage.create(); - // }); + fleetDashboardPage.goTo(); + fleetDashboardPage.waitForPage(); - // it('Should display cluster status', () => { - // // check if burguer menu nav is highlighted correctly for Fleet - // BurgerMenuPo.checkIfMenuItemLinkIsHighlighted('Continuous Delivery'); + // check if burguer menu nav is highlighted correctly for Fleet + BurgerMenuPo.checkIfMenuItemLinkIsHighlighted('Continuous Delivery'); - // const row = fleetDashboardPage.sortableTable('fleet-local').row(0); + const row = fleetDashboardPage.sortableTable(localWorkspace).row(0); - // row.get('.bg-success[data-testid="clusters-ready"]', LONG_TIMEOUT_OPT).should('exist'); - // row.get('.bg-success[data-testid="clusters-ready"] span').should('have.text', '1/1'); + row.get('.bg-success[data-testid="clusters-ready"]', LONG_TIMEOUT_OPT).should('exist'); + row.get('.bg-success[data-testid="clusters-ready"] span').should('have.text', '1/1'); - // row.get('.bg-success[data-testid="bundles-ready"]').should('exist'); - // row.get('.bg-success[data-testid="bundles-ready"] span').should('have.text', '1/1'); + row.get('.bg-success[data-testid="bundles-ready"]').should('exist'); + row.get('.bg-success[data-testid="bundles-ready"] span').should('have.text', '1/1'); - // row.get('.bg-success[data-testid="resources-ready"]').should('exist'); - // row.get('.bg-success[data-testid="resources-ready"] span').should('have.text', '1/1'); - // }); + row.get('.bg-success[data-testid="resources-ready"]').should('exist'); + row.get('.bg-success[data-testid="resources-ready"] span').should('have.text', '1/1'); + }); - // after(() => { - // fleetDashboardPage = new FleetDashboardPagePo('_'); - // fleetDashboardPage.goTo(); + it('can navigate to Git Repo details page from Fleet Dashboard', () => { + const gitRepoDetails = new FleetGitRepoDetailsPo(localWorkspace, repoName); - // const fleetLocalResourceTable = fleetDashboardPage.resourceTable('fleet-local'); + fleetDashboardPage.goTo(); + fleetDashboardPage.waitForPage(); + fleetDashboardPage.list().rowWithName(repoName).column(0).find('a') + .click(); + gitRepoDetails.waitForPage(null, 'bundles'); + }); - // fleetLocalResourceTable.sortableTable().deleteItemWithUI(repoName); - // }); + it('should only display action menu with allowed actions only', () => { + fleetDashboardPage.goTo(); + fleetDashboardPage.waitForPage(); + headerPo.selectWorkspace(localWorkspace); + + const constActionMenu = fleetDashboardPage.sortableTable().rowActionMenuOpen(repoName); + + const allowedActions: MenuActions[] = [ + MenuActions.Pause, + MenuActions.ForceUpdate, + MenuActions.EditYaml, + MenuActions.EditConfig, + MenuActions.Clone, + MenuActions.DownloadYaml, + MenuActions.Delete + ]; + + const disabledActions: MenuActions[] = [MenuActions.ChangeWorkspace]; + + allowedActions.forEach((action) => { + constActionMenu.getMenuItem(action).should('exist'); + }); + + // Disabled actions should not exist + disabledActions.forEach((action) => { + constActionMenu.getMenuItem(action).should('not.exist'); + }); + }); + + it('can clone a git repo', () => { + const gitRepoEditPage = new GitRepoEditPo(localWorkspace, repoName); + + cy.intercept('GET', '/v1/secrets?exclude=metadata.managedFields').as('getSecrets'); + + fleetDashboardPage.goTo(); + fleetDashboardPage.waitForPage(); + fleetDashboardPage.sortableTable().rowActionMenuOpen(repoName).getMenuItem('Clone').click(); + + gitRepoEditPage.waitForPage('mode=clone'); + cy.wait('@getSecrets', MEDIUM_TIMEOUT_OPT).its('response.statusCode').should('eq', 200); + gitRepoEditPage.title().contains(`Git Repo: Clone from ${ repoName }`).should('be.visible'); + headerPo.selectWorkspace('fleet-default'); + gitRepoEditPage.nameNsDescription().name().set(`clone-${ repoName }`); + gitRepoEditPage.footer().nextPage(); + gitRepoEditPage.footer().create().then(() => { + removeGitRepo = true; + reposToDelete.push(`fleet-default/clone-${ repoName }`); + }); + + FleetDashboardPagePo.navTo(); + fleetDashboardPage.waitForPage(); + fleetDashboardPage.sortableTable('fleet-default').rowElementWithName(`clone-${ repoName }`).should('be.visible'); + fleetDashboardPage.sortableTable('fleet-local').rowElementWithName(repoName).should('be.visible'); + }); + + it('user lands in correct git repo workspace when using workspace link on Fleet Dashboard', () => { + const gitrepoListPage = new FleetGitRepoListPagePo(); + + fleetDashboardPage.goTo(); + fleetDashboardPage.waitForPage(); + fleetDashboardPage.sortableTable('fleet-default').rowElementWithName(`clone-${ repoName }`).should('be.visible'); + fleetDashboardPage.sortableTable('fleet-local').rowElementWithName(repoName).should('be.visible'); + + // click workspace link: fleet default + fleetDashboardPage.goToGitRepoListLink('fleet-default').click(); + gitrepoListPage.waitForPage(); + headerPo.checkCurrentWorkspace('fleet-default'); + + // click workspace link: fleet local + fleetDashboardPage.goTo(); + fleetDashboardPage.waitForPage(); + fleetDashboardPage.goToGitRepoListLink('fleet-local').click(); + gitrepoListPage.waitForPage(); + headerPo.checkCurrentWorkspace('fleet-local'); + }); + + it('can Edit Yaml', () => { + const gitRepoEditPage = new GitRepoEditPo(localWorkspace, repoName); + + fleetDashboardPage.goTo(); + fleetDashboardPage.waitForPage(); + fleetDashboardPage.sortableTable().rowActionMenuOpen(repoName).getMenuItem('Edit YAML').click(); + gitRepoEditPage.waitForPage('mode=edit&as=yaml'); + gitRepoEditPage.title().contains(`Git Repo: ${ repoName }`).should('be.visible'); + }); + + it('can Download Yaml', () => { + cy.deleteDownloadsFolder(); + + fleetDashboardPage.goTo(); + fleetDashboardPage.waitForPage(); + fleetDashboardPage.sortableTable().rowActionMenuOpen(repoName).getMenuItem('Download YAML').click(); + + const downloadedFilename = path.join(downloadsFolder, `${ repoName }.yaml`); + + cy.readFile(downloadedFilename).then((buffer) => { + const obj: any = jsyaml.load(buffer); + + // Basic checks on the downloaded YAML + expect(obj.kind).to.equal('GitRepo'); + expect(obj.metadata['name']).to.equal(repoName); + expect(obj.metadata['namespace']).to.equal(localWorkspace); + expect(obj.spec['repo']).to.equal(gitRepoUrl); + }); + }); + + it('can Edit Config', () => { + const gitRepoEditPage = new GitRepoEditPo(localWorkspace, repoName); + const description = `${ repoName }-desc`; + + fleetDashboardPage.goTo(); + fleetDashboardPage.waitForPage(); + fleetDashboardPage.sortableTable().rowActionMenuOpen(repoName).getMenuItem('Edit Config').click(); + + gitRepoEditPage.waitForPage('mode=edit'); + gitRepoEditPage.nameNsDescription().description().set(description); + gitRepoEditPage.footer().nextPage(); + gitRepoEditPage.footer().save(); + fleetDashboardPage.waitForPage(); + }); + + after(() => { + if (removeGitRepo) { + // delete gitrepo + reposToDelete.forEach((r) => cy.deleteRancherResource('v1', 'fleet.cattle.io.gitrepo', r)); + } + }); }); diff --git a/cypress/e2e/tests/pages/global-settings/feature-flags.spec.ts b/cypress/e2e/tests/pages/global-settings/feature-flags.spec.ts index 016f1c49e2..6ff0f77d3a 100644 --- a/cypress/e2e/tests/pages/global-settings/feature-flags.spec.ts +++ b/cypress/e2e/tests/pages/global-settings/feature-flags.spec.ts @@ -191,6 +191,43 @@ describe('Feature Flags', { testIsolation: 'off' }, () => { sideNav.groups().contains('Legacy').should('not.exist'); }); + it('error when toggling a feature flag is handled correctly', { tags: ['@globalSettings', '@adminUser'] }, () => { + // Check Current State: should be disabled by default + FeatureFlagsPagePo.navTo(); + featureFlagsPage.list().details('unsupported-storage-drivers', 0).should('include.text', 'Disabled'); + + // Intercept the request to change the feature flag and return an error - 403, permission denied + cy.intercept({ + method: 'PUT', + pathname: '/v1/management.cattle.io.features/unsupported-storage-drivers', + times: 1, + }, { + statusCode: 403, + body: { + type: 'error', + links: {}, + code: 'Forbidden', + message: 'User does not have permission' + } + }).as('updateFeatureFlag'); + + // Activate + featureFlagsPage.list().elementWithName('unsupported-storage-drivers').scrollIntoView().should('be.visible'); + featureFlagsPage.list().clickRowActionMenuItem('unsupported-storage-drivers', 'Activate'); + featureFlagsPage.cardActionButton('Activate').click(); + + cy.wait(`@updateFeatureFlag`).its('response.statusCode').should('eq', 403); + + // Check Updated State: should be active + featureFlagsPage.list().details('unsupported-storage-drivers', 0).should('include.text', 'Disabled'); + + // Check error message is displayed + featureFlagsPage.cardActionError('User does not have permission'); + + // Press cancel + featureFlagsPage.cardActionButton('Cancel').click(); + }); + it('standard user has only read access to Feature Flag page', { tags: ['@globalSettings', '@standardUser'] }, () => { // verify action menus are hidden for standard user @@ -219,7 +256,7 @@ describe('Feature Flags', { testIsolation: 'off' }, () => { it('validate feature flags table header content', () => { FeatureFlagsPagePo.navTo(); // check table headers are visible - const expectedHeaders = ['State', 'Name', 'Description', 'Restart Required']; + const expectedHeaders = ['State', 'Name', 'Description', 'Restart Rancher']; featureFlagsPage.list().resourceTable().sortableTable().tableHeaderRow() .get('.table-header-container .content') diff --git a/cypress/e2e/tests/pages/manager/cluster-manager.spec.ts b/cypress/e2e/tests/pages/manager/cluster-manager.spec.ts index 10dd81531d..1a678bb191 100644 --- a/cypress/e2e/tests/pages/manager/cluster-manager.spec.ts +++ b/cypress/e2e/tests/pages/manager/cluster-manager.spec.ts @@ -39,6 +39,7 @@ const clusterNamePartial = `${ runPrefix }-create`; const rke1CustomName = `${ clusterNamePartial }-rke1-custom`; const rke2CustomName = `${ clusterNamePartial }-rke2-custom`; const importGenericName = `${ clusterNamePartial }-import-generic`; +let reenableAKS = false; const downloadsFolder = Cypress.config('downloadsFolder'); @@ -91,16 +92,35 @@ describe('Cluster Manager', { testIsolation: 'off', tags: ['@manager', '@adminUs }); it('deactivating a kontainer driver should hide its card from the cluster creation page', () => { + cy.intercept('GET', '/v3/kontainerdrivers').as('getKontainerDrivers'); + cy.intercept('POST', 'v3/kontainerDrivers/azurekubernetesservice?action=deactivate').as('deactivateDriver'); + cy.intercept('POST', 'v3/kontainerDrivers/azurekubernetesservice?action=activate').as('activateDriver'); + const driversPage = new KontainerDriversPagePo(); const clusterCreatePage = new ClusterManagerCreatePagePo(); - // deactivate the AKS driver KontainerDriversPagePo.navTo(); driversPage.waitForPage(); + + // assert AKS kontainer driver is in Active state + cy.wait('@getKontainerDrivers').then(({ response }) => { + response.body.data.forEach((item: any) => { + if (item.id === 'azurekubernetesservice') { + const state = item['active']; + + expect(state).to.eq(true); + } + }); + }); + + // deactivate the AKS driver driversPage.list().actionMenu('Azure AKS').getMenuItem('Deactivate').click(); const deactivateDialog = new DeactivateDriverDialogPo(); deactivateDialog.deactivate(); + cy.wait('@deactivateDriver').its('response.statusCode').should('eq', 200).then(() => { + reenableAKS = true; + }); // verify that the AKS card is not shown clusterList.goTo(); @@ -112,6 +132,9 @@ describe('Cluster Manager', { testIsolation: 'off', tags: ['@manager', '@adminUs KontainerDriversPagePo.navTo(); driversPage.waitForPage(); driversPage.list().actionMenu('Azure AKS').getMenuItem('Activate').click(); + cy.wait('@activateDriver').its('response.statusCode').should('eq', 200).then(() => { + reenableAKS = false; + }); // verify that the AKS card is back clusterList.goTo(); @@ -311,6 +334,31 @@ describe('Cluster Manager', { testIsolation: 'off', tags: ['@manager', '@adminUs editCreatedClusterPage.nameNsDescription().description().self().should('have.value', rke2CustomName); }); + it('will disbable saving if an addon config has invalid data', () => { + clusterList.goTo(); + + clusterList.checkIsCurrentPage(); + clusterList.createCluster(); + + createRKE2ClusterPage.waitForPage(); + + createRKE2ClusterPage.rkeToggle().set('RKE2/K3s'); + + createRKE2ClusterPage.selectCustom(0); + + createRKE2ClusterPage.nameNsDescription().name().set('abc'); + + createRKE2ClusterPage.clusterConfigurationTabs().clickTabWithSelector('#rke2-calico'); + + createRKE2ClusterPage.resourceDetail().createEditView().saveButtonPo().expectToBeEnabled(); + + createRKE2ClusterPage.calicoAddonConfig().yamlEditor().input().set('badvalue: -'); + createRKE2ClusterPage.resourceDetail().createEditView().saveButtonPo().expectToBeDisabled(); + + createRKE2ClusterPage.calicoAddonConfig().yamlEditor().input().set('goodvalue: yay'); + createRKE2ClusterPage.resourceDetail().createEditView().saveButtonPo().expectToBeEnabled(); + }); + it('can view cluster YAML editor', () => { clusterList.goTo(); clusterList.list().actionMenu(rke2CustomName).getMenuItem('Edit YAML').click(); @@ -803,4 +851,10 @@ describe('Cluster Manager', { testIsolation: 'off', tags: ['@manager', '@adminUs }); }); }); + + after(() => { + if (reenableAKS) { + cy.createRancherResource('v3', 'kontainerDrivers/azurekubernetesservice?action=activate', {}); + } + }); }); diff --git a/cypress/globals.d.ts b/cypress/globals.d.ts index 8d5b99566d..060c3b6e1e 100644 --- a/cypress/globals.d.ts +++ b/cypress/globals.d.ts @@ -99,7 +99,8 @@ declare global { deleteRancherResource(prefix: 'v3' | 'v1' | 'k8s', resourceType: string, resourceId: string, failOnStatusCode?: boolean): Chainable; deleteNodeTemplate(nodeTemplateId: string, timeout?: number, failOnStatusCode?: boolean) - tableRowsPerPageAndNamespaceFilter(rows: number, cluster: string, groupBy: string, namespacefilter: string, interation?: number) + tableRowsPerPageAndNamespaceFilter(rows: number, clusterName: string, groupBy: string, namespaceFilter: string) + tableRowsPerPageAndPreferences(rows: number, preferences: { clusterName: string, groupBy: string, namespaceFilter: string, allNamespaces: string}, iteration?: number) /** * update namespace filter @@ -162,6 +163,11 @@ declare global { * Fetch the steve `revision` / timestamp of request */ fetchRevision(): Chainable; + + /** + * Check if the vai FF is enabled + */ + isVaiCacheEnabled(): Chainable; } } } diff --git a/cypress/support/commands/rancher-api-commands.ts b/cypress/support/commands/rancher-api-commands.ts index 70a09fdb08..601db0080c 100644 --- a/cypress/support/commands/rancher-api-commands.ts +++ b/cypress/support/commands/rancher-api-commands.ts @@ -497,8 +497,8 @@ Cypress.Commands.add('createRancherResource', (prefix, resourceType, body) => { body }) .then((resp) => { - // Expect 201, Created HTTP status code - expect(resp.status).to.eq(201); + // Expect 200 or 201, Created HTTP status code + expect(resp.status).to.be.oneOf([200, 201]); }); }); @@ -1024,39 +1024,58 @@ Cypress.Commands.add('fetchRevision', () => { }); }); -Cypress.Commands.add('tableRowsPerPageAndNamespaceFilter', (rows: number, clusterName: string, groupBy: string, namespaceFilter: string, iteration = 0) => { +/** + * Check if the vai FF is enabled + */ +Cypress.Commands.add('isVaiCacheEnabled', () => { + return cy.getRancherResource('v1', 'management.cattle.io.features', 'ui-sql-cache', 200) + .then((res) => res.body.spec.value === true || res.body.spec.value === 'true'); +}); + +Cypress.Commands.add('tableRowsPerPageAndPreferences', (rows: number, preferences: { clusterName: string, groupBy: string, namespaceFilter: string, allNamespaces: string}, iteration = 0) => { + const { + clusterName, groupBy, namespaceFilter, allNamespaces + } = preferences; + return cy.getRancherResource('v3', 'users?me=true').then((resp: Cypress.Response) => { const userId = resp.body.data[0].id.trim(); const payload = { id: `${ userId }`, type: 'userpreference', data: { - cluster: clusterName, - 'per-page': `${ rows }`, - 'group-by': groupBy, - 'ns-by-cluster': namespaceFilter + cluster: clusterName, + 'per-page': `${ rows }`, + 'group-by': groupBy, + 'ns-by-cluster': namespaceFilter, + 'all-namespaces': allNamespaces, } }; - cy.log(`tableRowsPerPageAndNamespaceFilter: /v1/userpreferences/${ userId }. Payload: ${ JSON.stringify(payload) }`); + cy.log(`tableRowsPerPageAndPreferences: /v1/userpreferences/${ userId }. Payload: ${ JSON.stringify(payload) }`); cy.setRancherResource('v1', 'userpreferences', userId, payload).then(() => { return cy.waitForRancherResource('v1', 'userpreferences', userId, (resp: any) => compare(resp?.body, payload)) .then((res) => { if (res) { - cy.log(`tableRowsPerPageAndNamespaceFilter: Success!`); + cy.log(`tableRowsPerPageAndPreferences: Success!`); } else { if (iteration < 3) { - cy.log(`tableRowsPerPageAndNamespaceFilter: Failed! Going to retry...`); + cy.log(`tableRowsPerPageAndPreferences: Failed! Going to retry...`); - return cy.tableRowsPerPageAndNamespaceFilter(rows, clusterName, groupBy, namespaceFilter, iteration + 1); + return cy.tableRowsPerPageAndPreferences(rows, preferences, iteration + 1); } - cy.log(`tableRowsPerPageAndNamespaceFilter: Failed! Giving up...`); + cy.log(`tableRowsPerPageAndPreferences: Failed! Giving up...`); - return Promise.reject(new Error('tableRowsPerPageAndNamespaceFilter failed')); + return Promise.reject(new Error('tableRowsPerPageAndPreferences failed')); } }); }); }); }); + +Cypress.Commands.add('tableRowsPerPageAndNamespaceFilter', (rows: number, clusterName: string, groupBy: string, namespaceFilter: string) => { + return cy.tableRowsPerPageAndPreferences(rows, { + clusterName, groupBy, namespaceFilter + }); +}); diff --git a/cypress/support/types/menu-actions.ts b/cypress/support/types/menu-actions.ts index 72c1b46d00..9ea6fe0c67 100644 --- a/cypress/support/types/menu-actions.ts +++ b/cypress/support/types/menu-actions.ts @@ -8,4 +8,7 @@ export enum MenuActions { ViewInApi = 'View in API', // eslint-disable-line no-unused-vars ChangeWorkspace = 'Change workspace', // eslint-disable-line no-unused-vars Delete = 'Delete', // eslint-disable-line no-unused-vars + Clone = 'Clone', // eslint-disable-line no-unused-vars + DownloadYaml = 'Download YAML', // eslint-disable-line no-unused-vars + } diff --git a/docusaurus/docs/extensions/known-issues.md b/docusaurus/docs/extensions/known-issues.md index 12aa363ade..51b517621a 100644 --- a/docusaurus/docs/extensions/known-issues.md +++ b/docusaurus/docs/extensions/known-issues.md @@ -19,3 +19,23 @@ To resolve this add the following `resolution` to the root application's `packag ... } ``` + +- Running `yarn install` might throw the following errors: +``` +error @aws-sdk/types@3.723.0: The engine "node" is incompatible with this module. Expected version ">=18.0.0". Got "16.20.2" +error @aws-sdk/util-locate-window@3.723.0: The engine "node" is incompatible with this module. Expected version ">=18.0.0". Got "16.20.2" +``` + +To resolve this add the following `resolutions` to the root application's `package.json`: +``` +{ + "name": "app-name", + "version": "0.1.0", + ... + resolutions": { + "@aws-sdk/types": "3.714.0", + "@aws-sdk/util-locate-window": "3.693.0" + }, + ... +} +``` diff --git a/docusaurus/docs/extensions/publishing.md b/docusaurus/docs/extensions/publishing.md index 1cddca51d2..9ae69c7438 100644 --- a/docusaurus/docs/extensions/publishing.md +++ b/docusaurus/docs/extensions/publishing.md @@ -205,6 +205,29 @@ When building an extension that will be housed in a GitLab repository or hosted This pipeline will build an ECI and publish it to container registry (`registry.gitlab.com` by default) to allow for importing into Rancher Manager. The actual pipeline jobs are defined in the [Dashboard repo](https://github.com/rancher/dashboard/blob/master/.gitlab/workflows/build-extension-catalog.gitlab-ci.yml) to allow for proper versioning and to apply any updates to the pipeline without any additional work from the Extension developer. +> **_WARNING:_** Ensure the branch of `rancher/dashboard` in the `remote` url containing the reusable workflow matches the release version of your `@rancher/shell` npm dependency. For example: +> - If building for the latest version of Rancher: +> ```yaml +> #.gitlab-ci.yml +> ... +> include: +> - remote: 'https://raw.githubusercontent.com/rancher/dashboard/master/shell/scripts/.gitlab/workflows/build-extension-catalog.gitlab-ci.yml' +> ``` +> - If building for Rancher `v2.9`: +> ```yaml +> #.gitlab-ci.yml +> ... +> include: +> - remote: 'https://raw.githubusercontent.com/rancher/dashboard/release-2.9/shell/scripts/.gitlab/workflows/build-extension-catalog.gitlab-ci.yml' +> ``` +> - If building for Rancher `v2.8`: +> ```yaml +> #.gitlab-ci.yml +> ... +> include: +> - remote: 'https://raw.githubusercontent.com/rancher/dashboard/release-2.8/shell/scripts/.gitlab/workflows/build-extension-catalog.gitlab-ci.yml' +> ``` + ### Pipeline Configuration There are a few pipeline configuration options, mostly tied to the container registry: diff --git a/docusaurus/docs/internal/testing/e2e-test.md b/docusaurus/docs/internal/testing/e2e-test.md index a265a01b81..77a76055eb 100644 --- a/docusaurus/docs/internal/testing/e2e-test.md +++ b/docusaurus/docs/internal/testing/e2e-test.md @@ -109,6 +109,14 @@ These values are provided when you create a new project within Cypress dashboard It's also possible to run a workflow in GitHub Actions E2E test using these values to record on personal dashboards. +### Skip dashboard or tests + +CI gates can be disabled in the following way: + +- Use label `ci/skip-e2e` to skip the E2E tests in the PR +- Use label `ci/skip-e2e-cypress-dashboard` to run the E2E tests without Sorry Cypress dashboard in the PR (it will enable `TEST_DISABLE_DASHBOARD_LABEL` env var) +- Use GitHub settings and define env var `TEST_DISABLE_DASHBOARD` as `true` (which is string and not boolean) to disable the Cypress dashboard entirely in every CI run + ## Local and CI/prod run It is possible to start the project and run all the tests at once with a single command. There's however a difference between `dev` and `production` run. The first will not require an official certificate and will build the project in `dist`, while the production will enable all the SSL configurations to run encrypted. diff --git a/docusaurus/docusaurus.config.js b/docusaurus/docusaurus.config.js index 696f8cb3a2..67eb2b7568 100644 --- a/docusaurus/docusaurus.config.js +++ b/docusaurus/docusaurus.config.js @@ -77,10 +77,13 @@ const config = { '@docusaurus/plugin-client-redirects', { createRedirects(existingPath) { - if (existingPath.includes('/extensions') && !existingPath.includes('/next') && !existingPath.includes('/v2')) { - return [ - existingPath.replace('/extensions', '/extensions/next') - ]; + // This function is invoked once per existing doc page, and we + // must return the “old” routes that we want to map to that doc’s path + if (existingPath.startsWith('/extensions/next')) { + // Generate the "old" route we want to redirect from + const oldPath = existingPath.replace('/extensions/next', '/extensions'); + + return [oldPath]; } return undefined; // Return a falsy value: no redirect created diff --git a/docusaurus/extensions_versioned_docs/version-v2/publishing.md b/docusaurus/extensions_versioned_docs/version-v2/publishing.md index 1cddca51d2..f06ecdd33f 100644 --- a/docusaurus/extensions_versioned_docs/version-v2/publishing.md +++ b/docusaurus/extensions_versioned_docs/version-v2/publishing.md @@ -205,6 +205,22 @@ When building an extension that will be housed in a GitLab repository or hosted This pipeline will build an ECI and publish it to container registry (`registry.gitlab.com` by default) to allow for importing into Rancher Manager. The actual pipeline jobs are defined in the [Dashboard repo](https://github.com/rancher/dashboard/blob/master/.gitlab/workflows/build-extension-catalog.gitlab-ci.yml) to allow for proper versioning and to apply any updates to the pipeline without any additional work from the Extension developer. +> **_WARNING:_** Ensure the branch of `rancher/dashboard` in the `remote` url containing the reusable workflow matches the release version of your `@rancher/shell` npm dependency. For example: +> - If building for Rancher `v2.9`: +> ```yaml +> #.gitlab-ci.yml +> ... +> include: +> - remote: 'https://raw.githubusercontent.com/rancher/dashboard/release-2.9/shell/scripts/.gitlab/workflows/build-extension-catalog.gitlab-ci.yml' +> ``` +> - If building for Rancher `v2.8`: +> ```yaml +> #.gitlab-ci.yml +> ... +> include: +> - remote: 'https://raw.githubusercontent.com/rancher/dashboard/release-2.8/shell/scripts/.gitlab/workflows/build-extension-catalog.gitlab-ci.yml' +> ``` + ### Pipeline Configuration There are a few pipeline configuration options, mostly tied to the container registry: diff --git a/package.json b/package.json index 53d9ddb972..87207ac7bd 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "install:ci": "yarn install --frozen-lockfile", "dev": "bash -c 'source ./scripts/version && NODE_ENV=dev ./node_modules/.bin/vue-cli-service serve'", "mem-dev": "bash -c 'source ./scripts/version && NODE_ENV=dev node --max-old-space-size=8192 ./node_modules/.bin/vue-cli-service serve'", - "docker:local:start": "docker run -d --restart=unless-stopped -p 80:80 -p 443:443 -e CATTLE_BOOTSTRAP_PASSWORD=password -e CATTLE_PASSWORD_MIN_LENGTH=3 --name cypress --privileged rancher/rancher:head", + "docker:local:start": "docker run -d --restart=unless-stopped -p 80:80 -p 443:443 -e CATTLE_BOOTSTRAP_PASSWORD=password -e CATTLE_PASSWORD_MIN_LENGTH=3 --name cypress --privileged rancher/rancher:v2.11-2053ce644a31cd8053d1f58e2487154b0b8513b6-head", "docker:local:stop": "docker kill cypress || true && docker rm cypress || true", "build": "NODE_OPTIONS=--max_old_space_size=4096 ./node_modules/.bin/vue-cli-service build", "build:lib": "cd pkg/rancher-components && yarn build:lib", @@ -62,7 +62,7 @@ "@aws-sdk/client-iam": "3.658.1", "@aws-sdk/client-kms": "3.8.1", "@novnc/novnc": "1.2.0", - "@popperjs/core": "2.4.4", + "@popperjs/core": "2.11.8", "@rancher/icons": "2.0.29", "ansi_up": "5.0.0", "axios": "0.21.4", @@ -77,11 +77,10 @@ "cookie-universal": "2.2.2", "cron-validator": "1.2.0", "cronstrue": "1.95.0", - "cross-env": "6.0.3", + "cross-env": "7.0.3", "custom-event-polyfill": "1.0.7", "d3": "7.3.0", "d3-selection": "1.4.1", - "dagre-d3": "0.6.4", "dayjs": "1.8.29", "defu": "5.0.1", "diff2html": "3.4.24", @@ -96,9 +95,8 @@ "intl-messageformat": "7.8.4", "ip": "2.0.1", "ipaddr.js": "2.2.0", - "is-base64": "1.1.0", "is-url": "1.2.4", - "jexl": "2.2.2", + "jexl": "2.3.0", "jquery": "3.5.1", "js-cookie": "2.2.1", "js-yaml": "4.1.0", diff --git a/pkg/aks/components/CruAks.vue b/pkg/aks/components/CruAks.vue index 3c2b3f038a..5515d46f2a 100644 --- a/pkg/aks/components/CruAks.vue +++ b/pkg/aks/components/CruAks.vue @@ -645,7 +645,7 @@ export default defineComponent({ virtualNetwork: { get() { - return this.virtualNetworkOptions.find((opt) => opt.value === this.config.subnet) || this.t('generic.none'); + return this.virtualNetworkOptions.find((opt) => opt.value === this.config.subnet) || { label: this.t('generic.none') }; }, set(neu: {label: string, kind?: string, disabled?: boolean, value?: string, virtualNetwork?: any}) { if (neu.label === this.t('generic.none')) { @@ -804,8 +804,6 @@ export default defineComponent({ return; } this.loadingLocations = true; - // this will force the resourceLocation watcher to re-run every time new locations are fetched even if the default one selected hasn't changed - this.config['resourceLocation'] = ''; const { azureCredentialSecret } = this.config; @@ -839,6 +837,11 @@ export default defineComponent({ errors.push(this.t('aks.errors.regions', { e: parsedError || err })); } + + // once regions are loaded and a default selected, fetch resources that are region-scoped + this.getAksVersions(); + this.getVmSizes(); + this.getVirtualNetworks(); }, async getAksVersions(): Promise { diff --git a/pkg/aks/components/__tests__/CruAks.test.ts b/pkg/aks/components/__tests__/CruAks.test.ts index 2312359004..e676d30d5c 100644 --- a/pkg/aks/components/__tests__/CruAks.test.ts +++ b/pkg/aks/components/__tests__/CruAks.test.ts @@ -240,7 +240,7 @@ describe('aks provisioning form', () => { it('should display subnets grouped by network in the virtual network dropdown', async() => { const noneOption = { label: 'generic.none' }; const config = { - dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc' + dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc', resourceLocation: 'eastus' }; const wrapper = shallowMount(CruAks, { propsData: { @@ -254,7 +254,6 @@ describe('aks provisioning form', () => { const networkOpts = virtualNetworkSelect.props().options; expect(virtualNetworkSelect.props().value).toStrictEqual(noneOption); - expect(networkOpts).toStrictEqual([{ label: 'generic.none' }, { disabled: true, kind: 'group', label: 'network2' }, { @@ -299,7 +298,7 @@ describe('aks provisioning form', () => { it('should prevent saving if a node pool has taints missing keys or values', async() => { const config = { - dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc' + dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc', resourceLocation: 'eastus' }; const wrapper = shallowMount(CruAks, { propsData: { @@ -343,7 +342,7 @@ describe('aks provisioning form', () => { }], ])('should set virtualNetwork, virtualNetworkResourceGroup, and subnet when a virtual network is selected', async(optionIndex, { virtualNetwork, virtualNetworkResourceGroup, subnet }) => { const config = { - dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc' + dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc', resourceLocation: 'eastus' }; const wrapper = shallowMount(CruAks, { propsData: { @@ -366,7 +365,7 @@ describe('aks provisioning form', () => { it('should set config.monitoring to \'true\' and show log anaytics workspace name and log analytics workspace group inputs when the monitoring checkbox is checked', async() => { const config = { - dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc' + dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc', resourceLocation: 'eastus' }; const wrapper = shallowMount(CruAks, { propsData: { @@ -397,7 +396,7 @@ describe('aks provisioning form', () => { it('should clear virtualNetwork, virtualNetworkResourceGroup, and subnet when the \'none\' virtual network option is selected', async() => { const config = { - dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc' + dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc', resourceLocation: 'eastus' }; const wrapper = shallowMount(CruAks, { propsData: { @@ -430,7 +429,7 @@ describe('aks provisioning form', () => { name: 'abc', _validation: {}, _isNewOrUnprovisioned: false, orchestratorVersion: originalVersion }]; const config = { - dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc', kubernetesVersion: originalVersion, nodePools + dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc', kubernetesVersion: originalVersion, nodePools, resourceLocation: 'eastus' }; const wrapper = shallowMount(CruAks, { propsData: { @@ -451,7 +450,7 @@ describe('aks provisioning form', () => { it('should clear config.logAnalyticsWorkspaceName and config.logAnalyticsWorkspaceGroup when the monitoring checkbox is unchecked', async() => { const config = { - dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc', monitoring: true, logAnalyticsWorkspaceGroup: 'abc', logAnalyticsWorkspaceName: 'def' + dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc', monitoring: true, logAnalyticsWorkspaceGroup: 'abc', logAnalyticsWorkspaceName: 'def', resourceLocation: 'eastus' }; const wrapper = shallowMount(CruAks, { propsData: { diff --git a/pkg/aks/l10n/en-us.yaml b/pkg/aks/l10n/en-us.yaml index 4ee4dd73be..db79c58314 100644 --- a/pkg/aks/l10n/en-us.yaml +++ b/pkg/aks/l10n/en-us.yaml @@ -163,8 +163,8 @@ aks: availabilityZones: Availability zones are not available in the selected region. privateDnsZone: Private DNS Zone Resource ID must be in the format /subscriptions/SUBSCRIPTION_ID/resourceGroups/RESOURCEGROUP_NAME/providers/Microsoft.Network/privateDnsZones/PRIVATE_DNS_ZONE_NAME. The Private DNS Zone Resource Name must be in the format privatelink.REGION.azmk8s.io, SUBZONE.privatelink.REGION.azmk8s.io, private.REGION.azmk8s.io, or SUBZONE.private.REGION.azmk8s.io poolName: Node pool names must be 1-12 characters long, consist only of lowercase letters and numbers, and start with a letter. - poolCount: Node count must be at least one in System pools. - poolUserCount: Node count cannot be less than zero. + poolCount: Node count must be at least 1 and at most 1000 in System pools. + poolUserCount: Node count cannot be less than 0 or greater than 1000 in User pools. poolMinMax: The minimum number of nodes must be less than or equal to the maximum number of nodes, and the node count must be between or equal to the minimum and maximum. poolMin: The minimum number of nodes must be greater than 0 and at most 1000. poolMax: The maximum number of nodes must be greater than 0 and at most 1000. diff --git a/pkg/aks/util/__tests__/validators.test.ts b/pkg/aks/util/__tests__/validators.test.ts index fe0b8f6aa2..51a2bbe425 100644 --- a/pkg/aks/util/__tests__/validators.test.ts +++ b/pkg/aks/util/__tests__/validators.test.ts @@ -140,19 +140,24 @@ describe('fx: nodePoolNames', () => { describe('fx: nodePoolCount', () => { // AksNodePool unit tests check that the second arg is passed in as expected - it('validates that count is at least 1 when second arg is false', () => { + it('validates that count is at least 1 and at most 1000 when second arg is false', () => { const validator = validators.nodePoolCount(mockCtx); expect(validator(1, false)).toBeUndefined(); expect(validator(0, false)).toStrictEqual(MOCK_TRANSLATION); + expect(validator(1000, false)).toBeUndefined(); + expect(validator(1001, false)).toStrictEqual(MOCK_TRANSLATION); }); - it('validates that count is at least 0 when second arg is true', () => { + it('validates that count is at least 0 and at most 1000 when second arg is true', () => { const validator = validators.nodePoolCount(mockCtx); expect(validator(1, true)).toBeUndefined(); expect(validator(0, true)).toBeUndefined(); + expect(validator(1000, true)).toBeUndefined(); + expect(validator(-1, true)).toStrictEqual(MOCK_TRANSLATION); + expect(validator(1001, true)).toStrictEqual(MOCK_TRANSLATION); }); it('validates each node pool in the provided context when not passed a count value', () => { @@ -170,6 +175,12 @@ describe('fx: nodePoolCount', () => { }, { name: 'klm', _validation: {}, mode: 'User', count: -1 + }, + { + name: 'nop', _validation: {}, mode: 'User', count: 1001 + }, + { + name: 'qrs', _validation: {}, mode: 'System', count: 1001 } ] as unknown as AKSNodePool[] }; @@ -180,5 +191,7 @@ describe('fx: nodePoolCount', () => { expect(ctx.nodePools[1]?._validation?._validCount).toStrictEqual(true); expect(ctx.nodePools[2]?._validation?._validCount).toStrictEqual(true); expect(ctx.nodePools[3]?._validation?._validCount).toStrictEqual(false); + expect(ctx.nodePools[4]?._validation?._validCount).toStrictEqual(false); + expect(ctx.nodePools[5]?._validation?._validCount).toStrictEqual(false); }); }); diff --git a/pkg/aks/util/validators.ts b/pkg/aks/util/validators.ts index 0c52340c27..484f75130c 100644 --- a/pkg/aks/util/validators.ts +++ b/pkg/aks/util/validators.ts @@ -181,6 +181,7 @@ export const nodePoolNamesUnique = (ctx: any) => { export const nodePoolCount = (ctx:any) => { return (count?: number, canBeZero = false) => { let min = 1; + const max = 1000; let errMsg = ctx.t('aks.errors.poolCount'); if (canBeZero) { @@ -188,7 +189,7 @@ export const nodePoolCount = (ctx:any) => { errMsg = ctx.t('aks.errors.poolUserCount'); } if (count || count === 0) { - return count >= min ? undefined : errMsg; + return count >= min && count <= max ? undefined : errMsg; } else { let allValid = true; @@ -201,7 +202,7 @@ export const nodePoolCount = (ctx:any) => { min = 1; } - if (count < min) { + if (count < min || count > max) { pool._validation['_validCount'] = false; allValid = false; } else { diff --git a/pkg/gke/components/CruGKE.vue b/pkg/gke/components/CruGKE.vue index 59bfd16012..2ea85145e4 100644 --- a/pkg/gke/components/CruGKE.vue +++ b/pkg/gke/components/CruGKE.vue @@ -704,6 +704,7 @@ export default defineComponent({ label-key="generic.name" required :rules="fvGetAndReportPathRules('clusterName')" + data-testid="gke-cluster-name" @update:value="setClusterName" /> @@ -713,6 +714,7 @@ export default defineComponent({ :mode="mode" label-key="nameNsDescription.description.label" :placeholder="t('nameNsDescription.description.placeholder')" + data-testid="gke-cluster-description" @update:value="setClusterDescription" /> diff --git a/pkg/gke/components/Networking.vue b/pkg/gke/components/Networking.vue index 0e6cd4f146..2d5e593581 100644 --- a/pkg/gke/components/Networking.vue +++ b/pkg/gke/components/Networking.vue @@ -203,9 +203,15 @@ export default defineComponent({ networkOptions(neu) { if (neu && neu.length && !this.network) { - const firstnetwork = neu.find((network: GKENetwork) => network.kind !== 'group'); + const defaultNetwork = neu.find((network: GKENetwork) => network?.name === 'default'); - this.$emit('update:network', firstnetwork?.name); + if (defaultNetwork) { + this.$emit('update:network', defaultNetwork.name); + } else { + const firstnetwork = neu.find((network: GKENetwork) => network.kind !== 'group'); + + this.$emit('update:network', firstnetwork?.name); + } } }, diff --git a/pkg/gke/components/__tests__/Networking.test.ts b/pkg/gke/components/__tests__/Networking.test.ts index 3577549cb5..0ecda4b5f7 100644 --- a/pkg/gke/components/__tests__/Networking.test.ts +++ b/pkg/gke/components/__tests__/Networking.test.ts @@ -67,7 +67,7 @@ describe('gke Networking', () => { expect(spy).toHaveBeenCalledTimes(4); }); - it('should populate network dropdown and select the first option after loading gcp data', async() => { + it('should populate network dropdown and select the default network after loading gcp data', async() => { const setup = requiredSetup(); const wrapper = shallowMount(Networking, { @@ -85,8 +85,8 @@ describe('gke Networking', () => { const networksDropdown = wrapper.getComponent('[data-testid="gke-networks-dropdown"]'); - expect(networksDropdown.props().options).toHaveLength(4); - expect(wrapper.emitted('update:network')?.[0]?.[0]).toBe('host-shared-vpc'); + expect(networksDropdown.props().options).toHaveLength(5); + expect(wrapper.emitted('update:network')?.[0]?.[0]).toBe('default'); }); it('should populate subnetworks dropdown dependent on network selection', async() => { diff --git a/pkg/gke/util/__mocks__/gcp.ts b/pkg/gke/util/__mocks__/gcp.ts index e0df338150..c4f3dec34a 100644 --- a/pkg/gke/util/__mocks__/gcp.ts +++ b/pkg/gke/util/__mocks__/gcp.ts @@ -247,6 +247,64 @@ const mockedGKENetworksResponse = { 'https://www.googleapis.com/compute/v1/projects/test-project/regions/me-central1/subnetworks/test-network', 'https://www.googleapis.com/compute/v1/projects/test-project/regions/asia-east2/subnetworks/test-network', 'https://www.googleapis.com/compute/v1/projects/test-project/regions/me-central2/subnetworks/test-network'] + }, + { + autoCreateSubnetworks: true, + creationTimestamp: '2022-10-26T14:50:30.702-07:00', + id: '11111111', + kind: 'compute#network', + mtu: 1460, + name: 'default', + networkFirewallPolicyEnforcementOrder: 'AFTER_CLASSIC_FIREWALL', + routingConfig: { routingMode: 'REGIONAL' }, + selfLink: 'https://www.googleapis.com/compute/v1/projects/test-data-project/global/networks/default', + selfLinkWithId: 'https://www.googleapis.com/compute/v1/projects/test-data-project/global/networks/11111111', + subnetworks: [ + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/africa-south1/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/us-west8/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/asia-northeast3/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/asia-northeast2/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/asia-south2/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/europe-west3/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/us-west3/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/us-west2/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/asia-northeast1/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/europe-west12/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/asia-southeast1/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/us-south1/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/southamerica-west1/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/asia-southeast2/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/me-west1/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/us-east7/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/us-central1/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/southamerica-east1/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/europe-west9/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/europe-north1/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/us-east4/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/asia-east1/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/europe-west10/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/europe-central2/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/europe-north2/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/asia-south1/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/northamerica-south1/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/us-east5/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/us-east1/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/europe-west4/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/europe-west2/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/europe-west8/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/northamerica-northeast1/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/australia-southeast2/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/us-west4/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/europe-west6/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/northamerica-northeast2/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/europe-southwest1/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/us-west1/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/australia-southeast1/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/europe-west1/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/me-central1/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/asia-east2/subnetworks/default', + 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/me-central2/subnetworks/default' + ] }] }; @@ -267,7 +325,30 @@ const mockedGKESubnetworksResponse = { secondaryIpRanges: [{ ipCidrRange: '10.0.1.0/24', rangeName: 'range-1' }], selfLink: 'https://www.googleapis.com/compute/v1/projects/test-project/regions/us-central1/subnetworks/test-network', stackType: 'IPV4_ONLY', - }] + }, + { + creationTimestamp: '2022-10-26T14:50:38.688-07:00', + fingerprint: '3456', + gatewayAddress: '10.128.0.1', + id: '1234', + ipCidrRange: '10.128.0.0/20', + kind: 'compute#subnetwork', + name: 'default', + network: 'https://www.googleapis.com/compute/v1/projects/test-data-project/global/networks/default', + privateIpGoogleAccess: true, + privateIpv6GoogleAccess: 'DISABLE_GOOGLE_ACCESS', + purpose: 'PRIVATE', + region: 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/us-central1', + secondaryIpRanges: [ + { + ipCidrRange: '10.0.1.0/24', + rangeName: 'range-1' + } + ], + selfLink: 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/us-central1/subnetworks/default', + stackType: 'IPV4_ONLY' + }, + ] }; const mockedGKESharedSubnetworksResponse = { diff --git a/pkg/harvester-manager/machine-config/harvester.vue b/pkg/harvester-manager/machine-config/harvester.vue index 28dc209dce..af1539f762 100644 --- a/pkg/harvester-manager/machine-config/harvester.vue +++ b/pkg/harvester-manager/machine-config/harvester.vue @@ -3,7 +3,7 @@ import draggable from 'vuedraggable'; import isEmpty from 'lodash/isEmpty'; import jsyaml from 'js-yaml'; import YAML from 'yaml'; -import isBase64 from 'is-base64'; +import { isBase64 } from '@shell/utils/string'; import NodeAffinity from '@shell/components/form/NodeAffinity'; import PodAffinity from '@shell/components/form/PodAffinity'; diff --git a/pkg/rancher-components/src/components/Banner/Banner.vue b/pkg/rancher-components/src/components/Banner/Banner.vue index 24d172a21c..8ca20a5670 100644 --- a/pkg/rancher-components/src/components/Banner/Banner.vue +++ b/pkg/rancher-components/src/components/Banner/Banner.vue @@ -67,6 +67,7 @@ export default defineComponent({ :class="{ [color]: true, }" + role="banner" > @@ -72,6 +77,11 @@ export default { .close-button { position: absolute; visibility: hidden; + + &:focus-visible { + @include focus-outline; + outline-offset: 2px; + } } &:hover .close-button { diff --git a/shell/components/ButtonMultiAction.vue b/shell/components/ButtonMultiAction.vue index a762013873..c9992fa400 100644 --- a/shell/components/ButtonMultiAction.vue +++ b/shell/components/ButtonMultiAction.vue @@ -33,6 +33,12 @@ const buttonClass = computed(() => { .borderless { background-color: transparent; border: none; + + &:focus-visible { + @include focus-outline; + outline-offset: -2px; + } + &:hover, &:focus { background-color: var(--accent-btn); box-shadow: none; diff --git a/shell/components/ClusterIconMenu.vue b/shell/components/ClusterIconMenu.vue index 661821dd8b..6bbe093972 100644 --- a/shell/components/ClusterIconMenu.vue +++ b/shell/components/ClusterIconMenu.vue @@ -25,7 +25,7 @@ export default { }, customColor() { - return !this.cluster.removePreviewColor && this.cluster.badge?.iconText ? this.cluster.badge?.color : ''; + return this.cluster.iconColor || ''; }, }, diff --git a/shell/components/CodeMirror.vue b/shell/components/CodeMirror.vue index b15a8d976a..d125007f20 100644 --- a/shell/components/CodeMirror.vue +++ b/shell/components/CodeMirror.vue @@ -5,7 +5,7 @@ import { _EDIT, _VIEW } from '@shell/config/query-params'; export default { name: 'CodeMirror', - emits: ['onReady', 'onInput', 'onChanges', 'onFocus'], + emits: ['onReady', 'onInput', 'onChanges', 'onFocus', 'validationChanged'], props: { /** @@ -39,6 +39,7 @@ export default { codeMirrorRef: null, loaded: false, removeKeyMapBox: false, + hasLintErrors: false, }; }, @@ -65,6 +66,7 @@ export default { foldGutter: true, styleSelectedText: true, showCursorWhenSelecting: true, + autocorrect: false, }; if (this.asTextArea) { @@ -76,6 +78,11 @@ export default { Object.assign(out, this.options); + // parent components control lint with a boolean; if linting is enabled, we need to override that boolean with a custom error handler to wire lint errors into dashboard validation + if (this.options?.lint) { + out.lint = { onUpdateLinting: this.handleLintErrors }; + } + return out; }, @@ -104,7 +111,25 @@ export default { } }, + watch: { + hasLintErrors(neu) { + this.$emit('validationChanged', !neu); + } + }, + methods: { + /** + * Codemirror yaml linting uses js-yaml parse + * it does not distinguish between warnings and errors so we will treat all yaml lint messages as errors + * other codemirror linters (eg json) will report from, to, severity where severity may be 'warning' or 'error' + * only 'error' level linting will trigger a validation event from this component + */ + handleLintErrors(diagnostics = []) { + const hasLintErrors = diagnostics.filter((d) => !d.severity || d.severity === 'error').length > 0; + + this.hasLintErrors = hasLintErrors; + }, + focus() { if ( this.$refs.codeMirrorRef ) { this.$refs.codeMirrorRef.cminstance.focus(); @@ -118,6 +143,8 @@ export default { }, onReady(codeMirrorRef) { + this.$emit('validationChanged', true); + this.$nextTick(() => { codeMirrorRef.refresh(); this.codeMirrorRef = codeMirrorRef; diff --git a/shell/components/CommunityLinks.vue b/shell/components/CommunityLinks.vue index 70b39a2b61..37c31532d6 100644 --- a/shell/components/CommunityLinks.vue +++ b/shell/components/CommunityLinks.vue @@ -110,6 +110,8 @@ export default { {{ link.label }} @@ -118,6 +120,8 @@ export default { :href="link.value" rel="noopener noreferrer nofollow" target="_blank" + role="link" + :aria-label="link.label" > {{ link.label }} @@ -127,7 +131,11 @@ export default { > {{ t('footer.wechat.title') }} @@ -147,7 +155,12 @@ export default {
diff --git a/shell/components/GrowlManager.vue b/shell/components/GrowlManager.vue index f992716cdd..0038c11789 100644 --- a/shell/components/GrowlManager.vue +++ b/shell/components/GrowlManager.vue @@ -106,7 +106,10 @@ export default {
{{ growl.title }}
-

+

{{ growl.message }}

@@ -153,12 +156,16 @@ export default { word-break: break-all; box-shadow: 0 3px 5px 0px var(--shadow); + $growl-icon-size: 20px; + .icon-container { align-self: center; flex-basis: 10%; padding: 10px 20px 10px 10px; i { - font-size: 24px; + font-size: $growl-icon-size; + width: $growl-icon-size; + height: $growl-icon-size; } } @@ -183,11 +190,14 @@ export default { } .growl-text-title { font-size: 16px; - margin-bottom: 20px; } > P { - margin-top: 5px; + padding-top: 2px; + + &.has-title { + margin-top: 5px; + } } } } diff --git a/shell/components/LocaleSelector.vue b/shell/components/LocaleSelector.vue index e2abf5d0cb..3a310b61ea 100644 --- a/shell/components/LocaleSelector.vue +++ b/shell/components/LocaleSelector.vue @@ -11,7 +11,11 @@ export default { mode: { type: String, default: '' - }, + } + }, + + data() { + return { isLocaleSelectorOpen: false }; }, computed: { @@ -40,8 +44,15 @@ export default { }, methods: { + openLocaleSelector() { + this.isLocaleSelectorOpen = true; + }, + closeLocaleSelector() { + this.isLocaleSelectorOpen = false; + }, switchLocale($event) { this.$store.dispatch('i18n/switchTo', $event); + this.closeLocaleSelector(); }, } }; @@ -50,13 +61,28 @@ export default { diff --git a/shell/components/form/Password.vue b/shell/components/form/Password.vue index e9e81d75ee..6bd7dabf44 100644 --- a/shell/components/form/Password.vue +++ b/shell/components/form/Password.vue @@ -41,7 +41,7 @@ export default { mode: { type: String, default: _CREATE, - }, + } }, data() { return { reveal: false }; @@ -68,6 +68,9 @@ export default { } return attributes; + }, + hideShowLabel() { + return this.reveal ? this.t('action.hide') : this.t('action.show'); } }, watch: { @@ -92,6 +95,9 @@ export default { }, focus() { this.$refs.input.$refs.value.focus(); + }, + hideShowFn() { + this.reveal ? this.reveal = false : this.reveal = true; } } }; @@ -127,17 +133,15 @@ export default { class="addon" > {{ t('action.hide') }} - {{ t('action.show') }} + tabindex="0" + class="hide-show" + role="button" + @click.prevent.stop="hideShowFn" + @keyup.space.prevent.stop="hideShowFn" + > + {{ hideShowLabel }} + @@ -157,10 +161,16 @@ export default { .password { display: flex; flex-direction: column; + .labeled-input { .addon { - padding-left: 12px; - min-width: 65px; + padding-left: 12px; + min-width: 65px; + + .hide-show:focus-visible { + @include focus-outline; + outline-offset: 4px; + } } } .genPassword { diff --git a/shell/components/form/ResourceLabeledSelect.vue b/shell/components/form/ResourceLabeledSelect.vue index dd9e0e16e4..3326a99279 100644 --- a/shell/components/form/ResourceLabeledSelect.vue +++ b/shell/components/form/ResourceLabeledSelect.vue @@ -27,7 +27,7 @@ export interface ResourceLabeledSelectPaginateSettings extends SharedSettings { */ overrideRequest?: LabelSelectPaginateFn, /** - * Override the default settings used in the convience function to fetch a page of results + * Override the default settings used in the convenience function to fetch a page of results */ requestSettings?: PaginateTypeOverridesFn, } diff --git a/shell/components/form/__tests__/NodeScheduling.test.ts b/shell/components/form/__tests__/NodeScheduling.test.ts new file mode 100644 index 0000000000..ed185373bb --- /dev/null +++ b/shell/components/form/__tests__/NodeScheduling.test.ts @@ -0,0 +1,44 @@ +import { mount } from '@vue/test-utils'; +import NodeScheduling from '@shell/components/form/NodeScheduling.vue'; +import { _CREATE, _EDIT, _VIEW } from '@shell/config/query-params'; + +const requiredSetup = () => { + return { + global: { + mocks: { + $store: { + getters: { + currentProduct: { inStore: 'cluster' }, + 'i18n/t': (text: string) => text, + t: (text: string) => text, + } + } + }, + } + }; +}; + +describe('component: NodeScheduling', () => { + const value = { nodeName: 'node-1' }; + + const nodes = ['node-0', 'node-1']; + + it.each([ + _VIEW, + _CREATE, + _EDIT + ])('should show NodeName option', (mode) => { + const wrapper = mount( + NodeScheduling, + { + props: { + mode, loading: false, value, nodes + }, + ...requiredSetup(), + } + ); + + expect(wrapper.find('[data-testid="node-scheduling-selectNode"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-testid="node-scheduling-nodeSelector"]').element.textContent).toContain(value.nodeName); + }); +}); diff --git a/shell/components/formatter/Endpoints.vue b/shell/components/formatter/Endpoints.vue index 57035bb1e0..9742006141 100644 --- a/shell/components/formatter/Endpoints.vue +++ b/shell/components/formatter/Endpoints.vue @@ -27,7 +27,7 @@ export default { const nodeWithExternal = nodes.find((node) => !!node.externalIp) || {}; const externalIp = nodeWithExternal.externalIp; - if ( this.value && this.value.length ) { + if ( this.value?.length ) { let out ; try { diff --git a/shell/components/formatter/ServiceTargets.vue b/shell/components/formatter/ServiceTargets.vue index 15abb015cf..87b1c01848 100644 --- a/shell/components/formatter/ServiceTargets.vue +++ b/shell/components/formatter/ServiceTargets.vue @@ -95,7 +95,7 @@ export default { }, }, }; -> + + + diff --git a/shell/edit/provisioning.cattle.io.cluster/rke2.vue b/shell/edit/provisioning.cattle.io.cluster/rke2.vue index ea9c4117bf..f304a8bfe1 100644 --- a/shell/edit/provisioning.cattle.io.cluster/rke2.vue +++ b/shell/edit/provisioning.cattle.io.cluster/rke2.vue @@ -248,6 +248,7 @@ export default { busy: false, machinePoolValidation: {}, // map of validation states for each machine pool machinePoolErrors: {}, + addonConfigValidation: {}, // validation state of each addon config (boolean of whether codemirror's yaml lint passed) allNamespaces: [], extensionTabs: getApplicableExtensionEnhancements(this, ExtensionPoint.TAB, TabLocation.CLUSTER_CREATE_RKE2, this.$route, this), labelForAddon @@ -797,7 +798,9 @@ export default { // and in all of the validation statuses for each machine pool Object.values(this.machinePoolValidation).forEach((v) => (base = base && v)); - return validRequiredPools && base; + const hasAddonConfigErrors = Object.values(this.addonConfigValidation).filter((v) => v === false).length > 0; + + return validRequiredPools && base && !hasAddonConfigErrors; }, currentCluster() { if (this.mode === _EDIT) { @@ -1565,6 +1568,8 @@ export default { * 2) We're ready to cache any values the user provides for each addon */ async initAddons() { + this.addonConfigValidation = {}; + for (const chartName of this.addonNames) { const entry = this.chartVersions[chartName]; @@ -2133,7 +2138,11 @@ export default { } } } - } + }, + + addonConfigValidationChanged(configName, isValid) { + this.addonConfigValidation[configName] = isValid; + }, } }; @@ -2430,6 +2439,7 @@ export default { :label="labelForAddon($store, v.name, false)" :weight="9" :showHeader="false" + :error="addonConfigValidation[v.name]===false" @active="showAddons(v.name)" > diff --git a/shell/edit/provisioning.cattle.io.cluster/tabs/AddOnConfig.vue b/shell/edit/provisioning.cattle.io.cluster/tabs/AddOnConfig.vue index 720bd3dc53..671036ee0f 100644 --- a/shell/edit/provisioning.cattle.io.cluster/tabs/AddOnConfig.vue +++ b/shell/edit/provisioning.cattle.io.cluster/tabs/AddOnConfig.vue @@ -7,7 +7,7 @@ import { labelForAddon } from '@shell/utils/cluster'; import { _EDIT } from '@shell/config/query-params'; export default { - emits: ['additional-manifest-changed', 'update-questions', 'update-values'], + emits: ['additional-manifest-changed', 'update-questions', 'update-values', 'validationChanged'], components: { Banner, @@ -60,7 +60,13 @@ export default { isEdit() { return this.mode === _EDIT; } - } + }, + + methods: { + handleValidationChanged(e) { + this.$emit('validationChanged', e); + } + }, }; @@ -91,12 +97,14 @@ export default {
diff --git a/shell/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryConfigs.vue b/shell/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryConfigs.vue index f377781073..5bd2bd32d5 100644 --- a/shell/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryConfigs.vue +++ b/shell/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryConfigs.vue @@ -1,4 +1,5 @@ diff --git a/shell/pages/c/_cluster/explorer/index.vue b/shell/pages/c/_cluster/explorer/index.vue index 1183be7ec9..1c5e8218e7 100644 --- a/shell/pages/c/_cluster/explorer/index.vue +++ b/shell/pages/c/_cluster/explorer/index.vue @@ -155,6 +155,7 @@ export default { clusterCounts, selectedTab: 'cluster-events', extensionCards: getApplicableExtensionEnhancements(this, ExtensionPoint.CARD, CardLocation.CLUSTER_DASHBOARD_CARD, this.$route), + canViewEvents: !!this.$store.getters['cluster/schemaFor'](EVENT), clusterServiceIcons, }; }, @@ -473,16 +474,6 @@ export default { return !!this.currentCluster?.spec?.description; }, - allEventsLink() { - return { - name: 'c-cluster-product-resource', - params: { - product: EXPLORER, - resource: EVENT, - } - }; - }, - allSecretsLink() { return { name: 'c-cluster-product-resource', @@ -785,15 +776,11 @@ export default {
- - - {{ t('glance.eventsTable') }} - - { - resources.push(resource); + resources.push(!!resource.length ? resource : `${ resource.resource } (${ resource.context })`); }); if (settings.resources.enableSome.generic) { resources.push(this.t('performance.serverPagination.resources.generic', {}, true)); } } - storeResources.push(`${ store }: ${ resources.join(', ') }`); + storeResources.push(`Resources in store '${ store }': ${ resources.join(', ') }`); }); - return storeResources.join('. '); + return storeResources.join('

'); } }, @@ -187,6 +187,13 @@ export default { body: this.t(`performance.${ l10n[property] }.incompatibleDescription`, {}, true), }, }); + }, + + setPaginationDefaults() { + this.value = { + ...this.value, + serverPagination: { ...DEFAULT_PERF_SETTING.serverPagination } + }; } }, }; @@ -202,7 +209,9 @@ export default {
-

{{ t('performance.serverPagination.label') }}

+

+ {{ t('performance.serverPagination.label') }} +

{{ t('performance.serverPagination.description') }}

{{ t('performance.serverPagination.applicable') }}

-

- {{ steveCacheApplicableResources }} -

+

+

@@ -273,6 +290,11 @@ export default {

{{ t('performance.incrementalLoad.label') }}

+ + +

{{ t('performance.incrementalLoad.description') }}

-

{{ t('performance.manualRefresh.description') }}

+ color="warning" + > + + +

{{ t('performance.manualRefresh.description') }}

-

{{ t('performance.gc.description') }}

+ color="warning" + > + + +

{{ t('performance.gc.description') }}

{{ t('performance.nsFiltering.label') }}

-

{{ t('performance.nsFiltering.description') }}

+ color="warning" + > + + +

{{ t('performance.nsFiltering.description') }}

{{ t('performance.advancedWorker.label') }}

-

{{ t('performance.advancedWorker.description') }}

+ color="warning" + > + + +

{{ t('performance.advancedWorker.description') }}

-
@@ -583,6 +589,9 @@ export default defineComponent({ :to="manageLocation" class="btn btn-sm role-secondary" data-testid="cluster-management-manage-button" + role="button" + :aria-label="t('cluster.manageAction')" + @keyup.space="$router.push(manageLocation)" > {{ t('cluster.manageAction') }} @@ -591,6 +600,9 @@ export default defineComponent({ :to="importLocation" class="btn btn-sm role-primary" data-testid="cluster-create-import-button" + role="button" + :aria-label="t('cluster.importAction')" + @keyup.space="$router.push(importLocation)" > {{ t('cluster.importAction') }} @@ -599,6 +611,9 @@ export default defineComponent({ :to="createLocation" class="btn btn-sm role-primary" data-testid="cluster-create-button" + role="button" + :aria-label="t('generic.create')" + @keyup.space="$router.push(createLocation)" > {{ t('generic.create') }} @@ -614,6 +629,8 @@ export default defineComponent({ {{ row.nameDisplay }} @@ -687,6 +704,10 @@ export default defineComponent({