mirror of https://github.com/rancher/dashboard.git
Merge branch 'master' of github.com:rancher/dashboard into 12485-repositories-disabling-feature
This commit is contained in:
commit
b8c39e99df
|
|
@ -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"]
|
||||
|
|
@ -9,7 +9,7 @@ jobs:
|
|||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ jobs:
|
|||
name: Build & Upload Hosted
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ jobs:
|
|||
name: Build & Upload Embedded
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ jobs:
|
|||
|
||||
- name: Upload charts artifact
|
||||
if: github.ref_type == 'tag' || (github.ref == 'refs/heads/main' && github.event_name != 'pull_request') || inputs.is_test == 'true'
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: charts
|
||||
path: tmp
|
||||
|
|
@ -132,7 +132,7 @@ jobs:
|
|||
git config user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
|
||||
- name: Download build artifact
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: charts
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ jobs:
|
|||
unit-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
|
|
@ -19,7 +19,7 @@ jobs:
|
|||
i18n:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
|
|
@ -29,7 +29,7 @@ jobs:
|
|||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ jobs:
|
|||
shell: bash
|
||||
|
||||
- name: Upload files
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: test
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ jobs:
|
|||
]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Setup env
|
||||
|
|
@ -60,11 +60,17 @@ jobs:
|
|||
|
||||
# Upload to sorry cypress in case of failure
|
||||
- name: Upload screenshots
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ failure() }}
|
||||
with:
|
||||
name: ${{github.run_number}}-${{github.run_attempt}}-extensions-compatibility-tests-screenshots-${{ matrix.role.tag }}+${{ matrix.features[0] }}
|
||||
name: ${{github.run_number}}-${{github.run_attempt}}-extensions-compatibility-tests-screenshots-${{ matrix.role.tag }}+${{ matrix.features[0] }}-${{ matrix.rancherEnv }}
|
||||
path: cypress/screenshots
|
||||
|
||||
- name: Merge Artifacts
|
||||
uses: actions/upload-artifact/merge@v4
|
||||
with:
|
||||
name: ${{github.run_number}}-${{github.run_attempt}}-extensions-compatibility-tests-screenshots-${{ matrix.role.tag }}+${{ matrix.features[0] }}
|
||||
pattern: ${{github.run_number}}-${{github.run_attempt}}-extensions-compatibility-tests-screenshots-*
|
||||
|
||||
# Slack message with outcome - success
|
||||
- name: Slack message in workflow success
|
||||
|
|
@ -88,4 +94,4 @@ jobs:
|
|||
curl -X POST \
|
||||
-H "Content-type: application/json; charset=utf-8" \
|
||||
--data '{"name": "${{ matrix.rancherEnv[0] }} - image:${{ matrix.rancherEnv[1] }} - ${{ matrix.features[1] }}", "status": "Workflow failed! 🆘", "workflow_run": ${{ github.run_id }} }' \
|
||||
$SLACK_URL
|
||||
$SLACK_URL
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
id-token: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ jobs:
|
|||
steps:
|
||||
- if: inputs.is_test == 'true' && inputs.test_branch != ''
|
||||
name: Checkout (test flow)
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ inputs.test_branch }}
|
||||
|
|
@ -41,7 +41,7 @@ jobs:
|
|||
|
||||
- if: inputs.is_test != 'true'
|
||||
name: Checkout (normal flow)
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ jobs:
|
|||
SHELL_TAG: ${{ steps.retrieve-data.outputs.SHELL_TAG }}
|
||||
CREATORS_TAG: ${{ steps.retrieve-data.outputs.CREATORS_TAG }}
|
||||
ECI_TAG: ${{ steps.retrieve-data.outputs.ECI_TAG }}
|
||||
EXTENSIONS_TAG: ${{ steps.retrieve-data.outputs.EXTENSIONS_TAG }}
|
||||
EXTENSION_TAG: ${{ steps.retrieve-data.outputs.EXTENSION_TAG }}
|
||||
CURR_JOB_ID: ${{ github.job }}
|
||||
tags-job-status: ${{ job.status }}
|
||||
steps:
|
||||
|
|
@ -53,7 +53,7 @@ jobs:
|
|||
pages: write
|
||||
with:
|
||||
target_branch: 'gh-pages'
|
||||
tagged_release: ${{ needs.retrieve-tags-master.outputs.EXTENSIONS_TAG }}
|
||||
tagged_release: ${{ needs.retrieve-tags-master.outputs.EXTENSION_TAG }}
|
||||
is_test: 'true'
|
||||
test_ext_repo: 'ui-plugin-examples'
|
||||
test_ext_branch: 'main'
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ jobs:
|
|||
SHELL_TAG: ${{ steps.retrieve-data.outputs.SHELL_TAG }}
|
||||
CREATORS_TAG: ${{ steps.retrieve-data.outputs.CREATORS_TAG }}
|
||||
ECI_TAG: ${{ steps.retrieve-data.outputs.ECI_TAG }}
|
||||
EXTENSIONS_TAG: ${{ steps.retrieve-data.outputs.EXTENSIONS_TAG }}
|
||||
EXTENSION_TAG: ${{ steps.retrieve-data.outputs.EXTENSION_TAG }}
|
||||
CURR_JOB_ID: ${{ github.job }}
|
||||
tags-job-status: ${{ job.status }}
|
||||
steps:
|
||||
|
|
@ -40,7 +40,7 @@ jobs:
|
|||
pages: write
|
||||
with:
|
||||
target_branch: 'gh-pages'
|
||||
tagged_release: ${{ needs.retrieve-tags-release-2-dot-8.outputs.EXTENSIONS_TAG }}
|
||||
tagged_release: ${{ needs.retrieve-tags-release-2-dot-8.outputs.EXTENSION_TAG }}
|
||||
is_test: 'true'
|
||||
test_ext_repo: 'elemental-ui'
|
||||
test_ext_branch: 'release-2.8.x'
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ jobs:
|
|||
SHELL_TAG: ${{ steps.retrieve-data.outputs.SHELL_TAG }}
|
||||
CREATORS_TAG: ${{ steps.retrieve-data.outputs.CREATORS_TAG }}
|
||||
ECI_TAG: ${{ steps.retrieve-data.outputs.ECI_TAG }}
|
||||
EXTENSIONS_TAG: ${{ steps.retrieve-data.outputs.EXTENSIONS_TAG }}
|
||||
EXTENSION_TAG: ${{ steps.retrieve-data.outputs.EXTENSION_TAG }}
|
||||
CURR_JOB_ID: ${{ github.job }}
|
||||
tags-job-status: ${{ job.status }}
|
||||
steps:
|
||||
|
|
@ -40,7 +40,7 @@ jobs:
|
|||
pages: write
|
||||
with:
|
||||
target_branch: 'gh-pages'
|
||||
tagged_release: ${{ needs.retrieve-tags-release-2-dot-9.outputs.EXTENSIONS_TAG }}
|
||||
tagged_release: ${{ needs.retrieve-tags-release-2-dot-9.outputs.EXTENSION_TAG }}
|
||||
is_test: 'true'
|
||||
test_ext_repo: 'elemental-ui'
|
||||
test_ext_branch: 'release-2.9.x'
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ env:
|
|||
TEST_BASE_URL: https://127.0.0.1/dashboard
|
||||
API: https://127.0.0.1
|
||||
TEST_PROJECT_ID: rancher-dashboard
|
||||
TEST_DISABLE_DASHBOARD: ${{ vars.TEST_DISABLE_DASHBOARD }} # This is required to get it from the project configuration
|
||||
TEST_DISABLE_DASHBOARD_LABEL: "${{ contains(github.event.pull_request.labels.*.name, 'ci/skip-e2e-cypress-dashboard')}}"
|
||||
CYPRESS_API_URL: http://139.59.134.103:1234/
|
||||
TEST_RUN_ID: ${{github.run_number}}-${{github.run_attempt}}-${{github.event.pull_request.title || github.event.head_commit.message}}
|
||||
# Build the dashboard to use in tests. When set to false it will grab `latest` from CDN (useful for running e2e tests quickly)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Validate checklist has been completed
|
||||
env:
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ We welcome external contributions - please refer to the internal documentation a
|
|||
|
||||
License
|
||||
=======
|
||||
Copyright (c) 2014-2025 [Rancher Labs, Inc.](http://rancher.com)
|
||||
Copyright (c) 2014-2025 [SUSE](https://www.suse.com)
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
|
|||
|
|
@ -20,5 +20,6 @@ jobs:
|
|||
with:
|
||||
registry_target: ghcr.io
|
||||
registry_user: ${{ github.actor }}
|
||||
tagged_release: ${{ github.ref_name }}
|
||||
secrets:
|
||||
registry_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@rancher/create-extension",
|
||||
"description": "Rancher UI Extension generator",
|
||||
"version": "3.0.6",
|
||||
"version": "3.0.7",
|
||||
"license": "Apache-2.0",
|
||||
"author": "SUSE",
|
||||
"packageManager": "yarn@4.5.0",
|
||||
|
|
|
|||
|
|
@ -2474,7 +2474,7 @@ function generateFakeNavClusterData(provClusterId = 'some-prov-cluster-id', mgmt
|
|||
}
|
||||
|
||||
export function generateFakeClusterDataAndIntercepts(fakeProvClusterId = 'some-prov-cluster-id', fakeMgmtClusterId = 'some-mgmt-cluster-id', addEditClusterCapabilities = false): {} {
|
||||
const longClusterDescription = 'this-is-some-really-really-really-really-really-really-long-decription';
|
||||
const longClusterDescription = 'this-is-some-really-really-really-really-really-really-long-description';
|
||||
const fakeNavClusterData = generateFakeNavClusterData(fakeProvClusterId, fakeMgmtClusterId, addEditClusterCapabilities);
|
||||
|
||||
// add cluster to fleet clusters for testing https://github.com/rancher/dashboard/issues/9984
|
||||
|
|
|
|||
|
|
@ -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"]'));
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,10 @@ export default class CardPo extends ComponentPo {
|
|||
return this.self().get('[data-testid="card-body-slot"]');
|
||||
}
|
||||
|
||||
getError(): CypressChainable {
|
||||
return this.self().get('[data-testid="card-body-slot"] > .text-error');
|
||||
}
|
||||
|
||||
getActionButton(): CypressChainable {
|
||||
return this.self().get('[data-testid="card-actions-slot"]');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,4 +17,12 @@ export default class CreateEditViewPo extends ComponentPo {
|
|||
nextPage() {
|
||||
return new AsyncButtonPo(this.self().find('.cru-resource-footer .role-primary')).click();
|
||||
}
|
||||
|
||||
saveButtonPo() :AsyncButtonPo {
|
||||
return new AsyncButtonPo(this.self().find('.cru-resource-footer .role-primary'));
|
||||
}
|
||||
|
||||
editAsYaml() {
|
||||
return new AsyncButtonPo(this.self().find('[data-testid="form-yaml"]')).click();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,12 @@ export class HeaderPo extends ComponentPo {
|
|||
return wsFilter.clickOptionWithLabel(name);
|
||||
}
|
||||
|
||||
checkCurrentWorkspace(name: string) {
|
||||
const wsFilter = new WorkspaceSwitcherPo();
|
||||
|
||||
return wsFilter.checkOptionSelected(name);
|
||||
}
|
||||
|
||||
importYamlHeaderAction() {
|
||||
return this.self().find('[data-testid="header-action-import-yaml"]');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,35 @@
|
|||
import PagePo from '@/cypress/e2e/po/pages/page.po';
|
||||
import AsyncButtonPo from '@/cypress/e2e/po/components/async-button.po';
|
||||
import CodeMirrorPo from '@/cypress/e2e/po/components/code-mirror.po';
|
||||
|
||||
import ResourceDetailPo from '@/cypress/e2e/po/edit/resource-detail.po';
|
||||
import NameNsDescription from '@/cypress/e2e/po/components/name-ns-description.po';
|
||||
export default class FleetClusterGroupsCreateEditPo extends PagePo {
|
||||
private static createPath(clusterId: string, id?: string ) {
|
||||
const root = `/c/${ clusterId }/explorer/storage.k8s.io.storageclass/create`;
|
||||
private static createPath(clusterId: string, workspace?: string, id?: string ) {
|
||||
const root = `/c/${ clusterId }/fleet/fleet.cattle.io.clustergroup`;
|
||||
|
||||
return id ? `${ root }/${ id }` : `${ root }/create`;
|
||||
return id ? `${ root }/${ workspace }/${ id }` : `${ root }/create`;
|
||||
}
|
||||
|
||||
static goTo(path: string): Cypress.Chainable<Cypress.AUTWindow> {
|
||||
throw new Error('invalid');
|
||||
}
|
||||
|
||||
constructor(clusterId = '_', id?: string) {
|
||||
super(FleetClusterGroupsCreateEditPo.createPath(clusterId, id));
|
||||
constructor(clusterId = '_', workspace?: string, id?: string) {
|
||||
super(FleetClusterGroupsCreateEditPo.createPath(clusterId, workspace, id));
|
||||
}
|
||||
|
||||
editAsYaml() {
|
||||
return new AsyncButtonPo('[data-testid="form-yaml"]', this.self());
|
||||
title() {
|
||||
return this.self().get('.title .primaryheader h1');
|
||||
}
|
||||
|
||||
yamlEditor(): CodeMirrorPo {
|
||||
return CodeMirrorPo.bySelector(this.self(), '[data-testid="yaml-editor-code-mirror"]');
|
||||
nameNsDescription() {
|
||||
return new NameNsDescription(this.self());
|
||||
}
|
||||
|
||||
saveCreateForm(): ResourceDetailPo {
|
||||
return new ResourceDetailPo(this.self());
|
||||
}
|
||||
|
||||
saveButton() {
|
||||
return new AsyncButtonPo('[data-testid="form-save"]', this.self());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"]');
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import ClusterManagerCreatePagePo from '@/cypress/e2e/po/edit/provisioning.cattl
|
|||
import TabbedPo from '@/cypress/e2e/po/components/tabbed.po';
|
||||
import RegistriesTabPo from '@/cypress/e2e/po/components/registries-tab.po';
|
||||
import NetworkTabPo from '@/cypress/e2e/po/components/network-tab.po';
|
||||
import AddonConfigPo from '@/cypress/e2e/po/components/addon-config.po';
|
||||
|
||||
/**
|
||||
* Create page for an RKE2 custom cluster
|
||||
|
|
@ -44,4 +45,8 @@ export default class ClusterManagerCreateRke2CustomPagePo extends ClusterManager
|
|||
network(): NetworkTabPo {
|
||||
return new NetworkTabPo();
|
||||
}
|
||||
|
||||
calicoAddonConfig(): AddonConfigPo {
|
||||
return new AddonConfigPo();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ export default class ClusterDashboardPagePo extends PagePo {
|
|||
}
|
||||
|
||||
fullEventsLink() {
|
||||
return cy.get('.events-table-link').contains('Full events list');
|
||||
return cy.get('[data-testid="events-link"]').contains('Full events list');
|
||||
}
|
||||
|
||||
fullSecretsList() {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import RepositoriesPagePo from '@/cypress/e2e/po/pages/chart-repositories.po';
|
|||
import BannersPo from '@/cypress/e2e/po/components/banners.po';
|
||||
import ChartRepositoriesCreateEditPo from '@/cypress/e2e/po/edit/chart-repositories.po';
|
||||
import AppClusterRepoEditPo from '@/cypress/e2e/po/edit/catalog.cattle.io.clusterrepo.po';
|
||||
import { LONG_TIMEOUT_OPT } from '@/cypress/support/utils/timeouts';
|
||||
import { LONG_TIMEOUT_OPT, MEDIUM_TIMEOUT_OPT } from '@/cypress/support/utils/timeouts';
|
||||
|
||||
export default class ExtensionsPagePo extends PagePo {
|
||||
static url = '/c/local/uiplugins'
|
||||
|
|
@ -34,7 +34,7 @@ export default class ExtensionsPagePo extends PagePo {
|
|||
return this.title().should('contain', 'Extensions');
|
||||
}
|
||||
|
||||
loading() {
|
||||
loading(options: any) {
|
||||
return this.self().get('.data-loading');
|
||||
}
|
||||
|
||||
|
|
@ -50,12 +50,16 @@ export default class ExtensionsPagePo extends PagePo {
|
|||
* @returns {Cypress.Chainable}
|
||||
*/
|
||||
addExtensionsRepository(repo: string, branch: string, name: string): Cypress.Chainable {
|
||||
cy.intercept('GET', '/v1/catalog.cattle.io.clusterrepos?exclude=metadata.managedFields').as('getRepos');
|
||||
|
||||
// we should be on the extensions page
|
||||
this.waitForPage();
|
||||
this.waitForPage(null, 'available');
|
||||
this.loading(MEDIUM_TIMEOUT_OPT).should('not.exist');
|
||||
|
||||
// go to app repos
|
||||
this.extensionMenuToggle();
|
||||
this.manageReposClick();
|
||||
cy.wait('@getRepos').its('response.statusCode').should('eq', 200);
|
||||
|
||||
// create a new clusterrepo
|
||||
const appRepoList = new RepositoriesPagePo('local', 'apps');
|
||||
|
|
@ -82,6 +86,8 @@ export default class ExtensionsPagePo extends PagePo {
|
|||
|
||||
appRepoList.waitForPage();
|
||||
appRepoList.list().state(name).should('contain', 'Active');
|
||||
|
||||
return cy.wrap(appRepoList.list());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import PagePo from '@/cypress/e2e/po/pages/page.po';
|
|||
import ResourceTablePo from '@/cypress/e2e/po/components/resource-table.po';
|
||||
import BurgerMenuPo from '@/cypress/e2e/po/side-bars/burger-side-menu.po';
|
||||
import { LONG_TIMEOUT_OPT } from '@/cypress/support/utils/timeouts';
|
||||
import BaseResourceList from '@/cypress/e2e/po/lists/base-resource-list.po';
|
||||
|
||||
export class FleetDashboardPagePo extends PagePo {
|
||||
static url: string;
|
||||
|
|
@ -44,4 +45,20 @@ export class FleetDashboardPagePo extends PagePo {
|
|||
sortableTable(tableName = 'fleet-local') {
|
||||
return this.resourceTable(tableName).sortableTable();
|
||||
}
|
||||
|
||||
goToGitRepoListLink(name: 'fleet-local' | 'fleet-default') {
|
||||
return this.self().find(`[data-testid="collapsible-card-${ name }"] h2 span` );
|
||||
}
|
||||
|
||||
list() {
|
||||
return new BaseResourceList('[data-testid="sortable-table-list-container"]');
|
||||
}
|
||||
|
||||
fleetDashboardEmptyState() {
|
||||
return this.self().get('.fleet-empty-dashboard');
|
||||
}
|
||||
|
||||
getStartedButton() {
|
||||
return this.self().get('.btn').contains('Get started');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,14 +5,16 @@ import ResourceTablePo from '@/cypress/e2e/po/components/resource-table.po';
|
|||
import FleetClusterGroupsList from '@/cypress/e2e/po/lists/fleet/fleet.cattle.io.clustergroup';
|
||||
import FleetClusterGroupsCreateEditPo from '@/cypress/e2e/po/edit/fleet/fleet.cattle.io.clustergroup.po';
|
||||
export class FleetClusterGroupsListPagePo extends PagePo {
|
||||
static url = `/c/_/fleet/fleet.cattle.io.clustergroup`
|
||||
|
||||
constructor() {
|
||||
super(FleetClusterGroupsListPagePo.url);
|
||||
private static createPath(clusterId: string) {
|
||||
return `/c/${ clusterId }/fleet/fleet.cattle.io.clustergroup`;
|
||||
}
|
||||
|
||||
goTo() {
|
||||
return cy.visit(FleetClusterGroupsListPagePo.url);
|
||||
static goTo(clusterId: string): Cypress.Chainable<Cypress.AUTWindow> {
|
||||
return super.goTo(FleetClusterGroupsListPagePo.createPath(clusterId));
|
||||
}
|
||||
|
||||
constructor(private clusterId = '_') {
|
||||
super(FleetClusterGroupsListPagePo.createPath(clusterId));
|
||||
}
|
||||
|
||||
static navTo() {
|
||||
|
|
@ -44,7 +46,7 @@ export class FleetClusterGroupsListPagePo extends PagePo {
|
|||
return this.self().find('[data-testid="masthead-create"]').click();
|
||||
}
|
||||
|
||||
createFleetClusterGroupsForm(id? : string): FleetClusterGroupsCreateEditPo {
|
||||
return new FleetClusterGroupsCreateEditPo(id);
|
||||
createFleetClusterGroupsForm(workspace?: string, id? : string): FleetClusterGroupsCreateEditPo {
|
||||
return new FleetClusterGroupsCreateEditPo(this.clusterId, workspace, id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,17 @@ export class FeatureFlagsPagePo extends RootClusterPage {
|
|||
return card.getBody().contains(label);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get card body error
|
||||
* @param error
|
||||
* @returns
|
||||
*/
|
||||
cardActionError(error: string): CypressChainable {
|
||||
const card = new CardPo();
|
||||
|
||||
return card.getError().contains(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Click action button
|
||||
* @param label Activate or Deactivate
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import PagePo from '@/cypress/e2e/po/pages/page.po';
|
|||
import ProductNavPo from '@/cypress/e2e/po/side-bars/product-side-nav.po';
|
||||
import { generateFakeClusterDataAndIntercepts } from '@/cypress/e2e/blueprints/nav/fake-cluster';
|
||||
|
||||
const longClusterDescription = 'this-is-some-really-really-really-really-really-really-long-decription';
|
||||
const longClusterDescription = 'this-is-some-really-really-really-really-really-really-long-description';
|
||||
const fakeProvClusterId = 'some-fake-cluster-id';
|
||||
const fakeMgmtClusterId = 'some-fake-mgmt-id';
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ const chartsPage = new ChartsPage();
|
|||
|
||||
describe('Apps/Charts', { tags: ['@explorer', '@adminUser'] }, () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('GET', '/v1/catalog.cattle.io.clusterrepos/**').as('fetchChartData');
|
||||
|
||||
cy.login();
|
||||
chartsPage.goTo();
|
||||
chartsPage.waitForPage();
|
||||
|
|
@ -94,6 +96,9 @@ describe('Apps/Charts', { tags: ['@explorer', '@adminUser'] }, () => {
|
|||
it('should call fetch when route query changes with valid parameters', () => {
|
||||
const chartName = 'Logging';
|
||||
|
||||
cy.wait('@fetchChartData');
|
||||
cy.get('@fetchChartData.all').should('have.length.at.least', 3);
|
||||
|
||||
chartsPage.getChartByName(chartName)
|
||||
.should('exist')
|
||||
.scrollIntoView()
|
||||
|
|
@ -105,16 +110,19 @@ describe('Apps/Charts', { tags: ['@explorer', '@adminUser'] }, () => {
|
|||
chartPage.waitForPage();
|
||||
|
||||
// Set up intercept for the network request triggered by $fetch
|
||||
cy.intercept('GET', '**/v1/catalog.cattle.io.clusterrepos/**').as('fetchChartData');
|
||||
cy.intercept('GET', '**/v1/catalog.cattle.io.clusterrepos/**').as('fetchChartDataAfterSelect');
|
||||
|
||||
chartPage.selectVersion('103.1.1+up4.4.0');
|
||||
|
||||
cy.wait('@fetchChartData').its('response.statusCode').should('eq', 200);
|
||||
cy.wait('@fetchChartDataAfterSelect').its('response.statusCode').should('eq', 200);
|
||||
});
|
||||
|
||||
it('should not call fetch when navigating back to charts page', () => {
|
||||
const chartName = 'Logging';
|
||||
|
||||
cy.wait('@fetchChartData');
|
||||
cy.get('@fetchChartData.all').should('have.length.at.least', 3);
|
||||
|
||||
chartsPage.getChartByName(chartName)
|
||||
.should('exist')
|
||||
.scrollIntoView()
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ describe('Apps', () => {
|
|||
cy.intercept('GET', '/v1/catalog.cattle.io.clusterrepos/rancher-charts?*').as('rancherCharts1');
|
||||
|
||||
// Nav to a summary page for a specific chart
|
||||
ChartsPage.navTo(clusterId);
|
||||
chartsPage.goTo();
|
||||
chartsPage.chartsFilterCategoriesSelect().toggle();
|
||||
chartsPage.chartsFilterCategoriesSelect().clickOptionWithLabel('All Categories');
|
||||
chartsPage.chartsFilterReposSelect().toggle();
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ describe('Cluster Dashboard', { testIsolation: 'off', tags: ['@explorer', '@admi
|
|||
|
||||
clusterDashboard.waitForPage(undefined, 'cluster-events');
|
||||
|
||||
// check if burguer menu nav is highlighted correctly for local cluster
|
||||
// check if burger menu nav is highlighted correctly for local cluster
|
||||
BurgerMenuPo.checkIfClusterMenuLinkIsHighlighted('local');
|
||||
});
|
||||
|
||||
|
|
@ -257,48 +257,47 @@ describe('Cluster Dashboard', { testIsolation: 'off', tags: ['@explorer', '@admi
|
|||
});
|
||||
|
||||
it('can view events table empty if no events', { tags: ['@vai', '@adminUser'] }, () => {
|
||||
cy.visit(clusterDashboard.urlPath(), {
|
||||
onBeforeLoad(win) {
|
||||
cy.stub(win.console, 'error').as('consoleError');
|
||||
cy.stub(win.console, 'warn').as('consoleWarn');
|
||||
},
|
||||
});
|
||||
|
||||
eventsNoDataset();
|
||||
clusterDashboard.goTo();
|
||||
|
||||
cy.get('@consoleError').should('not.be.called'); // See error lot
|
||||
cy.get('@consoleWarn').should('not.be.called'); // See warning log (there will be some....)
|
||||
|
||||
cy.wait('@eventsNoData');
|
||||
clusterDashboard.waitForPage(undefined, 'cluster-events');
|
||||
|
||||
clusterDashboard.eventsList().resourceTable().sortableTable().checkRowCount(true, 1);
|
||||
|
||||
const expectedHeaders = ['Reason', 'Object', 'Message', 'Name', 'Date'];
|
||||
let expectedHeaders = ['Reason', 'Object', 'Message', 'Name', 'Date'];
|
||||
|
||||
clusterDashboard.eventsList().resourceTable().sortableTable().tableHeaderRow()
|
||||
.within('.table-header-container .content')
|
||||
.each((el, i) => {
|
||||
expect(el.text().trim()).to.eq(expectedHeaders[i]);
|
||||
});
|
||||
cy.isVaiCacheEnabled().then((isVaiCacheEnabled) => {
|
||||
if (isVaiCacheEnabled) {
|
||||
expectedHeaders = ['Reason', 'Object', 'Message', 'Name', 'First Seen', 'Last Seen', 'Count'];
|
||||
}
|
||||
|
||||
clusterDashboard.fullEventsLink().click();
|
||||
cy.wait('@eventsNoData');
|
||||
const events = new EventsPagePo('local');
|
||||
clusterDashboard.eventsList().resourceTable().sortableTable().tableHeaderRow()
|
||||
.self()
|
||||
.scrollIntoView();
|
||||
clusterDashboard.eventsList().resourceTable().sortableTable().tableHeaderRow()
|
||||
.within('.table-header-container .content')
|
||||
.each((el, i) => {
|
||||
expect(el.text().trim()).to.eq(expectedHeaders[i]);
|
||||
});
|
||||
|
||||
events.waitForPage();
|
||||
clusterDashboard.fullEventsLink().click();
|
||||
cy.wait('@eventsNoData');
|
||||
const events = new EventsPagePo('local');
|
||||
|
||||
events.eventslist().resourceTable().sortableTable().checkRowCount(true, 1);
|
||||
events.waitForPage();
|
||||
|
||||
const expectedFullHeaders = ['State', 'Last Seen', 'Type', 'Reason', 'Object',
|
||||
'Subobject', 'Source', 'Message', 'First Seen', 'Count', 'Name', 'Namespace'];
|
||||
events.eventslist().resourceTable().sortableTable().checkRowCount(true, 1);
|
||||
|
||||
events.eventslist().resourceTable().sortableTable().tableHeaderRow()
|
||||
.within('.table-header-container .content')
|
||||
.each((el, i) => {
|
||||
expect(el.text().trim()).to.eq(expectedFullHeaders[i]);
|
||||
});
|
||||
const expectedFullHeaders = ['State', 'Last Seen', 'Type', 'Reason', 'Object',
|
||||
'Subobject', 'Source', 'Message', 'First Seen', 'Count', 'Name', 'Namespace'];
|
||||
|
||||
events.eventslist().resourceTable().sortableTable().tableHeaderRow()
|
||||
.within('.table-header-container .content')
|
||||
.each((el, i) => {
|
||||
expect(el.text().trim()).to.eq(expectedFullHeaders[i]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cluster dashboard with limited permissions', () => {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,27 @@ import SortableTablePo from '@/cypress/e2e/po/components/sortable-table.po';
|
|||
const cluster = 'local';
|
||||
const clusterDashboard = new ClusterDashboardPagePo(cluster);
|
||||
const events = new EventsPagePo(cluster);
|
||||
const pageSize = 10;
|
||||
// Should be enough to create at least 3 pages of events
|
||||
const podCount = 15;
|
||||
|
||||
const countHelper = {
|
||||
setupCount: (vaiCacheEnabled: boolean, initialCount: number) => {
|
||||
if (vaiCacheEnabled) {
|
||||
cy.intercept('GET', '/v1/events?*').as('getCount');
|
||||
} else {
|
||||
cy.wrap(initialCount).as('count');
|
||||
}
|
||||
},
|
||||
handleCount: (vaiCacheEnabled) => {
|
||||
if (vaiCacheEnabled) {
|
||||
cy.wait('@getCount').then((interception) => {
|
||||
cy.wrap(interception.response.body.count).as('count');
|
||||
});
|
||||
}
|
||||
},
|
||||
getCount: () => cy.get('@count').then((count) => count as any as number),
|
||||
};
|
||||
|
||||
describe('Events', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] }, () => {
|
||||
before(() => {
|
||||
|
|
@ -20,7 +41,12 @@ describe('Events', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] },
|
|||
let nsName2: string;
|
||||
|
||||
before('set up', () => {
|
||||
cy.updateNamespaceFilter(cluster, 'none', '{\"local\":[]}');
|
||||
cy.tableRowsPerPageAndPreferences(pageSize, {
|
||||
clusterName: cluster,
|
||||
groupBy: 'none',
|
||||
namespaceFilter: '{\"local\":[]}',
|
||||
allNamespaces: 'true',
|
||||
});
|
||||
|
||||
cy.createE2EResourceName('ns1').then((ns1) => {
|
||||
nsName1 = ns1;
|
||||
|
|
@ -30,7 +56,7 @@ describe('Events', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] },
|
|||
// create pods
|
||||
let i = 0;
|
||||
|
||||
while (i < 125) {
|
||||
while (i < podCount) {
|
||||
const podName = Cypress._.uniqueId(Date.now().toString());
|
||||
|
||||
cy.createPod(nsName1, podName, 'nginx:latest', false, { createNameOptions: { prefixContext: true } }).then((resp) => {
|
||||
|
|
@ -52,6 +78,9 @@ describe('Events', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] },
|
|||
uniquePod = resp.body.metadata.name;
|
||||
});
|
||||
});
|
||||
|
||||
// I'm loathed to do this, but the events created from the pods need to settle before we start
|
||||
cy.wait(20000); // eslint-disable-line cypress/no-unnecessary-waiting
|
||||
});
|
||||
|
||||
it('pagination is visible and user is able to navigate through events data', () => {
|
||||
|
|
@ -61,73 +90,113 @@ describe('Events', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] },
|
|||
EventsPagePo.navTo();
|
||||
events.waitForPage();
|
||||
|
||||
cy.getRancherResource('v1', 'events').then((resp: Cypress.Response<any>) => {
|
||||
// Why 500? there's a hardcoded figure to stops ui from storing more than 500 events ...
|
||||
const count = resp.body.count < 500 ? resp.body.count : 500;
|
||||
let vaiCacheEnabled = false;
|
||||
|
||||
// Test break down if less than 400...
|
||||
expect(count).to.be.greaterThan(400);
|
||||
cy.isVaiCacheEnabled()
|
||||
.then((isVaiCacheEnabled) => {
|
||||
vaiCacheEnabled = isVaiCacheEnabled;
|
||||
|
||||
// pagination is visible
|
||||
events.sortableTable().pagination().checkVisible();
|
||||
return cy.getRancherResource('v1', 'events');
|
||||
})
|
||||
.then((resp: Cypress.Response<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
|
||||
events.sortableTable().pagination().beginningButton().isDisabled();
|
||||
events.sortableTable().pagination().leftButton().isDisabled();
|
||||
events.sortableTable().pagination().rightButton().isEnabled();
|
||||
events.sortableTable().pagination().endButton().isEnabled();
|
||||
// pagination is visible
|
||||
events.sortableTable().pagination().checkVisible();
|
||||
|
||||
// check text before navigation
|
||||
events.sortableTable().pagination().paginationText().then((el) => {
|
||||
expect(el.trim()).to.eq(`1 - 100 of ${ count } Events`);
|
||||
const loadingPo = new LoadingPo('.title .resource-loading-indicator');
|
||||
|
||||
loadingPo.checkNotExists();
|
||||
|
||||
// basic checks on navigation buttons
|
||||
events.sortableTable().pagination().beginningButton().isDisabled();
|
||||
events.sortableTable().pagination().leftButton().isDisabled();
|
||||
events.sortableTable().pagination().rightButton().isEnabled();
|
||||
events.sortableTable().pagination().endButton().isEnabled();
|
||||
|
||||
// check text before navigation
|
||||
events.sortableTable().pagination().self().scrollIntoView();
|
||||
events.sortableTable().pagination().paginationText().then((el) => {
|
||||
expect(el.trim()).to.eq(`1 - ${ pageSize } of ${ initialCount } Events`);
|
||||
});
|
||||
|
||||
// navigate to next page - right button
|
||||
countHelper.setupCount(vaiCacheEnabled, initialCount);
|
||||
events.sortableTable().pagination().rightButton().click();
|
||||
countHelper.handleCount(vaiCacheEnabled);
|
||||
|
||||
// check text and buttons after navigation
|
||||
events.sortableTable().pagination().self().scrollIntoView();
|
||||
countHelper.getCount().then((count) => {
|
||||
return events.sortableTable().pagination().paginationText().then((el) => {
|
||||
expect(el.trim()).to.eq(`${ pageSize + 1 } - ${ 2 * pageSize } of ${ count } Events`);
|
||||
});
|
||||
});
|
||||
events.sortableTable().pagination().beginningButton().isEnabled();
|
||||
events.sortableTable().pagination().leftButton().isEnabled();
|
||||
|
||||
// navigate to first page - left button
|
||||
countHelper.setupCount(vaiCacheEnabled, initialCount);
|
||||
events.sortableTable().pagination().leftButton().click();
|
||||
countHelper.handleCount(vaiCacheEnabled);
|
||||
|
||||
// check text and buttons after navigation
|
||||
events.sortableTable().pagination().self().scrollIntoView();
|
||||
countHelper.getCount().then((count) => {
|
||||
return events.sortableTable().pagination().paginationText().then((el) => {
|
||||
expect(el.trim()).to.eq(`1 - ${ pageSize } of ${ count } Events`);
|
||||
});
|
||||
});
|
||||
|
||||
events.sortableTable().pagination().beginningButton().isDisabled();
|
||||
events.sortableTable().pagination().leftButton().isDisabled();
|
||||
|
||||
// navigate to last page - end button
|
||||
countHelper.setupCount(vaiCacheEnabled, initialCount);
|
||||
events.sortableTable().pagination().endButton().scrollIntoView()
|
||||
.click();
|
||||
countHelper.handleCount(vaiCacheEnabled);
|
||||
|
||||
// check text after navigation
|
||||
events.sortableTable().pagination().self().scrollIntoView();
|
||||
countHelper.getCount().then((count) => {
|
||||
return events.sortableTable().pagination().paginationText().then((el) => {
|
||||
let pages = Math.floor(count / pageSize);
|
||||
|
||||
if (count % pageSize === 0) {
|
||||
pages--;
|
||||
}
|
||||
const from = (pages * pageSize) + 1;
|
||||
const to = count;
|
||||
|
||||
expect(el.trim()).to.eq(`${ from } - ${ to } of ${ to } Events`);
|
||||
});
|
||||
});
|
||||
|
||||
// navigate to first page - beginning button
|
||||
countHelper.setupCount(vaiCacheEnabled, initialCount);
|
||||
events.sortableTable().pagination().beginningButton().click();
|
||||
countHelper.handleCount(vaiCacheEnabled);
|
||||
|
||||
// check text and buttons after navigation
|
||||
events.sortableTable().pagination().self().scrollIntoView();
|
||||
countHelper.getCount().then((count) => {
|
||||
events.sortableTable().pagination().paginationText().then((el) => {
|
||||
expect(el.trim()).to.eq(`1 - ${ pageSize } of ${ count } Events`);
|
||||
});
|
||||
});
|
||||
|
||||
events.sortableTable().pagination().beginningButton().isDisabled();
|
||||
events.sortableTable().pagination().leftButton().isDisabled();
|
||||
});
|
||||
|
||||
// navigate to next page - right button
|
||||
events.sortableTable().pagination().rightButton().click();
|
||||
|
||||
// check text and buttons after navigation
|
||||
events.sortableTable().pagination().paginationText().then((el) => {
|
||||
expect(el.trim()).to.eq(`101 - 200 of ${ count } Events`);
|
||||
});
|
||||
events.sortableTable().pagination().beginningButton().isEnabled();
|
||||
events.sortableTable().pagination().leftButton().isEnabled();
|
||||
|
||||
// navigate to first page - left button
|
||||
events.sortableTable().pagination().leftButton().click();
|
||||
|
||||
// check text and buttons after navigation
|
||||
events.sortableTable().pagination().paginationText().then((el) => {
|
||||
expect(el.trim()).to.eq(`1 - 100 of ${ count } Events`);
|
||||
});
|
||||
events.sortableTable().pagination().beginningButton().isDisabled();
|
||||
events.sortableTable().pagination().leftButton().isDisabled();
|
||||
|
||||
// navigate to last page - end button
|
||||
events.sortableTable().pagination().endButton().scrollIntoView()
|
||||
.click();
|
||||
|
||||
// check row count on last page
|
||||
events.sortableTable().checkRowCount(false, 100);
|
||||
|
||||
// check text after navigation
|
||||
events.sortableTable().pagination().paginationText().then((el) => {
|
||||
expect(el.trim()).to.eq(`401 - ${ count } of ${ count } Events`);
|
||||
});
|
||||
|
||||
// navigate to first page - beginning button
|
||||
events.sortableTable().pagination().beginningButton().click();
|
||||
|
||||
// check text and buttons after navigation
|
||||
events.sortableTable().pagination().paginationText().then((el) => {
|
||||
expect(el.trim()).to.eq(`1 - 100 of ${ count } Events`);
|
||||
});
|
||||
events.sortableTable().pagination().beginningButton().isDisabled();
|
||||
events.sortableTable().pagination().leftButton().isDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('filter events', () => {
|
||||
|
|
@ -138,7 +207,7 @@ describe('Events', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] },
|
|||
|
||||
events.sortableTable().checkVisible();
|
||||
events.sortableTable().checkLoadingIndicatorNotVisible();
|
||||
events.sortableTable().checkRowCount(false, 100);
|
||||
events.sortableTable().checkRowCount(false, pageSize);
|
||||
|
||||
// filter by namespace
|
||||
events.sortableTable().filter(nsName2);
|
||||
|
|
@ -201,7 +270,12 @@ describe('Events', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] },
|
|||
});
|
||||
|
||||
after('clean up', () => {
|
||||
cy.updateNamespaceFilter(cluster, 'none', '{"local":["all://user"]}');
|
||||
cy.tableRowsPerPageAndPreferences(100, {
|
||||
clusterName: cluster,
|
||||
groupBy: 'none',
|
||||
namespaceFilter: '{"local":["all://user"]}',
|
||||
allNamespaces: 'false',
|
||||
});
|
||||
|
||||
// delete namespace (this will also delete all pods in it)
|
||||
cy.deleteRancherResource('v1', 'namespaces', nsName1);
|
||||
|
|
|
|||
|
|
@ -19,8 +19,11 @@ describe('HorizontalPodAutoscalers', { testIsolation: 'off', tags: ['@explorer',
|
|||
horizontalPodAutoscalersPage.waitForPage();
|
||||
cy.wait('@horizontalpodautoscalerNoData');
|
||||
|
||||
const expectedHeaders = ['State', 'Name', 'Workload', 'Minimum Replicas', 'Maximum Replicas', 'Current Replicas', 'Age'];
|
||||
const expectedHeaders = ['State', 'Name', 'Namespace', 'Workload', 'Minimum Replicas', 'Maximum Replicas', 'Current Replicas', 'Age'];
|
||||
|
||||
horizontalPodAutoscalersPage.list().resourceTable().sortableTable().tableHeaderRow()
|
||||
.self()
|
||||
.scrollIntoView();
|
||||
horizontalPodAutoscalersPage.list().resourceTable().sortableTable().tableHeaderRow()
|
||||
.get('.table-header-container .content')
|
||||
.each((el, i) => {
|
||||
|
|
@ -39,7 +42,7 @@ describe('HorizontalPodAutoscalers', { testIsolation: 'off', tags: ['@explorer',
|
|||
horizontalPodAutoscalersPage.header().selectNamespaceFilterOption('All Namespaces');
|
||||
|
||||
// check table headers are visible
|
||||
const expectedHeaders = ['State', 'Name', 'Workload', 'Minimum Replicas', 'Maximum Replicas', 'Current Replicas', 'Age'];
|
||||
const expectedHeaders = ['State', 'Name', 'Namespace', 'Workload', 'Minimum Replicas', 'Maximum Replicas', 'Current Replicas', 'Age'];
|
||||
|
||||
horizontalPodAutoscalersPage.list().resourceTable().sortableTable().tableHeaderRow()
|
||||
.get('.table-header-container .content')
|
||||
|
|
@ -65,7 +68,7 @@ describe('HorizontalPodAutoscalers', { testIsolation: 'off', tags: ['@explorer',
|
|||
horizontalPodAutoscalersPage.list().resourceTable().sortableTable().groupByButtons(1)
|
||||
.click();
|
||||
|
||||
// check table headers are visible
|
||||
// check table headers are visible (minus namespace given we're now grouped by it)
|
||||
const expectedHeaders = ['State', 'Name', 'Workload', 'Minimum Replicas', 'Maximum Replicas', 'Current Replicas', 'Age'];
|
||||
|
||||
horizontalPodAutoscalersPage.list().resourceTable().sortableTable().tableHeaderRow()
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { NamespaceFilterPo } from '@/cypress/e2e/po/components/namespace-filter.
|
|||
|
||||
const namespaceFilter = new NamespaceFilterPo();
|
||||
const cluster = 'local';
|
||||
let removeExtensions = false;
|
||||
|
||||
const DISABLED_CACHE_EXTENSION_NAME = 'large-extension';
|
||||
// const DISABLED_CACHE_EXTENSION_MENU_LABEL = 'Large-extension';
|
||||
|
|
@ -16,6 +17,7 @@ const UNAUTHENTICATED_EXTENSION_NAME = 'uk-locale';
|
|||
const EXTENSION_NAME = 'clock';
|
||||
const UI_PLUGINS_PARTNERS_REPO_URL = 'https://github.com/rancher/partner-extensions';
|
||||
const UI_PLUGINS_PARTNERS_REPO_NAME = 'partner-extensions';
|
||||
const GIT_REPO_NAME = 'rancher-plugin-examples';
|
||||
|
||||
describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => {
|
||||
beforeEach(() => {
|
||||
|
|
@ -69,7 +71,9 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => {
|
|||
extensionsPo.extensionTabInstalledClick(); // Avoid nav guard failures that probably auto move user to this tab
|
||||
|
||||
// install the rancher plugin examples
|
||||
extensionsPo.addExtensionsRepository('https://github.com/rancher/ui-plugin-examples', 'main', 'rancher-plugin-examples');
|
||||
extensionsPo.addExtensionsRepository('https://github.com/rancher/ui-plugin-examples', 'main', GIT_REPO_NAME).then(() => {
|
||||
removeExtensions = true;
|
||||
});
|
||||
});
|
||||
|
||||
it('has the correct title for Prime users and should display banner on main extensions screen EVEN IF setting is empty string', () => {
|
||||
|
|
@ -253,9 +257,11 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => {
|
|||
});
|
||||
|
||||
it('Should install an extension', () => {
|
||||
cy.intercept('POST', `/v1/catalog.cattle.io.clusterrepos/${ GIT_REPO_NAME }?action=install`).as('installExtension');
|
||||
const extensionsPo = new ExtensionsPagePo();
|
||||
|
||||
extensionsPo.goTo();
|
||||
extensionsPo.waitForPage();
|
||||
|
||||
extensionsPo.extensionTabAvailableClick();
|
||||
extensionsPo.waitForPage(null, 'available');
|
||||
|
|
@ -267,6 +273,7 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => {
|
|||
// select version and click install
|
||||
extensionsPo.installModalSelectVersionClick(2);
|
||||
extensionsPo.installModalInstallClick();
|
||||
cy.wait('@installExtension').its('response.statusCode').should('eq', 201);
|
||||
|
||||
// let's check the extension reload banner and reload the page
|
||||
extensionsPo.extensionReloadBanner().should('be.visible');
|
||||
|
|
@ -297,6 +304,7 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => {
|
|||
});
|
||||
|
||||
it('Should update an extension version', () => {
|
||||
cy.intercept('POST', `/v1/catalog.cattle.io.clusterrepos/${ GIT_REPO_NAME }?action=upgrade`).as('upgradeExtension');
|
||||
const extensionsPo = new ExtensionsPagePo();
|
||||
|
||||
extensionsPo.goTo();
|
||||
|
|
@ -308,6 +316,7 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => {
|
|||
// click on update button on card
|
||||
extensionsPo.extensionCardUpdateClick(EXTENSION_NAME);
|
||||
extensionsPo.installModalInstallClick();
|
||||
cy.wait('@upgradeExtension').its('response.statusCode').should('eq', 201);
|
||||
|
||||
// let's check the extension reload banner and reload the page
|
||||
extensionsPo.extensionReloadBanner().should('be.visible');
|
||||
|
|
@ -515,4 +524,10 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => {
|
|||
extensionsPo.extensionCardClick(DISABLED_CACHE_EXTENSION_NAME);
|
||||
extensionsPo.extensionDetailsTitle().should('contain', DISABLED_CACHE_EXTENSION_NAME);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if ( removeExtensions ) {
|
||||
cy.deleteRancherResource('v1', 'catalog.cattle.io.clusterrepos', GIT_REPO_NAME);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@ import KubewardenExtensionPo from '@/cypress/e2e/po/pages/extensions/kubewarden.
|
|||
import { catchTargetPageException } from '@/cypress/support/utils/exception-utils';
|
||||
|
||||
const extensionName = 'kubewarden';
|
||||
const gitRepoName = 'rancher-extensions';
|
||||
let removeExtensions = false;
|
||||
|
||||
describe('Kubewarden Extension', { tags: ['@extensions-temp-excluded', '@adminUser'] }, () => {
|
||||
describe('Kubewarden Extension', { tags: ['@extensions', '@adminUser'] }, () => {
|
||||
before(() => {
|
||||
catchTargetPageException('Navigation cancelled');
|
||||
cy.login();
|
||||
|
|
@ -18,7 +20,9 @@ describe('Kubewarden Extension', { tags: ['@extensions-temp-excluded', '@adminUs
|
|||
extensionsPo.waitForPage();
|
||||
|
||||
// install the ui-plugin-charts repo
|
||||
extensionsPo.addExtensionsRepository('https://github.com/rancher/ui-plugin-charts', 'main', 'rancher-extensions');
|
||||
extensionsPo.addExtensionsRepository('https://github.com/rancher/ui-plugin-charts', 'main', gitRepoName).then(() => {
|
||||
removeExtensions = true;
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -32,6 +36,7 @@ describe('Kubewarden Extension', { tags: ['@extensions-temp-excluded', '@adminUs
|
|||
extensionsPo.waitForPage();
|
||||
|
||||
extensionsPo.extensionTabAvailableClick();
|
||||
extensionsPo.waitForPage(null, 'available');
|
||||
|
||||
// click on install button on card
|
||||
extensionsPo.extensionCardInstallClick(extensionName);
|
||||
|
|
@ -110,4 +115,10 @@ describe('Kubewarden Extension', { tags: ['@extensions-temp-excluded', '@adminUs
|
|||
extensionsPo.extensionCardClick(extensionName);
|
||||
extensionsPo.extensionDetailsTitle().should('contain', extensionName);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if ( removeExtensions ) {
|
||||
cy.deleteRancherResource('v1', 'catalog.cattle.io.clusterrepos', gitRepoName);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,63 +1,147 @@
|
|||
import { FleetClusterGroupsListPagePo } from '@/cypress/e2e/po/pages/fleet/fleet.cattle.io.clustergroup.po';
|
||||
import FleetClusterGroupDetailsPo from '@/cypress/e2e/po/detail/fleet/fleet.cattle.io.clustergroup.po';
|
||||
import { HeaderPo } from '@/cypress/e2e/po/components/header.po';
|
||||
import PromptRemove from '@/cypress/e2e/po/prompts/promptRemove.po';
|
||||
|
||||
describe('Cluster Groups', { testIsolation: 'off', tags: ['@fleet', '@adminUser'] }, () => {
|
||||
const fleetClusterGroups = new FleetClusterGroupsListPagePo();
|
||||
const headerPo = new HeaderPo();
|
||||
const localWorkspace = 'fleet-local';
|
||||
let clusterGroupName;
|
||||
let removeClusterGroups = false;
|
||||
const clusterGroupsToDelete = [];
|
||||
|
||||
describe('List', { tags: ['@vai', '@adminUser'] }, () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
it('check table headers are available in list and details view', () => {
|
||||
const groupName = 'default';
|
||||
const workspace = 'fleet-local';
|
||||
|
||||
FleetClusterGroupsListPagePo.navTo();
|
||||
fleetClusterGroups.waitForPage();
|
||||
headerPo.selectWorkspace(workspace);
|
||||
fleetClusterGroups.clusterGroupsList().rowWithName(groupName).checkVisible();
|
||||
|
||||
// check table headers
|
||||
const expectedHeaders = ['State', 'Name', 'Clusters Ready', 'Resources', 'Age'];
|
||||
|
||||
fleetClusterGroups.clusterGroupsList().resourceTable().sortableTable().tableHeaderRow()
|
||||
.within('.table-header-container .content')
|
||||
.each((el, i) => {
|
||||
expect(el.text().trim()).to.eq(expectedHeaders[i]);
|
||||
});
|
||||
|
||||
// go to fleet cluster details
|
||||
fleetClusterGroups.goToDetailsPage(groupName);
|
||||
|
||||
const fleetClusterGroupDetailsPage = new FleetClusterGroupDetailsPo(workspace, groupName);
|
||||
|
||||
fleetClusterGroupDetailsPage.waitForPage(null, 'clusters');
|
||||
|
||||
// check table headers
|
||||
const expectedHeadersDetailsView = ['State', 'Name', 'Bundles Ready', 'Repos Ready', 'Resources', 'Last Seen', 'Age'];
|
||||
|
||||
fleetClusterGroupDetailsPage.clusterList().resourceTable().sortableTable()
|
||||
.tableHeaderRow()
|
||||
.within('.table-header-container .content')
|
||||
.each((el, i) => {
|
||||
expect(el.text().trim()).to.eq(expectedHeadersDetailsView[i]);
|
||||
});
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.createE2EResourceName('cluster-group').then((name) => {
|
||||
clusterGroupName = name;
|
||||
});
|
||||
});
|
||||
describe('Edit', { tags: ['@vai', '@adminUser'] }, () => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
it('can open "Edit as YAML"', () => {
|
||||
FleetClusterGroupsListPagePo.navTo();
|
||||
fleetClusterGroups.waitForPage();
|
||||
fleetClusterGroups.clickCreate();
|
||||
fleetClusterGroups.createFleetClusterGroupsForm().editAsYaml().click();
|
||||
fleetClusterGroups.createFleetClusterGroupsForm().yamlEditor().checkExists();
|
||||
});
|
||||
it('can create cluster group', () => {
|
||||
FleetClusterGroupsListPagePo.navTo();
|
||||
fleetClusterGroups.waitForPage();
|
||||
headerPo.selectWorkspace(localWorkspace);
|
||||
fleetClusterGroups.clickCreate();
|
||||
fleetClusterGroups.createFleetClusterGroupsForm().waitForPage();
|
||||
|
||||
fleetClusterGroups.createFleetClusterGroupsForm().nameNsDescription().name().set(clusterGroupName);
|
||||
fleetClusterGroups.createFleetClusterGroupsForm().saveCreateForm().cruResource().saveOrCreate()
|
||||
.click()
|
||||
.then(() => {
|
||||
removeClusterGroups = true;
|
||||
clusterGroupsToDelete.push(`${ localWorkspace }/${ clusterGroupName }`);
|
||||
});
|
||||
|
||||
fleetClusterGroups.waitForPage();
|
||||
fleetClusterGroups.clusterGroupsList().details(clusterGroupName, 1).should('be.visible');
|
||||
});
|
||||
|
||||
it('can edit a cluster group', () => {
|
||||
FleetClusterGroupsListPagePo.navTo();
|
||||
fleetClusterGroups.waitForPage();
|
||||
headerPo.selectWorkspace(localWorkspace);
|
||||
fleetClusterGroups.clusterGroupsList().actionMenu(clusterGroupName).getMenuItem('Edit Config').click();
|
||||
fleetClusterGroups.createFleetClusterGroupsForm(localWorkspace, clusterGroupName).waitForPage('mode=edit');
|
||||
fleetClusterGroups.createFleetClusterGroupsForm().nameNsDescription().description().set(`${ clusterGroupName }-fleet-desc`);
|
||||
fleetClusterGroups.createFleetClusterGroupsForm().saveCreateForm().cruResource().saveAndWaitForRequests('PUT', `v1/fleet.cattle.io.clustergroups/${ localWorkspace }/${ clusterGroupName }`)
|
||||
.then(({ response }) => {
|
||||
expect(response?.statusCode).to.eq(200);
|
||||
expect(response?.body.metadata).to.have.property('name', clusterGroupName);
|
||||
expect(response?.body.metadata.annotations).to.have.property('field.cattle.io/description', `${ clusterGroupName }-fleet-desc`);
|
||||
});
|
||||
fleetClusterGroups.waitForPage();
|
||||
});
|
||||
|
||||
it('can clone a cluster group', () => {
|
||||
FleetClusterGroupsListPagePo.navTo();
|
||||
fleetClusterGroups.waitForPage();
|
||||
headerPo.selectWorkspace(localWorkspace);
|
||||
fleetClusterGroups.clusterGroupsList().actionMenu(clusterGroupName).getMenuItem('Clone').click();
|
||||
fleetClusterGroups.createFleetClusterGroupsForm(localWorkspace, clusterGroupName).waitForPage('mode=clone');
|
||||
fleetClusterGroups.createFleetClusterGroupsForm().nameNsDescription().name().set(`clone-${ clusterGroupName }`);
|
||||
fleetClusterGroups.createFleetClusterGroupsForm().nameNsDescription().description().set(`${ clusterGroupName }-fleet-desc`);
|
||||
fleetClusterGroups.createFleetClusterGroupsForm().saveCreateForm().cruResource().saveAndWaitForRequests('POST', 'v1/fleet.cattle.io.clustergroups')
|
||||
.then(({ response }) => {
|
||||
expect(response?.statusCode).to.eq(201);
|
||||
removeClusterGroups = true;
|
||||
clusterGroupsToDelete.push(`${ localWorkspace }/clone-${ clusterGroupName }`);
|
||||
expect(response?.body.metadata).to.have.property('name', `clone-${ clusterGroupName }`);
|
||||
expect(response?.body.metadata.annotations).to.have.property('field.cattle.io/description', `${ clusterGroupName }-fleet-desc`);
|
||||
});
|
||||
fleetClusterGroups.waitForPage();
|
||||
fleetClusterGroups.clusterGroupsList().details(`clone-${ clusterGroupName }`, 1).should('be.visible');
|
||||
});
|
||||
|
||||
it('can delete cluster group', () => {
|
||||
FleetClusterGroupsListPagePo.navTo();
|
||||
fleetClusterGroups.waitForPage();
|
||||
headerPo.selectWorkspace(localWorkspace);
|
||||
fleetClusterGroups.clusterGroupsList().actionMenu(clusterGroupName).getMenuItem('Delete').click();
|
||||
fleetClusterGroups.clusterGroupsList().resourceTable().sortableTable().rowNames('.col-link-detail')
|
||||
.then((rows: any) => {
|
||||
const promptRemove = new PromptRemove();
|
||||
|
||||
cy.intercept('DELETE', `v1/fleet.cattle.io.clustergroups/${ localWorkspace }/clone-${ clusterGroupName }`).as('deleteClusterGroup');
|
||||
|
||||
promptRemove.remove();
|
||||
cy.wait('@deleteClusterGroup');
|
||||
fleetClusterGroups.waitForPage();
|
||||
fleetClusterGroups.clusterGroupsList().resourceTable().sortableTable().checkRowCount(false, rows.length - 1);
|
||||
fleetClusterGroups.clusterGroupsList().resourceTable().sortableTable().rowNames('.col-link-detail')
|
||||
.should('not.contain', `clone-${ clusterGroupName }`);
|
||||
});
|
||||
});
|
||||
|
||||
// testing https://github.com/rancher/dashboard/issues/11687
|
||||
it('can open "Edit as YAML"', () => {
|
||||
FleetClusterGroupsListPagePo.navTo();
|
||||
fleetClusterGroups.waitForPage();
|
||||
fleetClusterGroups.clickCreate();
|
||||
fleetClusterGroups.createFleetClusterGroupsForm().saveCreateForm().createEditView().editAsYaml();
|
||||
fleetClusterGroups.createFleetClusterGroupsForm().saveCreateForm().resourceYaml().codeMirror()
|
||||
.checkExists();
|
||||
});
|
||||
|
||||
it('check table headers are available in list and details view', { tags: ['@vai', '@adminUser'] }, () => {
|
||||
const groupName = 'default';
|
||||
|
||||
FleetClusterGroupsListPagePo.navTo();
|
||||
fleetClusterGroups.waitForPage();
|
||||
headerPo.selectWorkspace(localWorkspace);
|
||||
fleetClusterGroups.clusterGroupsList().rowWithName(groupName).checkVisible();
|
||||
|
||||
// check table headers
|
||||
const expectedHeaders = ['State', 'Name', 'Clusters Ready', 'Resources', 'Age'];
|
||||
|
||||
fleetClusterGroups.clusterGroupsList().resourceTable().sortableTable().tableHeaderRow()
|
||||
.within('.table-header-container .content')
|
||||
.each((el, i) => {
|
||||
expect(el.text().trim()).to.eq(expectedHeaders[i]);
|
||||
});
|
||||
|
||||
// go to fleet cluster details
|
||||
fleetClusterGroups.goToDetailsPage(groupName);
|
||||
|
||||
const fleetClusterGroupDetailsPage = new FleetClusterGroupDetailsPo(localWorkspace, groupName);
|
||||
|
||||
fleetClusterGroupDetailsPage.waitForPage(null, 'clusters');
|
||||
|
||||
// check table headers
|
||||
const expectedHeadersDetailsView = ['State', 'Name', 'Bundles Ready', 'Repos Ready', 'Resources', 'Last Seen', 'Age'];
|
||||
|
||||
fleetClusterGroupDetailsPage.clusterList().resourceTable().sortableTable()
|
||||
.tableHeaderRow()
|
||||
.within('.table-header-container .content')
|
||||
.each((el, i) => {
|
||||
expect(el.text().trim()).to.eq(expectedHeadersDetailsView[i]);
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if (removeClusterGroups) {
|
||||
// delete gitrepo
|
||||
clusterGroupsToDelete.forEach((r) => cy.deleteRancherResource('v1', 'fleet.cattle.io.clustergroups', r, false));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,65 +1,216 @@
|
|||
import { FleetDashboardPagePo } from '@/cypress/e2e/po/pages/fleet/fleet-dashboard.po';
|
||||
// import { GitRepoCreatePo } from '@/cypress/e2e/po/pages/fleet/gitrepo-create.po';
|
||||
// import BurgerMenuPo from '@/cypress/e2e/po/side-bars/burger-side-menu.po';
|
||||
// import { LONG_TIMEOUT_OPT } from '@/cypress/support/utils/timeouts';
|
||||
import FleetGitRepoDetailsPo from '@/cypress/e2e/po/detail/fleet/fleet.cattle.io.gitrepo.po';
|
||||
import { GitRepoCreatePo } from '@/cypress/e2e/po/pages/fleet/gitrepo-create.po';
|
||||
import { GitRepoEditPo } from '@/cypress/e2e/po/edit/fleet/gitrepo-edit.po';
|
||||
import BurgerMenuPo from '@/cypress/e2e/po/side-bars/burger-side-menu.po';
|
||||
import { LONG_TIMEOUT_OPT, MEDIUM_TIMEOUT_OPT } from '@/cypress/support/utils/timeouts';
|
||||
import { gitRepoTargetAllClustersRequest } from '@/cypress/e2e/blueprints/fleet/gitrepos';
|
||||
import { HeaderPo } from '@/cypress/e2e/po/components/header.po';
|
||||
import { MenuActions } from '@/cypress/support/types/menu-actions';
|
||||
import * as path from 'path';
|
||||
import * as jsyaml from 'js-yaml';
|
||||
import { FleetGitRepoListPagePo } from '@/cypress/e2e/po/pages/fleet/fleet.cattle.io.gitrepo.po';
|
||||
const downloadsFolder = Cypress.config('downloadsFolder');
|
||||
|
||||
describe('Fleet Dashboard', { tags: ['@fleet', '@adminUser'] }, () => {
|
||||
let fleetDashboardPage: FleetDashboardPagePo;
|
||||
// const repoName = 'fleet-e2e-test-dashboard';
|
||||
describe('Fleet Dashboard', { tags: ['@fleet', '@adminUser', '@jenkins'] }, () => {
|
||||
const fleetDashboardPage = new FleetDashboardPagePo('_');
|
||||
const gitRepoCreatePage = new GitRepoCreatePo('_');
|
||||
const headerPo = new HeaderPo();
|
||||
|
||||
// Note - The 'describe` previously had `.only`, which ironically meant this was not tested in our CI (probably something to so with grep tags)
|
||||
// Enabling the test results results in consistent failures (bundle does not become ready). For the short term comment these out
|
||||
let repoName;
|
||||
const gitRepoUrl = 'https://github.com/rancher/fleet-test-data';
|
||||
const branch = 'master';
|
||||
const paths = 'qa-test-apps/nginx-app';
|
||||
const localWorkspace = 'fleet-local';
|
||||
let removeGitRepo = false;
|
||||
const reposToDelete = [];
|
||||
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
fleetDashboardPage = new FleetDashboardPagePo('_');
|
||||
fleetDashboardPage.goTo();
|
||||
cy.createE2EResourceName('git-repo').then((name) => {
|
||||
repoName = name;
|
||||
});
|
||||
});
|
||||
|
||||
it('has the correct title', () => {
|
||||
cy.get('.fleet-empty-dashboard').should('be.visible');
|
||||
fleetDashboardPage.goTo();
|
||||
fleetDashboardPage.waitForPage();
|
||||
|
||||
fleetDashboardPage.fleetDashboardEmptyState().should('be.visible');
|
||||
|
||||
cy.title().should('eq', 'Rancher - Continuous Delivery - Dashboard');
|
||||
});
|
||||
|
||||
// before(() => {
|
||||
// cy.login();
|
||||
it('Get Started button takes you to the correct page', () => {
|
||||
fleetDashboardPage.goTo();
|
||||
fleetDashboardPage.waitForPage();
|
||||
|
||||
// const gitRepoCreatePage = new GitRepoCreatePo('_');
|
||||
fleetDashboardPage.fleetDashboardEmptyState().should('be.visible');
|
||||
fleetDashboardPage.getStartedButton().click();
|
||||
gitRepoCreatePage.waitForPage();
|
||||
gitRepoCreatePage.title().contains('Git Repo: Create').should('be.visible');
|
||||
});
|
||||
|
||||
// gitRepoCreatePage.goTo();
|
||||
it('Should display cluster status', () => {
|
||||
// create gitrepo
|
||||
cy.createRancherResource('v1', 'fleet.cattle.io.gitrepos', gitRepoTargetAllClustersRequest(localWorkspace, repoName, gitRepoUrl, branch, paths)).then(() => {
|
||||
removeGitRepo = true;
|
||||
reposToDelete.push(`fleet-local/${ repoName }`);
|
||||
});
|
||||
|
||||
// gitRepoCreatePage.setRepoName(repoName);
|
||||
// gitRepoCreatePage.selectWorkspace('fleet-local');
|
||||
// gitRepoCreatePage.setGitRepoUrl('https://github.com/rancher/fleet-test-data.git');
|
||||
// gitRepoCreatePage.setBranchName();
|
||||
// // NB - This step is here because DOM may not be ready
|
||||
// gitRepoCreatePage.goToNext();
|
||||
// gitRepoCreatePage.create();
|
||||
// });
|
||||
fleetDashboardPage.goTo();
|
||||
fleetDashboardPage.waitForPage();
|
||||
|
||||
// it('Should display cluster status', () => {
|
||||
// // check if burguer menu nav is highlighted correctly for Fleet
|
||||
// BurgerMenuPo.checkIfMenuItemLinkIsHighlighted('Continuous Delivery');
|
||||
// check if burguer menu nav is highlighted correctly for Fleet
|
||||
BurgerMenuPo.checkIfMenuItemLinkIsHighlighted('Continuous Delivery');
|
||||
|
||||
// const row = fleetDashboardPage.sortableTable('fleet-local').row(0);
|
||||
const row = fleetDashboardPage.sortableTable(localWorkspace).row(0);
|
||||
|
||||
// row.get('.bg-success[data-testid="clusters-ready"]', LONG_TIMEOUT_OPT).should('exist');
|
||||
// row.get('.bg-success[data-testid="clusters-ready"] span').should('have.text', '1/1');
|
||||
row.get('.bg-success[data-testid="clusters-ready"]', LONG_TIMEOUT_OPT).should('exist');
|
||||
row.get('.bg-success[data-testid="clusters-ready"] span').should('have.text', '1/1');
|
||||
|
||||
// row.get('.bg-success[data-testid="bundles-ready"]').should('exist');
|
||||
// row.get('.bg-success[data-testid="bundles-ready"] span').should('have.text', '1/1');
|
||||
row.get('.bg-success[data-testid="bundles-ready"]').should('exist');
|
||||
row.get('.bg-success[data-testid="bundles-ready"] span').should('have.text', '1/1');
|
||||
|
||||
// row.get('.bg-success[data-testid="resources-ready"]').should('exist');
|
||||
// row.get('.bg-success[data-testid="resources-ready"] span').should('have.text', '1/1');
|
||||
// });
|
||||
row.get('.bg-success[data-testid="resources-ready"]').should('exist');
|
||||
row.get('.bg-success[data-testid="resources-ready"] span').should('have.text', '1/1');
|
||||
});
|
||||
|
||||
// after(() => {
|
||||
// fleetDashboardPage = new FleetDashboardPagePo('_');
|
||||
// fleetDashboardPage.goTo();
|
||||
it('can navigate to Git Repo details page from Fleet Dashboard', () => {
|
||||
const gitRepoDetails = new FleetGitRepoDetailsPo(localWorkspace, repoName);
|
||||
|
||||
// const fleetLocalResourceTable = fleetDashboardPage.resourceTable('fleet-local');
|
||||
fleetDashboardPage.goTo();
|
||||
fleetDashboardPage.waitForPage();
|
||||
fleetDashboardPage.list().rowWithName(repoName).column(0).find('a')
|
||||
.click();
|
||||
gitRepoDetails.waitForPage(null, 'bundles');
|
||||
});
|
||||
|
||||
// fleetLocalResourceTable.sortableTable().deleteItemWithUI(repoName);
|
||||
// });
|
||||
it('should only display action menu with allowed actions only', () => {
|
||||
fleetDashboardPage.goTo();
|
||||
fleetDashboardPage.waitForPage();
|
||||
headerPo.selectWorkspace(localWorkspace);
|
||||
|
||||
const constActionMenu = fleetDashboardPage.sortableTable().rowActionMenuOpen(repoName);
|
||||
|
||||
const allowedActions: MenuActions[] = [
|
||||
MenuActions.Pause,
|
||||
MenuActions.ForceUpdate,
|
||||
MenuActions.EditYaml,
|
||||
MenuActions.EditConfig,
|
||||
MenuActions.Clone,
|
||||
MenuActions.DownloadYaml,
|
||||
MenuActions.Delete
|
||||
];
|
||||
|
||||
const disabledActions: MenuActions[] = [MenuActions.ChangeWorkspace];
|
||||
|
||||
allowedActions.forEach((action) => {
|
||||
constActionMenu.getMenuItem(action).should('exist');
|
||||
});
|
||||
|
||||
// Disabled actions should not exist
|
||||
disabledActions.forEach((action) => {
|
||||
constActionMenu.getMenuItem(action).should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
it('can clone a git repo', () => {
|
||||
const gitRepoEditPage = new GitRepoEditPo(localWorkspace, repoName);
|
||||
|
||||
cy.intercept('GET', '/v1/secrets?exclude=metadata.managedFields').as('getSecrets');
|
||||
|
||||
fleetDashboardPage.goTo();
|
||||
fleetDashboardPage.waitForPage();
|
||||
fleetDashboardPage.sortableTable().rowActionMenuOpen(repoName).getMenuItem('Clone').click();
|
||||
|
||||
gitRepoEditPage.waitForPage('mode=clone');
|
||||
cy.wait('@getSecrets', MEDIUM_TIMEOUT_OPT).its('response.statusCode').should('eq', 200);
|
||||
gitRepoEditPage.title().contains(`Git Repo: Clone from ${ repoName }`).should('be.visible');
|
||||
headerPo.selectWorkspace('fleet-default');
|
||||
gitRepoEditPage.nameNsDescription().name().set(`clone-${ repoName }`);
|
||||
gitRepoEditPage.footer().nextPage();
|
||||
gitRepoEditPage.footer().create().then(() => {
|
||||
removeGitRepo = true;
|
||||
reposToDelete.push(`fleet-default/clone-${ repoName }`);
|
||||
});
|
||||
|
||||
FleetDashboardPagePo.navTo();
|
||||
fleetDashboardPage.waitForPage();
|
||||
fleetDashboardPage.sortableTable('fleet-default').rowElementWithName(`clone-${ repoName }`).should('be.visible');
|
||||
fleetDashboardPage.sortableTable('fleet-local').rowElementWithName(repoName).should('be.visible');
|
||||
});
|
||||
|
||||
it('user lands in correct git repo workspace when using workspace link on Fleet Dashboard', () => {
|
||||
const gitrepoListPage = new FleetGitRepoListPagePo();
|
||||
|
||||
fleetDashboardPage.goTo();
|
||||
fleetDashboardPage.waitForPage();
|
||||
fleetDashboardPage.sortableTable('fleet-default').rowElementWithName(`clone-${ repoName }`).should('be.visible');
|
||||
fleetDashboardPage.sortableTable('fleet-local').rowElementWithName(repoName).should('be.visible');
|
||||
|
||||
// click workspace link: fleet default
|
||||
fleetDashboardPage.goToGitRepoListLink('fleet-default').click();
|
||||
gitrepoListPage.waitForPage();
|
||||
headerPo.checkCurrentWorkspace('fleet-default');
|
||||
|
||||
// click workspace link: fleet local
|
||||
fleetDashboardPage.goTo();
|
||||
fleetDashboardPage.waitForPage();
|
||||
fleetDashboardPage.goToGitRepoListLink('fleet-local').click();
|
||||
gitrepoListPage.waitForPage();
|
||||
headerPo.checkCurrentWorkspace('fleet-local');
|
||||
});
|
||||
|
||||
it('can Edit Yaml', () => {
|
||||
const gitRepoEditPage = new GitRepoEditPo(localWorkspace, repoName);
|
||||
|
||||
fleetDashboardPage.goTo();
|
||||
fleetDashboardPage.waitForPage();
|
||||
fleetDashboardPage.sortableTable().rowActionMenuOpen(repoName).getMenuItem('Edit YAML').click();
|
||||
gitRepoEditPage.waitForPage('mode=edit&as=yaml');
|
||||
gitRepoEditPage.title().contains(`Git Repo: ${ repoName }`).should('be.visible');
|
||||
});
|
||||
|
||||
it('can Download Yaml', () => {
|
||||
cy.deleteDownloadsFolder();
|
||||
|
||||
fleetDashboardPage.goTo();
|
||||
fleetDashboardPage.waitForPage();
|
||||
fleetDashboardPage.sortableTable().rowActionMenuOpen(repoName).getMenuItem('Download YAML').click();
|
||||
|
||||
const downloadedFilename = path.join(downloadsFolder, `${ repoName }.yaml`);
|
||||
|
||||
cy.readFile(downloadedFilename).then((buffer) => {
|
||||
const obj: any = jsyaml.load(buffer);
|
||||
|
||||
// Basic checks on the downloaded YAML
|
||||
expect(obj.kind).to.equal('GitRepo');
|
||||
expect(obj.metadata['name']).to.equal(repoName);
|
||||
expect(obj.metadata['namespace']).to.equal(localWorkspace);
|
||||
expect(obj.spec['repo']).to.equal(gitRepoUrl);
|
||||
});
|
||||
});
|
||||
|
||||
it('can Edit Config', () => {
|
||||
const gitRepoEditPage = new GitRepoEditPo(localWorkspace, repoName);
|
||||
const description = `${ repoName }-desc`;
|
||||
|
||||
fleetDashboardPage.goTo();
|
||||
fleetDashboardPage.waitForPage();
|
||||
fleetDashboardPage.sortableTable().rowActionMenuOpen(repoName).getMenuItem('Edit Config').click();
|
||||
|
||||
gitRepoEditPage.waitForPage('mode=edit');
|
||||
gitRepoEditPage.nameNsDescription().description().set(description);
|
||||
gitRepoEditPage.footer().nextPage();
|
||||
gitRepoEditPage.footer().save();
|
||||
fleetDashboardPage.waitForPage();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if (removeGitRepo) {
|
||||
// delete gitrepo
|
||||
reposToDelete.forEach((r) => cy.deleteRancherResource('v1', 'fleet.cattle.io.gitrepo', r));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -191,6 +191,43 @@ describe('Feature Flags', { testIsolation: 'off' }, () => {
|
|||
sideNav.groups().contains('Legacy').should('not.exist');
|
||||
});
|
||||
|
||||
it('error when toggling a feature flag is handled correctly', { tags: ['@globalSettings', '@adminUser'] }, () => {
|
||||
// Check Current State: should be disabled by default
|
||||
FeatureFlagsPagePo.navTo();
|
||||
featureFlagsPage.list().details('unsupported-storage-drivers', 0).should('include.text', 'Disabled');
|
||||
|
||||
// Intercept the request to change the feature flag and return an error - 403, permission denied
|
||||
cy.intercept({
|
||||
method: 'PUT',
|
||||
pathname: '/v1/management.cattle.io.features/unsupported-storage-drivers',
|
||||
times: 1,
|
||||
}, {
|
||||
statusCode: 403,
|
||||
body: {
|
||||
type: 'error',
|
||||
links: {},
|
||||
code: 'Forbidden',
|
||||
message: 'User does not have permission'
|
||||
}
|
||||
}).as('updateFeatureFlag');
|
||||
|
||||
// Activate
|
||||
featureFlagsPage.list().elementWithName('unsupported-storage-drivers').scrollIntoView().should('be.visible');
|
||||
featureFlagsPage.list().clickRowActionMenuItem('unsupported-storage-drivers', 'Activate');
|
||||
featureFlagsPage.cardActionButton('Activate').click();
|
||||
|
||||
cy.wait(`@updateFeatureFlag`).its('response.statusCode').should('eq', 403);
|
||||
|
||||
// Check Updated State: should be active
|
||||
featureFlagsPage.list().details('unsupported-storage-drivers', 0).should('include.text', 'Disabled');
|
||||
|
||||
// Check error message is displayed
|
||||
featureFlagsPage.cardActionError('User does not have permission');
|
||||
|
||||
// Press cancel
|
||||
featureFlagsPage.cardActionButton('Cancel').click();
|
||||
});
|
||||
|
||||
it('standard user has only read access to Feature Flag page', { tags: ['@globalSettings', '@standardUser'] }, () => {
|
||||
// verify action menus are hidden for standard user
|
||||
|
||||
|
|
@ -219,7 +256,7 @@ describe('Feature Flags', { testIsolation: 'off' }, () => {
|
|||
it('validate feature flags table header content', () => {
|
||||
FeatureFlagsPagePo.navTo();
|
||||
// check table headers are visible
|
||||
const expectedHeaders = ['State', 'Name', 'Description', 'Restart Required'];
|
||||
const expectedHeaders = ['State', 'Name', 'Description', 'Restart Rancher'];
|
||||
|
||||
featureFlagsPage.list().resourceTable().sortableTable().tableHeaderRow()
|
||||
.get('.table-header-container .content')
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ const clusterNamePartial = `${ runPrefix }-create`;
|
|||
const rke1CustomName = `${ clusterNamePartial }-rke1-custom`;
|
||||
const rke2CustomName = `${ clusterNamePartial }-rke2-custom`;
|
||||
const importGenericName = `${ clusterNamePartial }-import-generic`;
|
||||
let reenableAKS = false;
|
||||
|
||||
const downloadsFolder = Cypress.config('downloadsFolder');
|
||||
|
||||
|
|
@ -91,16 +92,35 @@ describe('Cluster Manager', { testIsolation: 'off', tags: ['@manager', '@adminUs
|
|||
});
|
||||
|
||||
it('deactivating a kontainer driver should hide its card from the cluster creation page', () => {
|
||||
cy.intercept('GET', '/v3/kontainerdrivers').as('getKontainerDrivers');
|
||||
cy.intercept('POST', 'v3/kontainerDrivers/azurekubernetesservice?action=deactivate').as('deactivateDriver');
|
||||
cy.intercept('POST', 'v3/kontainerDrivers/azurekubernetesservice?action=activate').as('activateDriver');
|
||||
|
||||
const driversPage = new KontainerDriversPagePo();
|
||||
const clusterCreatePage = new ClusterManagerCreatePagePo();
|
||||
|
||||
// deactivate the AKS driver
|
||||
KontainerDriversPagePo.navTo();
|
||||
driversPage.waitForPage();
|
||||
|
||||
// assert AKS kontainer driver is in Active state
|
||||
cy.wait('@getKontainerDrivers').then(({ response }) => {
|
||||
response.body.data.forEach((item: any) => {
|
||||
if (item.id === 'azurekubernetesservice') {
|
||||
const state = item['active'];
|
||||
|
||||
expect(state).to.eq(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// deactivate the AKS driver
|
||||
driversPage.list().actionMenu('Azure AKS').getMenuItem('Deactivate').click();
|
||||
const deactivateDialog = new DeactivateDriverDialogPo();
|
||||
|
||||
deactivateDialog.deactivate();
|
||||
cy.wait('@deactivateDriver').its('response.statusCode').should('eq', 200).then(() => {
|
||||
reenableAKS = true;
|
||||
});
|
||||
|
||||
// verify that the AKS card is not shown
|
||||
clusterList.goTo();
|
||||
|
|
@ -112,6 +132,9 @@ describe('Cluster Manager', { testIsolation: 'off', tags: ['@manager', '@adminUs
|
|||
KontainerDriversPagePo.navTo();
|
||||
driversPage.waitForPage();
|
||||
driversPage.list().actionMenu('Azure AKS').getMenuItem('Activate').click();
|
||||
cy.wait('@activateDriver').its('response.statusCode').should('eq', 200).then(() => {
|
||||
reenableAKS = false;
|
||||
});
|
||||
|
||||
// verify that the AKS card is back
|
||||
clusterList.goTo();
|
||||
|
|
@ -311,6 +334,31 @@ describe('Cluster Manager', { testIsolation: 'off', tags: ['@manager', '@adminUs
|
|||
editCreatedClusterPage.nameNsDescription().description().self().should('have.value', rke2CustomName);
|
||||
});
|
||||
|
||||
it('will disbable saving if an addon config has invalid data', () => {
|
||||
clusterList.goTo();
|
||||
|
||||
clusterList.checkIsCurrentPage();
|
||||
clusterList.createCluster();
|
||||
|
||||
createRKE2ClusterPage.waitForPage();
|
||||
|
||||
createRKE2ClusterPage.rkeToggle().set('RKE2/K3s');
|
||||
|
||||
createRKE2ClusterPage.selectCustom(0);
|
||||
|
||||
createRKE2ClusterPage.nameNsDescription().name().set('abc');
|
||||
|
||||
createRKE2ClusterPage.clusterConfigurationTabs().clickTabWithSelector('#rke2-calico');
|
||||
|
||||
createRKE2ClusterPage.resourceDetail().createEditView().saveButtonPo().expectToBeEnabled();
|
||||
|
||||
createRKE2ClusterPage.calicoAddonConfig().yamlEditor().input().set('badvalue: -');
|
||||
createRKE2ClusterPage.resourceDetail().createEditView().saveButtonPo().expectToBeDisabled();
|
||||
|
||||
createRKE2ClusterPage.calicoAddonConfig().yamlEditor().input().set('goodvalue: yay');
|
||||
createRKE2ClusterPage.resourceDetail().createEditView().saveButtonPo().expectToBeEnabled();
|
||||
});
|
||||
|
||||
it('can view cluster YAML editor', () => {
|
||||
clusterList.goTo();
|
||||
clusterList.list().actionMenu(rke2CustomName).getMenuItem('Edit YAML').click();
|
||||
|
|
@ -803,4 +851,10 @@ describe('Cluster Manager', { testIsolation: 'off', tags: ['@manager', '@adminUs
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if (reenableAKS) {
|
||||
cy.createRancherResource('v3', 'kontainerDrivers/azurekubernetesservice?action=activate', {});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -99,7 +99,8 @@ declare global {
|
|||
deleteRancherResource(prefix: 'v3' | 'v1' | 'k8s', resourceType: string, resourceId: string, failOnStatusCode?: boolean): Chainable;
|
||||
deleteNodeTemplate(nodeTemplateId: string, timeout?: number, failOnStatusCode?: boolean)
|
||||
|
||||
tableRowsPerPageAndNamespaceFilter(rows: number, cluster: string, groupBy: string, namespacefilter: string, interation?: number)
|
||||
tableRowsPerPageAndNamespaceFilter(rows: number, clusterName: string, groupBy: string, namespaceFilter: string)
|
||||
tableRowsPerPageAndPreferences(rows: number, preferences: { clusterName: string, groupBy: string, namespaceFilter: string, allNamespaces: string}, iteration?: number)
|
||||
|
||||
/**
|
||||
* update namespace filter
|
||||
|
|
@ -162,6 +163,11 @@ declare global {
|
|||
* Fetch the steve `revision` / timestamp of request
|
||||
*/
|
||||
fetchRevision(): Chainable<string>;
|
||||
|
||||
/**
|
||||
* Check if the vai FF is enabled
|
||||
*/
|
||||
isVaiCacheEnabled(): Chainable<boolean>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -497,8 +497,8 @@ Cypress.Commands.add('createRancherResource', (prefix, resourceType, body) => {
|
|||
body
|
||||
})
|
||||
.then((resp) => {
|
||||
// Expect 201, Created HTTP status code
|
||||
expect(resp.status).to.eq(201);
|
||||
// Expect 200 or 201, Created HTTP status code
|
||||
expect(resp.status).to.be.oneOf([200, 201]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -1024,39 +1024,58 @@ Cypress.Commands.add('fetchRevision', () => {
|
|||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('tableRowsPerPageAndNamespaceFilter', (rows: number, clusterName: string, groupBy: string, namespaceFilter: string, iteration = 0) => {
|
||||
/**
|
||||
* Check if the vai FF is enabled
|
||||
*/
|
||||
Cypress.Commands.add('isVaiCacheEnabled', () => {
|
||||
return cy.getRancherResource('v1', 'management.cattle.io.features', 'ui-sql-cache', 200)
|
||||
.then((res) => res.body.spec.value === true || res.body.spec.value === 'true');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('tableRowsPerPageAndPreferences', (rows: number, preferences: { clusterName: string, groupBy: string, namespaceFilter: string, allNamespaces: string}, iteration = 0) => {
|
||||
const {
|
||||
clusterName, groupBy, namespaceFilter, allNamespaces
|
||||
} = preferences;
|
||||
|
||||
return cy.getRancherResource('v3', 'users?me=true').then((resp: Cypress.Response<any>) => {
|
||||
const userId = resp.body.data[0].id.trim();
|
||||
const payload = {
|
||||
id: `${ userId }`,
|
||||
type: 'userpreference',
|
||||
data: {
|
||||
cluster: clusterName,
|
||||
'per-page': `${ rows }`,
|
||||
'group-by': groupBy,
|
||||
'ns-by-cluster': namespaceFilter
|
||||
cluster: clusterName,
|
||||
'per-page': `${ rows }`,
|
||||
'group-by': groupBy,
|
||||
'ns-by-cluster': namespaceFilter,
|
||||
'all-namespaces': allNamespaces,
|
||||
}
|
||||
};
|
||||
|
||||
cy.log(`tableRowsPerPageAndNamespaceFilter: /v1/userpreferences/${ userId }. Payload: ${ JSON.stringify(payload) }`);
|
||||
cy.log(`tableRowsPerPageAndPreferences: /v1/userpreferences/${ userId }. Payload: ${ JSON.stringify(payload) }`);
|
||||
|
||||
cy.setRancherResource('v1', 'userpreferences', userId, payload).then(() => {
|
||||
return cy.waitForRancherResource('v1', 'userpreferences', userId, (resp: any) => compare(resp?.body, payload))
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
cy.log(`tableRowsPerPageAndNamespaceFilter: Success!`);
|
||||
cy.log(`tableRowsPerPageAndPreferences: Success!`);
|
||||
} else {
|
||||
if (iteration < 3) {
|
||||
cy.log(`tableRowsPerPageAndNamespaceFilter: Failed! Going to retry...`);
|
||||
cy.log(`tableRowsPerPageAndPreferences: Failed! Going to retry...`);
|
||||
|
||||
return cy.tableRowsPerPageAndNamespaceFilter(rows, clusterName, groupBy, namespaceFilter, iteration + 1);
|
||||
return cy.tableRowsPerPageAndPreferences(rows, preferences, iteration + 1);
|
||||
}
|
||||
|
||||
cy.log(`tableRowsPerPageAndNamespaceFilter: Failed! Giving up...`);
|
||||
cy.log(`tableRowsPerPageAndPreferences: Failed! Giving up...`);
|
||||
|
||||
return Promise.reject(new Error('tableRowsPerPageAndNamespaceFilter failed'));
|
||||
return Promise.reject(new Error('tableRowsPerPageAndPreferences failed'));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('tableRowsPerPageAndNamespaceFilter', (rows: number, clusterName: string, groupBy: string, namespaceFilter: string) => {
|
||||
return cy.tableRowsPerPageAndPreferences(rows, {
|
||||
clusterName, groupBy, namespaceFilter
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,4 +8,7 @@ export enum MenuActions {
|
|||
ViewInApi = 'View in API', // eslint-disable-line no-unused-vars
|
||||
ChangeWorkspace = 'Change workspace', // eslint-disable-line no-unused-vars
|
||||
Delete = 'Delete', // eslint-disable-line no-unused-vars
|
||||
Clone = 'Clone', // eslint-disable-line no-unused-vars
|
||||
DownloadYaml = 'Download YAML', // eslint-disable-line no-unused-vars
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
...
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -205,6 +205,29 @@ When building an extension that will be housed in a GitLab repository or hosted
|
|||
This pipeline will build an ECI and publish it to container registry (`registry.gitlab.com` by default) to allow for importing into Rancher Manager.
|
||||
The actual pipeline jobs are defined in the [Dashboard repo](https://github.com/rancher/dashboard/blob/master/.gitlab/workflows/build-extension-catalog.gitlab-ci.yml) to allow for proper versioning and to apply any updates to the pipeline without any additional work from the Extension developer.
|
||||
|
||||
> **_WARNING:_** Ensure the branch of `rancher/dashboard` in the `remote` url containing the reusable workflow matches the release version of your `@rancher/shell` npm dependency. For example:
|
||||
> - If building for the latest version of Rancher:
|
||||
> ```yaml
|
||||
> #.gitlab-ci.yml
|
||||
> ...
|
||||
> include:
|
||||
> - remote: 'https://raw.githubusercontent.com/rancher/dashboard/master/shell/scripts/.gitlab/workflows/build-extension-catalog.gitlab-ci.yml'
|
||||
> ```
|
||||
> - If building for Rancher `v2.9`:
|
||||
> ```yaml
|
||||
> #.gitlab-ci.yml
|
||||
> ...
|
||||
> include:
|
||||
> - remote: 'https://raw.githubusercontent.com/rancher/dashboard/release-2.9/shell/scripts/.gitlab/workflows/build-extension-catalog.gitlab-ci.yml'
|
||||
> ```
|
||||
> - If building for Rancher `v2.8`:
|
||||
> ```yaml
|
||||
> #.gitlab-ci.yml
|
||||
> ...
|
||||
> include:
|
||||
> - remote: 'https://raw.githubusercontent.com/rancher/dashboard/release-2.8/shell/scripts/.gitlab/workflows/build-extension-catalog.gitlab-ci.yml'
|
||||
> ```
|
||||
|
||||
### Pipeline Configuration
|
||||
|
||||
There are a few pipeline configuration options, mostly tied to the container registry:
|
||||
|
|
|
|||
|
|
@ -109,6 +109,14 @@ These values are provided when you create a new project within Cypress dashboard
|
|||
|
||||
It's also possible to run a workflow in GitHub Actions E2E test using these values to record on personal dashboards.
|
||||
|
||||
### Skip dashboard or tests
|
||||
|
||||
CI gates can be disabled in the following way:
|
||||
|
||||
- Use label `ci/skip-e2e` to skip the E2E tests in the PR
|
||||
- Use label `ci/skip-e2e-cypress-dashboard` to run the E2E tests without Sorry Cypress dashboard in the PR (it will enable `TEST_DISABLE_DASHBOARD_LABEL` env var)
|
||||
- Use GitHub settings and define env var `TEST_DISABLE_DASHBOARD` as `true` (which is string and not boolean) to disable the Cypress dashboard entirely in every CI run
|
||||
|
||||
## Local and CI/prod run
|
||||
|
||||
It is possible to start the project and run all the tests at once with a single command. There's however a difference between `dev` and `production` run. The first will not require an official certificate and will build the project in `dist`, while the production will enable all the SSL configurations to run encrypted.
|
||||
|
|
|
|||
|
|
@ -77,10 +77,13 @@ const config = {
|
|||
'@docusaurus/plugin-client-redirects',
|
||||
{
|
||||
createRedirects(existingPath) {
|
||||
if (existingPath.includes('/extensions') && !existingPath.includes('/next') && !existingPath.includes('/v2')) {
|
||||
return [
|
||||
existingPath.replace('/extensions', '/extensions/next')
|
||||
];
|
||||
// This function is invoked once per existing doc page, and we
|
||||
// must return the “old” routes that we want to map to that doc’s path
|
||||
if (existingPath.startsWith('/extensions/next')) {
|
||||
// Generate the "old" route we want to redirect from
|
||||
const oldPath = existingPath.replace('/extensions/next', '/extensions');
|
||||
|
||||
return [oldPath];
|
||||
}
|
||||
|
||||
return undefined; // Return a falsy value: no redirect created
|
||||
|
|
|
|||
|
|
@ -205,6 +205,22 @@ When building an extension that will be housed in a GitLab repository or hosted
|
|||
This pipeline will build an ECI and publish it to container registry (`registry.gitlab.com` by default) to allow for importing into Rancher Manager.
|
||||
The actual pipeline jobs are defined in the [Dashboard repo](https://github.com/rancher/dashboard/blob/master/.gitlab/workflows/build-extension-catalog.gitlab-ci.yml) to allow for proper versioning and to apply any updates to the pipeline without any additional work from the Extension developer.
|
||||
|
||||
> **_WARNING:_** Ensure the branch of `rancher/dashboard` in the `remote` url containing the reusable workflow matches the release version of your `@rancher/shell` npm dependency. For example:
|
||||
> - If building for Rancher `v2.9`:
|
||||
> ```yaml
|
||||
> #.gitlab-ci.yml
|
||||
> ...
|
||||
> include:
|
||||
> - remote: 'https://raw.githubusercontent.com/rancher/dashboard/release-2.9/shell/scripts/.gitlab/workflows/build-extension-catalog.gitlab-ci.yml'
|
||||
> ```
|
||||
> - If building for Rancher `v2.8`:
|
||||
> ```yaml
|
||||
> #.gitlab-ci.yml
|
||||
> ...
|
||||
> include:
|
||||
> - remote: 'https://raw.githubusercontent.com/rancher/dashboard/release-2.8/shell/scripts/.gitlab/workflows/build-extension-catalog.gitlab-ci.yml'
|
||||
> ```
|
||||
|
||||
### Pipeline Configuration
|
||||
|
||||
There are a few pipeline configuration options, mostly tied to the container registry:
|
||||
|
|
|
|||
10
package.json
10
package.json
|
|
@ -26,7 +26,7 @@
|
|||
"install:ci": "yarn install --frozen-lockfile",
|
||||
"dev": "bash -c 'source ./scripts/version && NODE_ENV=dev ./node_modules/.bin/vue-cli-service serve'",
|
||||
"mem-dev": "bash -c 'source ./scripts/version && NODE_ENV=dev node --max-old-space-size=8192 ./node_modules/.bin/vue-cli-service serve'",
|
||||
"docker:local:start": "docker run -d --restart=unless-stopped -p 80:80 -p 443:443 -e CATTLE_BOOTSTRAP_PASSWORD=password -e CATTLE_PASSWORD_MIN_LENGTH=3 --name cypress --privileged rancher/rancher:head",
|
||||
"docker:local:start": "docker run -d --restart=unless-stopped -p 80:80 -p 443:443 -e CATTLE_BOOTSTRAP_PASSWORD=password -e CATTLE_PASSWORD_MIN_LENGTH=3 --name cypress --privileged rancher/rancher:v2.11-2053ce644a31cd8053d1f58e2487154b0b8513b6-head",
|
||||
"docker:local:stop": "docker kill cypress || true && docker rm cypress || true",
|
||||
"build": "NODE_OPTIONS=--max_old_space_size=4096 ./node_modules/.bin/vue-cli-service build",
|
||||
"build:lib": "cd pkg/rancher-components && yarn build:lib",
|
||||
|
|
@ -62,7 +62,7 @@
|
|||
"@aws-sdk/client-iam": "3.658.1",
|
||||
"@aws-sdk/client-kms": "3.8.1",
|
||||
"@novnc/novnc": "1.2.0",
|
||||
"@popperjs/core": "2.4.4",
|
||||
"@popperjs/core": "2.11.8",
|
||||
"@rancher/icons": "2.0.29",
|
||||
"ansi_up": "5.0.0",
|
||||
"axios": "0.21.4",
|
||||
|
|
@ -77,11 +77,10 @@
|
|||
"cookie-universal": "2.2.2",
|
||||
"cron-validator": "1.2.0",
|
||||
"cronstrue": "1.95.0",
|
||||
"cross-env": "6.0.3",
|
||||
"cross-env": "7.0.3",
|
||||
"custom-event-polyfill": "1.0.7",
|
||||
"d3": "7.3.0",
|
||||
"d3-selection": "1.4.1",
|
||||
"dagre-d3": "0.6.4",
|
||||
"dayjs": "1.8.29",
|
||||
"defu": "5.0.1",
|
||||
"diff2html": "3.4.24",
|
||||
|
|
@ -96,9 +95,8 @@
|
|||
"intl-messageformat": "7.8.4",
|
||||
"ip": "2.0.1",
|
||||
"ipaddr.js": "2.2.0",
|
||||
"is-base64": "1.1.0",
|
||||
"is-url": "1.2.4",
|
||||
"jexl": "2.2.2",
|
||||
"jexl": "2.3.0",
|
||||
"jquery": "3.5.1",
|
||||
"js-cookie": "2.2.1",
|
||||
"js-yaml": "4.1.0",
|
||||
|
|
|
|||
|
|
@ -645,7 +645,7 @@ export default defineComponent({
|
|||
|
||||
virtualNetwork: {
|
||||
get() {
|
||||
return this.virtualNetworkOptions.find((opt) => opt.value === this.config.subnet) || this.t('generic.none');
|
||||
return this.virtualNetworkOptions.find((opt) => opt.value === this.config.subnet) || { label: this.t('generic.none') };
|
||||
},
|
||||
set(neu: {label: string, kind?: string, disabled?: boolean, value?: string, virtualNetwork?: any}) {
|
||||
if (neu.label === this.t('generic.none')) {
|
||||
|
|
@ -804,8 +804,6 @@ export default defineComponent({
|
|||
return;
|
||||
}
|
||||
this.loadingLocations = true;
|
||||
// this will force the resourceLocation watcher to re-run every time new locations are fetched even if the default one selected hasn't changed
|
||||
this.config['resourceLocation'] = '';
|
||||
|
||||
const { azureCredentialSecret } = this.config;
|
||||
|
||||
|
|
@ -839,6 +837,11 @@ export default defineComponent({
|
|||
|
||||
errors.push(this.t('aks.errors.regions', { e: parsedError || err }));
|
||||
}
|
||||
|
||||
// once regions are loaded and a default selected, fetch resources that are region-scoped
|
||||
this.getAksVersions();
|
||||
this.getVmSizes();
|
||||
this.getVirtualNetworks();
|
||||
},
|
||||
|
||||
async getAksVersions(): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -240,7 +240,7 @@ describe('aks provisioning form', () => {
|
|||
it('should display subnets grouped by network in the virtual network dropdown', async() => {
|
||||
const noneOption = { label: 'generic.none' };
|
||||
const config = {
|
||||
dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc'
|
||||
dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc', resourceLocation: 'eastus'
|
||||
};
|
||||
const wrapper = shallowMount(CruAks, {
|
||||
propsData: {
|
||||
|
|
@ -254,7 +254,6 @@ describe('aks provisioning form', () => {
|
|||
const networkOpts = virtualNetworkSelect.props().options;
|
||||
|
||||
expect(virtualNetworkSelect.props().value).toStrictEqual(noneOption);
|
||||
|
||||
expect(networkOpts).toStrictEqual([{ label: 'generic.none' }, {
|
||||
disabled: true, kind: 'group', label: 'network2'
|
||||
}, {
|
||||
|
|
@ -299,7 +298,7 @@ describe('aks provisioning form', () => {
|
|||
|
||||
it('should prevent saving if a node pool has taints missing keys or values', async() => {
|
||||
const config = {
|
||||
dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc'
|
||||
dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc', resourceLocation: 'eastus'
|
||||
};
|
||||
const wrapper = shallowMount(CruAks, {
|
||||
propsData: {
|
||||
|
|
@ -343,7 +342,7 @@ describe('aks provisioning form', () => {
|
|||
}],
|
||||
])('should set virtualNetwork, virtualNetworkResourceGroup, and subnet when a virtual network is selected', async(optionIndex, { virtualNetwork, virtualNetworkResourceGroup, subnet }) => {
|
||||
const config = {
|
||||
dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc'
|
||||
dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc', resourceLocation: 'eastus'
|
||||
};
|
||||
const wrapper = shallowMount(CruAks, {
|
||||
propsData: {
|
||||
|
|
@ -366,7 +365,7 @@ describe('aks provisioning form', () => {
|
|||
|
||||
it('should set config.monitoring to \'true\' and show log anaytics workspace name and log analytics workspace group inputs when the monitoring checkbox is checked', async() => {
|
||||
const config = {
|
||||
dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc'
|
||||
dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc', resourceLocation: 'eastus'
|
||||
};
|
||||
const wrapper = shallowMount(CruAks, {
|
||||
propsData: {
|
||||
|
|
@ -397,7 +396,7 @@ describe('aks provisioning form', () => {
|
|||
|
||||
it('should clear virtualNetwork, virtualNetworkResourceGroup, and subnet when the \'none\' virtual network option is selected', async() => {
|
||||
const config = {
|
||||
dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc'
|
||||
dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc', resourceLocation: 'eastus'
|
||||
};
|
||||
const wrapper = shallowMount(CruAks, {
|
||||
propsData: {
|
||||
|
|
@ -430,7 +429,7 @@ describe('aks provisioning form', () => {
|
|||
name: 'abc', _validation: {}, _isNewOrUnprovisioned: false, orchestratorVersion: originalVersion
|
||||
}];
|
||||
const config = {
|
||||
dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc', kubernetesVersion: originalVersion, nodePools
|
||||
dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc', kubernetesVersion: originalVersion, nodePools, resourceLocation: 'eastus'
|
||||
};
|
||||
const wrapper = shallowMount(CruAks, {
|
||||
propsData: {
|
||||
|
|
@ -451,7 +450,7 @@ describe('aks provisioning form', () => {
|
|||
|
||||
it('should clear config.logAnalyticsWorkspaceName and config.logAnalyticsWorkspaceGroup when the monitoring checkbox is unchecked', async() => {
|
||||
const config = {
|
||||
dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc', monitoring: true, logAnalyticsWorkspaceGroup: 'abc', logAnalyticsWorkspaceName: 'def'
|
||||
dnsPrefix: 'abc-123', resourceGroup: 'abc', clusterName: 'abc', monitoring: true, logAnalyticsWorkspaceGroup: 'abc', logAnalyticsWorkspaceName: 'def', resourceLocation: 'eastus'
|
||||
};
|
||||
const wrapper = shallowMount(CruAks, {
|
||||
propsData: {
|
||||
|
|
|
|||
|
|
@ -163,8 +163,8 @@ aks:
|
|||
availabilityZones: Availability zones are not available in the selected region.
|
||||
privateDnsZone: Private DNS Zone Resource ID must be in the format /subscriptions/SUBSCRIPTION_ID/resourceGroups/RESOURCEGROUP_NAME/providers/Microsoft.Network/privateDnsZones/PRIVATE_DNS_ZONE_NAME. The Private DNS Zone Resource Name must be in the format privatelink.REGION.azmk8s.io, SUBZONE.privatelink.REGION.azmk8s.io, private.REGION.azmk8s.io, or SUBZONE.private.REGION.azmk8s.io
|
||||
poolName: Node pool names must be 1-12 characters long, consist only of lowercase letters and numbers, and start with a letter.
|
||||
poolCount: Node count must be at least one in System pools.
|
||||
poolUserCount: Node count cannot be less than zero.
|
||||
poolCount: Node count must be at least 1 and at most 1000 in System pools.
|
||||
poolUserCount: Node count cannot be less than 0 or greater than 1000 in User pools.
|
||||
poolMinMax: The minimum number of nodes must be less than or equal to the maximum number of nodes, and the node count must be between or equal to the minimum and maximum.
|
||||
poolMin: The minimum number of nodes must be greater than 0 and at most 1000.
|
||||
poolMax: The maximum number of nodes must be greater than 0 and at most 1000.
|
||||
|
|
|
|||
|
|
@ -140,19 +140,24 @@ describe('fx: nodePoolNames', () => {
|
|||
|
||||
describe('fx: nodePoolCount', () => {
|
||||
// AksNodePool unit tests check that the second arg is passed in as expected
|
||||
it('validates that count is at least 1 when second arg is false', () => {
|
||||
it('validates that count is at least 1 and at most 1000 when second arg is false', () => {
|
||||
const validator = validators.nodePoolCount(mockCtx);
|
||||
|
||||
expect(validator(1, false)).toBeUndefined();
|
||||
expect(validator(0, false)).toStrictEqual(MOCK_TRANSLATION);
|
||||
expect(validator(1000, false)).toBeUndefined();
|
||||
expect(validator(1001, false)).toStrictEqual(MOCK_TRANSLATION);
|
||||
});
|
||||
|
||||
it('validates that count is at least 0 when second arg is true', () => {
|
||||
it('validates that count is at least 0 and at most 1000 when second arg is true', () => {
|
||||
const validator = validators.nodePoolCount(mockCtx);
|
||||
|
||||
expect(validator(1, true)).toBeUndefined();
|
||||
expect(validator(0, true)).toBeUndefined();
|
||||
expect(validator(1000, true)).toBeUndefined();
|
||||
|
||||
expect(validator(-1, true)).toStrictEqual(MOCK_TRANSLATION);
|
||||
expect(validator(1001, true)).toStrictEqual(MOCK_TRANSLATION);
|
||||
});
|
||||
|
||||
it('validates each node pool in the provided context when not passed a count value', () => {
|
||||
|
|
@ -170,6 +175,12 @@ describe('fx: nodePoolCount', () => {
|
|||
},
|
||||
{
|
||||
name: 'klm', _validation: {}, mode: 'User', count: -1
|
||||
},
|
||||
{
|
||||
name: 'nop', _validation: {}, mode: 'User', count: 1001
|
||||
},
|
||||
{
|
||||
name: 'qrs', _validation: {}, mode: 'System', count: 1001
|
||||
}
|
||||
] as unknown as AKSNodePool[]
|
||||
};
|
||||
|
|
@ -180,5 +191,7 @@ describe('fx: nodePoolCount', () => {
|
|||
expect(ctx.nodePools[1]?._validation?._validCount).toStrictEqual(true);
|
||||
expect(ctx.nodePools[2]?._validation?._validCount).toStrictEqual(true);
|
||||
expect(ctx.nodePools[3]?._validation?._validCount).toStrictEqual(false);
|
||||
expect(ctx.nodePools[4]?._validation?._validCount).toStrictEqual(false);
|
||||
expect(ctx.nodePools[5]?._validation?._validCount).toStrictEqual(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -181,6 +181,7 @@ export const nodePoolNamesUnique = (ctx: any) => {
|
|||
export const nodePoolCount = (ctx:any) => {
|
||||
return (count?: number, canBeZero = false) => {
|
||||
let min = 1;
|
||||
const max = 1000;
|
||||
let errMsg = ctx.t('aks.errors.poolCount');
|
||||
|
||||
if (canBeZero) {
|
||||
|
|
@ -188,7 +189,7 @@ export const nodePoolCount = (ctx:any) => {
|
|||
errMsg = ctx.t('aks.errors.poolUserCount');
|
||||
}
|
||||
if (count || count === 0) {
|
||||
return count >= min ? undefined : errMsg;
|
||||
return count >= min && count <= max ? undefined : errMsg;
|
||||
} else {
|
||||
let allValid = true;
|
||||
|
||||
|
|
@ -201,7 +202,7 @@ export const nodePoolCount = (ctx:any) => {
|
|||
min = 1;
|
||||
}
|
||||
|
||||
if (count < min) {
|
||||
if (count < min || count > max) {
|
||||
pool._validation['_validCount'] = false;
|
||||
allValid = false;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -704,6 +704,7 @@ export default defineComponent({
|
|||
label-key="generic.name"
|
||||
required
|
||||
:rules="fvGetAndReportPathRules('clusterName')"
|
||||
data-testid="gke-cluster-name"
|
||||
@update:value="setClusterName"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -713,6 +714,7 @@ export default defineComponent({
|
|||
:mode="mode"
|
||||
label-key="nameNsDescription.description.label"
|
||||
:placeholder="t('nameNsDescription.description.placeholder')"
|
||||
data-testid="gke-cluster-description"
|
||||
@update:value="setClusterDescription"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -203,9 +203,15 @@ export default defineComponent({
|
|||
|
||||
networkOptions(neu) {
|
||||
if (neu && neu.length && !this.network) {
|
||||
const firstnetwork = neu.find((network: GKENetwork) => network.kind !== 'group');
|
||||
const defaultNetwork = neu.find((network: GKENetwork) => network?.name === 'default');
|
||||
|
||||
this.$emit('update:network', firstnetwork?.name);
|
||||
if (defaultNetwork) {
|
||||
this.$emit('update:network', defaultNetwork.name);
|
||||
} else {
|
||||
const firstnetwork = neu.find((network: GKENetwork) => network.kind !== 'group');
|
||||
|
||||
this.$emit('update:network', firstnetwork?.name);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ describe('gke Networking', () => {
|
|||
expect(spy).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
it('should populate network dropdown and select the first option after loading gcp data', async() => {
|
||||
it('should populate network dropdown and select the default network after loading gcp data', async() => {
|
||||
const setup = requiredSetup();
|
||||
|
||||
const wrapper = shallowMount(Networking, {
|
||||
|
|
@ -85,8 +85,8 @@ describe('gke Networking', () => {
|
|||
|
||||
const networksDropdown = wrapper.getComponent('[data-testid="gke-networks-dropdown"]');
|
||||
|
||||
expect(networksDropdown.props().options).toHaveLength(4);
|
||||
expect(wrapper.emitted('update:network')?.[0]?.[0]).toBe('host-shared-vpc');
|
||||
expect(networksDropdown.props().options).toHaveLength(5);
|
||||
expect(wrapper.emitted('update:network')?.[0]?.[0]).toBe('default');
|
||||
});
|
||||
|
||||
it('should populate subnetworks dropdown dependent on network selection', async() => {
|
||||
|
|
|
|||
|
|
@ -247,6 +247,64 @@ const mockedGKENetworksResponse = {
|
|||
'https://www.googleapis.com/compute/v1/projects/test-project/regions/me-central1/subnetworks/test-network',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-project/regions/asia-east2/subnetworks/test-network',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-project/regions/me-central2/subnetworks/test-network']
|
||||
},
|
||||
{
|
||||
autoCreateSubnetworks: true,
|
||||
creationTimestamp: '2022-10-26T14:50:30.702-07:00',
|
||||
id: '11111111',
|
||||
kind: 'compute#network',
|
||||
mtu: 1460,
|
||||
name: 'default',
|
||||
networkFirewallPolicyEnforcementOrder: 'AFTER_CLASSIC_FIREWALL',
|
||||
routingConfig: { routingMode: 'REGIONAL' },
|
||||
selfLink: 'https://www.googleapis.com/compute/v1/projects/test-data-project/global/networks/default',
|
||||
selfLinkWithId: 'https://www.googleapis.com/compute/v1/projects/test-data-project/global/networks/11111111',
|
||||
subnetworks: [
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/africa-south1/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/us-west8/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/asia-northeast3/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/asia-northeast2/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/asia-south2/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/europe-west3/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/us-west3/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/us-west2/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/asia-northeast1/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/europe-west12/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/asia-southeast1/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/us-south1/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/southamerica-west1/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/asia-southeast2/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/me-west1/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/us-east7/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/us-central1/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/southamerica-east1/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/europe-west9/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/europe-north1/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/us-east4/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/asia-east1/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/europe-west10/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/europe-central2/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/europe-north2/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/asia-south1/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/northamerica-south1/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/us-east5/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/us-east1/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/europe-west4/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/europe-west2/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/europe-west8/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/northamerica-northeast1/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/australia-southeast2/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/us-west4/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/europe-west6/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/northamerica-northeast2/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/europe-southwest1/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/us-west1/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/australia-southeast1/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/europe-west1/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/me-central1/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/asia-east2/subnetworks/default',
|
||||
'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/me-central2/subnetworks/default'
|
||||
]
|
||||
}]
|
||||
};
|
||||
|
||||
|
|
@ -267,7 +325,30 @@ const mockedGKESubnetworksResponse = {
|
|||
secondaryIpRanges: [{ ipCidrRange: '10.0.1.0/24', rangeName: 'range-1' }],
|
||||
selfLink: 'https://www.googleapis.com/compute/v1/projects/test-project/regions/us-central1/subnetworks/test-network',
|
||||
stackType: 'IPV4_ONLY',
|
||||
}]
|
||||
},
|
||||
{
|
||||
creationTimestamp: '2022-10-26T14:50:38.688-07:00',
|
||||
fingerprint: '3456',
|
||||
gatewayAddress: '10.128.0.1',
|
||||
id: '1234',
|
||||
ipCidrRange: '10.128.0.0/20',
|
||||
kind: 'compute#subnetwork',
|
||||
name: 'default',
|
||||
network: 'https://www.googleapis.com/compute/v1/projects/test-data-project/global/networks/default',
|
||||
privateIpGoogleAccess: true,
|
||||
privateIpv6GoogleAccess: 'DISABLE_GOOGLE_ACCESS',
|
||||
purpose: 'PRIVATE',
|
||||
region: 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/us-central1',
|
||||
secondaryIpRanges: [
|
||||
{
|
||||
ipCidrRange: '10.0.1.0/24',
|
||||
rangeName: 'range-1'
|
||||
}
|
||||
],
|
||||
selfLink: 'https://www.googleapis.com/compute/v1/projects/test-data-project/regions/us-central1/subnetworks/default',
|
||||
stackType: 'IPV4_ONLY'
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
const mockedGKESharedSubnetworksResponse = {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import draggable from 'vuedraggable';
|
|||
import isEmpty from 'lodash/isEmpty';
|
||||
import jsyaml from 'js-yaml';
|
||||
import YAML from 'yaml';
|
||||
import isBase64 from 'is-base64';
|
||||
import { isBase64 } from '@shell/utils/string';
|
||||
|
||||
import NodeAffinity from '@shell/components/form/NodeAffinity';
|
||||
import PodAffinity from '@shell/components/form/PodAffinity';
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ export default defineComponent({
|
|||
:class="{
|
||||
[color]: true,
|
||||
}"
|
||||
role="banner"
|
||||
>
|
||||
<div
|
||||
v-if="icon"
|
||||
|
|
@ -102,7 +103,12 @@ export default defineComponent({
|
|||
<div
|
||||
v-if="closable"
|
||||
class="banner__content__closer"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
:aria-label="t('generic.close')"
|
||||
@click="$emit('close')"
|
||||
@keyup.enter="$emit('close')"
|
||||
@keyup.space="$emit('close')"
|
||||
>
|
||||
<i
|
||||
data-testid="banner-close"
|
||||
|
|
@ -226,6 +232,7 @@ $icon-size: 24px;
|
|||
width: $icon-size;
|
||||
line-height: $icon-size;
|
||||
text-align: center;
|
||||
outline: none;
|
||||
|
||||
.closer-icon {
|
||||
opacity: 0.7;
|
||||
|
|
@ -235,6 +242,11 @@ $icon-size: 24px;
|
|||
color: var(--link);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible i {
|
||||
@include focus-outline;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&.icon {
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ export default defineComponent({
|
|||
primary: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
emits: ['update:value'],
|
||||
|
|
@ -135,7 +135,14 @@ export default defineComponent({
|
|||
*/
|
||||
isChecked(): boolean {
|
||||
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: {
|
||||
|
|
@ -214,6 +221,9 @@ export default defineComponent({
|
|||
<div
|
||||
class="checkbox-outer-container"
|
||||
data-checkbox-ctrl
|
||||
:class="{
|
||||
'v-popper--has-tooltip': hasTooltip,
|
||||
}"
|
||||
>
|
||||
<label
|
||||
class="checkbox-container"
|
||||
|
|
@ -227,9 +237,10 @@ export default defineComponent({
|
|||
:checked="isChecked"
|
||||
:value="valueWhenTrue"
|
||||
type="checkbox"
|
||||
:tabindex="-1"
|
||||
tabindex="-1"
|
||||
:name="id"
|
||||
@click.stop.prevent
|
||||
@keyup.enter.stop.prevent
|
||||
>
|
||||
<span
|
||||
class="checkbox-custom"
|
||||
|
|
@ -240,7 +251,7 @@ export default defineComponent({
|
|||
role="checkbox"
|
||||
/>
|
||||
<span
|
||||
v-if="$slots.label || label || labelKey || tooltipKey || tooltip"
|
||||
v-if="$slots.label || label || labelKey || hasTooltip"
|
||||
class="checkbox-label"
|
||||
:class="{ 'checkbox-primary': primary }"
|
||||
>
|
||||
|
|
@ -325,9 +336,14 @@ $fontColor: var(--input-label);
|
|||
width: 14px;
|
||||
background-color: var(--body-bg);
|
||||
border-radius: var(--border-radius);
|
||||
transition: all 0.3s ease-out;
|
||||
border: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
|
||||
&:focus-visible {
|
||||
@include focus-outline;
|
||||
outline-offset: 2px;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
|
|
@ -337,6 +353,12 @@ $fontColor: var(--input-label);
|
|||
z-index: -1;
|
||||
}
|
||||
|
||||
input:focus-visible ~ .checkbox-custom {
|
||||
@include focus-outline;
|
||||
outline-offset: 2px;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
input:checked ~ .checkbox-custom {
|
||||
background-color:var(--primary);
|
||||
-webkit-transform: rotate(0deg) scale(1);
|
||||
|
|
|
|||
19
scripts/e2e
19
scripts/e2e
|
|
@ -10,4 +10,21 @@ if [ $# -eq 1 ]; then
|
|||
SPEC_ARG="--spec ${1}"
|
||||
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
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ DASHBOARD_DIST=${DIR}/dist
|
|||
EMBER_DIST=${DIR}/dist_ember
|
||||
|
||||
# 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
|
||||
VOLUME_ARGS="-v ${DASHBOARD_DIST}:/usr/share/rancher/ui-dashboard/dashboard -v ${EMBER_DIST}:/usr/share/rancher/ui"
|
||||
|
|
|
|||
|
|
@ -159,4 +159,4 @@
|
|||
@mixin focus-outline {
|
||||
// Focus for form like elements (not to be confused with basic :focus style)
|
||||
outline: 2px solid var(--primary-keyboard-focus);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,6 +106,11 @@ button,
|
|||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
@include focus-outline;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.role-tertiary {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
generic:
|
||||
add: Add
|
||||
all: All
|
||||
ascending: ascending
|
||||
and: ' and '
|
||||
back: Back
|
||||
cancel: Cancel
|
||||
|
|
@ -19,6 +20,7 @@ generic:
|
|||
customize: Customize
|
||||
dashboard: Dashboard
|
||||
default: Default
|
||||
descending: descending
|
||||
disabled: Disabled
|
||||
done: Done
|
||||
enabled: Enabled
|
||||
|
|
@ -115,11 +117,26 @@ generic:
|
|||
basic: Basic
|
||||
|
||||
locale:
|
||||
menu: Locale selector menu
|
||||
en-us: English
|
||||
zh-hans: 简体中文
|
||||
none: (None)
|
||||
|
||||
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
|
||||
harvesterDashboard: Harvester Dashboard
|
||||
backToRancher: Cluster Manager
|
||||
|
|
@ -196,6 +213,7 @@ nav:
|
|||
placeholder: Filter clusters by...
|
||||
noResults: No matching clusters
|
||||
clusters: clusters
|
||||
ariaLabel: Filter clusters on main menu
|
||||
resourceSearch:
|
||||
label: Resource Search
|
||||
toolTip: Resource Search {key}
|
||||
|
|
@ -5818,6 +5836,7 @@ tableHeaders:
|
|||
resourcesReady: Resources Ready
|
||||
restarts: Restarts
|
||||
restart: Restart Required
|
||||
restartSystem: Restart { vendor }
|
||||
restore: Restore
|
||||
role: Role
|
||||
roles: Roles
|
||||
|
|
@ -7480,6 +7499,7 @@ advancedSettings:
|
|||
|
||||
featureFlags:
|
||||
label: Feature Flags
|
||||
title: "Are you sure?"
|
||||
warning: |-
|
||||
Feature flags allow {vendor} to gate certain features behind flags.
|
||||
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.
|
||||
promptActivate: Please confirm that you want to activate 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:
|
||||
title: Waiting for Restart
|
||||
wait: This may take a few moments
|
||||
|
|
@ -7496,6 +7516,7 @@ performance:
|
|||
label: UI Performance Settings
|
||||
settingName: Performance
|
||||
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:
|
||||
label: Incremental Loading
|
||||
setting: You can configure the threshold above which incremental loading will be used.
|
||||
|
|
@ -7565,6 +7586,7 @@ performance:
|
|||
resources:
|
||||
generic: most resources in the cluster's 'More Resources' section
|
||||
all: All Resources
|
||||
populateDefaults: Populate with latest pagination defaults
|
||||
banner:
|
||||
label: Fixed 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.
|
||||
three:
|
||||
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:
|
||||
title: Innovate with Freedom
|
||||
text: Take advantage of our certified compatibility with a wide range of Kubernetes providers, operating systems, and open source software.
|
||||
|
|
|
|||
|
|
@ -7310,7 +7310,7 @@ support:
|
|||
text: 我们的开发人员会快速解决问题,因此你可以放心使用 SUSE Rancher 的产品。
|
||||
three:
|
||||
title: 故障排除
|
||||
text: 无论你使用的 Rancher Labs 产品、Kubernetes、Docker 还是底层基础架构出现问题,我们都会努力找到问题的根本原因。
|
||||
text: 无论你使用的 SUSE 产品、Kubernetes、Docker 还是底层基础架构出现问题,我们都会努力找到问题的根本原因。
|
||||
four:
|
||||
title: 自由创新
|
||||
text: 基于我们与众多 Kubernetes 供应商、操作系统和开源软件认证的兼容性,实现自主创新。
|
||||
|
|
|
|||
|
|
@ -264,7 +264,10 @@ export default {
|
|||
:disabled="opt.disabled ? true : null"
|
||||
:class="{divider: opt.divider}"
|
||||
:data-testid="componentTestid + '-' + i + '-item'"
|
||||
:tabindex="opt.divider ? -1 : 0"
|
||||
@click="execute(opt, $event)"
|
||||
@keyup.enter="execute(opt, $event)"
|
||||
@keyup.space="execute(opt, $event)"
|
||||
>
|
||||
<IconOrSvg
|
||||
v-if="opt.icon || opt.svg"
|
||||
|
|
@ -311,6 +314,11 @@ export default {
|
|||
padding: 8px 10px;
|
||||
margin: 0;
|
||||
|
||||
&:focus-visible {
|
||||
@include focus-outline;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
cursor: not-allowed !important;
|
||||
color: var(--disabled-text);
|
||||
|
|
|
|||
|
|
@ -208,6 +208,10 @@ export default defineComponent({
|
|||
return this.disabled || this.phase === ASYNC_BUTTON_STATES.WAITING;
|
||||
},
|
||||
|
||||
isManualRefresh() {
|
||||
return this.mode === 'manual-refresh';
|
||||
},
|
||||
|
||||
tooltip(): { content: string, hideOnTargetClick: boolean} | null {
|
||||
if ( this.labelAs === TOOLTIP ) {
|
||||
return {
|
||||
|
|
@ -283,12 +287,14 @@ export default defineComponent({
|
|||
:data-testid="componentTestid + '-async-button'"
|
||||
@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
|
||||
v-if="displayIcon"
|
||||
v-clean-tooltip="tooltip"
|
||||
:class="{icon: true, 'icon-lg': true, [displayIcon]: true}"
|
||||
class="ml-5 mr-0"
|
||||
:class="{icon: true, 'icon-lg': true, [displayIcon]: true, 'mr-0': isManualRefresh}"
|
||||
/>
|
||||
<span
|
||||
v-if="labelAs === 'text' && displayLabel"
|
||||
|
|
|
|||
|
|
@ -55,7 +55,12 @@ export default {
|
|||
v-if="pref"
|
||||
class="close-button"
|
||||
data-testid="graphic-banner-close"
|
||||
tabindex="0"
|
||||
:aria-label="t('generic.close')"
|
||||
role="button"
|
||||
@click="hide()"
|
||||
@keyup.enter="hide()"
|
||||
@keyup.space="hide()"
|
||||
>
|
||||
<i class="icon icon-close" />
|
||||
</div>
|
||||
|
|
@ -72,6 +77,11 @@ export default {
|
|||
.close-button {
|
||||
position: absolute;
|
||||
visibility: hidden;
|
||||
|
||||
&:focus-visible {
|
||||
@include focus-outline;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .close-button {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,12 @@ const buttonClass = computed(() => {
|
|||
.borderless {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
|
||||
&:focus-visible {
|
||||
@include focus-outline;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
&:hover, &:focus {
|
||||
background-color: var(--accent-btn);
|
||||
box-shadow: none;
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export default {
|
|||
},
|
||||
|
||||
customColor() {
|
||||
return !this.cluster.removePreviewColor && this.cluster.badge?.iconText ? this.cluster.badge?.color : '';
|
||||
return this.cluster.iconColor || '';
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { _EDIT, _VIEW } from '@shell/config/query-params';
|
|||
export default {
|
||||
name: 'CodeMirror',
|
||||
|
||||
emits: ['onReady', 'onInput', 'onChanges', 'onFocus'],
|
||||
emits: ['onReady', 'onInput', 'onChanges', 'onFocus', 'validationChanged'],
|
||||
|
||||
props: {
|
||||
/**
|
||||
|
|
@ -39,6 +39,7 @@ export default {
|
|||
codeMirrorRef: null,
|
||||
loaded: false,
|
||||
removeKeyMapBox: false,
|
||||
hasLintErrors: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
|
@ -65,6 +66,7 @@ export default {
|
|||
foldGutter: true,
|
||||
styleSelectedText: true,
|
||||
showCursorWhenSelecting: true,
|
||||
autocorrect: false,
|
||||
};
|
||||
|
||||
if (this.asTextArea) {
|
||||
|
|
@ -76,6 +78,11 @@ export default {
|
|||
|
||||
Object.assign(out, this.options);
|
||||
|
||||
// parent components control lint with a boolean; if linting is enabled, we need to override that boolean with a custom error handler to wire lint errors into dashboard validation
|
||||
if (this.options?.lint) {
|
||||
out.lint = { onUpdateLinting: this.handleLintErrors };
|
||||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
|
|
@ -104,7 +111,25 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
hasLintErrors(neu) {
|
||||
this.$emit('validationChanged', !neu);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Codemirror yaml linting uses js-yaml parse
|
||||
* it does not distinguish between warnings and errors so we will treat all yaml lint messages as errors
|
||||
* other codemirror linters (eg json) will report from, to, severity where severity may be 'warning' or 'error'
|
||||
* only 'error' level linting will trigger a validation event from this component
|
||||
*/
|
||||
handleLintErrors(diagnostics = []) {
|
||||
const hasLintErrors = diagnostics.filter((d) => !d.severity || d.severity === 'error').length > 0;
|
||||
|
||||
this.hasLintErrors = hasLintErrors;
|
||||
},
|
||||
|
||||
focus() {
|
||||
if ( this.$refs.codeMirrorRef ) {
|
||||
this.$refs.codeMirrorRef.cminstance.focus();
|
||||
|
|
@ -118,6 +143,8 @@ export default {
|
|||
},
|
||||
|
||||
onReady(codeMirrorRef) {
|
||||
this.$emit('validationChanged', true);
|
||||
|
||||
this.$nextTick(() => {
|
||||
codeMirrorRef.refresh();
|
||||
this.codeMirrorRef = codeMirrorRef;
|
||||
|
|
|
|||
|
|
@ -110,6 +110,8 @@ export default {
|
|||
<router-link
|
||||
v-if="link.value.startsWith('/') "
|
||||
:to="link.value"
|
||||
role="link"
|
||||
:aria-label="link.label"
|
||||
>
|
||||
{{ link.label }}
|
||||
</router-link>
|
||||
|
|
@ -118,6 +120,8 @@ export default {
|
|||
:href="link.value"
|
||||
rel="noopener noreferrer nofollow"
|
||||
target="_blank"
|
||||
role="link"
|
||||
:aria-label="link.label"
|
||||
> {{ link.label }} </a>
|
||||
</div>
|
||||
<slot />
|
||||
|
|
@ -127,7 +131,11 @@ export default {
|
|||
>
|
||||
<a
|
||||
class="link"
|
||||
tabindex="0"
|
||||
:aria-label="t('footer.wechat.title')"
|
||||
role="link"
|
||||
@click="show"
|
||||
@keyup.enter="show"
|
||||
>
|
||||
{{ t('footer.wechat.title') }}
|
||||
</a>
|
||||
|
|
@ -147,7 +155,12 @@ export default {
|
|||
<div>
|
||||
<button
|
||||
class="btn role-primary"
|
||||
tabindex="0"
|
||||
:aria-label="t('generic.close')"
|
||||
role="button"
|
||||
@click="close"
|
||||
@keyup.enter="close"
|
||||
@keyup.space="close"
|
||||
>
|
||||
{{ t('generic.close') }}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -106,7 +106,10 @@ export default {
|
|||
<div class="growl-text-title">
|
||||
{{ growl.title }}
|
||||
</div>
|
||||
<p v-if="growl.message">
|
||||
<p
|
||||
v-if="growl.message"
|
||||
:class="{ 'has-title': !!growl.title }"
|
||||
>
|
||||
{{ growl.message }}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -153,12 +156,16 @@ export default {
|
|||
word-break: break-all;
|
||||
box-shadow: 0 3px 5px 0px var(--shadow);
|
||||
|
||||
$growl-icon-size: 20px;
|
||||
|
||||
.icon-container {
|
||||
align-self: center;
|
||||
flex-basis: 10%;
|
||||
padding: 10px 20px 10px 10px;
|
||||
i {
|
||||
font-size: 24px;
|
||||
font-size: $growl-icon-size;
|
||||
width: $growl-icon-size;
|
||||
height: $growl-icon-size;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -183,11 +190,14 @@ export default {
|
|||
}
|
||||
.growl-text-title {
|
||||
font-size: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
> P {
|
||||
margin-top: 5px;
|
||||
padding-top: 2px;
|
||||
|
||||
&.has-title {
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,11 @@ export default {
|
|||
mode: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return { isLocaleSelectorOpen: false };
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
|
@ -40,8 +44,15 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
openLocaleSelector() {
|
||||
this.isLocaleSelectorOpen = true;
|
||||
},
|
||||
closeLocaleSelector() {
|
||||
this.isLocaleSelectorOpen = false;
|
||||
},
|
||||
switchLocale($event) {
|
||||
this.$store.dispatch('i18n/switchTo', $event);
|
||||
this.closeLocaleSelector();
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
@ -50,13 +61,28 @@ export default {
|
|||
<template>
|
||||
<div>
|
||||
<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
|
||||
popperClass="localeSelector"
|
||||
:shown="isLocaleSelectorOpen"
|
||||
placement="top"
|
||||
distance="8"
|
||||
skidding="12"
|
||||
:triggers="['click']"
|
||||
:triggers="[]"
|
||||
:autoHide="false"
|
||||
:flip="false"
|
||||
:container="false"
|
||||
@focus.capture="openLocaleSelector"
|
||||
>
|
||||
<a
|
||||
data-testid="locale-selector"
|
||||
|
|
@ -74,13 +100,21 @@ export default {
|
|||
v-if="showNone"
|
||||
v-t="'locale.none'"
|
||||
class="hand"
|
||||
@click="switchLocale('none')"
|
||||
tabindex="0"
|
||||
role="menuitem"
|
||||
@click.stop="switchLocale('none')"
|
||||
@keyup.enter.stop="switchLocale('none')"
|
||||
@keyup.space.stop="switchLocale('none')"
|
||||
/>
|
||||
<li
|
||||
v-for="(label, name) in availableLocales"
|
||||
:key="name"
|
||||
tabindex="0"
|
||||
role="menuitem"
|
||||
class="hand"
|
||||
@click="switchLocale(name)"
|
||||
@click.stop="switchLocale(name)"
|
||||
@keyup.enter.stop="switchLocale(name)"
|
||||
@keyup.space.stop="switchLocale(name)"
|
||||
>
|
||||
{{ label }}
|
||||
</li>
|
||||
|
|
@ -114,6 +148,11 @@ export default {
|
|||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.hand:focus-visible {
|
||||
@include focus-outline;
|
||||
outline-offset: 4px;
|
||||
}
|
||||
|
||||
.locale-chooser {
|
||||
cursor: pointer;
|
||||
|
||||
|
|
@ -121,4 +160,9 @@ export default {
|
|||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.locale-login-container:focus-visible {
|
||||
@include focus-outline;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -93,8 +93,8 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
computed: {
|
||||
safeHeaders() {
|
||||
const customHeaders = this.canPaginate ? this.paginationHeaders : this.headers;
|
||||
safeHeaders(): any[] {
|
||||
const customHeaders: any[] = this.canPaginate ? this.paginationHeaders : this.headers;
|
||||
|
||||
return customHeaders || this.$store.getters['type-map/headersFor'](this.schema, this.canPaginate);
|
||||
}
|
||||
|
|
@ -109,7 +109,7 @@ export default defineComponent({
|
|||
v-bind="$attrs"
|
||||
:schema="schema"
|
||||
:rows="rows"
|
||||
:alt-loading="canPaginate"
|
||||
:alt-loading="canPaginate && !isFirstLoad"
|
||||
:loading="loading"
|
||||
:groupable="groupable"
|
||||
|
||||
|
|
@ -124,6 +124,7 @@ export default defineComponent({
|
|||
<template
|
||||
v-for="(_, slot) of $slots"
|
||||
v-slot:[slot]="scope"
|
||||
:key="slot"
|
||||
>
|
||||
<slot
|
||||
:name="slot"
|
||||
|
|
|
|||
|
|
@ -54,8 +54,10 @@ export default {
|
|||
this.customTypeDisplay = component.typeDisplay.apply(this);
|
||||
}
|
||||
|
||||
// If your list page has a fetch then it's responsible for populating rows itself
|
||||
if ( component?.fetch ) {
|
||||
// Is the custom component responsible fetching the resources?
|
||||
// - 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;
|
||||
}
|
||||
|
||||
|
|
@ -269,7 +271,7 @@ export default {
|
|||
v-else
|
||||
:schema="schema"
|
||||
:rows="rows"
|
||||
:alt-loading="canPaginate"
|
||||
:alt-loading="canPaginate && !isFirstLoad"
|
||||
:loading="loading"
|
||||
:headers="headers"
|
||||
:group-by="groupBy"
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ export default {
|
|||
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: {
|
||||
type: Number,
|
||||
|
|
|
|||
|
|
@ -162,6 +162,14 @@ export default {
|
|||
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) {
|
||||
// set menu position
|
||||
const menu = document.querySelector('.table-options-container');
|
||||
|
|
@ -235,7 +243,12 @@ export default {
|
|||
:align="col.align || 'left'"
|
||||
:width="col.width"
|
||||
: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)"
|
||||
@keyup.enter="changeSort($event, col)"
|
||||
@keyup.space="changeSort($event, col)"
|
||||
>
|
||||
<div
|
||||
class="table-header-container"
|
||||
|
|
@ -415,10 +428,7 @@ export default {
|
|||
background-color: var(--sortable-table-header-bg);
|
||||
color: var(--body-text);
|
||||
text-align: left;
|
||||
|
||||
&:not(.loading) {
|
||||
border-bottom: 1px solid var(--sortable-table-top-divider);
|
||||
}
|
||||
border-bottom: 1px solid var(--sortable-table-top-divider);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -428,6 +438,11 @@ export default {
|
|||
border: 0;
|
||||
color: var(--body-text);
|
||||
|
||||
&.sortable-table-head-element:focus-visible {
|
||||
@include focus-outline;
|
||||
outline-offset: -4px;
|
||||
}
|
||||
|
||||
.table-header-container {
|
||||
display: inline-flex;
|
||||
|
||||
|
|
|
|||
|
|
@ -380,8 +380,10 @@ export default {
|
|||
eventualSearchQuery = this.$route.query?.q;
|
||||
}
|
||||
|
||||
const isLoading = this.loading || false;
|
||||
|
||||
return {
|
||||
refreshButtonPhase: ASYNC_BUTTON_STATES.WAITING,
|
||||
refreshButtonPhase: isLoading ? ASYNC_BUTTON_STATES.WAITING : ASYNC_BUTTON_STATES.ACTION,
|
||||
expanded: {},
|
||||
searchQuery,
|
||||
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)
|
||||
*/
|
||||
isLoading: false,
|
||||
isLoading
|
||||
};
|
||||
},
|
||||
|
||||
|
|
@ -504,7 +506,7 @@ export default {
|
|||
if (neu) {
|
||||
this._altLoadingDelayTimer = setTimeout(() => {
|
||||
this.isLoading = true;
|
||||
}, 200); // this should be higher than the targetted quick response
|
||||
}, 200); // this should be higher than the targeted quick response
|
||||
} else {
|
||||
clearTimeout(this._altLoadingDelayTimer);
|
||||
this.isLoading = false;
|
||||
|
|
@ -536,9 +538,6 @@ export default {
|
|||
manualRefreshLoadingFinished() {
|
||||
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;
|
||||
},
|
||||
|
||||
|
|
@ -575,11 +574,13 @@ export default {
|
|||
},
|
||||
|
||||
showHeaderRow() {
|
||||
// All of these are used to show content in the header
|
||||
return this.search ||
|
||||
this.tableActions ||
|
||||
this.$slots['header-left']?.() ||
|
||||
this.$slots['header-middle']?.() ||
|
||||
this.$slots['header-right']?.();
|
||||
this.$slots['header-left'] ||
|
||||
this.$slots['header-middle'] ||
|
||||
this.$slots['header-right'] ||
|
||||
this.isTooManyItemsToAutoUpdate;
|
||||
},
|
||||
|
||||
columns() {
|
||||
|
|
@ -1222,6 +1223,7 @@ export default {
|
|||
class="sortable-table"
|
||||
:class="classObject"
|
||||
width="100%"
|
||||
role="table"
|
||||
>
|
||||
<THead
|
||||
v-if="showHeaders"
|
||||
|
|
@ -1451,6 +1453,8 @@ export default {
|
|||
:data-testid="componentTestid + '-' + i + '-action-button'"
|
||||
:borderless="true"
|
||||
@click="handleActionButtonClick(i, $event)"
|
||||
@keyup.enter="handleActionButtonClick(i, $event)"
|
||||
@keyup.space="handleActionButtonClick(i, $event)"
|
||||
/>
|
||||
</slot>
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { mapGetters } from 'vuex';
|
||||
import { isMore, isRange, suppressContextMenu, isAlternate } from '@shell/utils/platform';
|
||||
import { get } from '@shell/utils/object';
|
||||
import { filterBy } from '@shell/utils/array';
|
||||
|
|
@ -29,6 +30,13 @@ export default {
|
|||
},
|
||||
|
||||
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)
|
||||
howMuchSelected() {
|
||||
const total = this.pagedRows.length;
|
||||
|
|
@ -270,11 +278,17 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
this.$store.commit(`action-menu/show`, {
|
||||
resources,
|
||||
event: e,
|
||||
elem: actionElement
|
||||
});
|
||||
if (!this.targetElem && !this.shouldShow) {
|
||||
this.$store.commit(`action-menu/show`, {
|
||||
resources,
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export const EDITOR_MODES = {
|
|||
};
|
||||
|
||||
export default {
|
||||
emits: ['update:value', 'newObject', 'onInput', 'onReady', 'onChanges'],
|
||||
emits: ['update:value', 'newObject', 'onInput', 'onReady', 'onChanges', 'validationChanged'],
|
||||
|
||||
components: {
|
||||
CodeMirror,
|
||||
|
|
@ -236,6 +236,7 @@ export default {
|
|||
@onInput="onInput"
|
||||
@onReady="onReady"
|
||||
@onChanges="onChanges"
|
||||
@validationChanged="$emit('validationChanged', $event)"
|
||||
/>
|
||||
<FileDiff
|
||||
v-else
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
onSearch(str, loading, vm) {
|
||||
onSearch(str, loading) {
|
||||
str = (str || '').trim();
|
||||
|
||||
this.searchStr = str;
|
||||
|
|
|
|||
|
|
@ -127,7 +127,8 @@ export default {
|
|||
class="text-warning"
|
||||
>
|
||||
{{ 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>
|
||||
</ResourceTable>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export default {
|
|||
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: {
|
||||
appendToBody: {
|
||||
|
|
@ -147,7 +147,7 @@ export default {
|
|||
|
||||
// update placeholder text to inform user they can add their own opts when none are found
|
||||
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;
|
||||
},
|
||||
|
||||
onSearch(newSearchString) {
|
||||
onSearch(newSearchString, loading) {
|
||||
if (this.canPaginate) {
|
||||
this.setPaginationFilter(newSearchString);
|
||||
} else {
|
||||
|
|
@ -246,6 +246,7 @@ export default {
|
|||
this.dropdownShouldOpen(this.$refs['select-input'], true);
|
||||
}
|
||||
}
|
||||
this.$emit('search', newSearchString, loading);
|
||||
},
|
||||
|
||||
getOptionKey(opt) {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ export default {
|
|||
type: String,
|
||||
default: 'create'
|
||||
},
|
||||
|
||||
loading: {
|
||||
default: false,
|
||||
type: Boolean
|
||||
|
|
@ -169,6 +170,7 @@ export default {
|
|||
name="selectNode"
|
||||
:options="selectNodeOptions"
|
||||
:mode="mode"
|
||||
:data-testid="'node-scheduling-selectNode'"
|
||||
@input="update"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -182,7 +184,8 @@ export default {
|
|||
:mode="mode"
|
||||
:multiple="false"
|
||||
:loading="loading"
|
||||
@input="update"
|
||||
:data-testid="'node-scheduling-nodeSelector'"
|
||||
@update:value="update"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -191,6 +194,7 @@ export default {
|
|||
<NodeAffinity
|
||||
v-model:value="nodeAffinity"
|
||||
:mode="mode"
|
||||
:data-testid="'node-scheduling-nodeAffinity'"
|
||||
@input="update"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export default {
|
|||
mode: {
|
||||
type: String,
|
||||
default: _CREATE,
|
||||
},
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return { reveal: false };
|
||||
|
|
@ -68,6 +68,9 @@ export default {
|
|||
}
|
||||
|
||||
return attributes;
|
||||
},
|
||||
hideShowLabel() {
|
||||
return this.reveal ? this.t('action.hide') : this.t('action.show');
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
|
@ -92,6 +95,9 @@ export default {
|
|||
},
|
||||
focus() {
|
||||
this.$refs.input.$refs.value.focus();
|
||||
},
|
||||
hideShowFn() {
|
||||
this.reveal ? this.reveal = false : this.reveal = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -127,17 +133,15 @@ export default {
|
|||
class="addon"
|
||||
>
|
||||
<a
|
||||
v-if="reveal"
|
||||
tabindex="-1"
|
||||
href="#"
|
||||
@click.prevent.stop="reveal = false"
|
||||
>{{ t('action.hide') }}</a>
|
||||
<a
|
||||
v-else
|
||||
tabindex="-1"
|
||||
href="#"
|
||||
@click.prevent.stop="reveal=true"
|
||||
>{{ t('action.show') }}</a>
|
||||
tabindex="0"
|
||||
class="hide-show"
|
||||
role="button"
|
||||
@click.prevent.stop="hideShowFn"
|
||||
@keyup.space.prevent.stop="hideShowFn"
|
||||
>
|
||||
{{ hideShowLabel }}
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
</LabeledInput>
|
||||
|
|
@ -157,10 +161,16 @@ export default {
|
|||
.password {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.labeled-input {
|
||||
.addon {
|
||||
padding-left: 12px;
|
||||
min-width: 65px;
|
||||
padding-left: 12px;
|
||||
min-width: 65px;
|
||||
|
||||
.hide-show:focus-visible {
|
||||
@include focus-outline;
|
||||
outline-offset: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.genPassword {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export interface ResourceLabeledSelectPaginateSettings extends SharedSettings {
|
|||
*/
|
||||
overrideRequest?: LabelSelectPaginateFn,
|
||||
/**
|
||||
* Override the default settings used in the convience function to fetch a page of results
|
||||
* Override the default settings used in the convenience function to fetch a page of results
|
||||
*/
|
||||
requestSettings?: PaginateTypeOverridesFn,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -27,7 +27,7 @@ export default {
|
|||
const nodeWithExternal = nodes.find((node) => !!node.externalIp) || {};
|
||||
const externalIp = nodeWithExternal.externalIp;
|
||||
|
||||
if ( this.value && this.value.length ) {
|
||||
if ( this.value?.length ) {
|
||||
let out ;
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ export default {
|
|||
},
|
||||
},
|
||||
};
|
||||
</script>>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -17,28 +17,30 @@ export default {
|
|||
},
|
||||
},
|
||||
data() {
|
||||
const { row } = this;
|
||||
let cloned = this.value.toLowerCase();
|
||||
const cloned = this.getLabel(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') {
|
||||
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 };
|
||||
return { translated: cloned, headless };
|
||||
},
|
||||
|
||||
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>
|
||||
<span>{{ translated }}</span>
|
||||
<span>{{ translated }}{{ headless ? ` (${headless})` : '' }}</span>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ export default {
|
|||
cluster: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
tabOrder: {
|
||||
type: Number,
|
||||
default: null,
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -28,11 +32,12 @@ export default {
|
|||
|
||||
<template>
|
||||
<i
|
||||
:tabindex="0"
|
||||
:tabindex="tabOrder"
|
||||
:aria-checked="!!pinned"
|
||||
class="pin icon"
|
||||
:class="{'icon-pin-outlined': !pinned, 'icon-pin': pinned}"
|
||||
aria-role="button"
|
||||
:aria-label="`${t('nav.ariaLabel.pinCluster')} ${ cluster.label }`"
|
||||
@click.stop.prevent="toggle"
|
||||
@keydown.enter.prevent="toggle"
|
||||
@keydown.space.prevent="toggle"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ interface TopLevelMenuCluster {
|
|||
ready: boolean
|
||||
providerNavLogo: string,
|
||||
badge: string,
|
||||
iconColor: string,
|
||||
isLocal: boolean,
|
||||
pinned: boolean,
|
||||
description: string,
|
||||
|
|
@ -143,6 +144,7 @@ export abstract class BaseTopLevelMenuHelper {
|
|||
ready: mgmtCluster.isReady, // && !provCluster?.hasError,
|
||||
providerNavLogo: mgmtCluster.providerMenuLogo,
|
||||
badge: mgmtCluster.badge,
|
||||
iconColor: mgmtCluster.iconColor,
|
||||
isLocal: mgmtCluster.isLocal,
|
||||
pinned: mgmtCluster.pinned,
|
||||
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 {
|
||||
constructor({ $store }: {
|
||||
|
|
|
|||
|
|
@ -485,6 +485,8 @@ export default {
|
|||
:class="{'menu-open': shown, 'menu-close':!shown}"
|
||||
:style="sideMenuStyle"
|
||||
tabindex="-1"
|
||||
role="navigation"
|
||||
:aria-label="t('nav.ariaLabel.topLevelMenu')"
|
||||
>
|
||||
<!-- Logo and name -->
|
||||
<div class="title">
|
||||
|
|
@ -504,6 +506,7 @@ export default {
|
|||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
:alt="t('nav.alt.mainMenuIcon')"
|
||||
><path
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
|
|
@ -512,6 +515,7 @@ export default {
|
|||
<div class="side-menu-logo">
|
||||
<BrandImage
|
||||
data-testid="side-menu__brand-img"
|
||||
:alt="t('nav.alt.mainMenuRancherLogo')"
|
||||
file-name="rancher-logo.svg"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -525,9 +529,12 @@ export default {
|
|||
<router-link
|
||||
class="option cluster selector home"
|
||||
:to="{ name: 'home' }"
|
||||
role="link"
|
||||
:aria-label="t('nav.ariaLabel.homePage')"
|
||||
>
|
||||
<svg
|
||||
v-clean-tooltip="getTooltipConfig(t('nav.home'))"
|
||||
class="top-menu-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
|
|
@ -562,6 +569,8 @@ export default {
|
|||
ref="clusterFilter"
|
||||
v-model="clusterFilter"
|
||||
:placeholder="t('nav.search.placeholder')"
|
||||
:tabindex="!shown ? -1 : 0"
|
||||
:aria-label="t('nav.search.ariaLabel')"
|
||||
>
|
||||
<i
|
||||
class="magnifier icon icon-search"
|
||||
|
|
@ -583,10 +592,11 @@ export default {
|
|||
<a
|
||||
v-if="isRancherInHarvester"
|
||||
class="option"
|
||||
tabindex="0"
|
||||
@click="goToHarvesterCluster()"
|
||||
>
|
||||
<i
|
||||
class="icon icon-dashboard"
|
||||
class="icon icon-dashboard app-icon"
|
||||
/>
|
||||
<div>
|
||||
{{ t('nav.harvesterDashboard') }}
|
||||
|
|
@ -602,8 +612,11 @@ export default {
|
|||
class="option"
|
||||
:to="a.to"
|
||||
:class="{'active-menu-link': a.isMenuActive }"
|
||||
role="link"
|
||||
:aria-label="`${t('nav.ariaLabel.harvesterCluster')} ${ a.label }`"
|
||||
>
|
||||
<IconOrSvg
|
||||
class="app-icon"
|
||||
:icon="a.icon"
|
||||
:src="a.svg"
|
||||
/>
|
||||
|
|
@ -637,6 +650,8 @@ export default {
|
|||
class="cluster selector option"
|
||||
:class="{'active-menu-link': c.isMenuActive }"
|
||||
:to="c.clusterRoute"
|
||||
role="button"
|
||||
:aria-label="`${t('nav.ariaLabel.cluster')} ${ c.label }`"
|
||||
@click.prevent="clusterMenuClick($event, c)"
|
||||
@shortkey="handleKeyComboClick"
|
||||
>
|
||||
|
|
@ -660,6 +675,7 @@ export default {
|
|||
</div>
|
||||
<Pinned
|
||||
:cluster="c"
|
||||
:tab-order="shown ? 0 : -1"
|
||||
/>
|
||||
</button>
|
||||
<span
|
||||
|
|
@ -686,6 +702,7 @@ export default {
|
|||
</div>
|
||||
<Pinned
|
||||
:cluster="c"
|
||||
:tab-order="shown ? 0 : -1"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -712,6 +729,8 @@ export default {
|
|||
class="cluster selector option"
|
||||
:class="{'active-menu-link': c.isMenuActive }"
|
||||
:to="c.clusterRoute"
|
||||
role="button"
|
||||
:aria-label="`${t('nav.ariaLabel.cluster')} ${ c.label }`"
|
||||
@click="clusterMenuClick($event, c)"
|
||||
@shortkey="handleKeyComboClick"
|
||||
>
|
||||
|
|
@ -725,7 +744,6 @@ export default {
|
|||
v-clean-tooltip="getTooltipConfig(c)"
|
||||
class="cluster-name"
|
||||
>
|
||||
<!-- HERE LOCAL CLUSTER! -->
|
||||
<p>{{ c.label }}</p>
|
||||
<p
|
||||
v-if="c.description"
|
||||
|
|
@ -736,6 +754,7 @@ export default {
|
|||
</div>
|
||||
<Pinned
|
||||
:class="{'showPin': c.pinned}"
|
||||
:tab-order="shown ? 0 : -1"
|
||||
:cluster="c"
|
||||
/>
|
||||
</button>
|
||||
|
|
@ -763,6 +782,7 @@ export default {
|
|||
</div>
|
||||
<Pinned
|
||||
:class="{'showPin': c.pinned}"
|
||||
:tab-order="shown ? 0 : -1"
|
||||
:cluster="c"
|
||||
/>
|
||||
</span>
|
||||
|
|
@ -788,6 +808,8 @@ export default {
|
|||
product: 'manager',
|
||||
resource: 'provisioning.cattle.io.cluster'
|
||||
} }"
|
||||
role="link"
|
||||
:aria-label="t('nav.ariaLabel.seeAll')"
|
||||
>
|
||||
<span>
|
||||
{{ shown ? t('nav.seeAllClusters') : t('nav.seeAllClustersCollapsed') }}
|
||||
|
|
@ -796,6 +818,7 @@ export default {
|
|||
</router-link>
|
||||
</template>
|
||||
|
||||
<!-- MULTI CLUSTER APPS -->
|
||||
<div class="category">
|
||||
<template v-if="multiClusterApps.length">
|
||||
<div
|
||||
|
|
@ -815,9 +838,12 @@ export default {
|
|||
class="option"
|
||||
:class="{'active-menu-link': a.isMenuActive }"
|
||||
:to="a.to"
|
||||
role="link"
|
||||
:aria-label="`${t('nav.ariaLabel.multiClusterApps')} ${ a.label }`"
|
||||
>
|
||||
<IconOrSvg
|
||||
v-clean-tooltip="getTooltipConfig(a.label)"
|
||||
class="app-icon"
|
||||
:icon="a.icon"
|
||||
:src="a.svg"
|
||||
/>
|
||||
|
|
@ -826,7 +852,7 @@ export default {
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<!-- App menu -->
|
||||
<!-- Configuration apps menu -->
|
||||
<template v-if="configurationApps.length">
|
||||
<div
|
||||
class="category-title"
|
||||
|
|
@ -845,9 +871,12 @@ export default {
|
|||
class="option"
|
||||
:class="{'active-menu-link': a.isMenuActive }"
|
||||
:to="a.to"
|
||||
role="link"
|
||||
:aria-label="`${t('nav.ariaLabel.configurationApps')} ${ a.label }`"
|
||||
>
|
||||
<IconOrSvg
|
||||
v-clean-tooltip="getTooltipConfig(a.label)"
|
||||
class="app-icon"
|
||||
:icon="a.icon"
|
||||
:src="a.svg"
|
||||
/>
|
||||
|
|
@ -869,6 +898,8 @@ export default {
|
|||
>
|
||||
<router-link
|
||||
:to="{name: 'support'}"
|
||||
role="link"
|
||||
:aria-label="t('nav.ariaLabel.support')"
|
||||
>
|
||||
{{ t('nav.support', {hasSupport}) }}
|
||||
</router-link>
|
||||
|
|
@ -880,6 +911,8 @@ export default {
|
|||
>
|
||||
<router-link
|
||||
:to="{ name: 'about' }"
|
||||
role="link"
|
||||
:aria-label="t('nav.ariaLabel.about')"
|
||||
>
|
||||
{{ aboutText }}
|
||||
</router-link>
|
||||
|
|
@ -983,13 +1016,27 @@ export default {
|
|||
overflow: hidden;
|
||||
transition: width 250ms;
|
||||
|
||||
&:focus {
|
||||
&:focus, &:focus-visible {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&.menu-open {
|
||||
.option:focus-visible {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&.menu-open {
|
||||
width: 300px;
|
||||
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 {
|
||||
|
|
@ -1099,10 +1146,6 @@ export default {
|
|||
&:focus {
|
||||
outline: 0;
|
||||
box-shadow: none;
|
||||
|
||||
> div {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
> i, > img {
|
||||
|
|
@ -1120,7 +1163,22 @@ export default {
|
|||
fill: var(--link);
|
||||
}
|
||||
|
||||
.top-menu-icon {
|
||||
outline-offset: 4px;
|
||||
}
|
||||
|
||||
&.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);
|
||||
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 {
|
||||
color: var(--primary-hover-text);
|
||||
background: var(--primary-hover-bg);
|
||||
|
|
@ -1213,10 +1277,19 @@ export default {
|
|||
margin-right: 16px;
|
||||
margin-top: 10px;
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&:focus-visible span {
|
||||
@include focus-outline;
|
||||
outline-offset: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.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 {
|
||||
.side-menu-logo {
|
||||
opacity: 0;
|
||||
|
|
@ -1412,8 +1512,18 @@ export default {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.support a:focus-visible {
|
||||
@include focus-outline;
|
||||
outline-offset: 4px;
|
||||
}
|
||||
|
||||
.version {
|
||||
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
Loading…
Reference in New Issue