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

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

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

@ -0,0 +1,25 @@
version: 2
updates:
# Maintain dependencies for GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
labels: ["component/dependencies"]
# Maintain dependencies for dashboard, shell, and packages
- package-ecosystem: "npm"
directories:
- "/"
- "/shell"
- "/creators/extension"
- "/creators/extension/*"
- "/docusaurus"
- "/storybook"
- "/pkg/*"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
labels: ["component/dependencies"]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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'

View File

@ -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,12 +60,18 @@ 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
env:

View File

@ -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:

View File

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

View File

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

View File

@ -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'

View File

@ -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'

View File

@ -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'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2474,7 +2474,7 @@ function generateFakeNavClusterData(provClusterId = 'some-prov-cluster-id', mgmt
}
export function generateFakeClusterDataAndIntercepts(fakeProvClusterId = 'some-prov-cluster-id', fakeMgmtClusterId = 'some-mgmt-cluster-id', addEditClusterCapabilities = false): {} {
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

View File

@ -0,0 +1,12 @@
import ComponentPo from '@/cypress/e2e/po/components/component.po';
import YamlEditorPo from '~/cypress/e2e/po/components/yaml-editor.po';
export default class AddonConfigPo extends ComponentPo {
constructor(selector = '.dashboard-root') {
super(selector);
}
yamlEditor() :YamlEditorPo {
return new YamlEditorPo(this.self().find('[data-testid="addon-yaml-editor"]'));
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,65 @@
import PagePo from '@/cypress/e2e/po/pages/page.po';
import ArrayListPo from '@/cypress/e2e/po/components/array-list.po';
import CreateEditViewPo from '@/cypress/e2e/po/components/create-edit-view.po';
import LabeledInputPo from '@/cypress/e2e/po/components/labeled-input.po';
import LabeledSelectPo from '@/cypress/e2e/po/components/labeled-select.po';
import SelectOrCreateAuthPo from '@/cypress/e2e/po/components/select-or-create-auth.po';
import NameNsDescription from '@/cypress/e2e/po/components/name-ns-description.po';
export class GitRepoEditPo extends PagePo {
private static createPath(fleetWorkspace: string, gitRepoName: string) {
return `/c/_/fleet/fleet.cattle.io.gitrepo/${ fleetWorkspace }/${ gitRepoName }`;
}
static goTo(path: string): Cypress.Chainable<Cypress.AUTWindow> {
throw new Error('invalid');
}
constructor(fleetWorkspace: string, gitRepoName: string) {
super(GitRepoEditPo.createPath(fleetWorkspace, gitRepoName));
}
title() {
return this.self().get('.title .primaryheader h1');
}
nameNsDescription() {
return new NameNsDescription(this.self());
}
setBranchName(branch = 'dashboard-e2e-basic') {
return LabeledInputPo.byLabel(this.self(), 'Branch').set(branch);
}
setGitRepoUrl(url: string) {
return LabeledInputPo.byLabel(this.self(), 'Repository URL').set(url);
}
setHelmRepoURLRegex(regexStr = 'https://charts.rancher.io/*') {
return LabeledInputPo.bySelector(this.self(), '[data-testid="gitrepo-helm-repo-url-regex"]').set(regexStr);
}
setGitRepoPath(path: string, index = 0) {
return this.gitRepoPaths().setValueAtIndex(path, index);
}
targetCluster(): LabeledSelectPo {
return new LabeledSelectPo('[data-testid="fleet-gitrepo-target-cluster"]');
}
footer() {
return new CreateEditViewPo(this.self());
}
gitRepoPaths() {
return new ArrayListPo('[data-testid="gitRepo-paths"]');
}
authSelectOrCreate(selector: string) {
return new SelectOrCreateAuthPo(selector);
}
helmAuthSelectOrCreate() {
return this.authSelectOrCreate('[data-testid="gitrepo-helm-auth"]');
}
}

View File

@ -4,6 +4,7 @@ import ClusterManagerCreatePagePo from '@/cypress/e2e/po/edit/provisioning.cattl
import TabbedPo from '@/cypress/e2e/po/components/tabbed.po';
import 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();
}
}

View File

@ -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() {

View File

@ -7,7 +7,7 @@ import RepositoriesPagePo from '@/cypress/e2e/po/pages/chart-repositories.po';
import BannersPo from '@/cypress/e2e/po/components/banners.po';
import 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());
}
/**

View File

@ -2,6 +2,7 @@ import PagePo from '@/cypress/e2e/po/pages/page.po';
import ResourceTablePo from '@/cypress/e2e/po/components/resource-table.po';
import 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');
}
}

View File

@ -5,14 +5,16 @@ import ResourceTablePo from '@/cypress/e2e/po/components/resource-table.po';
import FleetClusterGroupsList from '@/cypress/e2e/po/lists/fleet/fleet.cattle.io.clustergroup';
import 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);
}
}

View File

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

View File

@ -4,7 +4,7 @@ import PagePo from '@/cypress/e2e/po/pages/page.po';
import ProductNavPo from '@/cypress/e2e/po/side-bars/product-side-nav.po';
import { 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';

View File

@ -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()

View File

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

View File

@ -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,26 +257,24 @@ 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'];
cy.isVaiCacheEnabled().then((isVaiCacheEnabled) => {
if (isVaiCacheEnabled) {
expectedHeaders = ['Reason', 'Object', 'Message', 'Name', 'First Seen', 'Last Seen', 'Count'];
}
clusterDashboard.eventsList().resourceTable().sortableTable().tableHeaderRow()
.self()
.scrollIntoView();
clusterDashboard.eventsList().resourceTable().sortableTable().tableHeaderRow()
.within('.table-header-container .content')
.each((el, i) => {
@ -300,6 +298,7 @@ describe('Cluster Dashboard', { testIsolation: 'off', tags: ['@explorer', '@admi
expect(el.text().trim()).to.eq(expectedFullHeaders[i]);
});
});
});
describe('Cluster dashboard with limited permissions', () => {
let stdProjectName;

View File

@ -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,12 +90,24 @@ 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;
return cy.getRancherResource('v1', 'events');
})
.then((resp: Cypress.Response<any>) => {
let initialCount = resp.body.count;
if (!vaiCacheEnabled && resp.body.count > 500) {
// Why 500? there's a hardcoded figure to stops ui from storing more than 500 events ...
initialCount = 500;
}
// Test break down if less than 3 pages...
expect(initialCount).to.be.greaterThan(3 * pageSize);
// pagination is visible
events.sortableTable().pagination().checkVisible();
@ -82,49 +123,77 @@ describe('Events', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] },
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 - 100 of ${ count } Events`);
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().paginationText().then((el) => {
expect(el.trim()).to.eq(`101 - 200 of ${ count } Events`);
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().paginationText().then((el) => {
expect(el.trim()).to.eq(`1 - 100 of ${ count } Events`);
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();
// check row count on last page
events.sortableTable().checkRowCount(false, 100);
countHelper.handleCount(vaiCacheEnabled);
// check text after navigation
events.sortableTable().pagination().paginationText().then((el) => {
expect(el.trim()).to.eq(`401 - ${ count } of ${ count } Events`);
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 - 100 of ${ count } Events`);
expect(el.trim()).to.eq(`1 - ${ pageSize } of ${ count } Events`);
});
});
events.sortableTable().pagination().beginningButton().isDisabled();
events.sortableTable().pagination().leftButton().isDisabled();
});
@ -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);

View File

@ -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()

View File

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

View File

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

View File

@ -1,23 +1,114 @@
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();
cy.createE2EResourceName('cluster-group').then((name) => {
clusterGroupName = name;
});
});
it('check table headers are available in list and details view', () => {
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';
const workspace = 'fleet-local';
FleetClusterGroupsListPagePo.navTo();
fleetClusterGroups.waitForPage();
headerPo.selectWorkspace(workspace);
headerPo.selectWorkspace(localWorkspace);
fleetClusterGroups.clusterGroupsList().rowWithName(groupName).checkVisible();
// check table headers
@ -32,7 +123,7 @@ describe('Cluster Groups', { testIsolation: 'off', tags: ['@fleet', '@adminUser'
// go to fleet cluster details
fleetClusterGroups.goToDetailsPage(groupName);
const fleetClusterGroupDetailsPage = new FleetClusterGroupDetailsPo(workspace, groupName);
const fleetClusterGroupDetailsPage = new FleetClusterGroupDetailsPo(localWorkspace, groupName);
fleetClusterGroupDetailsPage.waitForPage(null, 'clusters');
@ -46,18 +137,11 @@ describe('Cluster Groups', { testIsolation: 'off', tags: ['@fleet', '@adminUser'
expect(el.text().trim()).to.eq(expectedHeadersDetailsView[i]);
});
});
});
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();
});
after(() => {
if (removeClusterGroups) {
// delete gitrepo
clusterGroupsToDelete.forEach((r) => cy.deleteRancherResource('v1', 'fleet.cattle.io.clustergroups', r, false));
}
});
});

View File

@ -1,65 +1,216 @@
import { FleetDashboardPagePo } from '@/cypress/e2e/po/pages/fleet/fleet-dashboard.po';
// import { 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));
}
});
});

View File

@ -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')

View File

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

View File

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

View File

@ -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,7 +1024,19 @@ 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 = {
@ -1034,29 +1046,36 @@ Cypress.Commands.add('tableRowsPerPageAndNamespaceFilter', (rows: number, cluste
cluster: clusterName,
'per-page': `${ rows }`,
'group-by': groupBy,
'ns-by-cluster': namespaceFilter
'ns-by-cluster': namespaceFilter,
'all-namespaces': allNamespaces,
}
};
cy.log(`tableRowsPerPageAndNamespaceFilter: /v1/userpreferences/${ userId }. Payload: ${ JSON.stringify(payload) }`);
cy.log(`tableRowsPerPageAndPreferences: /v1/userpreferences/${ userId }. Payload: ${ JSON.stringify(payload) }`);
cy.setRancherResource('v1', 'userpreferences', userId, payload).then(() => {
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
});
});

View File

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

View File

@ -19,3 +19,23 @@ To resolve this add the following `resolution` to the root application's `packag
...
}
```
- Running `yarn install` might throw the following errors:
```
error @aws-sdk/types@3.723.0: The engine "node" is incompatible with this module. Expected version ">=18.0.0". Got "16.20.2"
error @aws-sdk/util-locate-window@3.723.0: The engine "node" is incompatible with this module. Expected version ">=18.0.0". Got "16.20.2"
```
To resolve this add the following `resolutions` to the root application's `package.json`:
```
{
"name": "app-name",
"version": "0.1.0",
...
resolutions": {
"@aws-sdk/types": "3.714.0",
"@aws-sdk/util-locate-window": "3.693.0"
},
...
}
```

View File

@ -205,6 +205,29 @@ When building an extension that will be housed in a GitLab repository or hosted
This pipeline will build an ECI and publish it to container registry (`registry.gitlab.com` by default) to allow for importing into Rancher Manager.
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:

View File

@ -109,6 +109,14 @@ These values are provided when you create a new project within Cypress dashboard
It's also possible to run a workflow in GitHub Actions E2E test using these values to record on personal dashboards.
### 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.

View File

@ -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 docs 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

View File

@ -205,6 +205,22 @@ When building an extension that will be housed in a GitLab repository or hosted
This pipeline will build an ECI and publish it to container registry (`registry.gitlab.com` by default) to allow for importing into Rancher Manager.
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:

View File

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

View File

@ -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> {

View File

@ -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: {

View File

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

View File

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

View File

@ -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 {

View File

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

View File

@ -203,10 +203,16 @@ export default defineComponent({
networkOptions(neu) {
if (neu && neu.length && !this.network) {
const defaultNetwork = neu.find((network: GKENetwork) => network?.name === 'default');
if (defaultNetwork) {
this.$emit('update:network', defaultNetwork.name);
} else {
const firstnetwork = neu.find((network: GKENetwork) => network.kind !== 'group');
this.$emit('update:network', firstnetwork?.name);
}
}
},
subnetworkOptions(neu) {

View File

@ -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() => {

View File

@ -247,6 +247,64 @@ const mockedGKENetworksResponse = {
'https://www.googleapis.com/compute/v1/projects/test-project/regions/me-central1/subnetworks/test-network',
'https://www.googleapis.com/compute/v1/projects/test-project/regions/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 = {

View File

@ -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';

View File

@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {

View File

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

View File

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

View File

@ -5,7 +5,7 @@ import { _EDIT, _VIEW } from '@shell/config/query-params';
export default {
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;

View File

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

View File

@ -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,13 +190,16 @@ export default {
}
.growl-text-title {
font-size: 16px;
margin-bottom: 20px;
}
> P {
padding-top: 2px;
&.has-title {
margin-top: 5px;
}
}
}
}
}
</style>

View File

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

View File

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

View File

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

View File

@ -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,

View File

@ -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,12 +428,9 @@ 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);
}
}
}
th {
padding: 8px 5px;
@ -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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) {

View File

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

View File

@ -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;
.hide-show:focus-visible {
@include focus-outline;
outline-offset: 4px;
}
}
}
.genPassword {

View File

@ -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,
}

View File

@ -0,0 +1,44 @@
import { mount } from '@vue/test-utils';
import NodeScheduling from '@shell/components/form/NodeScheduling.vue';
import { _CREATE, _EDIT, _VIEW } from '@shell/config/query-params';
const requiredSetup = () => {
return {
global: {
mocks: {
$store: {
getters: {
currentProduct: { inStore: 'cluster' },
'i18n/t': (text: string) => text,
t: (text: string) => text,
}
}
},
}
};
};
describe('component: NodeScheduling', () => {
const value = { nodeName: 'node-1' };
const nodes = ['node-0', 'node-1'];
it.each([
_VIEW,
_CREATE,
_EDIT
])('should show NodeName option', (mode) => {
const wrapper = mount(
NodeScheduling,
{
props: {
mode, loading: false, value, nodes
},
...requiredSetup(),
}
);
expect(wrapper.find('[data-testid="node-scheduling-selectNode"]').exists()).toBeTruthy();
expect(wrapper.find('[data-testid="node-scheduling-nodeSelector"]').element.textContent).toContain(value.nodeName);
});
});

View File

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

View File

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

View File

@ -17,14 +17,15 @@ 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';
}
return { translated: cloned, headless };
},
const match = DEFAULT_SERVICE_TYPES.find((s) => s.id.toLowerCase() === cloned);
methods: {
getLabel(type) {
const match = DEFAULT_SERVICE_TYPES.find((s) => s.id.toLowerCase() === type);
const translationLabel = match?.label;
let translated;
@ -34,11 +35,12 @@ export default {
translated = this.value;
}
return { translated };
},
return translated;
}
}
};
</script>>
</script>
<template>
<span>{{ translated }}</span>
<span>{{ translated }}{{ headless ? ` (${headless})` : '' }}</span>
</template>

View File

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

View File

@ -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 }: {

View File

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

View File

@ -1,4 +1,6 @@
import { STATE, NAME as NAME_COL, NAMESPACE as NAMESPACE_COL, AGE } from '@shell/config/table-headers';
import {
STATE, NAME as NAME_COL, NAMESPACE as NAMESPACE_COL, AGE, OBJECT
} from '@shell/config/table-headers';
// This file contains table headers
// These table headers are used for server side pagination
@ -44,6 +46,12 @@ export const STEVE_NAMESPACE_COL = {
search: 'metadata.namespace',
};
export const STEVE_EVENT_OBJECT = {
...OBJECT,
sort: 'involvedObject.kind',
search: 'involvedObject.kind',
};
export const STEVE_LIST_GROUPS = [{
tooltipKey: 'resourceTable.groupBy.none',
icon: 'icon-list-flat',

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