Merge branch 'master' of github.com:rancher/dashboard into 12485-repositories-disabling-feature

This commit is contained in:
Mo Mesgin 2025-01-20 15:22:19 -08:00
commit b8c39e99df
145 changed files with 2660 additions and 1424 deletions

25
.github/dependabot.yml vendored Normal file
View File

@ -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"]

View File

@ -9,7 +9,7 @@ jobs:
permissions: permissions:
issues: write issues: write
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- name: Use Node.js - name: Use Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:

View File

@ -22,7 +22,7 @@ jobs:
name: Build & Upload Hosted name: Build & Upload Hosted
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
fetch-depth: 1 fetch-depth: 1
@ -69,7 +69,7 @@ jobs:
name: Build & Upload Embedded name: Build & Upload Embedded
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
fetch-depth: 1 fetch-depth: 1

View File

@ -103,7 +103,7 @@ jobs:
- name: Upload charts artifact - name: Upload charts artifact
if: github.ref_type == 'tag' || (github.ref == 'refs/heads/main' && github.event_name != 'pull_request') || inputs.is_test == 'true' 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: with:
name: charts name: charts
path: tmp path: tmp
@ -132,7 +132,7 @@ jobs:
git config user.email 'github-actions[bot]@users.noreply.github.com' git config user.email 'github-actions[bot]@users.noreply.github.com'
- name: Download build artifact - name: Download build artifact
uses: actions/download-artifact@v3 uses: actions/download-artifact@v4
with: with:
name: charts name: charts

View File

@ -9,7 +9,7 @@ jobs:
unit-test: unit-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
fetch-depth: 1 fetch-depth: 1
@ -19,7 +19,7 @@ jobs:
i18n: i18n:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
fetch-depth: 1 fetch-depth: 1
@ -29,7 +29,7 @@ jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
fetch-depth: 1 fetch-depth: 1

View File

@ -26,7 +26,7 @@ jobs:
shell: bash shell: bash
- name: Upload files - name: Upload files
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
if: failure() if: failure()
with: with:
name: test name: test

View File

@ -12,7 +12,7 @@ jobs:
name: Build name: Build
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version-file: '.nvmrc' node-version-file: '.nvmrc'

View File

@ -30,7 +30,7 @@ jobs:
] ]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
fetch-depth: 1 fetch-depth: 1
- name: Setup env - name: Setup env
@ -60,11 +60,17 @@ jobs:
# Upload to sorry cypress in case of failure # Upload to sorry cypress in case of failure
- name: Upload screenshots - name: Upload screenshots
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
if: ${{ failure() }} if: ${{ failure() }}
with: 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 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 # Slack message with outcome - success
- name: Slack message in workflow success - name: Slack message in workflow success
@ -88,4 +94,4 @@ jobs:
curl -X POST \ curl -X POST \
-H "Content-type: application/json; charset=utf-8" \ -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 }} }' \ --data '{"name": "${{ matrix.rancherEnv[0] }} - image:${{ matrix.rancherEnv[1] }} - ${{ matrix.features[1] }}", "status": "Workflow failed! 🆘", "workflow_run": ${{ github.run_id }} }' \
$SLACK_URL $SLACK_URL

View File

@ -12,7 +12,7 @@ jobs:
id-token: write id-token: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- name: Use Node.js - name: Use Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:

View File

@ -12,7 +12,7 @@ jobs:
contents: read contents: read
packages: write packages: write
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false

View File

@ -33,7 +33,7 @@ jobs:
steps: steps:
- if: inputs.is_test == 'true' && inputs.test_branch != '' - if: inputs.is_test == 'true' && inputs.test_branch != ''
name: Checkout (test flow) name: Checkout (test flow)
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
persist-credentials: false persist-credentials: false
ref: ${{ inputs.test_branch }} ref: ${{ inputs.test_branch }}
@ -41,7 +41,7 @@ jobs:
- if: inputs.is_test != 'true' - if: inputs.is_test != 'true'
name: Checkout (normal flow) name: Checkout (normal flow)
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false

View File

@ -30,7 +30,7 @@ jobs:
SHELL_TAG: ${{ steps.retrieve-data.outputs.SHELL_TAG }} SHELL_TAG: ${{ steps.retrieve-data.outputs.SHELL_TAG }}
CREATORS_TAG: ${{ steps.retrieve-data.outputs.CREATORS_TAG }} CREATORS_TAG: ${{ steps.retrieve-data.outputs.CREATORS_TAG }}
ECI_TAG: ${{ steps.retrieve-data.outputs.ECI_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 }} CURR_JOB_ID: ${{ github.job }}
tags-job-status: ${{ job.status }} tags-job-status: ${{ job.status }}
steps: steps:
@ -53,7 +53,7 @@ jobs:
pages: write pages: write
with: with:
target_branch: 'gh-pages' 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' is_test: 'true'
test_ext_repo: 'ui-plugin-examples' test_ext_repo: 'ui-plugin-examples'
test_ext_branch: 'main' test_ext_branch: 'main'

View File

@ -17,7 +17,7 @@ jobs:
SHELL_TAG: ${{ steps.retrieve-data.outputs.SHELL_TAG }} SHELL_TAG: ${{ steps.retrieve-data.outputs.SHELL_TAG }}
CREATORS_TAG: ${{ steps.retrieve-data.outputs.CREATORS_TAG }} CREATORS_TAG: ${{ steps.retrieve-data.outputs.CREATORS_TAG }}
ECI_TAG: ${{ steps.retrieve-data.outputs.ECI_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 }} CURR_JOB_ID: ${{ github.job }}
tags-job-status: ${{ job.status }} tags-job-status: ${{ job.status }}
steps: steps:
@ -40,7 +40,7 @@ jobs:
pages: write pages: write
with: with:
target_branch: 'gh-pages' 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' is_test: 'true'
test_ext_repo: 'elemental-ui' test_ext_repo: 'elemental-ui'
test_ext_branch: 'release-2.8.x' test_ext_branch: 'release-2.8.x'

View File

@ -17,7 +17,7 @@ jobs:
SHELL_TAG: ${{ steps.retrieve-data.outputs.SHELL_TAG }} SHELL_TAG: ${{ steps.retrieve-data.outputs.SHELL_TAG }}
CREATORS_TAG: ${{ steps.retrieve-data.outputs.CREATORS_TAG }} CREATORS_TAG: ${{ steps.retrieve-data.outputs.CREATORS_TAG }}
ECI_TAG: ${{ steps.retrieve-data.outputs.ECI_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 }} CURR_JOB_ID: ${{ github.job }}
tags-job-status: ${{ job.status }} tags-job-status: ${{ job.status }}
steps: steps:
@ -40,7 +40,7 @@ jobs:
pages: write pages: write
with: with:
target_branch: 'gh-pages' 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' is_test: 'true'
test_ext_repo: 'elemental-ui' test_ext_repo: 'elemental-ui'
test_ext_branch: 'release-2.9.x' test_ext_branch: 'release-2.9.x'

View File

@ -22,6 +22,8 @@ env:
TEST_BASE_URL: https://127.0.0.1/dashboard TEST_BASE_URL: https://127.0.0.1/dashboard
API: https://127.0.0.1 API: https://127.0.0.1
TEST_PROJECT_ID: rancher-dashboard 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/ 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}} 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) # Build the dashboard to use in tests. When set to false it will grab `latest` from CDN (useful for running e2e tests quickly)

View File

@ -11,7 +11,7 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Validate checklist has been completed - name: Validate checklist has been completed
env: env:

View File

@ -23,7 +23,7 @@ We welcome external contributions - please refer to the internal documentation a
License 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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@ -20,5 +20,6 @@ jobs:
with: with:
registry_target: ghcr.io registry_target: ghcr.io
registry_user: ${{ github.actor }} registry_user: ${{ github.actor }}
tagged_release: ${{ github.ref_name }}
secrets: secrets:
registry_token: ${{ secrets.GITHUB_TOKEN }} registry_token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,7 +1,7 @@
{ {
"name": "@rancher/create-extension", "name": "@rancher/create-extension",
"description": "Rancher UI Extension generator", "description": "Rancher UI Extension generator",
"version": "3.0.6", "version": "3.0.7",
"license": "Apache-2.0", "license": "Apache-2.0",
"author": "SUSE", "author": "SUSE",
"packageManager": "yarn@4.5.0", "packageManager": "yarn@4.5.0",

View File

@ -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): {} { 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); const fakeNavClusterData = generateFakeNavClusterData(fakeProvClusterId, fakeMgmtClusterId, addEditClusterCapabilities);
// add cluster to fleet clusters for testing https://github.com/rancher/dashboard/issues/9984 // add cluster to fleet clusters for testing https://github.com/rancher/dashboard/issues/9984

View File

@ -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"]'));
}
}

View File

@ -14,6 +14,10 @@ export default class CardPo extends ComponentPo {
return this.self().get('[data-testid="card-body-slot"]'); return this.self().get('[data-testid="card-body-slot"]');
} }
getError(): CypressChainable {
return this.self().get('[data-testid="card-body-slot"] > .text-error');
}
getActionButton(): CypressChainable { getActionButton(): CypressChainable {
return this.self().get('[data-testid="card-actions-slot"]'); return this.self().get('[data-testid="card-actions-slot"]');
} }

View File

@ -17,4 +17,12 @@ export default class CreateEditViewPo extends ComponentPo {
nextPage() { nextPage() {
return new AsyncButtonPo(this.self().find('.cru-resource-footer .role-primary')).click(); 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();
}
} }

View File

@ -28,6 +28,12 @@ export class HeaderPo extends ComponentPo {
return wsFilter.clickOptionWithLabel(name); return wsFilter.clickOptionWithLabel(name);
} }
checkCurrentWorkspace(name: string) {
const wsFilter = new WorkspaceSwitcherPo();
return wsFilter.checkOptionSelected(name);
}
importYamlHeaderAction() { importYamlHeaderAction() {
return this.self().find('[data-testid="header-action-import-yaml"]'); return this.self().find('[data-testid="header-action-import-yaml"]');
} }

View File

@ -1,27 +1,35 @@
import PagePo from '@/cypress/e2e/po/pages/page.po'; import PagePo from '@/cypress/e2e/po/pages/page.po';
import AsyncButtonPo from '@/cypress/e2e/po/components/async-button.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 { export default class FleetClusterGroupsCreateEditPo extends PagePo {
private static createPath(clusterId: string, id?: string ) { private static createPath(clusterId: string, workspace?: string, id?: string ) {
const root = `/c/${ clusterId }/explorer/storage.k8s.io.storageclass/create`; 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<Cypress.AUTWindow> { static goTo(path: string): Cypress.Chainable<Cypress.AUTWindow> {
throw new Error('invalid'); throw new Error('invalid');
} }
constructor(clusterId = '_', id?: string) { constructor(clusterId = '_', workspace?: string, id?: string) {
super(FleetClusterGroupsCreateEditPo.createPath(clusterId, id)); super(FleetClusterGroupsCreateEditPo.createPath(clusterId, workspace, id));
} }
editAsYaml() { title() {
return new AsyncButtonPo('[data-testid="form-yaml"]', this.self()); return this.self().get('.title .primaryheader h1');
} }
yamlEditor(): CodeMirrorPo { nameNsDescription() {
return CodeMirrorPo.bySelector(this.self(), '[data-testid="yaml-editor-code-mirror"]'); return new NameNsDescription(this.self());
}
saveCreateForm(): ResourceDetailPo {
return new ResourceDetailPo(this.self());
}
saveButton() {
return new AsyncButtonPo('[data-testid="form-save"]', this.self());
} }
} }

View File

@ -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<Cypress.AUTWindow> {
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"]');
}
}

View File

@ -4,6 +4,7 @@ import ClusterManagerCreatePagePo from '@/cypress/e2e/po/edit/provisioning.cattl
import TabbedPo from '@/cypress/e2e/po/components/tabbed.po'; import TabbedPo from '@/cypress/e2e/po/components/tabbed.po';
import RegistriesTabPo from '@/cypress/e2e/po/components/registries-tab.po'; import RegistriesTabPo from '@/cypress/e2e/po/components/registries-tab.po';
import NetworkTabPo from '@/cypress/e2e/po/components/network-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 * Create page for an RKE2 custom cluster
@ -44,4 +45,8 @@ export default class ClusterManagerCreateRke2CustomPagePo extends ClusterManager
network(): NetworkTabPo { network(): NetworkTabPo {
return new NetworkTabPo(); return new NetworkTabPo();
} }
calicoAddonConfig(): AddonConfigPo {
return new AddonConfigPo();
}
} }

View File

@ -57,7 +57,7 @@ export default class ClusterDashboardPagePo extends PagePo {
} }
fullEventsLink() { fullEventsLink() {
return cy.get('.events-table-link').contains('Full events list'); return cy.get('[data-testid="events-link"]').contains('Full events list');
} }
fullSecretsList() { fullSecretsList() {

View File

@ -7,7 +7,7 @@ import RepositoriesPagePo from '@/cypress/e2e/po/pages/chart-repositories.po';
import BannersPo from '@/cypress/e2e/po/components/banners.po'; import BannersPo from '@/cypress/e2e/po/components/banners.po';
import ChartRepositoriesCreateEditPo from '@/cypress/e2e/po/edit/chart-repositories.po'; import ChartRepositoriesCreateEditPo from '@/cypress/e2e/po/edit/chart-repositories.po';
import AppClusterRepoEditPo from '@/cypress/e2e/po/edit/catalog.cattle.io.clusterrepo.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 { export default class ExtensionsPagePo extends PagePo {
static url = '/c/local/uiplugins' static url = '/c/local/uiplugins'
@ -34,7 +34,7 @@ export default class ExtensionsPagePo extends PagePo {
return this.title().should('contain', 'Extensions'); return this.title().should('contain', 'Extensions');
} }
loading() { loading(options: any) {
return this.self().get('.data-loading'); return this.self().get('.data-loading');
} }
@ -50,12 +50,16 @@ export default class ExtensionsPagePo extends PagePo {
* @returns {Cypress.Chainable} * @returns {Cypress.Chainable}
*/ */
addExtensionsRepository(repo: string, branch: string, name: string): 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 // 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 // go to app repos
this.extensionMenuToggle(); this.extensionMenuToggle();
this.manageReposClick(); this.manageReposClick();
cy.wait('@getRepos').its('response.statusCode').should('eq', 200);
// create a new clusterrepo // create a new clusterrepo
const appRepoList = new RepositoriesPagePo('local', 'apps'); const appRepoList = new RepositoriesPagePo('local', 'apps');
@ -82,6 +86,8 @@ export default class ExtensionsPagePo extends PagePo {
appRepoList.waitForPage(); appRepoList.waitForPage();
appRepoList.list().state(name).should('contain', 'Active'); appRepoList.list().state(name).should('contain', 'Active');
return cy.wrap(appRepoList.list());
} }
/** /**

View File

@ -2,6 +2,7 @@ import PagePo from '@/cypress/e2e/po/pages/page.po';
import ResourceTablePo from '@/cypress/e2e/po/components/resource-table.po'; import ResourceTablePo from '@/cypress/e2e/po/components/resource-table.po';
import BurgerMenuPo from '@/cypress/e2e/po/side-bars/burger-side-menu.po'; import BurgerMenuPo from '@/cypress/e2e/po/side-bars/burger-side-menu.po';
import { LONG_TIMEOUT_OPT } from '@/cypress/support/utils/timeouts'; 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 { export class FleetDashboardPagePo extends PagePo {
static url: string; static url: string;
@ -44,4 +45,20 @@ export class FleetDashboardPagePo extends PagePo {
sortableTable(tableName = 'fleet-local') { sortableTable(tableName = 'fleet-local') {
return this.resourceTable(tableName).sortableTable(); 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');
}
} }

View File

@ -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 FleetClusterGroupsList from '@/cypress/e2e/po/lists/fleet/fleet.cattle.io.clustergroup';
import FleetClusterGroupsCreateEditPo from '@/cypress/e2e/po/edit/fleet/fleet.cattle.io.clustergroup.po'; import FleetClusterGroupsCreateEditPo from '@/cypress/e2e/po/edit/fleet/fleet.cattle.io.clustergroup.po';
export class FleetClusterGroupsListPagePo extends PagePo { export class FleetClusterGroupsListPagePo extends PagePo {
static url = `/c/_/fleet/fleet.cattle.io.clustergroup` private static createPath(clusterId: string) {
return `/c/${ clusterId }/fleet/fleet.cattle.io.clustergroup`;
constructor() {
super(FleetClusterGroupsListPagePo.url);
} }
goTo() { static goTo(clusterId: string): Cypress.Chainable<Cypress.AUTWindow> {
return cy.visit(FleetClusterGroupsListPagePo.url); return super.goTo(FleetClusterGroupsListPagePo.createPath(clusterId));
}
constructor(private clusterId = '_') {
super(FleetClusterGroupsListPagePo.createPath(clusterId));
} }
static navTo() { static navTo() {
@ -44,7 +46,7 @@ export class FleetClusterGroupsListPagePo extends PagePo {
return this.self().find('[data-testid="masthead-create"]').click(); return this.self().find('[data-testid="masthead-create"]').click();
} }
createFleetClusterGroupsForm(id? : string): FleetClusterGroupsCreateEditPo { createFleetClusterGroupsForm(workspace?: string, id? : string): FleetClusterGroupsCreateEditPo {
return new FleetClusterGroupsCreateEditPo(id); return new FleetClusterGroupsCreateEditPo(this.clusterId, workspace, id);
} }
} }

View File

@ -54,6 +54,17 @@ export class FeatureFlagsPagePo extends RootClusterPage {
return card.getBody().contains(label); 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 * Click action button
* @param label Activate or Deactivate * @param label Activate or Deactivate

View File

@ -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 ProductNavPo from '@/cypress/e2e/po/side-bars/product-side-nav.po';
import { generateFakeClusterDataAndIntercepts } from '@/cypress/e2e/blueprints/nav/fake-cluster'; 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 fakeProvClusterId = 'some-fake-cluster-id';
const fakeMgmtClusterId = 'some-fake-mgmt-id'; const fakeMgmtClusterId = 'some-fake-mgmt-id';

View File

@ -6,6 +6,8 @@ const chartsPage = new ChartsPage();
describe('Apps/Charts', { tags: ['@explorer', '@adminUser'] }, () => { describe('Apps/Charts', { tags: ['@explorer', '@adminUser'] }, () => {
beforeEach(() => { beforeEach(() => {
cy.intercept('GET', '/v1/catalog.cattle.io.clusterrepos/**').as('fetchChartData');
cy.login(); cy.login();
chartsPage.goTo(); chartsPage.goTo();
chartsPage.waitForPage(); chartsPage.waitForPage();
@ -94,6 +96,9 @@ describe('Apps/Charts', { tags: ['@explorer', '@adminUser'] }, () => {
it('should call fetch when route query changes with valid parameters', () => { it('should call fetch when route query changes with valid parameters', () => {
const chartName = 'Logging'; const chartName = 'Logging';
cy.wait('@fetchChartData');
cy.get('@fetchChartData.all').should('have.length.at.least', 3);
chartsPage.getChartByName(chartName) chartsPage.getChartByName(chartName)
.should('exist') .should('exist')
.scrollIntoView() .scrollIntoView()
@ -105,16 +110,19 @@ describe('Apps/Charts', { tags: ['@explorer', '@adminUser'] }, () => {
chartPage.waitForPage(); chartPage.waitForPage();
// Set up intercept for the network request triggered by $fetch // 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'); 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', () => { it('should not call fetch when navigating back to charts page', () => {
const chartName = 'Logging'; const chartName = 'Logging';
cy.wait('@fetchChartData');
cy.get('@fetchChartData.all').should('have.length.at.least', 3);
chartsPage.getChartByName(chartName) chartsPage.getChartByName(chartName)
.should('exist') .should('exist')
.scrollIntoView() .scrollIntoView()

View File

@ -172,7 +172,7 @@ describe('Apps', () => {
cy.intercept('GET', '/v1/catalog.cattle.io.clusterrepos/rancher-charts?*').as('rancherCharts1'); cy.intercept('GET', '/v1/catalog.cattle.io.clusterrepos/rancher-charts?*').as('rancherCharts1');
// Nav to a summary page for a specific chart // Nav to a summary page for a specific chart
ChartsPage.navTo(clusterId); chartsPage.goTo();
chartsPage.chartsFilterCategoriesSelect().toggle(); chartsPage.chartsFilterCategoriesSelect().toggle();
chartsPage.chartsFilterCategoriesSelect().clickOptionWithLabel('All Categories'); chartsPage.chartsFilterCategoriesSelect().clickOptionWithLabel('All Categories');
chartsPage.chartsFilterReposSelect().toggle(); chartsPage.chartsFilterReposSelect().toggle();

View File

@ -48,7 +48,7 @@ describe('Cluster Dashboard', { testIsolation: 'off', tags: ['@explorer', '@admi
clusterDashboard.waitForPage(undefined, 'cluster-events'); 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'); 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'] }, () => { 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(); eventsNoDataset();
clusterDashboard.goTo(); 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'); cy.wait('@eventsNoData');
clusterDashboard.waitForPage(undefined, 'cluster-events'); clusterDashboard.waitForPage(undefined, 'cluster-events');
clusterDashboard.eventsList().resourceTable().sortableTable().checkRowCount(true, 1); 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() cy.isVaiCacheEnabled().then((isVaiCacheEnabled) => {
.within('.table-header-container .content') if (isVaiCacheEnabled) {
.each((el, i) => { expectedHeaders = ['Reason', 'Object', 'Message', 'Name', 'First Seen', 'Last Seen', 'Count'];
expect(el.text().trim()).to.eq(expectedHeaders[i]); }
});
clusterDashboard.fullEventsLink().click(); clusterDashboard.eventsList().resourceTable().sortableTable().tableHeaderRow()
cy.wait('@eventsNoData'); .self()
const events = new EventsPagePo('local'); .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', events.eventslist().resourceTable().sortableTable().checkRowCount(true, 1);
'Subobject', 'Source', 'Message', 'First Seen', 'Count', 'Name', 'Namespace'];
events.eventslist().resourceTable().sortableTable().tableHeaderRow() const expectedFullHeaders = ['State', 'Last Seen', 'Type', 'Reason', 'Object',
.within('.table-header-container .content') 'Subobject', 'Source', 'Message', 'First Seen', 'Count', 'Name', 'Namespace'];
.each((el, i) => {
expect(el.text().trim()).to.eq(expectedFullHeaders[i]); 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', () => { describe('Cluster dashboard with limited permissions', () => {

View File

@ -7,6 +7,27 @@ import SortableTablePo from '@/cypress/e2e/po/components/sortable-table.po';
const cluster = 'local'; const cluster = 'local';
const clusterDashboard = new ClusterDashboardPagePo(cluster); const clusterDashboard = new ClusterDashboardPagePo(cluster);
const events = new EventsPagePo(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'] }, () => { describe('Events', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] }, () => {
before(() => { before(() => {
@ -20,7 +41,12 @@ describe('Events', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] },
let nsName2: string; let nsName2: string;
before('set up', () => { before('set up', () => {
cy.updateNamespaceFilter(cluster, 'none', '{\"local\":[]}'); cy.tableRowsPerPageAndPreferences(pageSize, {
clusterName: cluster,
groupBy: 'none',
namespaceFilter: '{\"local\":[]}',
allNamespaces: 'true',
});
cy.createE2EResourceName('ns1').then((ns1) => { cy.createE2EResourceName('ns1').then((ns1) => {
nsName1 = ns1; nsName1 = ns1;
@ -30,7 +56,7 @@ describe('Events', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] },
// create pods // create pods
let i = 0; let i = 0;
while (i < 125) { while (i < podCount) {
const podName = Cypress._.uniqueId(Date.now().toString()); const podName = Cypress._.uniqueId(Date.now().toString());
cy.createPod(nsName1, podName, 'nginx:latest', false, { createNameOptions: { prefixContext: true } }).then((resp) => { 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; 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', () => { 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(); EventsPagePo.navTo();
events.waitForPage(); events.waitForPage();
cy.getRancherResource('v1', 'events').then((resp: Cypress.Response<any>) => { let vaiCacheEnabled = false;
// 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;
// Test break down if less than 400... cy.isVaiCacheEnabled()
expect(count).to.be.greaterThan(400); .then((isVaiCacheEnabled) => {
vaiCacheEnabled = isVaiCacheEnabled;
// pagination is visible return cy.getRancherResource('v1', 'events');
events.sortableTable().pagination().checkVisible(); })
.then((resp: Cypress.Response<any>) => {
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 // pagination is visible
events.sortableTable().pagination().beginningButton().isDisabled(); events.sortableTable().pagination().checkVisible();
events.sortableTable().pagination().leftButton().isDisabled();
events.sortableTable().pagination().rightButton().isEnabled();
events.sortableTable().pagination().endButton().isEnabled();
// check text before navigation const loadingPo = new LoadingPo('.title .resource-loading-indicator');
events.sortableTable().pagination().paginationText().then((el) => {
expect(el.trim()).to.eq(`1 - 100 of ${ count } Events`); 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', () => { it('filter events', () => {
@ -138,7 +207,7 @@ describe('Events', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] },
events.sortableTable().checkVisible(); events.sortableTable().checkVisible();
events.sortableTable().checkLoadingIndicatorNotVisible(); events.sortableTable().checkLoadingIndicatorNotVisible();
events.sortableTable().checkRowCount(false, 100); events.sortableTable().checkRowCount(false, pageSize);
// filter by namespace // filter by namespace
events.sortableTable().filter(nsName2); events.sortableTable().filter(nsName2);
@ -201,7 +270,12 @@ describe('Events', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] },
}); });
after('clean up', () => { 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) // delete namespace (this will also delete all pods in it)
cy.deleteRancherResource('v1', 'namespaces', nsName1); cy.deleteRancherResource('v1', 'namespaces', nsName1);

View File

@ -19,8 +19,11 @@ describe('HorizontalPodAutoscalers', { testIsolation: 'off', tags: ['@explorer',
horizontalPodAutoscalersPage.waitForPage(); horizontalPodAutoscalersPage.waitForPage();
cy.wait('@horizontalpodautoscalerNoData'); 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() horizontalPodAutoscalersPage.list().resourceTable().sortableTable().tableHeaderRow()
.get('.table-header-container .content') .get('.table-header-container .content')
.each((el, i) => { .each((el, i) => {
@ -39,7 +42,7 @@ describe('HorizontalPodAutoscalers', { testIsolation: 'off', tags: ['@explorer',
horizontalPodAutoscalersPage.header().selectNamespaceFilterOption('All Namespaces'); horizontalPodAutoscalersPage.header().selectNamespaceFilterOption('All Namespaces');
// check table headers are visible // 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() horizontalPodAutoscalersPage.list().resourceTable().sortableTable().tableHeaderRow()
.get('.table-header-container .content') .get('.table-header-container .content')
@ -65,7 +68,7 @@ describe('HorizontalPodAutoscalers', { testIsolation: 'off', tags: ['@explorer',
horizontalPodAutoscalersPage.list().resourceTable().sortableTable().groupByButtons(1) horizontalPodAutoscalersPage.list().resourceTable().sortableTable().groupByButtons(1)
.click(); .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']; const expectedHeaders = ['State', 'Name', 'Workload', 'Minimum Replicas', 'Maximum Replicas', 'Current Replicas', 'Age'];
horizontalPodAutoscalersPage.list().resourceTable().sortableTable().tableHeaderRow() horizontalPodAutoscalersPage.list().resourceTable().sortableTable().tableHeaderRow()

View File

@ -8,6 +8,7 @@ import { NamespaceFilterPo } from '@/cypress/e2e/po/components/namespace-filter.
const namespaceFilter = new NamespaceFilterPo(); const namespaceFilter = new NamespaceFilterPo();
const cluster = 'local'; const cluster = 'local';
let removeExtensions = false;
const DISABLED_CACHE_EXTENSION_NAME = 'large-extension'; const DISABLED_CACHE_EXTENSION_NAME = 'large-extension';
// const DISABLED_CACHE_EXTENSION_MENU_LABEL = '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 EXTENSION_NAME = 'clock';
const UI_PLUGINS_PARTNERS_REPO_URL = 'https://github.com/rancher/partner-extensions'; const UI_PLUGINS_PARTNERS_REPO_URL = 'https://github.com/rancher/partner-extensions';
const UI_PLUGINS_PARTNERS_REPO_NAME = 'partner-extensions'; const UI_PLUGINS_PARTNERS_REPO_NAME = 'partner-extensions';
const GIT_REPO_NAME = 'rancher-plugin-examples';
describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => { describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => {
beforeEach(() => { 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 extensionsPo.extensionTabInstalledClick(); // Avoid nav guard failures that probably auto move user to this tab
// install the rancher plugin examples // 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', () => { 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', () => { it('Should install an extension', () => {
cy.intercept('POST', `/v1/catalog.cattle.io.clusterrepos/${ GIT_REPO_NAME }?action=install`).as('installExtension');
const extensionsPo = new ExtensionsPagePo(); const extensionsPo = new ExtensionsPagePo();
extensionsPo.goTo(); extensionsPo.goTo();
extensionsPo.waitForPage();
extensionsPo.extensionTabAvailableClick(); extensionsPo.extensionTabAvailableClick();
extensionsPo.waitForPage(null, 'available'); extensionsPo.waitForPage(null, 'available');
@ -267,6 +273,7 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => {
// select version and click install // select version and click install
extensionsPo.installModalSelectVersionClick(2); extensionsPo.installModalSelectVersionClick(2);
extensionsPo.installModalInstallClick(); extensionsPo.installModalInstallClick();
cy.wait('@installExtension').its('response.statusCode').should('eq', 201);
// let's check the extension reload banner and reload the page // let's check the extension reload banner and reload the page
extensionsPo.extensionReloadBanner().should('be.visible'); extensionsPo.extensionReloadBanner().should('be.visible');
@ -297,6 +304,7 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => {
}); });
it('Should update an extension version', () => { 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(); const extensionsPo = new ExtensionsPagePo();
extensionsPo.goTo(); extensionsPo.goTo();
@ -308,6 +316,7 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => {
// click on update button on card // click on update button on card
extensionsPo.extensionCardUpdateClick(EXTENSION_NAME); extensionsPo.extensionCardUpdateClick(EXTENSION_NAME);
extensionsPo.installModalInstallClick(); extensionsPo.installModalInstallClick();
cy.wait('@upgradeExtension').its('response.statusCode').should('eq', 201);
// let's check the extension reload banner and reload the page // let's check the extension reload banner and reload the page
extensionsPo.extensionReloadBanner().should('be.visible'); extensionsPo.extensionReloadBanner().should('be.visible');
@ -515,4 +524,10 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => {
extensionsPo.extensionCardClick(DISABLED_CACHE_EXTENSION_NAME); extensionsPo.extensionCardClick(DISABLED_CACHE_EXTENSION_NAME);
extensionsPo.extensionDetailsTitle().should('contain', 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);
}
});
}); });

View File

@ -6,8 +6,10 @@ import KubewardenExtensionPo from '@/cypress/e2e/po/pages/extensions/kubewarden.
import { catchTargetPageException } from '@/cypress/support/utils/exception-utils'; import { catchTargetPageException } from '@/cypress/support/utils/exception-utils';
const extensionName = 'kubewarden'; 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(() => { before(() => {
catchTargetPageException('Navigation cancelled'); catchTargetPageException('Navigation cancelled');
cy.login(); cy.login();
@ -18,7 +20,9 @@ describe('Kubewarden Extension', { tags: ['@extensions-temp-excluded', '@adminUs
extensionsPo.waitForPage(); extensionsPo.waitForPage();
// install the ui-plugin-charts repo // 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(() => { beforeEach(() => {
@ -32,6 +36,7 @@ describe('Kubewarden Extension', { tags: ['@extensions-temp-excluded', '@adminUs
extensionsPo.waitForPage(); extensionsPo.waitForPage();
extensionsPo.extensionTabAvailableClick(); extensionsPo.extensionTabAvailableClick();
extensionsPo.waitForPage(null, 'available');
// click on install button on card // click on install button on card
extensionsPo.extensionCardInstallClick(extensionName); extensionsPo.extensionCardInstallClick(extensionName);
@ -110,4 +115,10 @@ describe('Kubewarden Extension', { tags: ['@extensions-temp-excluded', '@adminUs
extensionsPo.extensionCardClick(extensionName); extensionsPo.extensionCardClick(extensionName);
extensionsPo.extensionDetailsTitle().should('contain', extensionName); extensionsPo.extensionDetailsTitle().should('contain', extensionName);
}); });
after(() => {
if ( removeExtensions ) {
cy.deleteRancherResource('v1', 'catalog.cattle.io.clusterrepos', gitRepoName);
}
});
}); });

View File

@ -1,63 +1,147 @@
import { FleetClusterGroupsListPagePo } from '@/cypress/e2e/po/pages/fleet/fleet.cattle.io.clustergroup.po'; 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 FleetClusterGroupDetailsPo from '@/cypress/e2e/po/detail/fleet/fleet.cattle.io.clustergroup.po';
import { HeaderPo } from '@/cypress/e2e/po/components/header.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'] }, () => { describe('Cluster Groups', { testIsolation: 'off', tags: ['@fleet', '@adminUser'] }, () => {
const fleetClusterGroups = new FleetClusterGroupsListPagePo(); const fleetClusterGroups = new FleetClusterGroupsListPagePo();
const headerPo = new HeaderPo(); const headerPo = new HeaderPo();
const localWorkspace = 'fleet-local';
let clusterGroupName;
let removeClusterGroups = false;
const clusterGroupsToDelete = [];
describe('List', { tags: ['@vai', '@adminUser'] }, () => { before(() => {
before(() => { cy.login();
cy.login(); cy.createE2EResourceName('cluster-group').then((name) => {
}); clusterGroupName = name;
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]);
});
}); });
}); });
describe('Edit', { tags: ['@vai', '@adminUser'] }, () => {
before(() => {
cy.login();
});
it('can open "Edit as YAML"', () => { it('can create cluster group', () => {
FleetClusterGroupsListPagePo.navTo(); FleetClusterGroupsListPagePo.navTo();
fleetClusterGroups.waitForPage(); fleetClusterGroups.waitForPage();
fleetClusterGroups.clickCreate(); headerPo.selectWorkspace(localWorkspace);
fleetClusterGroups.createFleetClusterGroupsForm().editAsYaml().click(); fleetClusterGroups.clickCreate();
fleetClusterGroups.createFleetClusterGroupsForm().yamlEditor().checkExists(); 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));
}
}); });
}); });

View File

@ -1,65 +1,216 @@
import { FleetDashboardPagePo } from '@/cypress/e2e/po/pages/fleet/fleet-dashboard.po'; import { FleetDashboardPagePo } from '@/cypress/e2e/po/pages/fleet/fleet-dashboard.po';
// import { GitRepoCreatePo } from '@/cypress/e2e/po/pages/fleet/gitrepo-create.po'; import FleetGitRepoDetailsPo from '@/cypress/e2e/po/detail/fleet/fleet.cattle.io.gitrepo.po';
// import BurgerMenuPo from '@/cypress/e2e/po/side-bars/burger-side-menu.po'; import { GitRepoCreatePo } from '@/cypress/e2e/po/pages/fleet/gitrepo-create.po';
// import { LONG_TIMEOUT_OPT } from '@/cypress/support/utils/timeouts'; 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'] }, () => { describe('Fleet Dashboard', { tags: ['@fleet', '@adminUser', '@jenkins'] }, () => {
let fleetDashboardPage: FleetDashboardPagePo; const fleetDashboardPage = new FleetDashboardPagePo('_');
// const repoName = 'fleet-e2e-test-dashboard'; 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) let repoName;
// Enabling the test results results in consistent failures (bundle does not become ready). For the short term comment these out 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(() => { beforeEach(() => {
cy.login(); cy.login();
fleetDashboardPage = new FleetDashboardPagePo('_'); cy.createE2EResourceName('git-repo').then((name) => {
fleetDashboardPage.goTo(); repoName = name;
});
}); });
it('has the correct title', () => { 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'); cy.title().should('eq', 'Rancher - Continuous Delivery - Dashboard');
}); });
// before(() => { it('Get Started button takes you to the correct page', () => {
// cy.login(); 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); fleetDashboardPage.goTo();
// gitRepoCreatePage.selectWorkspace('fleet-local'); fleetDashboardPage.waitForPage();
// 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();
// });
// it('Should display cluster status', () => { // check if burguer menu nav is highlighted correctly for Fleet
// // check if burguer menu nav is highlighted correctly for Fleet BurgerMenuPo.checkIfMenuItemLinkIsHighlighted('Continuous Delivery');
// 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"]', 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"] span').should('have.text', '1/1');
// row.get('.bg-success[data-testid="bundles-ready"]').should('exist'); 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"] span').should('have.text', '1/1');
// row.get('.bg-success[data-testid="resources-ready"]').should('exist'); 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"] span').should('have.text', '1/1');
// }); });
// after(() => { it('can navigate to Git Repo details page from Fleet Dashboard', () => {
// fleetDashboardPage = new FleetDashboardPagePo('_'); const gitRepoDetails = new FleetGitRepoDetailsPo(localWorkspace, repoName);
// fleetDashboardPage.goTo();
// 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));
}
});
}); });

View File

@ -191,6 +191,43 @@ describe('Feature Flags', { testIsolation: 'off' }, () => {
sideNav.groups().contains('Legacy').should('not.exist'); 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'] }, () => { it('standard user has only read access to Feature Flag page', { tags: ['@globalSettings', '@standardUser'] }, () => {
// verify action menus are hidden for standard user // verify action menus are hidden for standard user
@ -219,7 +256,7 @@ describe('Feature Flags', { testIsolation: 'off' }, () => {
it('validate feature flags table header content', () => { it('validate feature flags table header content', () => {
FeatureFlagsPagePo.navTo(); FeatureFlagsPagePo.navTo();
// check table headers are visible // check table headers are visible
const expectedHeaders = ['State', 'Name', 'Description', 'Restart Required']; const expectedHeaders = ['State', 'Name', 'Description', 'Restart Rancher'];
featureFlagsPage.list().resourceTable().sortableTable().tableHeaderRow() featureFlagsPage.list().resourceTable().sortableTable().tableHeaderRow()
.get('.table-header-container .content') .get('.table-header-container .content')

View File

@ -39,6 +39,7 @@ const clusterNamePartial = `${ runPrefix }-create`;
const rke1CustomName = `${ clusterNamePartial }-rke1-custom`; const rke1CustomName = `${ clusterNamePartial }-rke1-custom`;
const rke2CustomName = `${ clusterNamePartial }-rke2-custom`; const rke2CustomName = `${ clusterNamePartial }-rke2-custom`;
const importGenericName = `${ clusterNamePartial }-import-generic`; const importGenericName = `${ clusterNamePartial }-import-generic`;
let reenableAKS = false;
const downloadsFolder = Cypress.config('downloadsFolder'); 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', () => { 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 driversPage = new KontainerDriversPagePo();
const clusterCreatePage = new ClusterManagerCreatePagePo(); const clusterCreatePage = new ClusterManagerCreatePagePo();
// deactivate the AKS driver
KontainerDriversPagePo.navTo(); KontainerDriversPagePo.navTo();
driversPage.waitForPage(); 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(); driversPage.list().actionMenu('Azure AKS').getMenuItem('Deactivate').click();
const deactivateDialog = new DeactivateDriverDialogPo(); const deactivateDialog = new DeactivateDriverDialogPo();
deactivateDialog.deactivate(); deactivateDialog.deactivate();
cy.wait('@deactivateDriver').its('response.statusCode').should('eq', 200).then(() => {
reenableAKS = true;
});
// verify that the AKS card is not shown // verify that the AKS card is not shown
clusterList.goTo(); clusterList.goTo();
@ -112,6 +132,9 @@ describe('Cluster Manager', { testIsolation: 'off', tags: ['@manager', '@adminUs
KontainerDriversPagePo.navTo(); KontainerDriversPagePo.navTo();
driversPage.waitForPage(); driversPage.waitForPage();
driversPage.list().actionMenu('Azure AKS').getMenuItem('Activate').click(); 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 // verify that the AKS card is back
clusterList.goTo(); clusterList.goTo();
@ -311,6 +334,31 @@ describe('Cluster Manager', { testIsolation: 'off', tags: ['@manager', '@adminUs
editCreatedClusterPage.nameNsDescription().description().self().should('have.value', rke2CustomName); 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', () => { it('can view cluster YAML editor', () => {
clusterList.goTo(); clusterList.goTo();
clusterList.list().actionMenu(rke2CustomName).getMenuItem('Edit YAML').click(); 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', {});
}
});
}); });

View File

@ -99,7 +99,8 @@ declare global {
deleteRancherResource(prefix: 'v3' | 'v1' | 'k8s', resourceType: string, resourceId: string, failOnStatusCode?: boolean): Chainable; deleteRancherResource(prefix: 'v3' | 'v1' | 'k8s', resourceType: string, resourceId: string, failOnStatusCode?: boolean): Chainable;
deleteNodeTemplate(nodeTemplateId: string, timeout?: number, failOnStatusCode?: boolean) 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 * update namespace filter
@ -162,6 +163,11 @@ declare global {
* Fetch the steve `revision` / timestamp of request * Fetch the steve `revision` / timestamp of request
*/ */
fetchRevision(): Chainable<string>; fetchRevision(): Chainable<string>;
/**
* Check if the vai FF is enabled
*/
isVaiCacheEnabled(): Chainable<boolean>;
} }
} }
} }

View File

@ -497,8 +497,8 @@ Cypress.Commands.add('createRancherResource', (prefix, resourceType, body) => {
body body
}) })
.then((resp) => { .then((resp) => {
// Expect 201, Created HTTP status code // Expect 200 or 201, Created HTTP status code
expect(resp.status).to.eq(201); 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<any>) => { return cy.getRancherResource('v3', 'users?me=true').then((resp: Cypress.Response<any>) => {
const userId = resp.body.data[0].id.trim(); const userId = resp.body.data[0].id.trim();
const payload = { const payload = {
id: `${ userId }`, id: `${ userId }`,
type: 'userpreference', type: 'userpreference',
data: { data: {
cluster: clusterName, cluster: clusterName,
'per-page': `${ rows }`, 'per-page': `${ rows }`,
'group-by': groupBy, 'group-by': groupBy,
'ns-by-cluster': namespaceFilter '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(() => { cy.setRancherResource('v1', 'userpreferences', userId, payload).then(() => {
return cy.waitForRancherResource('v1', 'userpreferences', userId, (resp: any) => compare(resp?.body, payload)) return cy.waitForRancherResource('v1', 'userpreferences', userId, (resp: any) => compare(resp?.body, payload))
.then((res) => { .then((res) => {
if (res) { if (res) {
cy.log(`tableRowsPerPageAndNamespaceFilter: Success!`); cy.log(`tableRowsPerPageAndPreferences: Success!`);
} else { } else {
if (iteration < 3) { 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
});
});

View File

@ -8,4 +8,7 @@ export enum MenuActions {
ViewInApi = 'View in API', // eslint-disable-line no-unused-vars ViewInApi = 'View in API', // eslint-disable-line no-unused-vars
ChangeWorkspace = 'Change workspace', // eslint-disable-line no-unused-vars ChangeWorkspace = 'Change workspace', // eslint-disable-line no-unused-vars
Delete = 'Delete', // 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
} }

View File

@ -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"
},
...
}
```

View File

@ -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. 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. 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 ### Pipeline Configuration
There are a few pipeline configuration options, mostly tied to the container registry: There are a few pipeline configuration options, mostly tied to the container registry:

View File

@ -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. 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 ## 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. 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.

View File

@ -77,10 +77,13 @@ const config = {
'@docusaurus/plugin-client-redirects', '@docusaurus/plugin-client-redirects',
{ {
createRedirects(existingPath) { createRedirects(existingPath) {
if (existingPath.includes('/extensions') && !existingPath.includes('/next') && !existingPath.includes('/v2')) { // This function is invoked once per existing doc page, and we
return [ // must return the “old” routes that we want to map to that docs path
existingPath.replace('/extensions', '/extensions/next') 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 return undefined; // Return a falsy value: no redirect created

View File

@ -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. 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. 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 ### Pipeline Configuration
There are a few pipeline configuration options, mostly tied to the container registry: There are a few pipeline configuration options, mostly tied to the container registry:

View File

@ -26,7 +26,7 @@
"install:ci": "yarn install --frozen-lockfile", "install:ci": "yarn install --frozen-lockfile",
"dev": "bash -c 'source ./scripts/version && NODE_ENV=dev ./node_modules/.bin/vue-cli-service serve'", "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'", "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", "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": "NODE_OPTIONS=--max_old_space_size=4096 ./node_modules/.bin/vue-cli-service build",
"build:lib": "cd pkg/rancher-components && yarn build:lib", "build:lib": "cd pkg/rancher-components && yarn build:lib",
@ -62,7 +62,7 @@
"@aws-sdk/client-iam": "3.658.1", "@aws-sdk/client-iam": "3.658.1",
"@aws-sdk/client-kms": "3.8.1", "@aws-sdk/client-kms": "3.8.1",
"@novnc/novnc": "1.2.0", "@novnc/novnc": "1.2.0",
"@popperjs/core": "2.4.4", "@popperjs/core": "2.11.8",
"@rancher/icons": "2.0.29", "@rancher/icons": "2.0.29",
"ansi_up": "5.0.0", "ansi_up": "5.0.0",
"axios": "0.21.4", "axios": "0.21.4",
@ -77,11 +77,10 @@
"cookie-universal": "2.2.2", "cookie-universal": "2.2.2",
"cron-validator": "1.2.0", "cron-validator": "1.2.0",
"cronstrue": "1.95.0", "cronstrue": "1.95.0",
"cross-env": "6.0.3", "cross-env": "7.0.3",
"custom-event-polyfill": "1.0.7", "custom-event-polyfill": "1.0.7",
"d3": "7.3.0", "d3": "7.3.0",
"d3-selection": "1.4.1", "d3-selection": "1.4.1",
"dagre-d3": "0.6.4",
"dayjs": "1.8.29", "dayjs": "1.8.29",
"defu": "5.0.1", "defu": "5.0.1",
"diff2html": "3.4.24", "diff2html": "3.4.24",
@ -96,9 +95,8 @@
"intl-messageformat": "7.8.4", "intl-messageformat": "7.8.4",
"ip": "2.0.1", "ip": "2.0.1",
"ipaddr.js": "2.2.0", "ipaddr.js": "2.2.0",
"is-base64": "1.1.0",
"is-url": "1.2.4", "is-url": "1.2.4",
"jexl": "2.2.2", "jexl": "2.3.0",
"jquery": "3.5.1", "jquery": "3.5.1",
"js-cookie": "2.2.1", "js-cookie": "2.2.1",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",

View File

@ -645,7 +645,7 @@ export default defineComponent({
virtualNetwork: { virtualNetwork: {
get() { 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}) { set(neu: {label: string, kind?: string, disabled?: boolean, value?: string, virtualNetwork?: any}) {
if (neu.label === this.t('generic.none')) { if (neu.label === this.t('generic.none')) {
@ -804,8 +804,6 @@ export default defineComponent({
return; return;
} }
this.loadingLocations = true; 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; const { azureCredentialSecret } = this.config;
@ -839,6 +837,11 @@ export default defineComponent({
errors.push(this.t('aks.errors.regions', { e: parsedError || err })); 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<void> { async getAksVersions(): Promise<void> {

View File

@ -240,7 +240,7 @@ describe('aks provisioning form', () => {
it('should display subnets grouped by network in the virtual network dropdown', async() => { it('should display subnets grouped by network in the virtual network dropdown', async() => {
const noneOption = { label: 'generic.none' }; const noneOption = { label: 'generic.none' };
const config = { const config = {
dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc' dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc', resourceLocation: 'eastus'
}; };
const wrapper = shallowMount(CruAks, { const wrapper = shallowMount(CruAks, {
propsData: { propsData: {
@ -254,7 +254,6 @@ describe('aks provisioning form', () => {
const networkOpts = virtualNetworkSelect.props().options; const networkOpts = virtualNetworkSelect.props().options;
expect(virtualNetworkSelect.props().value).toStrictEqual(noneOption); expect(virtualNetworkSelect.props().value).toStrictEqual(noneOption);
expect(networkOpts).toStrictEqual([{ label: 'generic.none' }, { expect(networkOpts).toStrictEqual([{ label: 'generic.none' }, {
disabled: true, kind: 'group', label: 'network2' 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() => { it('should prevent saving if a node pool has taints missing keys or values', async() => {
const config = { const config = {
dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc' dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc', resourceLocation: 'eastus'
}; };
const wrapper = shallowMount(CruAks, { const wrapper = shallowMount(CruAks, {
propsData: { 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 }) => { ])('should set virtualNetwork, virtualNetworkResourceGroup, and subnet when a virtual network is selected', async(optionIndex, { virtualNetwork, virtualNetworkResourceGroup, subnet }) => {
const config = { const config = {
dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc' dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc', resourceLocation: 'eastus'
}; };
const wrapper = shallowMount(CruAks, { const wrapper = shallowMount(CruAks, {
propsData: { 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() => { 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 = { const config = {
dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc' dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc', resourceLocation: 'eastus'
}; };
const wrapper = shallowMount(CruAks, { const wrapper = shallowMount(CruAks, {
propsData: { 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() => { it('should clear virtualNetwork, virtualNetworkResourceGroup, and subnet when the \'none\' virtual network option is selected', async() => {
const config = { const config = {
dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc' dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc', resourceLocation: 'eastus'
}; };
const wrapper = shallowMount(CruAks, { const wrapper = shallowMount(CruAks, {
propsData: { propsData: {
@ -430,7 +429,7 @@ describe('aks provisioning form', () => {
name: 'abc', _validation: {}, _isNewOrUnprovisioned: false, orchestratorVersion: originalVersion name: 'abc', _validation: {}, _isNewOrUnprovisioned: false, orchestratorVersion: originalVersion
}]; }];
const config = { 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, { const wrapper = shallowMount(CruAks, {
propsData: { propsData: {
@ -451,7 +450,7 @@ describe('aks provisioning form', () => {
it('should clear config.logAnalyticsWorkspaceName and config.logAnalyticsWorkspaceGroup when the monitoring checkbox is unchecked', async() => { it('should clear config.logAnalyticsWorkspaceName and config.logAnalyticsWorkspaceGroup when the monitoring checkbox is unchecked', async() => {
const config = { 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, { const wrapper = shallowMount(CruAks, {
propsData: { propsData: {

View File

@ -163,8 +163,8 @@ aks:
availabilityZones: Availability zones are not available in the selected region. 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 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. 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. poolCount: Node count must be at least 1 and at most 1000 in System pools.
poolUserCount: Node count cannot be less than zero. 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. 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. 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. poolMax: The maximum number of nodes must be greater than 0 and at most 1000.

View File

@ -140,19 +140,24 @@ describe('fx: nodePoolNames', () => {
describe('fx: nodePoolCount', () => { describe('fx: nodePoolCount', () => {
// AksNodePool unit tests check that the second arg is passed in as expected // 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); const validator = validators.nodePoolCount(mockCtx);
expect(validator(1, false)).toBeUndefined(); expect(validator(1, false)).toBeUndefined();
expect(validator(0, false)).toStrictEqual(MOCK_TRANSLATION); 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); const validator = validators.nodePoolCount(mockCtx);
expect(validator(1, true)).toBeUndefined(); expect(validator(1, true)).toBeUndefined();
expect(validator(0, true)).toBeUndefined(); expect(validator(0, true)).toBeUndefined();
expect(validator(1000, true)).toBeUndefined();
expect(validator(-1, true)).toStrictEqual(MOCK_TRANSLATION); 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', () => { 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: 'klm', _validation: {}, mode: 'User', count: -1
},
{
name: 'nop', _validation: {}, mode: 'User', count: 1001
},
{
name: 'qrs', _validation: {}, mode: 'System', count: 1001
} }
] as unknown as AKSNodePool[] ] as unknown as AKSNodePool[]
}; };
@ -180,5 +191,7 @@ describe('fx: nodePoolCount', () => {
expect(ctx.nodePools[1]?._validation?._validCount).toStrictEqual(true); expect(ctx.nodePools[1]?._validation?._validCount).toStrictEqual(true);
expect(ctx.nodePools[2]?._validation?._validCount).toStrictEqual(true); expect(ctx.nodePools[2]?._validation?._validCount).toStrictEqual(true);
expect(ctx.nodePools[3]?._validation?._validCount).toStrictEqual(false); expect(ctx.nodePools[3]?._validation?._validCount).toStrictEqual(false);
expect(ctx.nodePools[4]?._validation?._validCount).toStrictEqual(false);
expect(ctx.nodePools[5]?._validation?._validCount).toStrictEqual(false);
}); });
}); });

View File

@ -181,6 +181,7 @@ export const nodePoolNamesUnique = (ctx: any) => {
export const nodePoolCount = (ctx:any) => { export const nodePoolCount = (ctx:any) => {
return (count?: number, canBeZero = false) => { return (count?: number, canBeZero = false) => {
let min = 1; let min = 1;
const max = 1000;
let errMsg = ctx.t('aks.errors.poolCount'); let errMsg = ctx.t('aks.errors.poolCount');
if (canBeZero) { if (canBeZero) {
@ -188,7 +189,7 @@ export const nodePoolCount = (ctx:any) => {
errMsg = ctx.t('aks.errors.poolUserCount'); errMsg = ctx.t('aks.errors.poolUserCount');
} }
if (count || count === 0) { if (count || count === 0) {
return count >= min ? undefined : errMsg; return count >= min && count <= max ? undefined : errMsg;
} else { } else {
let allValid = true; let allValid = true;
@ -201,7 +202,7 @@ export const nodePoolCount = (ctx:any) => {
min = 1; min = 1;
} }
if (count < min) { if (count < min || count > max) {
pool._validation['_validCount'] = false; pool._validation['_validCount'] = false;
allValid = false; allValid = false;
} else { } else {

View File

@ -704,6 +704,7 @@ export default defineComponent({
label-key="generic.name" label-key="generic.name"
required required
:rules="fvGetAndReportPathRules('clusterName')" :rules="fvGetAndReportPathRules('clusterName')"
data-testid="gke-cluster-name"
@update:value="setClusterName" @update:value="setClusterName"
/> />
</div> </div>
@ -713,6 +714,7 @@ export default defineComponent({
:mode="mode" :mode="mode"
label-key="nameNsDescription.description.label" label-key="nameNsDescription.description.label"
:placeholder="t('nameNsDescription.description.placeholder')" :placeholder="t('nameNsDescription.description.placeholder')"
data-testid="gke-cluster-description"
@update:value="setClusterDescription" @update:value="setClusterDescription"
/> />
</div> </div>

View File

@ -203,9 +203,15 @@ export default defineComponent({
networkOptions(neu) { networkOptions(neu) {
if (neu && neu.length && !this.network) { 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);
}
} }
}, },

View File

@ -67,7 +67,7 @@ describe('gke Networking', () => {
expect(spy).toHaveBeenCalledTimes(4); 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 setup = requiredSetup();
const wrapper = shallowMount(Networking, { const wrapper = shallowMount(Networking, {
@ -85,8 +85,8 @@ describe('gke Networking', () => {
const networksDropdown = wrapper.getComponent('[data-testid="gke-networks-dropdown"]'); const networksDropdown = wrapper.getComponent('[data-testid="gke-networks-dropdown"]');
expect(networksDropdown.props().options).toHaveLength(4); expect(networksDropdown.props().options).toHaveLength(5);
expect(wrapper.emitted('update:network')?.[0]?.[0]).toBe('host-shared-vpc'); expect(wrapper.emitted('update:network')?.[0]?.[0]).toBe('default');
}); });
it('should populate subnetworks dropdown dependent on network selection', async() => { it('should populate subnetworks dropdown dependent on network selection', async() => {

View File

@ -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/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/asia-east2/subnetworks/test-network',
'https://www.googleapis.com/compute/v1/projects/test-project/regions/me-central2/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' }], 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', selfLink: 'https://www.googleapis.com/compute/v1/projects/test-project/regions/us-central1/subnetworks/test-network',
stackType: 'IPV4_ONLY', 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 = { const mockedGKESharedSubnetworksResponse = {

View File

@ -3,7 +3,7 @@ import draggable from 'vuedraggable';
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
import jsyaml from 'js-yaml'; import jsyaml from 'js-yaml';
import YAML from 'yaml'; import YAML from 'yaml';
import isBase64 from 'is-base64'; import { isBase64 } from '@shell/utils/string';
import NodeAffinity from '@shell/components/form/NodeAffinity'; import NodeAffinity from '@shell/components/form/NodeAffinity';
import PodAffinity from '@shell/components/form/PodAffinity'; import PodAffinity from '@shell/components/form/PodAffinity';

View File

@ -67,6 +67,7 @@ export default defineComponent({
:class="{ :class="{
[color]: true, [color]: true,
}" }"
role="banner"
> >
<div <div
v-if="icon" v-if="icon"
@ -102,7 +103,12 @@ export default defineComponent({
<div <div
v-if="closable" v-if="closable"
class="banner__content__closer" class="banner__content__closer"
tabindex="0"
role="button"
:aria-label="t('generic.close')"
@click="$emit('close')" @click="$emit('close')"
@keyup.enter="$emit('close')"
@keyup.space="$emit('close')"
> >
<i <i
data-testid="banner-close" data-testid="banner-close"
@ -226,6 +232,7 @@ $icon-size: 24px;
width: $icon-size; width: $icon-size;
line-height: $icon-size; line-height: $icon-size;
text-align: center; text-align: center;
outline: none;
.closer-icon { .closer-icon {
opacity: 0.7; opacity: 0.7;
@ -235,6 +242,11 @@ $icon-size: 24px;
color: var(--link); color: var(--link);
} }
} }
&:focus-visible i {
@include focus-outline;
outline-offset: 2px;
}
} }
&.icon { &.icon {

View File

@ -113,7 +113,7 @@ export default defineComponent({
primary: { primary: {
type: Boolean, type: Boolean,
default: false default: false
}, }
}, },
emits: ['update:value'], emits: ['update:value'],
@ -135,7 +135,14 @@ export default defineComponent({
*/ */
isChecked(): boolean { isChecked(): boolean {
return this.isMulti(this.value) ? this.findTrueValues(this.value) : this.value === this.valueWhenTrue; return this.isMulti(this.value) ? this.findTrueValues(this.value) : this.value === this.valueWhenTrue;
} },
/**
* Determines if the Labeled Input should display a tooltip.
*/
hasTooltip(): boolean {
return !!this.tooltip || !!this.tooltipKey;
},
}, },
methods: { methods: {
@ -214,6 +221,9 @@ export default defineComponent({
<div <div
class="checkbox-outer-container" class="checkbox-outer-container"
data-checkbox-ctrl data-checkbox-ctrl
:class="{
'v-popper--has-tooltip': hasTooltip,
}"
> >
<label <label
class="checkbox-container" class="checkbox-container"
@ -227,9 +237,10 @@ export default defineComponent({
:checked="isChecked" :checked="isChecked"
:value="valueWhenTrue" :value="valueWhenTrue"
type="checkbox" type="checkbox"
:tabindex="-1" tabindex="-1"
:name="id" :name="id"
@click.stop.prevent @click.stop.prevent
@keyup.enter.stop.prevent
> >
<span <span
class="checkbox-custom" class="checkbox-custom"
@ -240,7 +251,7 @@ export default defineComponent({
role="checkbox" role="checkbox"
/> />
<span <span
v-if="$slots.label || label || labelKey || tooltipKey || tooltip" v-if="$slots.label || label || labelKey || hasTooltip"
class="checkbox-label" class="checkbox-label"
:class="{ 'checkbox-primary': primary }" :class="{ 'checkbox-primary': primary }"
> >
@ -325,9 +336,14 @@ $fontColor: var(--input-label);
width: 14px; width: 14px;
background-color: var(--body-bg); background-color: var(--body-bg);
border-radius: var(--border-radius); border-radius: var(--border-radius);
transition: all 0.3s ease-out;
border: 1px solid var(--border); border: 1px solid var(--border);
flex-shrink: 0; flex-shrink: 0;
&:focus-visible {
@include focus-outline;
outline-offset: 2px;
border-radius: 0;
}
} }
input { input {
@ -337,6 +353,12 @@ $fontColor: var(--input-label);
z-index: -1; z-index: -1;
} }
input:focus-visible ~ .checkbox-custom {
@include focus-outline;
outline-offset: 2px;
border-radius: 0;
}
input:checked ~ .checkbox-custom { input:checked ~ .checkbox-custom {
background-color:var(--primary); background-color:var(--primary);
-webkit-transform: rotate(0deg) scale(1); -webkit-transform: rotate(0deg) scale(1);

View File

@ -10,4 +10,21 @@ if [ $# -eq 1 ]; then
SPEC_ARG="--spec ${1}" SPEC_ARG="--spec ${1}"
fi fi
CYPRESS_coverage=true CYPRESS_API_URL='http://139.59.134.103:1234' cy2 run ${SPEC_ARG} --group "$GREP_TAGS" --browser chrome --record --key rancher-dashboard --parallel --ci-build-id "$ID" # Define the command to use (cy2 or cypress)
# TEST_DISABLE_DASHBOARD: Disable Cypress dashboard using GH repository settings
# TEST_DISABLE_DASHBOARD_LABEL: Disable Cypress dashboard using PR label `ci/skip-e2e-cypress-dashboard`
echo "TEST_DISABLE_DASHBOARD: ${TEST_DISABLE_DASHBOARD}"
echo "TEST_DISABLE_DASHBOARD_LABEL: ${TEST_DISABLE_DASHBOARD_LABEL}"
if [ "${TEST_DISABLE_DASHBOARD}" = "true" ] || [ "${TEST_DISABLE_DASHBOARD_LABEL}" = "true" ]; then
echo "Running Cypress without dashboard"
CYPRESS_COMMAND="cypress run ${SPEC_ARG}"
else
echo "Running Cypress with Sorry Cypress on dashboard"
CYPRESS_COMMAND="cy2 run ${SPEC_ARG} --ci-build-id \"$ID\" --record --key rancher-dashboard --parallel --group \"$GREP_TAGS\""
fi
# Construct the full command
E2E_COMMAND="CYPRESS_API_URL='http://139.59.134.103:1234' ${CYPRESS_COMMAND} --browser chrome"
# Execute the command
eval $E2E_COMMAND

View File

@ -7,7 +7,7 @@ DASHBOARD_DIST=${DIR}/dist
EMBER_DIST=${DIR}/dist_ember EMBER_DIST=${DIR}/dist_ember
# Image version # Image version
RANCHER_IMG_VERSION=head RANCHER_IMG_VERSION=v2.11-2053ce644a31cd8053d1f58e2487154b0b8513b6-head
# Docker volume args when mounting the locally-built UI into the container # Docker volume args when mounting the locally-built UI into the container
VOLUME_ARGS="-v ${DASHBOARD_DIST}:/usr/share/rancher/ui-dashboard/dashboard -v ${EMBER_DIST}:/usr/share/rancher/ui" VOLUME_ARGS="-v ${DASHBOARD_DIST}:/usr/share/rancher/ui-dashboard/dashboard -v ${EMBER_DIST}:/usr/share/rancher/ui"

View File

@ -159,4 +159,4 @@
@mixin focus-outline { @mixin focus-outline {
// Focus for form like elements (not to be confused with basic :focus style) // Focus for form like elements (not to be confused with basic :focus style)
outline: 2px solid var(--primary-keyboard-focus); outline: 2px solid var(--primary-keyboard-focus);
} }

View File

@ -106,6 +106,11 @@ button,
border: 0; border: 0;
} }
} }
&:focus-visible {
@include focus-outline;
outline-offset: 2px;
}
} }
.role-tertiary { .role-tertiary {

View File

@ -4,6 +4,7 @@
generic: generic:
add: Add add: Add
all: All all: All
ascending: ascending
and: ' and ' and: ' and '
back: Back back: Back
cancel: Cancel cancel: Cancel
@ -19,6 +20,7 @@ generic:
customize: Customize customize: Customize
dashboard: Dashboard dashboard: Dashboard
default: Default default: Default
descending: descending
disabled: Disabled disabled: Disabled
done: Done done: Done
enabled: Enabled enabled: Enabled
@ -115,11 +117,26 @@ generic:
basic: Basic basic: Basic
locale: locale:
menu: Locale selector menu
en-us: English en-us: English
zh-hans: 简体中文 zh-hans: 简体中文
none: (None) none: (None)
nav: nav:
ariaLabel:
topLevelMenu: Main menu
homePage: Home page navigation menu
cluster: Cluster menu item
harvesterCluster: Harvester cluster menu item
seeAll: See all clusters menu item
multiClusterApps: Main menu multi cluster app menu item
configurationApps: Main menu configuration app menu item
support: Support page link
about: About page link
pinCluster: Pin/Unpin cluster
alt:
mainMenuIcon: Main menu icon
mainMenuRancherLogo: Main menu Rancher logo
expandCollapseAppBar: Expand/Collapse the Application Bar expandCollapseAppBar: Expand/Collapse the Application Bar
harvesterDashboard: Harvester Dashboard harvesterDashboard: Harvester Dashboard
backToRancher: Cluster Manager backToRancher: Cluster Manager
@ -196,6 +213,7 @@ nav:
placeholder: Filter clusters by... placeholder: Filter clusters by...
noResults: No matching clusters noResults: No matching clusters
clusters: clusters clusters: clusters
ariaLabel: Filter clusters on main menu
resourceSearch: resourceSearch:
label: Resource Search label: Resource Search
toolTip: Resource Search {key} toolTip: Resource Search {key}
@ -5818,6 +5836,7 @@ tableHeaders:
resourcesReady: Resources Ready resourcesReady: Resources Ready
restarts: Restarts restarts: Restarts
restart: Restart Required restart: Restart Required
restartSystem: Restart { vendor }
restore: Restore restore: Restore
role: Role role: Role
roles: Roles roles: Roles
@ -7480,6 +7499,7 @@ advancedSettings:
featureFlags: featureFlags:
label: Feature Flags label: Feature Flags
title: "Are you sure?"
warning: |- warning: |-
Feature flags allow {vendor} to gate certain features behind flags. Feature flags allow {vendor} to gate certain features behind flags.
Features that are off by default should be considered experimental functionality. Features that are off by default should be considered experimental functionality.
@ -7487,7 +7507,7 @@ featureFlags:
This will result in a short outage of the API and UI, but not affect running clusters or workloads. This will result in a short outage of the API and UI, but not affect running clusters or workloads.
promptActivate: Please confirm that you want to activate the feature flag "{flag}" promptActivate: Please confirm that you want to activate the feature flag "{flag}"
promptDeactivate: Please confirm that you want to deactivate the feature flag "{flag}" promptDeactivate: Please confirm that you want to deactivate the feature flag "{flag}"
restartRequired: "Note: Updating this feature flag requires a restart" restartRequired: "Note: Updating this feature flag will restart {vendor}"
restart: restart:
title: Waiting for Restart title: Waiting for Restart
wait: This may take a few moments wait: This may take a few moments
@ -7496,6 +7516,7 @@ performance:
label: UI Performance Settings label: UI Performance Settings
settingName: Performance settingName: Performance
experimental: This setting is experimental and may be removed or updated in future versions. experimental: This setting is experimental and may be removed or updated in future versions.
deprecatedForSSP: The <i class="mr-5">"{setting}"</i> setting is now deprecated and will be removed in a future release. Please use the <a href="#ssp-setting" class="ml-5 mr-5">Server-side Pagination</a> setting instead.
incrementalLoad: incrementalLoad:
label: Incremental Loading label: Incremental Loading
setting: You can configure the threshold above which incremental loading will be used. setting: You can configure the threshold above which incremental loading will be used.
@ -7565,6 +7586,7 @@ performance:
resources: resources:
generic: most resources in the cluster's 'More Resources' section generic: most resources in the cluster's 'More Resources' section
all: All Resources all: All Resources
populateDefaults: Populate with latest pagination defaults
banner: banner:
label: Fixed Banners label: Fixed Banners
settingName: Banners settingName: Banners
@ -7759,7 +7781,7 @@ support:
text: Run SUSE Rancher products with confidence, knowing that the developers who built them are available to quickly resolve issues. text: Run SUSE Rancher products with confidence, knowing that the developers who built them are available to quickly resolve issues.
three: three:
title: Troubleshooting title: Troubleshooting
text: We focus on uncovering the root cause of any issue, whether it is related to Rancher Labs products, Kubernetes, Docker or your underlying infrastructure. text: We focus on uncovering the root cause of any issue, whether it is related to SUSE products, Kubernetes, Docker or your underlying infrastructure.
four: four:
title: Innovate with Freedom title: Innovate with Freedom
text: Take advantage of our certified compatibility with a wide range of Kubernetes providers, operating systems, and open source software. text: Take advantage of our certified compatibility with a wide range of Kubernetes providers, operating systems, and open source software.

View File

@ -7310,7 +7310,7 @@ support:
text: 我们的开发人员会快速解决问题,因此你可以放心使用 SUSE Rancher 的产品。 text: 我们的开发人员会快速解决问题,因此你可以放心使用 SUSE Rancher 的产品。
three: three:
title: 故障排除 title: 故障排除
text: 无论你使用的 Rancher Labs 产品、Kubernetes、Docker 还是底层基础架构出现问题,我们都会努力找到问题的根本原因。 text: 无论你使用的 SUSE 产品、Kubernetes、Docker 还是底层基础架构出现问题,我们都会努力找到问题的根本原因。
four: four:
title: 自由创新 title: 自由创新
text: 基于我们与众多 Kubernetes 供应商、操作系统和开源软件认证的兼容性,实现自主创新。 text: 基于我们与众多 Kubernetes 供应商、操作系统和开源软件认证的兼容性,实现自主创新。

View File

@ -264,7 +264,10 @@ export default {
:disabled="opt.disabled ? true : null" :disabled="opt.disabled ? true : null"
:class="{divider: opt.divider}" :class="{divider: opt.divider}"
:data-testid="componentTestid + '-' + i + '-item'" :data-testid="componentTestid + '-' + i + '-item'"
:tabindex="opt.divider ? -1 : 0"
@click="execute(opt, $event)" @click="execute(opt, $event)"
@keyup.enter="execute(opt, $event)"
@keyup.space="execute(opt, $event)"
> >
<IconOrSvg <IconOrSvg
v-if="opt.icon || opt.svg" v-if="opt.icon || opt.svg"
@ -311,6 +314,11 @@ export default {
padding: 8px 10px; padding: 8px 10px;
margin: 0; margin: 0;
&:focus-visible {
@include focus-outline;
outline-offset: -2px;
}
&[disabled] { &[disabled] {
cursor: not-allowed !important; cursor: not-allowed !important;
color: var(--disabled-text); color: var(--disabled-text);

View File

@ -208,6 +208,10 @@ export default defineComponent({
return this.disabled || this.phase === ASYNC_BUTTON_STATES.WAITING; return this.disabled || this.phase === ASYNC_BUTTON_STATES.WAITING;
}, },
isManualRefresh() {
return this.mode === 'manual-refresh';
},
tooltip(): { content: string, hideOnTargetClick: boolean} | null { tooltip(): { content: string, hideOnTargetClick: boolean} | null {
if ( this.labelAs === TOOLTIP ) { if ( this.labelAs === TOOLTIP ) {
return { return {
@ -283,12 +287,14 @@ export default defineComponent({
:data-testid="componentTestid + '-async-button'" :data-testid="componentTestid + '-async-button'"
@click="clicked" @click="clicked"
> >
<span v-if="mode === 'manual-refresh'">{{ t('action.refresh') }}</span> <span
v-if="isManualRefresh"
:class="{'mr-10': displayIcon && size !== 'sm', 'mr-5': displayIcon && size === 'sm'}"
>{{ t('action.refresh') }}</span>
<i <i
v-if="displayIcon" v-if="displayIcon"
v-clean-tooltip="tooltip" v-clean-tooltip="tooltip"
:class="{icon: true, 'icon-lg': true, [displayIcon]: true}" :class="{icon: true, 'icon-lg': true, [displayIcon]: true, 'mr-0': isManualRefresh}"
class="ml-5 mr-0"
/> />
<span <span
v-if="labelAs === 'text' && displayLabel" v-if="labelAs === 'text' && displayLabel"

View File

@ -55,7 +55,12 @@ export default {
v-if="pref" v-if="pref"
class="close-button" class="close-button"
data-testid="graphic-banner-close" data-testid="graphic-banner-close"
tabindex="0"
:aria-label="t('generic.close')"
role="button"
@click="hide()" @click="hide()"
@keyup.enter="hide()"
@keyup.space="hide()"
> >
<i class="icon icon-close" /> <i class="icon icon-close" />
</div> </div>
@ -72,6 +77,11 @@ export default {
.close-button { .close-button {
position: absolute; position: absolute;
visibility: hidden; visibility: hidden;
&:focus-visible {
@include focus-outline;
outline-offset: 2px;
}
} }
&:hover .close-button { &:hover .close-button {

View File

@ -33,6 +33,12 @@ const buttonClass = computed(() => {
.borderless { .borderless {
background-color: transparent; background-color: transparent;
border: none; border: none;
&:focus-visible {
@include focus-outline;
outline-offset: -2px;
}
&:hover, &:focus { &:hover, &:focus {
background-color: var(--accent-btn); background-color: var(--accent-btn);
box-shadow: none; box-shadow: none;

View File

@ -25,7 +25,7 @@ export default {
}, },
customColor() { customColor() {
return !this.cluster.removePreviewColor && this.cluster.badge?.iconText ? this.cluster.badge?.color : ''; return this.cluster.iconColor || '';
}, },
}, },

View File

@ -5,7 +5,7 @@ import { _EDIT, _VIEW } from '@shell/config/query-params';
export default { export default {
name: 'CodeMirror', name: 'CodeMirror',
emits: ['onReady', 'onInput', 'onChanges', 'onFocus'], emits: ['onReady', 'onInput', 'onChanges', 'onFocus', 'validationChanged'],
props: { props: {
/** /**
@ -39,6 +39,7 @@ export default {
codeMirrorRef: null, codeMirrorRef: null,
loaded: false, loaded: false,
removeKeyMapBox: false, removeKeyMapBox: false,
hasLintErrors: false,
}; };
}, },
@ -65,6 +66,7 @@ export default {
foldGutter: true, foldGutter: true,
styleSelectedText: true, styleSelectedText: true,
showCursorWhenSelecting: true, showCursorWhenSelecting: true,
autocorrect: false,
}; };
if (this.asTextArea) { if (this.asTextArea) {
@ -76,6 +78,11 @@ export default {
Object.assign(out, this.options); 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; return out;
}, },
@ -104,7 +111,25 @@ export default {
} }
}, },
watch: {
hasLintErrors(neu) {
this.$emit('validationChanged', !neu);
}
},
methods: { 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() { focus() {
if ( this.$refs.codeMirrorRef ) { if ( this.$refs.codeMirrorRef ) {
this.$refs.codeMirrorRef.cminstance.focus(); this.$refs.codeMirrorRef.cminstance.focus();
@ -118,6 +143,8 @@ export default {
}, },
onReady(codeMirrorRef) { onReady(codeMirrorRef) {
this.$emit('validationChanged', true);
this.$nextTick(() => { this.$nextTick(() => {
codeMirrorRef.refresh(); codeMirrorRef.refresh();
this.codeMirrorRef = codeMirrorRef; this.codeMirrorRef = codeMirrorRef;

View File

@ -110,6 +110,8 @@ export default {
<router-link <router-link
v-if="link.value.startsWith('/') " v-if="link.value.startsWith('/') "
:to="link.value" :to="link.value"
role="link"
:aria-label="link.label"
> >
{{ link.label }} {{ link.label }}
</router-link> </router-link>
@ -118,6 +120,8 @@ export default {
:href="link.value" :href="link.value"
rel="noopener noreferrer nofollow" rel="noopener noreferrer nofollow"
target="_blank" target="_blank"
role="link"
:aria-label="link.label"
> {{ link.label }} </a> > {{ link.label }} </a>
</div> </div>
<slot /> <slot />
@ -127,7 +131,11 @@ export default {
> >
<a <a
class="link" class="link"
tabindex="0"
:aria-label="t('footer.wechat.title')"
role="link"
@click="show" @click="show"
@keyup.enter="show"
> >
{{ t('footer.wechat.title') }} {{ t('footer.wechat.title') }}
</a> </a>
@ -147,7 +155,12 @@ export default {
<div> <div>
<button <button
class="btn role-primary" class="btn role-primary"
tabindex="0"
:aria-label="t('generic.close')"
role="button"
@click="close" @click="close"
@keyup.enter="close"
@keyup.space="close"
> >
{{ t('generic.close') }} {{ t('generic.close') }}
</button> </button>

View File

@ -106,7 +106,10 @@ export default {
<div class="growl-text-title"> <div class="growl-text-title">
{{ growl.title }} {{ growl.title }}
</div> </div>
<p v-if="growl.message"> <p
v-if="growl.message"
:class="{ 'has-title': !!growl.title }"
>
{{ growl.message }} {{ growl.message }}
</p> </p>
</div> </div>
@ -153,12 +156,16 @@ export default {
word-break: break-all; word-break: break-all;
box-shadow: 0 3px 5px 0px var(--shadow); box-shadow: 0 3px 5px 0px var(--shadow);
$growl-icon-size: 20px;
.icon-container { .icon-container {
align-self: center; align-self: center;
flex-basis: 10%; flex-basis: 10%;
padding: 10px 20px 10px 10px; padding: 10px 20px 10px 10px;
i { 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 { .growl-text-title {
font-size: 16px; font-size: 16px;
margin-bottom: 20px;
} }
> P { > P {
margin-top: 5px; padding-top: 2px;
&.has-title {
margin-top: 5px;
}
} }
} }
} }

View File

@ -11,7 +11,11 @@ export default {
mode: { mode: {
type: String, type: String,
default: '' default: ''
}, }
},
data() {
return { isLocaleSelectorOpen: false };
}, },
computed: { computed: {
@ -40,8 +44,15 @@ export default {
}, },
methods: { methods: {
openLocaleSelector() {
this.isLocaleSelectorOpen = true;
},
closeLocaleSelector() {
this.isLocaleSelectorOpen = false;
},
switchLocale($event) { switchLocale($event) {
this.$store.dispatch('i18n/switchTo', $event); this.$store.dispatch('i18n/switchTo', $event);
this.closeLocaleSelector();
}, },
} }
}; };
@ -50,13 +61,28 @@ export default {
<template> <template>
<div> <div>
<div v-if="mode === 'login'"> <div v-if="mode === 'login'">
<div v-if="showLocale"> <div
v-if="showLocale"
role="menu"
:aria-label="t('locale.menu')"
class="locale-login-container"
tabindex="0"
@click="openLocaleSelector"
@blur.capture="closeLocaleSelector"
@keyup.enter="openLocaleSelector"
@keyup.space="openLocaleSelector"
>
<v-dropdown <v-dropdown
popperClass="localeSelector" popperClass="localeSelector"
:shown="isLocaleSelectorOpen"
placement="top" placement="top"
distance="8" distance="8"
skidding="12" skidding="12"
:triggers="['click']" :triggers="[]"
:autoHide="false"
:flip="false"
:container="false"
@focus.capture="openLocaleSelector"
> >
<a <a
data-testid="locale-selector" data-testid="locale-selector"
@ -74,13 +100,21 @@ export default {
v-if="showNone" v-if="showNone"
v-t="'locale.none'" v-t="'locale.none'"
class="hand" class="hand"
@click="switchLocale('none')" tabindex="0"
role="menuitem"
@click.stop="switchLocale('none')"
@keyup.enter.stop="switchLocale('none')"
@keyup.space.stop="switchLocale('none')"
/> />
<li <li
v-for="(label, name) in availableLocales" v-for="(label, name) in availableLocales"
:key="name" :key="name"
tabindex="0"
role="menuitem"
class="hand" class="hand"
@click="switchLocale(name)" @click.stop="switchLocale(name)"
@keyup.enter.stop="switchLocale(name)"
@keyup.space.stop="switchLocale(name)"
> >
{{ label }} {{ label }}
</li> </li>
@ -114,6 +148,11 @@ export default {
border-radius: 4px; border-radius: 4px;
} }
.hand:focus-visible {
@include focus-outline;
outline-offset: 4px;
}
.locale-chooser { .locale-chooser {
cursor: pointer; cursor: pointer;
@ -121,4 +160,9 @@ export default {
text-decoration: none; text-decoration: none;
} }
} }
.locale-login-container:focus-visible {
@include focus-outline;
outline-offset: 2px;
}
</style> </style>

View File

@ -93,8 +93,8 @@ export default defineComponent({
}, },
computed: { computed: {
safeHeaders() { safeHeaders(): any[] {
const customHeaders = this.canPaginate ? this.paginationHeaders : this.headers; const customHeaders: any[] = this.canPaginate ? this.paginationHeaders : this.headers;
return customHeaders || this.$store.getters['type-map/headersFor'](this.schema, this.canPaginate); return customHeaders || this.$store.getters['type-map/headersFor'](this.schema, this.canPaginate);
} }
@ -109,7 +109,7 @@ export default defineComponent({
v-bind="$attrs" v-bind="$attrs"
:schema="schema" :schema="schema"
:rows="rows" :rows="rows"
:alt-loading="canPaginate" :alt-loading="canPaginate && !isFirstLoad"
:loading="loading" :loading="loading"
:groupable="groupable" :groupable="groupable"
@ -124,6 +124,7 @@ export default defineComponent({
<template <template
v-for="(_, slot) of $slots" v-for="(_, slot) of $slots"
v-slot:[slot]="scope" v-slot:[slot]="scope"
:key="slot"
> >
<slot <slot
:name="slot" :name="slot"

View File

@ -54,8 +54,10 @@ export default {
this.customTypeDisplay = component.typeDisplay.apply(this); this.customTypeDisplay = component.typeDisplay.apply(this);
} }
// If your list page has a fetch then it's responsible for populating rows itself // Is the custom component responsible fetching the resources?
if ( component?.fetch ) { // - Component has a fetch method - legacy method. fetch will handle the requests
// - Component contains the PaginatedResourceTable component - go forward method. PaginatedResourceTable owns fetching the resources
if ( component?.fetch || component?.components?.['PaginatedResourceTable']) {
this.componentWillFetch = true; this.componentWillFetch = true;
} }
@ -269,7 +271,7 @@ export default {
v-else v-else
:schema="schema" :schema="schema"
:rows="rows" :rows="rows"
:alt-loading="canPaginate" :alt-loading="canPaginate && !isFirstLoad"
:loading="loading" :loading="loading"
:headers="headers" :headers="headers"
:group-by="groupBy" :group-by="groupBy"

View File

@ -167,7 +167,7 @@ export default {
default: false default: false
}, },
/** /**
* Manaul force the update of live and delayed cells. Change this number to kick off the update * Manual force the update of live and delayed cells. Change this number to kick off the update
*/ */
forceUpdateLiveAndDelayed: { forceUpdateLiveAndDelayed: {
type: Number, type: Number,

View File

@ -162,6 +162,14 @@ export default {
return col.name === this.sortBy; return col.name === this.sortBy;
}, },
ariaSort(col) {
if (this.isCurrent(col)) {
return this.descending ? this.t('generic.descending') : this.t('generic.ascending');
}
return this.t('generic.none');
},
tableColsOptionsClick(ev) { tableColsOptionsClick(ev) {
// set menu position // set menu position
const menu = document.querySelector('.table-options-container'); const menu = document.querySelector('.table-options-container');
@ -235,7 +243,12 @@ export default {
:align="col.align || 'left'" :align="col.align || 'left'"
:width="col.width" :width="col.width"
:class="{ sortable: col.sort, [col.breakpoint]: !!col.breakpoint}" :class="{ sortable: col.sort, [col.breakpoint]: !!col.breakpoint}"
:tabindex="col.sort ? 0 : -1"
class="sortable-table-head-element"
:aria-sort="ariaSort(col)"
@click.prevent="changeSort($event, col)" @click.prevent="changeSort($event, col)"
@keyup.enter="changeSort($event, col)"
@keyup.space="changeSort($event, col)"
> >
<div <div
class="table-header-container" class="table-header-container"
@ -415,10 +428,7 @@ export default {
background-color: var(--sortable-table-header-bg); background-color: var(--sortable-table-header-bg);
color: var(--body-text); color: var(--body-text);
text-align: left; text-align: left;
border-bottom: 1px solid var(--sortable-table-top-divider);
&:not(.loading) {
border-bottom: 1px solid var(--sortable-table-top-divider);
}
} }
} }
@ -428,6 +438,11 @@ export default {
border: 0; border: 0;
color: var(--body-text); color: var(--body-text);
&.sortable-table-head-element:focus-visible {
@include focus-outline;
outline-offset: -4px;
}
.table-header-container { .table-header-container {
display: inline-flex; display: inline-flex;

View File

@ -380,8 +380,10 @@ export default {
eventualSearchQuery = this.$route.query?.q; eventualSearchQuery = this.$route.query?.q;
} }
const isLoading = this.loading || false;
return { return {
refreshButtonPhase: ASYNC_BUTTON_STATES.WAITING, refreshButtonPhase: isLoading ? ASYNC_BUTTON_STATES.WAITING : ASYNC_BUTTON_STATES.ACTION,
expanded: {}, expanded: {},
searchQuery, searchQuery,
eventualSearchQuery, eventualSearchQuery,
@ -392,7 +394,7 @@ export default {
/** /**
* The is the bool the DOM uses to show loading state. it's proxied from `loading` to avoid blipping the indicator (see usages) * The is the bool the DOM uses to show loading state. it's proxied from `loading` to avoid blipping the indicator (see usages)
*/ */
isLoading: false, isLoading
}; };
}, },
@ -504,7 +506,7 @@ export default {
if (neu) { if (neu) {
this._altLoadingDelayTimer = setTimeout(() => { this._altLoadingDelayTimer = setTimeout(() => {
this.isLoading = true; this.isLoading = true;
}, 200); // this should be higher than the targetted quick response }, 200); // this should be higher than the targeted quick response
} else { } else {
clearTimeout(this._altLoadingDelayTimer); clearTimeout(this._altLoadingDelayTimer);
this.isLoading = false; this.isLoading = false;
@ -536,9 +538,6 @@ export default {
manualRefreshLoadingFinished() { manualRefreshLoadingFinished() {
const res = !!(!this.isLoading && this._didinit && this.rows?.length && !this.isManualRefreshLoading); const res = !!(!this.isLoading && this._didinit && this.rows?.length && !this.isManualRefreshLoading);
// Always ensure the Refresh button phase aligns with loading state (regardless of if manualRefreshLoadingFinished has changed or not)
this.refreshButtonPhase = !res || this.loading ? ASYNC_BUTTON_STATES.WAITING : ASYNC_BUTTON_STATES.ACTION;
return res; return res;
}, },
@ -575,11 +574,13 @@ export default {
}, },
showHeaderRow() { showHeaderRow() {
// All of these are used to show content in the header
return this.search || return this.search ||
this.tableActions || this.tableActions ||
this.$slots['header-left']?.() || this.$slots['header-left'] ||
this.$slots['header-middle']?.() || this.$slots['header-middle'] ||
this.$slots['header-right']?.(); this.$slots['header-right'] ||
this.isTooManyItemsToAutoUpdate;
}, },
columns() { columns() {
@ -1222,6 +1223,7 @@ export default {
class="sortable-table" class="sortable-table"
:class="classObject" :class="classObject"
width="100%" width="100%"
role="table"
> >
<THead <THead
v-if="showHeaders" v-if="showHeaders"
@ -1451,6 +1453,8 @@ export default {
:data-testid="componentTestid + '-' + i + '-action-button'" :data-testid="componentTestid + '-' + i + '-action-button'"
:borderless="true" :borderless="true"
@click="handleActionButtonClick(i, $event)" @click="handleActionButtonClick(i, $event)"
@keyup.enter="handleActionButtonClick(i, $event)"
@keyup.space="handleActionButtonClick(i, $event)"
/> />
</slot> </slot>
</td> </td>

View File

@ -1,3 +1,4 @@
import { mapGetters } from 'vuex';
import { isMore, isRange, suppressContextMenu, isAlternate } from '@shell/utils/platform'; import { isMore, isRange, suppressContextMenu, isAlternate } from '@shell/utils/platform';
import { get } from '@shell/utils/object'; import { get } from '@shell/utils/object';
import { filterBy } from '@shell/utils/array'; import { filterBy } from '@shell/utils/array';
@ -29,6 +30,13 @@ export default {
}, },
computed: { computed: {
...mapGetters({
// Use either these Vuex getters
// OR the props to set the action menu state,
// but don't use both.
targetElem: 'action-menu/elem',
shouldShow: 'action-menu/showing',
}),
// Used for the table-level selection check-box to show checked (all selected)/intermediate (some selected)/unchecked (none selected) // Used for the table-level selection check-box to show checked (all selected)/intermediate (some selected)/unchecked (none selected)
howMuchSelected() { howMuchSelected() {
const total = this.pagedRows.length; const total = this.pagedRows.length;
@ -270,11 +278,17 @@ export default {
} }
} }
this.$store.commit(`action-menu/show`, { if (!this.targetElem && !this.shouldShow) {
resources, this.$store.commit(`action-menu/show`, {
event: e, resources,
elem: actionElement event: e,
}); elem: actionElement
});
} else if (this.targetElem === actionElement && this.shouldShow) {
// this condition is needed so that we can "toggle" the action menu with
// the keyboard for accessibility (row action menu)
this.$store.commit('action-menu/hide');
}
return; return;
} }

View File

@ -13,7 +13,7 @@ export const EDITOR_MODES = {
}; };
export default { export default {
emits: ['update:value', 'newObject', 'onInput', 'onReady', 'onChanges'], emits: ['update:value', 'newObject', 'onInput', 'onReady', 'onChanges', 'validationChanged'],
components: { components: {
CodeMirror, CodeMirror,
@ -236,6 +236,7 @@ export default {
@onInput="onInput" @onInput="onInput"
@onReady="onReady" @onReady="onReady"
@onChanges="onChanges" @onChanges="onChanges"
@validationChanged="$emit('validationChanged', $event)"
/> />
<FileDiff <FileDiff
v-else v-else

View File

@ -127,7 +127,7 @@ export default {
} }
}, },
onSearch(str, loading, vm) { onSearch(str, loading) {
str = (str || '').trim(); str = (str || '').trim();
this.searchStr = str; this.searchStr = str;

View File

@ -127,7 +127,8 @@ export default {
class="text-warning" class="text-warning"
> >
{{ row.status.summary.ready }}/{{ row.status.summary.desiredReady }}</span> {{ row.status.summary.ready }}/{{ row.status.summary.desiredReady }}</span>
<span v-else-if="row.status">{{ row.status.summary.desiredReady }}</span> <span v-else-if="row.status && row.status.summary">{{ row.status.summary.desiredReady }}</span>
<span v-else>-</span>
</template> </template>
</ResourceTable> </ResourceTable>
</div> </div>

View File

@ -22,7 +22,7 @@ export default {
LabeledSelectPagination LabeledSelectPagination
], ],
emits: ['on-open', 'on-close', 'selecting', 'deselecting', 'update:validation', 'update:value'], emits: ['on-open', 'on-close', 'selecting', 'deselecting', 'search', 'update:validation', 'update:value'],
props: { props: {
appendToBody: { appendToBody: {
@ -147,7 +147,7 @@ export default {
// update placeholder text to inform user they can add their own opts when none are found // update placeholder text to inform user they can add their own opts when none are found
showTagPrompts() { showTagPrompts() {
return !this.options.length && this.$attrs.taggable; return !this.options.length && this.$attrs.taggable && this.isSearchable;
} }
}, },
@ -238,7 +238,7 @@ export default {
return noDrop ? false : open && shouldOpen && !mutableLoading; return noDrop ? false : open && shouldOpen && !mutableLoading;
}, },
onSearch(newSearchString) { onSearch(newSearchString, loading) {
if (this.canPaginate) { if (this.canPaginate) {
this.setPaginationFilter(newSearchString); this.setPaginationFilter(newSearchString);
} else { } else {
@ -246,6 +246,7 @@ export default {
this.dropdownShouldOpen(this.$refs['select-input'], true); this.dropdownShouldOpen(this.$refs['select-input'], true);
} }
} }
this.$emit('search', newSearchString, loading);
}, },
getOptionKey(opt) { getOptionKey(opt) {

View File

@ -32,6 +32,7 @@ export default {
type: String, type: String,
default: 'create' default: 'create'
}, },
loading: { loading: {
default: false, default: false,
type: Boolean type: Boolean
@ -169,6 +170,7 @@ export default {
name="selectNode" name="selectNode"
:options="selectNodeOptions" :options="selectNodeOptions"
:mode="mode" :mode="mode"
:data-testid="'node-scheduling-selectNode'"
@input="update" @input="update"
/> />
</div> </div>
@ -182,7 +184,8 @@ export default {
:mode="mode" :mode="mode"
:multiple="false" :multiple="false"
:loading="loading" :loading="loading"
@input="update" :data-testid="'node-scheduling-nodeSelector'"
@update:value="update"
/> />
</div> </div>
</div> </div>
@ -191,6 +194,7 @@ export default {
<NodeAffinity <NodeAffinity
v-model:value="nodeAffinity" v-model:value="nodeAffinity"
:mode="mode" :mode="mode"
:data-testid="'node-scheduling-nodeAffinity'"
@input="update" @input="update"
/> />
</template> </template>

View File

@ -41,7 +41,7 @@ export default {
mode: { mode: {
type: String, type: String,
default: _CREATE, default: _CREATE,
}, }
}, },
data() { data() {
return { reveal: false }; return { reveal: false };
@ -68,6 +68,9 @@ export default {
} }
return attributes; return attributes;
},
hideShowLabel() {
return this.reveal ? this.t('action.hide') : this.t('action.show');
} }
}, },
watch: { watch: {
@ -92,6 +95,9 @@ export default {
}, },
focus() { focus() {
this.$refs.input.$refs.value.focus(); this.$refs.input.$refs.value.focus();
},
hideShowFn() {
this.reveal ? this.reveal = false : this.reveal = true;
} }
} }
}; };
@ -127,17 +133,15 @@ export default {
class="addon" class="addon"
> >
<a <a
v-if="reveal"
tabindex="-1"
href="#" href="#"
@click.prevent.stop="reveal = false" tabindex="0"
>{{ t('action.hide') }}</a> class="hide-show"
<a role="button"
v-else @click.prevent.stop="hideShowFn"
tabindex="-1" @keyup.space.prevent.stop="hideShowFn"
href="#" >
@click.prevent.stop="reveal=true" {{ hideShowLabel }}
>{{ t('action.show') }}</a> </a>
</div> </div>
</template> </template>
</LabeledInput> </LabeledInput>
@ -157,10 +161,16 @@ export default {
.password { .password {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.labeled-input { .labeled-input {
.addon { .addon {
padding-left: 12px; padding-left: 12px;
min-width: 65px; min-width: 65px;
.hide-show:focus-visible {
@include focus-outline;
outline-offset: 4px;
}
} }
} }
.genPassword { .genPassword {

View File

@ -27,7 +27,7 @@ export interface ResourceLabeledSelectPaginateSettings extends SharedSettings {
*/ */
overrideRequest?: LabelSelectPaginateFn, 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, requestSettings?: PaginateTypeOverridesFn,
} }

View File

@ -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);
});
});

View File

@ -27,7 +27,7 @@ export default {
const nodeWithExternal = nodes.find((node) => !!node.externalIp) || {}; const nodeWithExternal = nodes.find((node) => !!node.externalIp) || {};
const externalIp = nodeWithExternal.externalIp; const externalIp = nodeWithExternal.externalIp;
if ( this.value && this.value.length ) { if ( this.value?.length ) {
let out ; let out ;
try { try {

View File

@ -95,7 +95,7 @@ export default {
}, },
}, },
}; };
</script>> </script>
<template> <template>
<div> <div>

View File

@ -17,28 +17,30 @@ export default {
}, },
}, },
data() { data() {
const { row } = this; const cloned = this.getLabel(this.value.toLowerCase());
let cloned = this.value.toLowerCase(); const headless = this.value === 'ClusterIP' && this.row?.spec?.clusterIP === 'None' ? this.getLabel('headless') : undefined;
if (this.value === 'ClusterIP' && row?.spec?.clusterIP === 'None') { return { translated: cloned, headless };
cloned = 'headless';
}
const match = DEFAULT_SERVICE_TYPES.find((s) => s.id.toLowerCase() === cloned);
const translationLabel = match?.label;
let translated;
if (translationLabel && this.$store.getters['i18n/exists'](translationLabel)) {
translated = this.$store.getters['i18n/t'](translationLabel);
} else {
translated = this.value;
}
return { translated };
}, },
methods: {
getLabel(type) {
const match = DEFAULT_SERVICE_TYPES.find((s) => s.id.toLowerCase() === type);
const translationLabel = match?.label;
let translated;
if (translationLabel && this.$store.getters['i18n/exists'](translationLabel)) {
translated = this.$store.getters['i18n/t'](translationLabel);
} else {
translated = this.value;
}
return translated;
}
}
}; };
</script>> </script>
<template> <template>
<span>{{ translated }}</span> <span>{{ translated }}{{ headless ? ` (${headless})` : '' }}</span>
</template> </template>

View File

@ -5,6 +5,10 @@ export default {
cluster: { cluster: {
type: Object, type: Object,
required: true, required: true,
},
tabOrder: {
type: Number,
default: null,
} }
}, },
@ -28,11 +32,12 @@ export default {
<template> <template>
<i <i
:tabindex="0" :tabindex="tabOrder"
:aria-checked="!!pinned" :aria-checked="!!pinned"
class="pin icon" class="pin icon"
:class="{'icon-pin-outlined': !pinned, 'icon-pin': pinned}" :class="{'icon-pin-outlined': !pinned, 'icon-pin': pinned}"
aria-role="button" aria-role="button"
:aria-label="`${t('nav.ariaLabel.pinCluster')} ${ cluster.label }`"
@click.stop.prevent="toggle" @click.stop.prevent="toggle"
@keydown.enter.prevent="toggle" @keydown.enter.prevent="toggle"
@keydown.space.prevent="toggle" @keydown.space.prevent="toggle"

View File

@ -13,6 +13,7 @@ interface TopLevelMenuCluster {
ready: boolean ready: boolean
providerNavLogo: string, providerNavLogo: string,
badge: string, badge: string,
iconColor: string,
isLocal: boolean, isLocal: boolean,
pinned: boolean, pinned: boolean,
description: string, description: string,
@ -143,6 +144,7 @@ export abstract class BaseTopLevelMenuHelper {
ready: mgmtCluster.isReady, // && !provCluster?.hasError, ready: mgmtCluster.isReady, // && !provCluster?.hasError,
providerNavLogo: mgmtCluster.providerMenuLogo, providerNavLogo: mgmtCluster.providerMenuLogo,
badge: mgmtCluster.badge, badge: mgmtCluster.badge,
iconColor: mgmtCluster.iconColor,
isLocal: mgmtCluster.isLocal, isLocal: mgmtCluster.isLocal,
pinned: mgmtCluster.pinned, pinned: mgmtCluster.pinned,
description: provCluster?.description || mgmtCluster.description, description: provCluster?.description || mgmtCluster.description,
@ -390,7 +392,7 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme
} }
/** /**
* Helper designed to supply non-pagainted results for the top level menu cluster resources * Helper designed to supply non-paginated results for the top level menu cluster resources
*/ */
export class TopLevelMenuHelperLegacy extends BaseTopLevelMenuHelper implements TopLevelMenuHelper { export class TopLevelMenuHelperLegacy extends BaseTopLevelMenuHelper implements TopLevelMenuHelper {
constructor({ $store }: { constructor({ $store }: {

View File

@ -485,6 +485,8 @@ export default {
:class="{'menu-open': shown, 'menu-close':!shown}" :class="{'menu-open': shown, 'menu-close':!shown}"
:style="sideMenuStyle" :style="sideMenuStyle"
tabindex="-1" tabindex="-1"
role="navigation"
:aria-label="t('nav.ariaLabel.topLevelMenu')"
> >
<!-- Logo and name --> <!-- Logo and name -->
<div class="title"> <div class="title">
@ -504,6 +506,7 @@ export default {
height="24" height="24"
viewBox="0 0 24 24" viewBox="0 0 24 24"
width="24" width="24"
:alt="t('nav.alt.mainMenuIcon')"
><path ><path
d="M0 0h24v24H0z" d="M0 0h24v24H0z"
fill="none" fill="none"
@ -512,6 +515,7 @@ export default {
<div class="side-menu-logo"> <div class="side-menu-logo">
<BrandImage <BrandImage
data-testid="side-menu__brand-img" data-testid="side-menu__brand-img"
:alt="t('nav.alt.mainMenuRancherLogo')"
file-name="rancher-logo.svg" file-name="rancher-logo.svg"
/> />
</div> </div>
@ -525,9 +529,12 @@ export default {
<router-link <router-link
class="option cluster selector home" class="option cluster selector home"
:to="{ name: 'home' }" :to="{ name: 'home' }"
role="link"
:aria-label="t('nav.ariaLabel.homePage')"
> >
<svg <svg
v-clean-tooltip="getTooltipConfig(t('nav.home'))" v-clean-tooltip="getTooltipConfig(t('nav.home'))"
class="top-menu-icon"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
height="24" height="24"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@ -562,6 +569,8 @@ export default {
ref="clusterFilter" ref="clusterFilter"
v-model="clusterFilter" v-model="clusterFilter"
:placeholder="t('nav.search.placeholder')" :placeholder="t('nav.search.placeholder')"
:tabindex="!shown ? -1 : 0"
:aria-label="t('nav.search.ariaLabel')"
> >
<i <i
class="magnifier icon icon-search" class="magnifier icon icon-search"
@ -583,10 +592,11 @@ export default {
<a <a
v-if="isRancherInHarvester" v-if="isRancherInHarvester"
class="option" class="option"
tabindex="0"
@click="goToHarvesterCluster()" @click="goToHarvesterCluster()"
> >
<i <i
class="icon icon-dashboard" class="icon icon-dashboard app-icon"
/> />
<div> <div>
{{ t('nav.harvesterDashboard') }} {{ t('nav.harvesterDashboard') }}
@ -602,8 +612,11 @@ export default {
class="option" class="option"
:to="a.to" :to="a.to"
:class="{'active-menu-link': a.isMenuActive }" :class="{'active-menu-link': a.isMenuActive }"
role="link"
:aria-label="`${t('nav.ariaLabel.harvesterCluster')} ${ a.label }`"
> >
<IconOrSvg <IconOrSvg
class="app-icon"
:icon="a.icon" :icon="a.icon"
:src="a.svg" :src="a.svg"
/> />
@ -637,6 +650,8 @@ export default {
class="cluster selector option" class="cluster selector option"
:class="{'active-menu-link': c.isMenuActive }" :class="{'active-menu-link': c.isMenuActive }"
:to="c.clusterRoute" :to="c.clusterRoute"
role="button"
:aria-label="`${t('nav.ariaLabel.cluster')} ${ c.label }`"
@click.prevent="clusterMenuClick($event, c)" @click.prevent="clusterMenuClick($event, c)"
@shortkey="handleKeyComboClick" @shortkey="handleKeyComboClick"
> >
@ -660,6 +675,7 @@ export default {
</div> </div>
<Pinned <Pinned
:cluster="c" :cluster="c"
:tab-order="shown ? 0 : -1"
/> />
</button> </button>
<span <span
@ -686,6 +702,7 @@ export default {
</div> </div>
<Pinned <Pinned
:cluster="c" :cluster="c"
:tab-order="shown ? 0 : -1"
/> />
</span> </span>
</div> </div>
@ -712,6 +729,8 @@ export default {
class="cluster selector option" class="cluster selector option"
:class="{'active-menu-link': c.isMenuActive }" :class="{'active-menu-link': c.isMenuActive }"
:to="c.clusterRoute" :to="c.clusterRoute"
role="button"
:aria-label="`${t('nav.ariaLabel.cluster')} ${ c.label }`"
@click="clusterMenuClick($event, c)" @click="clusterMenuClick($event, c)"
@shortkey="handleKeyComboClick" @shortkey="handleKeyComboClick"
> >
@ -725,7 +744,6 @@ export default {
v-clean-tooltip="getTooltipConfig(c)" v-clean-tooltip="getTooltipConfig(c)"
class="cluster-name" class="cluster-name"
> >
<!-- HERE LOCAL CLUSTER! -->
<p>{{ c.label }}</p> <p>{{ c.label }}</p>
<p <p
v-if="c.description" v-if="c.description"
@ -736,6 +754,7 @@ export default {
</div> </div>
<Pinned <Pinned
:class="{'showPin': c.pinned}" :class="{'showPin': c.pinned}"
:tab-order="shown ? 0 : -1"
:cluster="c" :cluster="c"
/> />
</button> </button>
@ -763,6 +782,7 @@ export default {
</div> </div>
<Pinned <Pinned
:class="{'showPin': c.pinned}" :class="{'showPin': c.pinned}"
:tab-order="shown ? 0 : -1"
:cluster="c" :cluster="c"
/> />
</span> </span>
@ -788,6 +808,8 @@ export default {
product: 'manager', product: 'manager',
resource: 'provisioning.cattle.io.cluster' resource: 'provisioning.cattle.io.cluster'
} }" } }"
role="link"
:aria-label="t('nav.ariaLabel.seeAll')"
> >
<span> <span>
{{ shown ? t('nav.seeAllClusters') : t('nav.seeAllClustersCollapsed') }} {{ shown ? t('nav.seeAllClusters') : t('nav.seeAllClustersCollapsed') }}
@ -796,6 +818,7 @@ export default {
</router-link> </router-link>
</template> </template>
<!-- MULTI CLUSTER APPS -->
<div class="category"> <div class="category">
<template v-if="multiClusterApps.length"> <template v-if="multiClusterApps.length">
<div <div
@ -815,9 +838,12 @@ export default {
class="option" class="option"
:class="{'active-menu-link': a.isMenuActive }" :class="{'active-menu-link': a.isMenuActive }"
:to="a.to" :to="a.to"
role="link"
:aria-label="`${t('nav.ariaLabel.multiClusterApps')} ${ a.label }`"
> >
<IconOrSvg <IconOrSvg
v-clean-tooltip="getTooltipConfig(a.label)" v-clean-tooltip="getTooltipConfig(a.label)"
class="app-icon"
:icon="a.icon" :icon="a.icon"
:src="a.svg" :src="a.svg"
/> />
@ -826,7 +852,7 @@ export default {
</div> </div>
</template> </template>
<!-- App menu --> <!-- Configuration apps menu -->
<template v-if="configurationApps.length"> <template v-if="configurationApps.length">
<div <div
class="category-title" class="category-title"
@ -845,9 +871,12 @@ export default {
class="option" class="option"
:class="{'active-menu-link': a.isMenuActive }" :class="{'active-menu-link': a.isMenuActive }"
:to="a.to" :to="a.to"
role="link"
:aria-label="`${t('nav.ariaLabel.configurationApps')} ${ a.label }`"
> >
<IconOrSvg <IconOrSvg
v-clean-tooltip="getTooltipConfig(a.label)" v-clean-tooltip="getTooltipConfig(a.label)"
class="app-icon"
:icon="a.icon" :icon="a.icon"
:src="a.svg" :src="a.svg"
/> />
@ -869,6 +898,8 @@ export default {
> >
<router-link <router-link
:to="{name: 'support'}" :to="{name: 'support'}"
role="link"
:aria-label="t('nav.ariaLabel.support')"
> >
{{ t('nav.support', {hasSupport}) }} {{ t('nav.support', {hasSupport}) }}
</router-link> </router-link>
@ -880,6 +911,8 @@ export default {
> >
<router-link <router-link
:to="{ name: 'about' }" :to="{ name: 'about' }"
role="link"
:aria-label="t('nav.ariaLabel.about')"
> >
{{ aboutText }} {{ aboutText }}
</router-link> </router-link>
@ -983,13 +1016,27 @@ export default {
overflow: hidden; overflow: hidden;
transition: width 250ms; transition: width 250ms;
&:focus { &:focus, &:focus-visible {
outline: 0; outline: 0;
} }
&.menu-open { .option:focus-visible {
outline: 0;
}
&.menu-open {
width: 300px; width: 300px;
box-shadow: 3px 1px 3px var(--shadow); box-shadow: 3px 1px 3px var(--shadow);
// because of accessibility, we force pin action to be visible on menu open
.pin {
display: block !important;
&:focus-visible {
@include focus-outline;
outline-offset: 4px;
}
}
} }
.title { .title {
@ -1099,10 +1146,6 @@ export default {
&:focus { &:focus {
outline: 0; outline: 0;
box-shadow: none; box-shadow: none;
> div {
text-decoration: underline;
}
} }
> i, > img { > i, > img {
@ -1120,7 +1163,22 @@ export default {
fill: var(--link); fill: var(--link);
} }
.top-menu-icon {
outline-offset: 4px;
}
&.router-link-active, &.active-menu-link { &.router-link-active, &.active-menu-link {
&:focus-visible {
.top-menu-icon, .app-icon {
@include focus-outline;
}
}
&:focus-visible .rancher-provider-icon {
@include focus-outline;
outline-offset: -4px;
}
background: var(--primary-hover-bg); background: var(--primary-hover-bg);
color: var(--primary-hover-text); color: var(--primary-hover-text);
@ -1137,6 +1195,12 @@ export default {
} }
} }
&:focus-visible {
.top-menu-icon, .rancher-provider-icon, .app-icon {
@include focus-outline;
}
}
&:hover { &:hover {
color: var(--primary-hover-text); color: var(--primary-hover-text);
background: var(--primary-hover-bg); background: var(--primary-hover-bg);
@ -1213,10 +1277,19 @@ export default {
margin-right: 16px; margin-right: 16px;
margin-top: 10px; margin-top: 10px;
&:focus-visible {
outline: none;
}
span { span {
display: flex; display: flex;
align-items: center; align-items: center;
} }
&:focus-visible span {
@include focus-outline;
outline-offset: 4px;
}
} }
.clusters { .clusters {
@ -1339,6 +1412,33 @@ export default {
} }
} }
&.menu-open {
.option {
&.router-link-active, &.active-menu-link {
&:focus-visible {
@include focus-outline;
border-radius: 0;
outline-offset: -4px;
.top-menu-icon, .app-icon, .rancher-provider-icon {
outline: none;
border-radius: 0;
}
}
}
&:focus-visible {
@include focus-outline;
outline-offset: -4px;
.top-menu-icon, .app-icon, .rancher-provider-icon {
outline: none;
border-radius: 0;
}
}
}
}
&.menu-close { &.menu-close {
.side-menu-logo { .side-menu-logo {
opacity: 0; opacity: 0;
@ -1412,8 +1512,18 @@ export default {
text-align: center; text-align: center;
} }
.support a:focus-visible {
@include focus-outline;
outline-offset: 4px;
}
.version { .version {
cursor: pointer; cursor: pointer;
a:focus-visible {
@include focus-outline;
outline-offset: 4px;
}
} }
} }
} }

Some files were not shown because too many files have changed in this diff Show More