diff --git a/.eslintrc.js b/.eslintrc.js index 0671773c3c2..4b3b98c4dca 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -59,7 +59,8 @@ module.exports = { '@typescript-eslint/no-var-requires': 'off', 'vue/one-component-per-file': 'off', 'vue/no-deprecated-slot-attribute': 'off', - 'vue/v-on-event-hyphenation': 'off' + 'vue/v-on-event-hyphenation': 'off', + 'jest/no-hooks': 'off', }, overrides: [ { diff --git a/.github/workflows/scripts/pr-check-checklist.sh b/.github/workflows/scripts/pr-check-checklist.sh new file mode 100755 index 00000000000..26d78fa2596 --- /dev/null +++ b/.github/workflows/scripts/pr-check-checklist.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -e + +echo "Fetching description..." +PR_BODY=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github.v3+json" \ + "$PR_URL" | jq -r '.body') + +echo "Validating checklist..." +CHECKBOXES=$(echo "$PR_BODY" | grep -o '\[.\]') +UNCHECKED=$(echo "$CHECKBOXES" | grep '\[ \]' || true) + +if [ -n "$UNCHECKED" ]; then + echo "❌ Checklist has not been completed" + exit 1 +else + echo "✅ Checked Checklist, all checks checked and checked" +fi \ No newline at end of file diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index c1a9bc8f15f..646866f4078 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -82,7 +82,7 @@ jobs: ['@manager'], ['@userMenu', '@usersAndAuths'], ['@components'], - # ['@vai'] // https://github.com/rancher/dashboard/issues/12856 + ['@vai'] ] runs-on: ubuntu-latest steps: diff --git a/.github/workflows/valid-pr-description.yaml b/.github/workflows/valid-pr-description.yaml new file mode 100644 index 00000000000..72e7ff85b78 --- /dev/null +++ b/.github/workflows/valid-pr-description.yaml @@ -0,0 +1,20 @@ +name: Validate Pull Request Description + +on: + pull_request: + types: [opened, edited, synchronize] + +jobs: + validate-description: + name: Validate Description + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Validate checklist has been completed + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_URL: ${{ github.event.pull_request.url }} + run: ./.github/workflows/scripts/pr-check-checklist.sh \ No newline at end of file diff --git a/README.md b/README.md index b2352100bab..20576a67cdf 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ We welcome external contributions - please refer to the internal documentation a License ======= -Copyright (c) 2014-2024 [Rancher Labs, Inc.](http://rancher.com) +Copyright (c) 2014-2025 [Rancher Labs, Inc.](http://rancher.com) Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/creators/extension/app/files/.github/workflows/build-extension-catalog.yml b/creators/extension/app/files/.github/workflows/build-extension-catalog.yml index 61400830efd..1c2bb812a93 100644 --- a/creators/extension/app/files/.github/workflows/build-extension-catalog.yml +++ b/creators/extension/app/files/.github/workflows/build-extension-catalog.yml @@ -20,5 +20,6 @@ jobs: with: registry_target: ghcr.io registry_user: ${{ github.actor }} + tagged_release: ${{ github.ref_name }} secrets: registry_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/creators/extension/package.json b/creators/extension/package.json index 663e0af4139..9fc4d23e7f8 100644 --- a/creators/extension/package.json +++ b/creators/extension/package.json @@ -1,7 +1,7 @@ { "name": "@rancher/create-extension", "description": "Rancher UI Extension generator", - "version": "3.0.5", + "version": "3.0.7", "license": "Apache-2.0", "author": "SUSE", "packageManager": "yarn@4.5.0", diff --git a/cypress/e2e/blueprints/nav/fake-cluster.ts b/cypress/e2e/blueprints/nav/fake-cluster.ts index 8391e69528d..de9a85f57be 100644 --- a/cypress/e2e/blueprints/nav/fake-cluster.ts +++ b/cypress/e2e/blueprints/nav/fake-cluster.ts @@ -2474,7 +2474,7 @@ function generateFakeNavClusterData(provClusterId = 'some-prov-cluster-id', mgmt } export function generateFakeClusterDataAndIntercepts(fakeProvClusterId = 'some-prov-cluster-id', fakeMgmtClusterId = 'some-mgmt-cluster-id', addEditClusterCapabilities = false): {} { - const longClusterDescription = 'this-is-some-really-really-really-really-really-really-long-decription'; + const longClusterDescription = 'this-is-some-really-really-really-really-really-really-long-description'; const fakeNavClusterData = generateFakeNavClusterData(fakeProvClusterId, fakeMgmtClusterId, addEditClusterCapabilities); // add cluster to fleet clusters for testing https://github.com/rancher/dashboard/issues/9984 diff --git a/cypress/e2e/po/components/namespace-filter.po.ts b/cypress/e2e/po/components/namespace-filter.po.ts index cd9f5dfb67b..f56b8570d4a 100644 --- a/cypress/e2e/po/components/namespace-filter.po.ts +++ b/cypress/e2e/po/components/namespace-filter.po.ts @@ -1,4 +1,5 @@ import ComponentPo from '@/cypress/e2e/po/components/component.po'; + export class NamespaceFilterPo extends ComponentPo { constructor() { super('[data-testid="namespaces-filter"]'); @@ -52,6 +53,10 @@ export class NamespaceFilterPo extends ComponentPo { return this.namespaceDropdown().find('[data-testid="namespaces-values"]'); } + allSelected() { + return this.self().find('[data-testid="namespaces-values-none"]').should('exist'); + } + moreOptionsSelected() { return this.namespaceDropdown().find('.ns-more'); } diff --git a/cypress/e2e/po/edit/provisioning.cattle.io.cluster/create/cluster-create.po.ts b/cypress/e2e/po/edit/provisioning.cattle.io.cluster/create/cluster-create.po.ts index 53b782381cb..4be22a79773 100644 --- a/cypress/e2e/po/edit/provisioning.cattle.io.cluster/create/cluster-create.po.ts +++ b/cypress/e2e/po/edit/provisioning.cattle.io.cluster/create/cluster-create.po.ts @@ -42,6 +42,10 @@ export default class ClusterManagerCreatePagePo extends ClusterManagerCreateImpo return this.self().contains('.grid .name', name, { timeout: 10000 }).should(assertion); } + gridElementGroupTitles() { + return this.self().find('.subtypes-container > div > h4'); + } + selectKubeProvider(index: number) { return this.resourceDetail().cruResource().selectSubType(0, index).click(); } diff --git a/cypress/e2e/po/pages/chart-repositories.po.ts b/cypress/e2e/po/pages/chart-repositories.po.ts index ba5c1fdb010..fac6bf3963e 100644 --- a/cypress/e2e/po/pages/chart-repositories.po.ts +++ b/cypress/e2e/po/pages/chart-repositories.po.ts @@ -28,8 +28,7 @@ export default class ChartRepositoriesPagePo extends PagePo { const burgerMenu = new BurgerMenuPo(); const sideNav = new ProductNavPo(); - BurgerMenuPo.toggle(); - burgerMenu.clusterNotPinnedList().contains(clusterId).click(); + burgerMenu.goToCluster(clusterId); sideNav.navToSideMenuGroupByLabel('Apps'); sideNav.navToSideMenuEntryByLabel('Repositories'); } else { diff --git a/cypress/e2e/po/pages/explorer/charts/charts.po.ts b/cypress/e2e/po/pages/explorer/charts/charts.po.ts index d09520aad30..615d10ef1b1 100644 --- a/cypress/e2e/po/pages/explorer/charts/charts.po.ts +++ b/cypress/e2e/po/pages/explorer/charts/charts.po.ts @@ -23,8 +23,7 @@ export class ChartsPage extends PagePo { const burgerMenu = new BurgerMenuPo(); const sideNav = new ProductNavPo(); - BurgerMenuPo.toggle(); - burgerMenu.clusterNotPinnedList().contains(clusterId).click(); + burgerMenu.goToCluster(clusterId); sideNav.navToSideMenuGroupByLabel('Apps'); } diff --git a/cypress/e2e/po/pages/explorer/cluster-dashboard.po.ts b/cypress/e2e/po/pages/explorer/cluster-dashboard.po.ts index 693a942152e..e3e6ad73d71 100644 --- a/cypress/e2e/po/pages/explorer/cluster-dashboard.po.ts +++ b/cypress/e2e/po/pages/explorer/cluster-dashboard.po.ts @@ -4,7 +4,8 @@ import CustomBadgeDialogPo from '@/cypress/e2e/po/components/custom-badge-dialog import EventsListPo from '@/cypress/e2e/po/lists/events-list.po'; import TabbedPo from '@/cypress/e2e/po/components/tabbed.po'; import CertificatesPo from '@/cypress/e2e/po/components/certificates.po'; -import { HeaderPo } from '~/cypress/e2e/po/components/header.po'; +import { HeaderPo } from '@/cypress/e2e/po/components/header.po'; +import { NamespaceFilterPo } from '@/cypress/e2e/po/components/namespace-filter.po'; export default class ClusterDashboardPagePo extends PagePo { private static createPath(clusterId: string) { @@ -15,6 +16,10 @@ export default class ClusterDashboardPagePo extends PagePo { return super.goTo(ClusterDashboardPagePo.createPath(clusterId)); } + urlPath(clusterId = 'local') { + return ClusterDashboardPagePo.createPath(clusterId); + } + constructor(clusterId: string) { super(ClusterDashboardPagePo.createPath(clusterId)); } @@ -22,8 +27,7 @@ export default class ClusterDashboardPagePo extends PagePo { static navTo(clusterId = 'local') { const burgerMenu = new BurgerMenuPo(); - BurgerMenuPo.toggle(); - burgerMenu.clusterNotPinnedList().contains(clusterId).click(); + burgerMenu.goToCluster(clusterId); } customizeAppearanceButton() { @@ -53,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() { @@ -79,4 +83,45 @@ export default class ClusterDashboardPagePo extends PagePo { controllerManagerStatus() { return cy.get('[data-testid="k8s-service-controller-manager"]'); } + + /** + * Confirm that the ns filter is set correctly before navigating to a page that will use it + * 1. nav to cluster dashboard + * 2. check ns filter values + */ + static goToAndConfirmNsValues(cluster: string, { + nsProject, + all + }: { + nsProject?: { + values: string[] + }, + all?: { + is: boolean, + } + }) { + const instance = new ClusterDashboardPagePo(cluster); + const nsfilter = new NamespaceFilterPo(); + + instance.goTo(); + instance.waitForPage(); + nsfilter.checkVisible(); + + if (nsProject) { + for (let i = 0; i < nsProject.values.length; i++) { + nsfilter.selectedValues().contains(nsProject.values[i]); + } + } else if (all) { + nsfilter.allSelected(); + } else { + throw new Error('Bad Config'); + } + } + + static goToAndWait(cluster: string) { + const instance = new ClusterDashboardPagePo(cluster); + + instance.goTo(); + instance.clusterActionsHeader().checkVisible(); + } } diff --git a/cypress/e2e/po/pages/explorer/config-map.po.ts b/cypress/e2e/po/pages/explorer/config-map.po.ts index 930f55e86cc..631797b0126 100644 --- a/cypress/e2e/po/pages/explorer/config-map.po.ts +++ b/cypress/e2e/po/pages/explorer/config-map.po.ts @@ -16,8 +16,7 @@ export class ConfigMapPagePo extends PagePo { const burgerMenu = new BurgerMenuPo(); const sideNav = new ProductNavPo(); - BurgerMenuPo.toggle(); - burgerMenu.clusterNotPinnedList().contains(clusterId).click(); + burgerMenu.goToCluster(clusterId); sideNav.navToSideMenuGroupByLabel('Storage'); sideNav.navToSideMenuEntryByLabel('ConfigMaps'); } diff --git a/cypress/e2e/po/pages/explorer/custom-resource-definitions.po.ts b/cypress/e2e/po/pages/explorer/custom-resource-definitions.po.ts index ab689fc0afe..4de4b4a5f82 100644 --- a/cypress/e2e/po/pages/explorer/custom-resource-definitions.po.ts +++ b/cypress/e2e/po/pages/explorer/custom-resource-definitions.po.ts @@ -22,8 +22,7 @@ export class CustomResourceDefinitionsPagePo extends PagePo { const burgerMenu = new BurgerMenuPo(); const sideNav = new ProductNavPo(); - BurgerMenuPo.toggle(); - burgerMenu.clusterNotPinnedList().contains(clusterId).click(); + burgerMenu.goToCluster(clusterId); sideNav.navToSideMenuGroupByLabel('More Resources'); sideNav.navToSideMenuGroupByLabel('API'); sideNav.navToSideMenuEntryByLabel('CustomResourceDefinitions'); diff --git a/cypress/e2e/po/pages/explorer/horizontal-pod-autoscalers.po.ts b/cypress/e2e/po/pages/explorer/horizontal-pod-autoscalers.po.ts index 6d762443b67..1291e2e8250 100644 --- a/cypress/e2e/po/pages/explorer/horizontal-pod-autoscalers.po.ts +++ b/cypress/e2e/po/pages/explorer/horizontal-pod-autoscalers.po.ts @@ -20,8 +20,7 @@ export class HorizontalPodAutoscalersPagePo extends PagePo { const burgerMenu = new BurgerMenuPo(); const sideNav = new ProductNavPo(); - BurgerMenuPo.toggle(); - burgerMenu.clusterNotPinnedList().contains(clusterId).click(); + burgerMenu.goToCluster(clusterId); sideNav.navToSideMenuGroupByLabel('Service Discovery'); sideNav.navToSideMenuEntryByLabel('HorizontalPodAutoscalers'); } diff --git a/cypress/e2e/po/pages/explorer/ingress.po.ts b/cypress/e2e/po/pages/explorer/ingress.po.ts index ebccd71e6aa..807f5cf990f 100644 --- a/cypress/e2e/po/pages/explorer/ingress.po.ts +++ b/cypress/e2e/po/pages/explorer/ingress.po.ts @@ -21,8 +21,7 @@ export class IngressPagePo extends PagePo { const burgerMenu = new BurgerMenuPo(); const sideNav = new ProductNavPo(); - BurgerMenuPo.toggle(); - burgerMenu.clusterNotPinnedList().contains(clusterId).click(); + burgerMenu.goToCluster(clusterId); sideNav.navToSideMenuGroupByLabel('Service Discovery'); sideNav.navToSideMenuEntryByLabel('Ingresses'); } diff --git a/cypress/e2e/po/pages/explorer/network-policy.po.ts b/cypress/e2e/po/pages/explorer/network-policy.po.ts index 01d89a9be4f..35db7da0842 100644 --- a/cypress/e2e/po/pages/explorer/network-policy.po.ts +++ b/cypress/e2e/po/pages/explorer/network-policy.po.ts @@ -16,8 +16,7 @@ export class NetworkPolicyPagePo extends PagePo { const burgerMenu = new BurgerMenuPo(); const sideNav = new ProductNavPo(); - BurgerMenuPo.toggle(); - burgerMenu.clusterNotPinnedList().contains(clusterId).click(); + burgerMenu.goToCluster(clusterId); sideNav.navToSideMenuGroupByLabel('Policy'); sideNav.navToSideMenuEntryByLabel('Network Policies'); } diff --git a/cypress/e2e/po/pages/explorer/persistent-volume-claims.po.ts b/cypress/e2e/po/pages/explorer/persistent-volume-claims.po.ts index 7e26e458325..3bea96f7e15 100644 --- a/cypress/e2e/po/pages/explorer/persistent-volume-claims.po.ts +++ b/cypress/e2e/po/pages/explorer/persistent-volume-claims.po.ts @@ -20,8 +20,7 @@ export class PersistentVolumeClaimsPagePo extends PagePo { const burgerMenu = new BurgerMenuPo(); const sideNav = new ProductNavPo(); - BurgerMenuPo.toggle(); - burgerMenu.clusterNotPinnedList().contains(clusterId).click(); + burgerMenu.goToCluster(clusterId); sideNav.navToSideMenuGroupByLabel('Storage'); sideNav.navToSideMenuEntryByLabel('PersistentVolumeClaims'); } diff --git a/cypress/e2e/po/pages/explorer/persistent-volumes.po.ts b/cypress/e2e/po/pages/explorer/persistent-volumes.po.ts index ec10860bba3..1c43e264e5f 100644 --- a/cypress/e2e/po/pages/explorer/persistent-volumes.po.ts +++ b/cypress/e2e/po/pages/explorer/persistent-volumes.po.ts @@ -21,8 +21,7 @@ export class PersistentVolumesPagePo extends PagePo { const burgerMenu = new BurgerMenuPo(); const sideNav = new ProductNavPo(); - BurgerMenuPo.toggle(); - burgerMenu.clusterNotPinnedList().contains(clusterId).click(); + burgerMenu.goToCluster(clusterId); sideNav.navToSideMenuGroupByLabel('Storage'); sideNav.navToSideMenuEntryByLabel('PersistentVolumes'); } diff --git a/cypress/e2e/po/pages/explorer/service-accounts.po.ts b/cypress/e2e/po/pages/explorer/service-accounts.po.ts index 2c174bda427..51dbe2f05d3 100644 --- a/cypress/e2e/po/pages/explorer/service-accounts.po.ts +++ b/cypress/e2e/po/pages/explorer/service-accounts.po.ts @@ -1,5 +1,7 @@ import PagePo from '@/cypress/e2e/po/pages/page.po'; import BaseResourceList from '@/cypress/e2e/po/lists/base-resource-list.po'; +import BurgerMenuPo from '@/cypress/e2e/po/side-bars/burger-side-menu.po'; +import ProductNavPo from '@/cypress/e2e/po/side-bars/product-side-nav.po'; export class ServiceAccountsPagePo extends PagePo { private static createPath(clusterId: string) { @@ -14,6 +16,16 @@ export class ServiceAccountsPagePo extends PagePo { return super.goTo(ServiceAccountsPagePo.createPath(clusterId)); } + static navTo(clusterId = 'local') { + const burgerMenu = new BurgerMenuPo(); + const sideNav = new ProductNavPo(); + + burgerMenu.goToCluster(clusterId); + sideNav.navToSideMenuGroupByLabel('More Resources'); + sideNav.navToSideMenuGroupByLabel('Core'); + sideNav.navToSideMenuEntryByLabel('ServiceAccount'); + } + constructor(clusterId = 'local') { super(ServiceAccountsPagePo.createPath(clusterId)); } diff --git a/cypress/e2e/po/pages/explorer/services.po.ts b/cypress/e2e/po/pages/explorer/services.po.ts index 0aca6ad8a21..e9fbc44ae32 100644 --- a/cypress/e2e/po/pages/explorer/services.po.ts +++ b/cypress/e2e/po/pages/explorer/services.po.ts @@ -21,10 +21,9 @@ export class ServicesPagePo extends PagePo { const burgerMenu = new BurgerMenuPo(); const sideNav = new ProductNavPo(); - BurgerMenuPo.toggle(); - burgerMenu.clusterNotPinnedList().contains(clusterId).click(); + burgerMenu.goToCluster(clusterId); sideNav.navToSideMenuGroupByLabel('Service Discovery'); - sideNav.navToSideMenuEntryByLabel('Ingresses'); + sideNav.navToSideMenuEntryByLabel('Service'); } constructor(clusterId = 'local') { diff --git a/cypress/e2e/po/pages/explorer/storage-classes.po.ts b/cypress/e2e/po/pages/explorer/storage-classes.po.ts index 4859841ac64..6ca1756341c 100644 --- a/cypress/e2e/po/pages/explorer/storage-classes.po.ts +++ b/cypress/e2e/po/pages/explorer/storage-classes.po.ts @@ -21,8 +21,7 @@ export class StorageClassesPagePo extends PagePo { const burgerMenu = new BurgerMenuPo(); const sideNav = new ProductNavPo(); - BurgerMenuPo.toggle(); - burgerMenu.clusterNotPinnedList().contains(clusterId).click(); + burgerMenu.goToCluster(clusterId); sideNav.navToSideMenuGroupByLabel('Storage'); sideNav.navToSideMenuEntryByLabel('StorageClasses'); } diff --git a/cypress/e2e/po/pages/explorer/workloads-pods.po.ts b/cypress/e2e/po/pages/explorer/workloads-pods.po.ts index d1185681bad..bf5f250eeb0 100644 --- a/cypress/e2e/po/pages/explorer/workloads-pods.po.ts +++ b/cypress/e2e/po/pages/explorer/workloads-pods.po.ts @@ -22,8 +22,7 @@ export class WorkloadsPodsListPagePo extends PagePo { const burgerMenu = new BurgerMenuPo(); const sideNav = new ProductNavPo(); - BurgerMenuPo.toggle(); - burgerMenu.clusterNotPinnedList().contains(clusterId).click(); + burgerMenu.goToCluster(clusterId); sideNav.navToSideMenuGroupByLabel('Workloads'); sideNav.navToSideMenuEntryByLabel('Pods'); } diff --git a/cypress/e2e/po/side-bars/burger-side-menu.po.ts b/cypress/e2e/po/side-bars/burger-side-menu.po.ts index c9937b99391..fa0ae030d36 100644 --- a/cypress/e2e/po/side-bars/burger-side-menu.po.ts +++ b/cypress/e2e/po/side-bars/burger-side-menu.po.ts @@ -134,7 +134,13 @@ export default class BurgerMenuPo extends ComponentPo { return this.self().find('.body .cluster.selector.option'); } - goToCluster(clusterId = 'local') { + goToCluster(clusterId = 'local', toggleOpen = true) { + if (toggleOpen) { + BurgerMenuPo.toggle(); + } + + this.self().find('.cluster-name').contains(clusterId).should('exist'); + return this.self().find('.cluster-name').contains(clusterId).click(); } diff --git a/cypress/e2e/tests/navigation/side-nav/main-side-menu.spec.ts b/cypress/e2e/tests/navigation/side-nav/main-side-menu.spec.ts index 9c27cd6fbfb..ffc0ca298a8 100644 --- a/cypress/e2e/tests/navigation/side-nav/main-side-menu.spec.ts +++ b/cypress/e2e/tests/navigation/side-nav/main-side-menu.spec.ts @@ -4,7 +4,7 @@ import PagePo from '@/cypress/e2e/po/pages/page.po'; import ProductNavPo from '@/cypress/e2e/po/side-bars/product-side-nav.po'; import { generateFakeClusterDataAndIntercepts } from '@/cypress/e2e/blueprints/nav/fake-cluster'; -const longClusterDescription = 'this-is-some-really-really-really-really-really-really-long-decription'; +const longClusterDescription = 'this-is-some-really-really-really-really-really-really-long-description'; const fakeProvClusterId = 'some-fake-cluster-id'; const fakeMgmtClusterId = 'some-fake-mgmt-id'; @@ -29,6 +29,9 @@ describe('Side Menu: main', () => { pagePoFake.navToClusterMenuEntry(fakeProvClusterId); sideNav.navToSideMenuEntryByLabel('Projects/Namespaces'); + BurgerMenuPo.burgerMenuGetNavClusterbyLabel('local').should('exist'); + BurgerMenuPo.burgerMenuGetNavClusterbyLabel(fakeProvClusterId).should('exist'); + // press key combo cy.get('body').focus().type('{alt}', { release: false }); diff --git a/cypress/e2e/tests/navigation/side-nav/product-side-nav.spec.ts b/cypress/e2e/tests/navigation/side-nav/product-side-nav.spec.ts index 937ea0c0f42..9afbe19ad05 100644 --- a/cypress/e2e/tests/navigation/side-nav/product-side-nav.spec.ts +++ b/cypress/e2e/tests/navigation/side-nav/product-side-nav.spec.ts @@ -21,10 +21,9 @@ describe('Side navigation: Cluster ', { tags: ['@navigation', '@adminUser'] }, ( cy.login(); HomePagePo.goTo(); - BurgerMenuPo.toggle(); const burgerMenuPo = new BurgerMenuPo(); - burgerMenuPo.goToCluster('local').click(); + burgerMenuPo.goToCluster('local'); }); it('Can access to first navigation link on click', () => { diff --git a/cypress/e2e/tests/pages/explorer/dashboard/cluster-dashboard.spec.ts b/cypress/e2e/tests/pages/explorer/dashboard/cluster-dashboard.spec.ts index c1f9ef6a7e1..5ea576f3673 100644 --- a/cypress/e2e/tests/pages/explorer/dashboard/cluster-dashboard.spec.ts +++ b/cypress/e2e/tests/pages/explorer/dashboard/cluster-dashboard.spec.ts @@ -9,7 +9,6 @@ import { NodesPagePo } from '@/cypress/e2e/po/pages/explorer/nodes.po'; import { EventsPagePo } from '@/cypress/e2e/po/pages/explorer/events.po'; import * as path from 'path'; import { eventsNoDataset } from '@/cypress/e2e/blueprints/explorer/cluster/events'; -import HomePagePo from '@/cypress/e2e/po/pages/home.po'; const configMapYaml = `apiVersion: v1 kind: ConfigMap @@ -49,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'); }); @@ -258,39 +257,47 @@ describe('Cluster Dashboard', { testIsolation: 'off', tags: ['@explorer', '@admi }); it('can view events table empty if no events', { tags: ['@vai', '@adminUser'] }, () => { - const events = new EventsPagePo('local'); - - HomePagePo.goTo(); - eventsNoDataset(); - ClusterDashboardPagePo.navTo(); + clusterDashboard.goTo(); + cy.wait('@eventsNoData'); clusterDashboard.waitForPage(undefined, 'cluster-events'); clusterDashboard.eventsList().resourceTable().sortableTable().checkRowCount(true, 1); - const expectedHeaders = ['Reason', 'Object', 'Message', 'Name', 'Date']; + let expectedHeaders = ['Reason', 'Object', 'Message', 'Name', 'Date']; - clusterDashboard.eventsList().resourceTable().sortableTable().tableHeaderRow() - .within('.table-header-container .content') - .each((el, i) => { - expect(el.text().trim()).to.eq(expectedHeaders[i]); - }); + cy.isVaiCacheEnabled().then((isVaiCacheEnabled) => { + if (isVaiCacheEnabled) { + expectedHeaders = ['Reason', 'Object', 'Message', 'Name', 'First Seen', 'Last Seen', 'Count']; + } - clusterDashboard.fullEventsLink().click(); - cy.wait('@eventsNoData'); - events.waitForPage(); + clusterDashboard.eventsList().resourceTable().sortableTable().tableHeaderRow() + .self() + .scrollIntoView(); + clusterDashboard.eventsList().resourceTable().sortableTable().tableHeaderRow() + .within('.table-header-container .content') + .each((el, i) => { + expect(el.text().trim()).to.eq(expectedHeaders[i]); + }); - events.eventslist().resourceTable().sortableTable().checkRowCount(true, 1); + clusterDashboard.fullEventsLink().click(); + cy.wait('@eventsNoData'); + const events = new EventsPagePo('local'); - const expectedFullHeaders = ['State', 'Last Seen', 'Type', 'Reason', 'Object', - 'Subobject', 'Source', 'Message', 'First Seen', 'Count', 'Name', 'Namespace']; + events.waitForPage(); - events.eventslist().resourceTable().sortableTable().tableHeaderRow() - .within('.table-header-container .content') - .each((el, i) => { - expect(el.text().trim()).to.eq(expectedFullHeaders[i]); - }); + events.eventslist().resourceTable().sortableTable().checkRowCount(true, 1); + + const expectedFullHeaders = ['State', 'Last Seen', 'Type', 'Reason', 'Object', + 'Subobject', 'Source', 'Message', 'First Seen', 'Count', 'Name', 'Namespace']; + + events.eventslist().resourceTable().sortableTable().tableHeaderRow() + .within('.table-header-container .content') + .each((el, i) => { + expect(el.text().trim()).to.eq(expectedFullHeaders[i]); + }); + }); }); describe('Cluster dashboard with limited permissions', () => { diff --git a/cypress/e2e/tests/pages/explorer/dashboard/events.spec.ts b/cypress/e2e/tests/pages/explorer/dashboard/events.spec.ts index cb3698c3568..4bac54da0cc 100644 --- a/cypress/e2e/tests/pages/explorer/dashboard/events.spec.ts +++ b/cypress/e2e/tests/pages/explorer/dashboard/events.spec.ts @@ -4,8 +4,30 @@ import { generateEventsDataSmall } from '@/cypress/e2e/blueprints/explorer/clust import LoadingPo from '@/cypress/e2e/po/components/loading.po'; import SortableTablePo from '@/cypress/e2e/po/components/sortable-table.po'; -const clusterDashboard = new ClusterDashboardPagePo('local'); -const events = new EventsPagePo('local'); +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(() => { @@ -19,7 +41,12 @@ describe('Events', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] }, let nsName2: string; before('set up', () => { - cy.updateNamespaceFilter('local', 'none', '{\"local\":[]}'); + cy.tableRowsPerPageAndPreferences(pageSize, { + clusterName: cluster, + groupBy: 'none', + namespaceFilter: '{\"local\":[]}', + allNamespaces: 'true', + }); cy.createE2EResourceName('ns1').then((ns1) => { nsName1 = ns1; @@ -29,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) => { @@ -51,81 +78,125 @@ 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', () => { - ClusterDashboardPagePo.goTo('local'); + ClusterDashboardPagePo.goToAndConfirmNsValues(cluster, { all: { is: true } }); + clusterDashboard.waitForPage(undefined, 'cluster-events'); EventsPagePo.navTo(); events.waitForPage(); - cy.getRancherResource('v1', 'events').then((resp: Cypress.Response) => { - // Why 500? there's a hardcoded figure to stops ui from storing more than 500 events ... - const count = resp.body.count < 500 ? resp.body.count : 500; + let vaiCacheEnabled = false; - // Test break down if less than 400... - expect(count).to.be.greaterThan(400); + cy.isVaiCacheEnabled() + .then((isVaiCacheEnabled) => { + vaiCacheEnabled = isVaiCacheEnabled; - // pagination is visible - events.sortableTable().pagination().checkVisible(); + return cy.getRancherResource('v1', 'events'); + }) + .then((resp: Cypress.Response) => { + let initialCount = resp.body.count; - const loadingPo = new LoadingPo('.title .resource-loading-indicator'); + if (!vaiCacheEnabled && resp.body.count > 500) { + // Why 500? there's a hardcoded figure to stops ui from storing more than 500 events ... + initialCount = 500; + } - loadingPo.checkNotExists(); + // Test break down if less than 3 pages... + expect(initialCount).to.be.greaterThan(3 * pageSize); - // basic checks on navigation buttons - events.sortableTable().pagination().beginningButton().isDisabled(); - events.sortableTable().pagination().leftButton().isDisabled(); - events.sortableTable().pagination().rightButton().isEnabled(); - events.sortableTable().pagination().endButton().isEnabled(); + // pagination is visible + events.sortableTable().pagination().checkVisible(); - // check text before navigation - events.sortableTable().pagination().paginationText().then((el) => { - expect(el.trim()).to.eq(`1 - 100 of ${ count } Events`); - }); + const loadingPo = new LoadingPo('.title .resource-loading-indicator'); - // navigate to next page - right button - events.sortableTable().pagination().rightButton().click(); + loadingPo.checkNotExists(); - // check text and buttons after navigation - events.sortableTable().pagination().paginationText().then((el) => { - expect(el.trim()).to.eq(`101 - 200 of ${ count } Events`); - }); - events.sortableTable().pagination().beginningButton().isEnabled(); - events.sortableTable().pagination().leftButton().isEnabled(); - - // navigate to first page - left button - events.sortableTable().pagination().leftButton().click(); - - // check text and buttons after navigation - events.sortableTable().pagination().paginationText().then((el) => { - expect(el.trim()).to.eq(`1 - 100 of ${ count } Events`); - }); - events.sortableTable().pagination().beginningButton().isDisabled(); - events.sortableTable().pagination().leftButton().isDisabled(); + // basic checks on navigation buttons + events.sortableTable().pagination().beginningButton().isDisabled(); + events.sortableTable().pagination().leftButton().isDisabled(); + events.sortableTable().pagination().rightButton().isEnabled(); + events.sortableTable().pagination().endButton().isEnabled(); - // navigate to last page - end button - events.sortableTable().pagination().endButton().scrollIntoView() - .click(); + // check text before navigation + events.sortableTable().pagination().self().scrollIntoView(); + events.sortableTable().pagination().paginationText().then((el) => { + expect(el.trim()).to.eq(`1 - ${ pageSize } of ${ initialCount } Events`); + }); - // check row count on last page - events.sortableTable().checkRowCount(false, 100); + // navigate to next page - right button + countHelper.setupCount(vaiCacheEnabled, initialCount); + events.sortableTable().pagination().rightButton().click(); + countHelper.handleCount(vaiCacheEnabled); + + // check text and buttons after navigation + events.sortableTable().pagination().self().scrollIntoView(); + countHelper.getCount().then((count) => { + return events.sortableTable().pagination().paginationText().then((el) => { + expect(el.trim()).to.eq(`${ pageSize + 1 } - ${ 2 * pageSize } of ${ count } Events`); + }); + }); + events.sortableTable().pagination().beginningButton().isEnabled(); + events.sortableTable().pagination().leftButton().isEnabled(); + + // navigate to first page - left button + countHelper.setupCount(vaiCacheEnabled, initialCount); + events.sortableTable().pagination().leftButton().click(); + countHelper.handleCount(vaiCacheEnabled); + + // check text and buttons after navigation + events.sortableTable().pagination().self().scrollIntoView(); + countHelper.getCount().then((count) => { + return events.sortableTable().pagination().paginationText().then((el) => { + expect(el.trim()).to.eq(`1 - ${ pageSize } of ${ count } Events`); + }); + }); - // check text after navigation - events.sortableTable().pagination().paginationText().then((el) => { - expect(el.trim()).to.eq(`401 - ${ count } of ${ count } Events`); - }); + events.sortableTable().pagination().beginningButton().isDisabled(); + events.sortableTable().pagination().leftButton().isDisabled(); + + // navigate to last page - end button + countHelper.setupCount(vaiCacheEnabled, initialCount); + events.sortableTable().pagination().endButton().scrollIntoView() + .click(); + countHelper.handleCount(vaiCacheEnabled); + + // check text after navigation + events.sortableTable().pagination().self().scrollIntoView(); + countHelper.getCount().then((count) => { + return events.sortableTable().pagination().paginationText().then((el) => { + let pages = Math.floor(count / pageSize); + + if (count % pageSize === 0) { + pages--; + } + const from = (pages * pageSize) + 1; + const to = count; + + expect(el.trim()).to.eq(`${ from } - ${ to } of ${ to } Events`); + }); + }); - // navigate to first page - beginning button - events.sortableTable().pagination().beginningButton().click(); + // navigate to first page - beginning button + countHelper.setupCount(vaiCacheEnabled, initialCount); + events.sortableTable().pagination().beginningButton().click(); + countHelper.handleCount(vaiCacheEnabled); + + // check text and buttons after navigation + events.sortableTable().pagination().self().scrollIntoView(); + countHelper.getCount().then((count) => { + events.sortableTable().pagination().paginationText().then((el) => { + expect(el.trim()).to.eq(`1 - ${ pageSize } of ${ count } Events`); + }); + }); - // check text and buttons after navigation - events.sortableTable().pagination().paginationText().then((el) => { - expect(el.trim()).to.eq(`1 - 100 of ${ count } Events`); + events.sortableTable().pagination().beginningButton().isDisabled(); + events.sortableTable().pagination().leftButton().isDisabled(); }); - events.sortableTable().pagination().beginningButton().isDisabled(); - events.sortableTable().pagination().leftButton().isDisabled(); - }); }); it('filter events', () => { @@ -136,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); @@ -199,7 +270,12 @@ describe('Events', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] }, }); after('clean up', () => { - cy.updateNamespaceFilter('local', 'none', '{"local":["all://user"]}'); + cy.tableRowsPerPageAndPreferences(100, { + clusterName: cluster, + groupBy: 'none', + namespaceFilter: '{"local":["all://user"]}', + allNamespaces: 'false', + }); // delete namespace (this will also delete all pods in it) cy.deleteRancherResource('v1', 'namespaces', nsName1); diff --git a/cypress/e2e/tests/pages/explorer/more-resources/api/custom-resource-definitions.spec.ts b/cypress/e2e/tests/pages/explorer/more-resources/api/custom-resource-definitions.spec.ts index c3da910c863..0c9c16e9189 100644 --- a/cypress/e2e/tests/pages/explorer/more-resources/api/custom-resource-definitions.spec.ts +++ b/cypress/e2e/tests/pages/explorer/more-resources/api/custom-resource-definitions.spec.ts @@ -2,8 +2,10 @@ import { CustomResourceDefinitionsPagePo } from '@/cypress/e2e/po/pages/explorer import { generateCrdsDataSmall } from '@/cypress/e2e/blueprints/explorer/more-resources/api/custom-resource-definition-get'; import * as jsyaml from 'js-yaml'; import HomePagePo from '@/cypress/e2e/po/pages/home.po'; +import ClusterDashboardPagePo from '@/cypress/e2e/po/pages/explorer/cluster-dashboard.po'; -const crdsPage = new CustomResourceDefinitionsPagePo('local'); +const cluster = 'local'; +const crdsPage = new CustomResourceDefinitionsPagePo(cluster); const crdName = `e2etests.${ +new Date() }.example.com`; const crdGroup = `${ +new Date() }.example.com`; @@ -14,11 +16,14 @@ describe('CustomResourceDefinitions', { testIsolation: 'off', tags: ['@explorer' describe('List', { tags: ['@vai', '@adminUser'] }, () => { before(() => { - cy.tableRowsPerPageAndNamespaceFilter(10, 'local', 'none', '{\"local\":[]}'); + ClusterDashboardPagePo.goToAndWait(cluster); // Ensure we're at a solid state before messing with preferences (given login/load might change them) + cy.tableRowsPerPageAndNamespaceFilter(10, cluster, 'none', '{\"local\":[]}'); }); it('can create a crd and see it in list view', () => { - crdsPage.goTo(); + ClusterDashboardPagePo.goToAndConfirmNsValues(cluster, { all: { is: true } } ); + + CustomResourceDefinitionsPagePo.navTo(); crdsPage.waitForPage(); crdsPage.create(); diff --git a/cypress/e2e/tests/pages/explorer/more-resources/core/service-accounts.spec.ts b/cypress/e2e/tests/pages/explorer/more-resources/core/service-accounts.spec.ts index 95bc134c8a3..e0eb6689eac 100644 --- a/cypress/e2e/tests/pages/explorer/more-resources/core/service-accounts.spec.ts +++ b/cypress/e2e/tests/pages/explorer/more-resources/core/service-accounts.spec.ts @@ -1,6 +1,8 @@ import { ServiceAccountsPagePo } from '@/cypress/e2e/po/pages/explorer/service-accounts.po'; import { generateServiceAccDataSmall, serviceAccNoData } from '@/cypress/e2e/blueprints/explorer/core/service-accounts-get'; +import ClusterDashboardPagePo from '@/cypress/e2e/po/pages/explorer/cluster-dashboard.po'; +const cluster = 'local'; const serviceAccountsPagePo = new ServiceAccountsPagePo(); describe('Service Accounts', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] }, () => { @@ -10,12 +12,14 @@ describe('Service Accounts', { testIsolation: 'off', tags: ['@explorer', '@admin describe('List', { tags: ['@vai', '@adminUser'] }, () => { before('set up', () => { - cy.updateNamespaceFilter('local', 'none', '{\"local\":[]}'); + cy.updateNamespaceFilter(cluster, 'none', '{\"local\":[]}'); }); it('validate services table in empty state', () => { + ClusterDashboardPagePo.goToAndConfirmNsValues(cluster, { all: { is: true } } ); + serviceAccNoData(); - serviceAccountsPagePo.goTo(); + ServiceAccountsPagePo.navTo(); serviceAccountsPagePo.waitForPage(); cy.wait('@serviceAccNoData'); @@ -80,7 +84,7 @@ describe('Service Accounts', { testIsolation: 'off', tags: ['@explorer', '@admin }); after('clean up', () => { - cy.updateNamespaceFilter('local', 'none', '{"local":["all://user"]}'); + cy.updateNamespaceFilter(cluster, 'none', '{"local":["all://user"]}'); }); }); }); diff --git a/cypress/e2e/tests/pages/explorer/service-discovery/horizontal-pod-autoscalers.spec.ts b/cypress/e2e/tests/pages/explorer/service-discovery/horizontal-pod-autoscalers.spec.ts index 2fd6984c29a..d7ba6706b1c 100644 --- a/cypress/e2e/tests/pages/explorer/service-discovery/horizontal-pod-autoscalers.spec.ts +++ b/cypress/e2e/tests/pages/explorer/service-discovery/horizontal-pod-autoscalers.spec.ts @@ -19,8 +19,11 @@ describe('HorizontalPodAutoscalers', { testIsolation: 'off', tags: ['@explorer', horizontalPodAutoscalersPage.waitForPage(); cy.wait('@horizontalpodautoscalerNoData'); - const expectedHeaders = ['State', 'Name', 'Workload', 'Minimum Replicas', 'Maximum Replicas', 'Current Replicas', 'Age']; + const expectedHeaders = ['State', 'Name', 'Namespace', 'Workload', 'Minimum Replicas', 'Maximum Replicas', 'Current Replicas', 'Age']; + horizontalPodAutoscalersPage.list().resourceTable().sortableTable().tableHeaderRow() + .self() + .scrollIntoView(); horizontalPodAutoscalersPage.list().resourceTable().sortableTable().tableHeaderRow() .get('.table-header-container .content') .each((el, i) => { @@ -39,7 +42,7 @@ describe('HorizontalPodAutoscalers', { testIsolation: 'off', tags: ['@explorer', horizontalPodAutoscalersPage.header().selectNamespaceFilterOption('All Namespaces'); // check table headers are visible - const expectedHeaders = ['State', 'Name', 'Workload', 'Minimum Replicas', 'Maximum Replicas', 'Current Replicas', 'Age']; + const expectedHeaders = ['State', 'Name', 'Namespace', 'Workload', 'Minimum Replicas', 'Maximum Replicas', 'Current Replicas', 'Age']; horizontalPodAutoscalersPage.list().resourceTable().sortableTable().tableHeaderRow() .get('.table-header-container .content') @@ -65,7 +68,7 @@ describe('HorizontalPodAutoscalers', { testIsolation: 'off', tags: ['@explorer', horizontalPodAutoscalersPage.list().resourceTable().sortableTable().groupByButtons(1) .click(); - // check table headers are visible + // check table headers are visible (minus namespace given we're now grouped by it) const expectedHeaders = ['State', 'Name', 'Workload', 'Minimum Replicas', 'Maximum Replicas', 'Current Replicas', 'Age']; horizontalPodAutoscalersPage.list().resourceTable().sortableTable().tableHeaderRow() diff --git a/cypress/e2e/tests/pages/explorer/service-discovery/ingress.spec.ts b/cypress/e2e/tests/pages/explorer/service-discovery/ingress.spec.ts index b14cb274387..8ad0ffe38ae 100644 --- a/cypress/e2e/tests/pages/explorer/service-discovery/ingress.spec.ts +++ b/cypress/e2e/tests/pages/explorer/service-discovery/ingress.spec.ts @@ -1,6 +1,8 @@ import { IngressPagePo } from '@/cypress/e2e/po/pages/explorer/ingress.po'; import { generateIngressesDataSmall, ingressesNoData } from '@/cypress/e2e/blueprints/explorer/workloads/service-discovery/ingresses-get'; +import ClusterDashboardPagePo from '@/cypress/e2e/po/pages/explorer/cluster-dashboard.po'; +const cluster = 'local'; const ingressPagePo = new IngressPagePo(); describe('Ingresses', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] }, () => { @@ -32,12 +34,14 @@ describe('Ingresses', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] describe('List', { tags: ['@vai', '@adminUser'] }, () => { before('set up', () => { - cy.updateNamespaceFilter('local', 'none', '{\"local\":[]}'); + cy.updateNamespaceFilter(cluster, 'none', '{\"local\":[]}'); }); it('validate services table in empty state', () => { + ClusterDashboardPagePo.goToAndConfirmNsValues(cluster, { all: { is: true } }); + ingressesNoData(); - ingressPagePo.goTo(); + IngressPagePo.navTo(); ingressPagePo.waitForPage(); cy.wait('@ingressesNoData'); @@ -101,7 +105,7 @@ describe('Ingresses', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] }); after('clean up', () => { - cy.updateNamespaceFilter('local', 'none', '{"local":["all://user"]}'); + cy.updateNamespaceFilter(cluster, 'none', '{"local":["all://user"]}'); }); }); }); diff --git a/cypress/e2e/tests/pages/explorer/service-discovery/services.spec.ts b/cypress/e2e/tests/pages/explorer/service-discovery/services.spec.ts index 54ed377f6b9..43aa56c97e6 100644 --- a/cypress/e2e/tests/pages/explorer/service-discovery/services.spec.ts +++ b/cypress/e2e/tests/pages/explorer/service-discovery/services.spec.ts @@ -1,6 +1,8 @@ import { ServicesPagePo } from '@/cypress/e2e/po/pages/explorer/services.po'; import { generateServicesDataSmall, servicesNoData } from '@/cypress/e2e/blueprints/explorer/workloads/service-discovery/services-get'; +import ClusterDashboardPagePo from '@/cypress/e2e/po/pages/explorer/cluster-dashboard.po'; +const cluster = 'local'; const servicesPagePo = new ServicesPagePo(); describe('Services', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] }, () => { @@ -10,12 +12,15 @@ describe('Services', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] } describe('List', { tags: ['@vai', '@adminUser'] }, () => { before('set up', () => { - cy.updateNamespaceFilter('local', 'none', '{\"local\":[]}'); + ClusterDashboardPagePo.goToAndWait(cluster); // Ensure we're at a solid state before messing with preferences (given login/load might change them) + cy.updateNamespaceFilter(cluster, 'none', '{\"local\":[]}'); }); it('validate services table in empty state', () => { + ClusterDashboardPagePo.goToAndConfirmNsValues(cluster, { all: { is: true } } ); + servicesNoData(); - servicesPagePo.goTo(); + ServicesPagePo.navTo(); servicesPagePo.waitForPage(); cy.wait('@servicesNoData'); @@ -86,7 +91,7 @@ describe('Services', { testIsolation: 'off', tags: ['@explorer', '@adminUser'] } }); after('clean up', () => { - cy.updateNamespaceFilter('local', 'none', '{"local":["all://user"]}'); + cy.updateNamespaceFilter(cluster, 'none', '{"local":["all://user"]}'); }); }); }); diff --git a/cypress/e2e/tests/pages/explorer2/storage/persistent-volume-claims.spec.ts b/cypress/e2e/tests/pages/explorer2/storage/persistent-volume-claims.spec.ts index ebd5bde6af6..58aa0985600 100644 --- a/cypress/e2e/tests/pages/explorer2/storage/persistent-volume-claims.spec.ts +++ b/cypress/e2e/tests/pages/explorer2/storage/persistent-volume-claims.spec.ts @@ -1,6 +1,8 @@ import { generatePersistentVolumeClaimsDataSmall, persistentVolumeClaimsNoData } from '@/cypress/e2e/blueprints/explorer/storage/persistent-volume-claims-get'; import { PersistentVolumeClaimsPagePo } from '@/cypress/e2e/po/pages/explorer/persistent-volume-claims.po'; +import ClusterDashboardPagePo from '@/cypress/e2e/po/pages/explorer/cluster-dashboard.po'; +const cluster = 'local'; const persistentVolumeClaimsPage = new PersistentVolumeClaimsPagePo(); describe('PersistentVolumeClaims', { testIsolation: 'off', tags: ['@explorer2', '@adminUser'] }, () => { @@ -14,10 +16,12 @@ describe('PersistentVolumeClaims', { testIsolation: 'off', tags: ['@explorer2', }); it('validate persistent volume claims table in empty state', () => { + ClusterDashboardPagePo.goToAndConfirmNsValues(cluster, { all: { is: true } } ); + const tag = 'persistentvolumeclaimsNoData'; persistentVolumeClaimsNoData(tag); - persistentVolumeClaimsPage.goTo(); + PersistentVolumeClaimsPagePo.navTo(); persistentVolumeClaimsPage.waitForPage(); cy.wait(`@${ tag }`); diff --git a/cypress/e2e/tests/pages/explorer2/workloads/pods.spec.ts b/cypress/e2e/tests/pages/explorer2/workloads/pods.spec.ts index 9570cb67ed8..645b760a040 100644 --- a/cypress/e2e/tests/pages/explorer2/workloads/pods.spec.ts +++ b/cypress/e2e/tests/pages/explorer2/workloads/pods.spec.ts @@ -6,9 +6,12 @@ import PodPo from '@/cypress/e2e/po/components/workloads/pod.po'; import HomePagePo from '@/cypress/e2e/po/pages/home.po'; import { generatePodsDataSmall } from '@/cypress/e2e/blueprints/explorer/workloads/pods/pods-get'; import SortableTablePo from '@/cypress/e2e/po/components/sortable-table.po'; +import ClusterDashboardPagePo from '@/cypress/e2e/po/pages/explorer/cluster-dashboard.po'; + +const cluster = 'local'; describe('Pods', { testIsolation: 'off', tags: ['@explorer2', '@adminUser'] }, () => { - const workloadsPodPage = new WorkloadsPodsListPagePo('local'); + const workloadsPodPage = new WorkloadsPodsListPagePo(cluster); before(() => { cy.login(); @@ -55,13 +58,15 @@ describe('Pods', { testIsolation: 'off', tags: ['@explorer2', '@adminUser'] }, ( uniquePod = resp.body.metadata.name; }); - cy.tableRowsPerPageAndNamespaceFilter(10, 'local', 'none', `{\"local\":[\"ns://${ nsName1 }\",\"ns://${ nsName2 }\"]}`); + cy.tableRowsPerPageAndNamespaceFilter(10, cluster, 'none', `{\"local\":[\"ns://${ nsName1 }\",\"ns://${ nsName2 }\"]}`); }); }); }); it('pagination is visible and user is able to navigate through pods data', () => { - WorkloadsPodsListPagePo.goTo('local'); + ClusterDashboardPagePo.goToAndConfirmNsValues(cluster, { nsProject: { values: [nsName1, nsName2] } }); + + WorkloadsPodsListPagePo.navTo(); workloadsPodPage.waitForPage(); // check pods count @@ -178,7 +183,7 @@ describe('Pods', { testIsolation: 'off', tags: ['@explorer2', '@adminUser'] }, ( }); it('pagination is hidden', () => { - cy.tableRowsPerPageAndNamespaceFilter(10, 'local', 'none', '{"local":[]}'); + cy.tableRowsPerPageAndNamespaceFilter(10, cluster, 'none', '{"local":[]}'); // generate small set of pods data generatePodsDataSmall(); @@ -195,7 +200,7 @@ describe('Pods', { testIsolation: 'off', tags: ['@explorer2', '@adminUser'] }, ( after('clean up', () => { // Ensure the default rows per page value is set after running the tests - cy.tableRowsPerPageAndNamespaceFilter(100, 'local', 'none', '{"local":["all://user"]}'); + cy.tableRowsPerPageAndNamespaceFilter(100, cluster, 'none', '{"local":["all://user"]}'); // delete namespace (this will also delete all pods in it) cy.deleteRancherResource('v1', 'namespaces', nsName1); @@ -351,14 +356,14 @@ describe('Pods', { testIsolation: 'off', tags: ['@explorer2', '@adminUser'] }, ( // }); // it('dialog should open/close as expected', () => { - // const podCreatePage = new WorkloadsPodsCreatePagePo('local'); + // const podCreatePage = new WorkloadsPodsCreatePagePo(cluster); // podCreatePage.goTo(); // podCreatePage.createWithUI(podName, 'nginx', 'default'); // // Should be on the list view - // const podsListPage = new WorkloadsPodsListPagePo('local'); + // const podsListPage = new WorkloadsPodsListPagePo(cluster); // // Filter the list to just show the newly created pod // podsListPage.list().resourceTable().sortableTable().filter(podName); diff --git a/cypress/e2e/tests/pages/extensions/extensions.spec.ts b/cypress/e2e/tests/pages/extensions/extensions.spec.ts index 21e089876fa..6b8c4e01a24 100644 --- a/cypress/e2e/tests/pages/extensions/extensions.spec.ts +++ b/cypress/e2e/tests/pages/extensions/extensions.spec.ts @@ -7,6 +7,7 @@ import UiPluginsPagePo from '@/cypress/e2e/po/pages/explorer/uiplugins.po'; import { NamespaceFilterPo } from '@/cypress/e2e/po/components/namespace-filter.po'; const namespaceFilter = new NamespaceFilterPo(); +const cluster = 'local'; const DISABLED_CACHE_EXTENSION_NAME = 'large-extension'; // const DISABLED_CACHE_EXTENSION_MENU_LABEL = 'Large-extension'; @@ -150,7 +151,7 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => { BurgerMenuPo.checkIfMenuItemLinkIsHighlighted('Extensions'); // catching regression https://github.com/rancher/dashboard/issues/10576 - BurgerMenuPo.checkIfClusterMenuLinkIsHighlighted('local', false); + BurgerMenuPo.checkIfClusterMenuLinkIsHighlighted(cluster, false); // go to "add rancher repositories" extensionsPo.extensionMenuToggle(); @@ -161,7 +162,7 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => { extensionsPo.addReposModal().should('not.exist'); // go to repos list page - const appRepoList = new RepositoriesPagePo('local', 'apps'); + const appRepoList = new RepositoriesPagePo(cluster, 'apps'); appRepoList.goTo(); appRepoList.waitForPage(); @@ -185,7 +186,7 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => { } }); - const appRepoList = new RepositoriesPagePo('local', 'apps'); + const appRepoList = new RepositoriesPagePo(cluster, 'apps'); // Ensure that the banner should be shown (by confirming that a required repo isn't there) appRepoList.goTo(); @@ -344,7 +345,7 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => { }); // ui-plugin-operator updated cache disabled threshold to 30mb as per https://github.com/rancher/rancher/pull/47565 - it('An extension larger than 30mb, which will trigger chacheState disabled, should install and work fine', () => { + it('An extension larger than 30mb, which will trigger cacheState disabled, should install and work fine', () => { const extensionsPo = new ExtensionsPagePo(); extensionsPo.goTo(); @@ -378,7 +379,7 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => { // cy.get('h1').should('have.text', DISABLED_CACHE_EXTENSION_TITLE); // check if cache state is disabled - const uiPluginsPo = new UiPluginsPagePo('local'); + const uiPluginsPo = new UiPluginsPagePo(cluster); uiPluginsPo.goTo(); uiPluginsPo.waitForPage(); @@ -460,7 +461,7 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => { extensionsPo.extensionDetailsTitle().should('contain', EXTENSION_NAME); }); - it('Should uninstall unathenticated extensions', () => { + it('Should uninstall unauthenticated extensions', () => { // Because we logged out in the previous test this one will also have to use an uncached login cy.login(undefined, undefined, false); const extensionsPo = new ExtensionsPagePo(); diff --git a/cypress/e2e/tests/pages/manager/cluster-manager.spec.ts b/cypress/e2e/tests/pages/manager/cluster-manager.spec.ts index c54c3c24cdb..10dd81531d9 100644 --- a/cypress/e2e/tests/pages/manager/cluster-manager.spec.ts +++ b/cypress/e2e/tests/pages/manager/cluster-manager.spec.ts @@ -25,6 +25,8 @@ import ProductNavPo from '@/cypress/e2e/po/side-bars/product-side-nav.po'; import TabbedPo from '@/cypress/e2e/po/components/tabbed.po'; import LoadingPo from '@/cypress/e2e/po/components/loading.po'; import { EXTRA_LONG_TIMEOUT_OPT, MEDIUM_TIMEOUT_OPT } from '@/cypress/support/utils/timeouts'; +import KontainerDriversPagePo from '@/cypress/e2e/po/pages/cluster-manager/kontainer-drivers.po'; +import DeactivateDriverDialogPo from '@/cypress/e2e/po/prompts/deactivateDriverDialog.po'; // At some point these will come from somewhere central, then we can make tools to remove resources from this or all runs const runTimestamp = +new Date(); @@ -88,6 +90,68 @@ describe('Cluster Manager', { testIsolation: 'off', tags: ['@manager', '@adminUs }); }); + it('deactivating a kontainer driver should hide its card from the cluster creation page', () => { + const driversPage = new KontainerDriversPagePo(); + const clusterCreatePage = new ClusterManagerCreatePagePo(); + + // deactivate the AKS driver + KontainerDriversPagePo.navTo(); + driversPage.waitForPage(); + driversPage.list().actionMenu('Azure AKS').getMenuItem('Deactivate').click(); + const deactivateDialog = new DeactivateDriverDialogPo(); + + deactivateDialog.deactivate(); + + // verify that the AKS card is not shown + clusterList.goTo(); + clusterList.checkIsCurrentPage(); + clusterList.createCluster(); + clusterCreatePage.gridElementExistanceByName('Azure AKS', 'not.exist'); + + // re-enable the AKS kontainer driver + KontainerDriversPagePo.navTo(); + driversPage.waitForPage(); + driversPage.list().actionMenu('Azure AKS').getMenuItem('Activate').click(); + + // verify that the AKS card is back + clusterList.goTo(); + clusterList.checkIsCurrentPage(); + clusterList.createCluster(); + clusterCreatePage.gridElementExistanceByName('Azure AKS', 'exist'); + }); + + it('deleting a kontainer driver should hide its card from the cluster creation page', () => { + // intercept get request for kontainer drivers + cy.intercept('GET', '/v1/management.cattle.io.kontainerdriver*', (req) => { + req.reply( { + type: 'collection', + resourceType: 'management.cattle.io.kontainerdriver', + count: 0, + data: [] + }); + } ).as('kontainerDrivers'); + + const clusterCreatePage = new ClusterManagerCreatePagePo(); + + // verify that the AKS card is not shown + clusterList.goTo(); + clusterList.checkIsCurrentPage(); + clusterList.createCluster(); + + clusterCreatePage.waitForPage(); + cy.wait('@kontainerDrivers'); + + clusterCreatePage.rkeToggleExistance('exist'); + clusterCreatePage.gridElementExistanceByName('Azure AKS', 'not.exist'); + + clusterCreatePage.gridElementGroupTitles().should('have.length', 2); + + clusterCreatePage.gridElementGroupTitles().eq(0).should('not.contain.text', 'Create a cluster'); + + clusterCreatePage.gridElementGroupTitles().eq(0).should('contain.text', 'Provision new nodes'); + clusterCreatePage.gridElementGroupTitles().eq(1).should('contain.text', 'Use existing nodes'); + }); + describe('All providers', () => { providersList.forEach((prov) => { prov.conditions.forEach((condition) => { diff --git a/cypress/globals.d.ts b/cypress/globals.d.ts index 8d5b99566de..060c3b6e1eb 100644 --- a/cypress/globals.d.ts +++ b/cypress/globals.d.ts @@ -99,7 +99,8 @@ declare global { deleteRancherResource(prefix: 'v3' | 'v1' | 'k8s', resourceType: string, resourceId: string, failOnStatusCode?: boolean): Chainable; deleteNodeTemplate(nodeTemplateId: string, timeout?: number, failOnStatusCode?: boolean) - tableRowsPerPageAndNamespaceFilter(rows: number, cluster: string, groupBy: string, namespacefilter: string, interation?: number) + tableRowsPerPageAndNamespaceFilter(rows: number, clusterName: string, groupBy: string, namespaceFilter: string) + tableRowsPerPageAndPreferences(rows: number, preferences: { clusterName: string, groupBy: string, namespaceFilter: string, allNamespaces: string}, iteration?: number) /** * update namespace filter @@ -162,6 +163,11 @@ declare global { * Fetch the steve `revision` / timestamp of request */ fetchRevision(): Chainable; + + /** + * Check if the vai FF is enabled + */ + isVaiCacheEnabled(): Chainable; } } } diff --git a/cypress/support/commands/rancher-api-commands.ts b/cypress/support/commands/rancher-api-commands.ts index 70a09fdb084..7afc70efa1a 100644 --- a/cypress/support/commands/rancher-api-commands.ts +++ b/cypress/support/commands/rancher-api-commands.ts @@ -1024,39 +1024,58 @@ Cypress.Commands.add('fetchRevision', () => { }); }); -Cypress.Commands.add('tableRowsPerPageAndNamespaceFilter', (rows: number, clusterName: string, groupBy: string, namespaceFilter: string, iteration = 0) => { +/** + * Check if the vai FF is enabled + */ +Cypress.Commands.add('isVaiCacheEnabled', () => { + return cy.getRancherResource('v1', 'management.cattle.io.features', 'ui-sql-cache', 200) + .then((res) => res.body.spec.value === true || res.body.spec.value === 'true'); +}); + +Cypress.Commands.add('tableRowsPerPageAndPreferences', (rows: number, preferences: { clusterName: string, groupBy: string, namespaceFilter: string, allNamespaces: string}, iteration = 0) => { + const { + clusterName, groupBy, namespaceFilter, allNamespaces + } = preferences; + return cy.getRancherResource('v3', 'users?me=true').then((resp: Cypress.Response) => { const userId = resp.body.data[0].id.trim(); const payload = { id: `${ userId }`, type: 'userpreference', data: { - cluster: clusterName, - 'per-page': `${ rows }`, - 'group-by': groupBy, - 'ns-by-cluster': namespaceFilter + cluster: clusterName, + 'per-page': `${ rows }`, + 'group-by': groupBy, + 'ns-by-cluster': namespaceFilter, + 'all-namespaces': allNamespaces, } }; - cy.log(`tableRowsPerPageAndNamespaceFilter: /v1/userpreferences/${ userId }. Payload: ${ JSON.stringify(payload) }`); + cy.log(`tableRowsPerPageAndPreferences: /v1/userpreferences/${ userId }. Payload: ${ JSON.stringify(payload) }`); cy.setRancherResource('v1', 'userpreferences', userId, payload).then(() => { return cy.waitForRancherResource('v1', 'userpreferences', userId, (resp: any) => compare(resp?.body, payload)) .then((res) => { if (res) { - cy.log(`tableRowsPerPageAndNamespaceFilter: Success!`); + cy.log(`tableRowsPerPageAndPreferences: Success!`); } else { if (iteration < 3) { - cy.log(`tableRowsPerPageAndNamespaceFilter: Failed! Going to retry...`); + cy.log(`tableRowsPerPageAndPreferences: Failed! Going to retry...`); - return cy.tableRowsPerPageAndNamespaceFilter(rows, clusterName, groupBy, namespaceFilter, iteration + 1); + return cy.tableRowsPerPageAndPreferences(rows, preferences, iteration + 1); } - cy.log(`tableRowsPerPageAndNamespaceFilter: Failed! Giving up...`); + cy.log(`tableRowsPerPageAndPreferences: Failed! Giving up...`); - return Promise.reject(new Error('tableRowsPerPageAndNamespaceFilter failed')); + return Promise.reject(new Error('tableRowsPerPageAndPreferences failed')); } }); }); }); }); + +Cypress.Commands.add('tableRowsPerPageAndNamespaceFilter', (rows: number, clusterName: string, groupBy: string, namespaceFilter: string) => { + return cy.tableRowsPerPageAndPreferences(rows, { + clusterName, groupBy, namespaceFilter + }); +}); diff --git a/docusaurus/docs/extensions/known-issues.md b/docusaurus/docs/extensions/known-issues.md index 12aa363aded..51b517621a9 100644 --- a/docusaurus/docs/extensions/known-issues.md +++ b/docusaurus/docs/extensions/known-issues.md @@ -19,3 +19,23 @@ To resolve this add the following `resolution` to the root application's `packag ... } ``` + +- Running `yarn install` might throw the following errors: +``` +error @aws-sdk/types@3.723.0: The engine "node" is incompatible with this module. Expected version ">=18.0.0". Got "16.20.2" +error @aws-sdk/util-locate-window@3.723.0: The engine "node" is incompatible with this module. Expected version ">=18.0.0". Got "16.20.2" +``` + +To resolve this add the following `resolutions` to the root application's `package.json`: +``` +{ + "name": "app-name", + "version": "0.1.0", + ... + resolutions": { + "@aws-sdk/types": "3.714.0", + "@aws-sdk/util-locate-window": "3.693.0" + }, + ... +} +``` diff --git a/package.json b/package.json index c510483a4fd..54d2968e330 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,6 @@ "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", @@ -97,7 +96,6 @@ "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", "jquery": "3.5.1", diff --git a/pkg/aks/provisioner.ts b/pkg/aks/provisioner.ts index 14f05f473b8..b049894e769 100644 --- a/pkg/aks/provisioner.ts +++ b/pkg/aks/provisioner.ts @@ -2,6 +2,7 @@ import { IClusterProvisioner, ClusterProvisionerContext } from '@shell/core/type import CruAks from './components/CruAks.vue'; import { mapDriver } from '@shell/store/plugins'; import type { Component } from 'vue'; +import { MANAGEMENT } from '@shell/config/types'; export class AKSProvisioner implements IClusterProvisioner { static ID = 'azureaks' @@ -30,6 +31,12 @@ export class AKSProvisioner implements IClusterProvisioner { return CruAks; } + get hidden(): boolean { + const kontainerDriver = this.context.getters['management/byId'](MANAGEMENT.KONTAINER_DRIVER, 'azurekubernetesservice'); + + return !kontainerDriver?.spec?.active; + } + get detailTabs(): any { return { machines: false, diff --git a/pkg/eks/provisioner.ts b/pkg/eks/provisioner.ts index 7300e77f2d0..91b435d74b2 100644 --- a/pkg/eks/provisioner.ts +++ b/pkg/eks/provisioner.ts @@ -1,7 +1,8 @@ import { IClusterProvisioner, ClusterProvisionerContext } from '@shell/core/types'; import CruEKS from './components/CruEKS.vue'; import { mapDriver } from '@shell/store/plugins'; -import { Component } from 'vue/types/umd'; +import { Component } from 'vue'; +import { MANAGEMENT } from '@shell/config/types'; export class EKSProvisioner implements IClusterProvisioner { static ID = 'amazoneks' @@ -30,6 +31,12 @@ export class EKSProvisioner implements IClusterProvisioner { return CruEKS; } + get hidden(): boolean { + const kontainerDriver = this.context.getters['management/byId'](MANAGEMENT.KONTAINER_DRIVER, 'amazonelasticcontainerservice'); + + return !kontainerDriver?.spec?.active; + } + get detailTabs(): any { return { machines: false, diff --git a/pkg/gke/provisioner.ts b/pkg/gke/provisioner.ts index a0a7a131a3e..937de5ccb87 100644 --- a/pkg/gke/provisioner.ts +++ b/pkg/gke/provisioner.ts @@ -1,7 +1,8 @@ import { IClusterProvisioner, ClusterProvisionerContext } from '@shell/core/types'; import CruGKE from './components/CruGKE.vue'; import { mapDriver } from '@shell/store/plugins'; -import { Component } from 'vue/types/umd'; +import { Component } from 'vue'; +import { MANAGEMENT } from '@shell/config/types'; export class GKEProvisioner implements IClusterProvisioner { static ID = 'googlegke' @@ -30,6 +31,12 @@ export class GKEProvisioner implements IClusterProvisioner { return CruGKE; } + get hidden(): boolean { + const kontainerDriver = this.context.getters['management/byId'](MANAGEMENT.KONTAINER_DRIVER, 'googlekubernetesengine'); + + return !kontainerDriver?.spec?.active; + } + get detailTabs(): any { return { machines: false, diff --git a/pkg/harvester-manager/machine-config/harvester.vue b/pkg/harvester-manager/machine-config/harvester.vue index 28dc209dce2..af1539f762f 100644 --- a/pkg/harvester-manager/machine-config/harvester.vue +++ b/pkg/harvester-manager/machine-config/harvester.vue @@ -3,7 +3,7 @@ import draggable from 'vuedraggable'; import isEmpty from 'lodash/isEmpty'; import jsyaml from 'js-yaml'; import YAML from 'yaml'; -import isBase64 from 'is-base64'; +import { isBase64 } from '@shell/utils/string'; import NodeAffinity from '@shell/components/form/NodeAffinity'; import PodAffinity from '@shell/components/form/PodAffinity'; diff --git a/pkg/rancher-components/src/components/Banner/Banner.vue b/pkg/rancher-components/src/components/Banner/Banner.vue index 24d172a21c0..8ca20a56701 100644 --- a/pkg/rancher-components/src/components/Banner/Banner.vue +++ b/pkg/rancher-components/src/components/Banner/Banner.vue @@ -67,6 +67,7 @@ export default defineComponent({ :class="{ [color]: true, }" + role="banner" > @@ -72,6 +77,11 @@ export default { .close-button { position: absolute; visibility: hidden; + + &:focus-visible { + @include focus-outline; + outline-offset: 2px; + } } &:hover .close-button { diff --git a/shell/components/ButtonMultiAction.vue b/shell/components/ButtonMultiAction.vue index a762013873b..c9992fa400b 100644 --- a/shell/components/ButtonMultiAction.vue +++ b/shell/components/ButtonMultiAction.vue @@ -33,6 +33,12 @@ const buttonClass = computed(() => { .borderless { background-color: transparent; border: none; + + &:focus-visible { + @include focus-outline; + outline-offset: -2px; + } + &:hover, &:focus { background-color: var(--accent-btn); box-shadow: none; diff --git a/shell/components/CommunityLinks.vue b/shell/components/CommunityLinks.vue index 70b39a2b61a..37c31532d6e 100644 --- a/shell/components/CommunityLinks.vue +++ b/shell/components/CommunityLinks.vue @@ -110,6 +110,8 @@ export default { {{ link.label }} @@ -118,6 +120,8 @@ export default { :href="link.value" rel="noopener noreferrer nofollow" target="_blank" + role="link" + :aria-label="link.label" > {{ link.label }} @@ -127,7 +131,11 @@ export default { > {{ t('footer.wechat.title') }} @@ -147,7 +155,12 @@ export default {
diff --git a/shell/components/LocaleSelector.vue b/shell/components/LocaleSelector.vue index e2abf5d0cb2..3a310b61eab 100644 --- a/shell/components/LocaleSelector.vue +++ b/shell/components/LocaleSelector.vue @@ -11,7 +11,11 @@ export default { mode: { type: String, default: '' - }, + } + }, + + data() { + return { isLocaleSelectorOpen: false }; }, computed: { @@ -40,8 +44,15 @@ export default { }, methods: { + openLocaleSelector() { + this.isLocaleSelectorOpen = true; + }, + closeLocaleSelector() { + this.isLocaleSelectorOpen = false; + }, switchLocale($event) { this.$store.dispatch('i18n/switchTo', $event); + this.closeLocaleSelector(); }, } }; @@ -50,13 +61,28 @@ export default { diff --git a/shell/components/form/NodeScheduling.vue b/shell/components/form/NodeScheduling.vue index f2b4150afd4..99055787546 100644 --- a/shell/components/form/NodeScheduling.vue +++ b/shell/components/form/NodeScheduling.vue @@ -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" />
@@ -182,7 +184,8 @@ export default { :mode="mode" :multiple="false" :loading="loading" - @input="update" + :data-testid="'node-scheduling-nodeSelector'" + @update:value="update" /> @@ -191,6 +194,7 @@ export default { diff --git a/shell/components/form/Password.vue b/shell/components/form/Password.vue index e9e81d75ee2..6bd7dabf442 100644 --- a/shell/components/form/Password.vue +++ b/shell/components/form/Password.vue @@ -41,7 +41,7 @@ export default { mode: { type: String, default: _CREATE, - }, + } }, data() { return { reveal: false }; @@ -68,6 +68,9 @@ export default { } return attributes; + }, + hideShowLabel() { + return this.reveal ? this.t('action.hide') : this.t('action.show'); } }, watch: { @@ -92,6 +95,9 @@ export default { }, focus() { this.$refs.input.$refs.value.focus(); + }, + hideShowFn() { + this.reveal ? this.reveal = false : this.reveal = true; } } }; @@ -127,17 +133,15 @@ export default { class="addon" > {{ t('action.hide') }} - {{ t('action.show') }} + tabindex="0" + class="hide-show" + role="button" + @click.prevent.stop="hideShowFn" + @keyup.space.prevent.stop="hideShowFn" + > + {{ hideShowLabel }} + @@ -157,10 +161,16 @@ export default { .password { display: flex; flex-direction: column; + .labeled-input { .addon { - padding-left: 12px; - min-width: 65px; + padding-left: 12px; + min-width: 65px; + + .hide-show:focus-visible { + @include focus-outline; + outline-offset: 4px; + } } } .genPassword { diff --git a/shell/components/form/ResourceLabeledSelect.vue b/shell/components/form/ResourceLabeledSelect.vue index a4cbc64fabe..3326a992793 100644 --- a/shell/components/form/ResourceLabeledSelect.vue +++ b/shell/components/form/ResourceLabeledSelect.vue @@ -27,7 +27,7 @@ export interface ResourceLabeledSelectPaginateSettings extends SharedSettings { */ overrideRequest?: LabelSelectPaginateFn, /** - * Override the default settings used in the convience function to fetch a page of results + * Override the default settings used in the convenience function to fetch a page of results */ requestSettings?: PaginateTypeOverridesFn, } @@ -52,14 +52,14 @@ export enum RESOURCE_LABEL_SELECT_MODE { } /** - * Convience wrapper around the LabelSelect component to support pagination + * Convenience wrapper around the LabelSelect component to support pagination * * Handles * * 1) Conditionally enabling the pagination feature given system settings * 2) Helper function to fetch the pagination result * - * A number of ways can be provided to override the convienences (see props) + * A number of ways can be provided to override the conveniences (see props) */ export default defineComponent({ name: 'ResourceLabeledSelect', diff --git a/shell/components/form/ResourceTabs/index.vue b/shell/components/form/ResourceTabs/index.vue index 19bdf1b9a76..b8f469c6b92 100644 --- a/shell/components/form/ResourceTabs/index.vue +++ b/shell/components/form/ResourceTabs/index.vue @@ -11,8 +11,6 @@ import { EVENT } from '@shell/config/types'; import SortableTable from '@shell/components/SortableTable'; import { _VIEW } from '@shell/config/query-params'; import RelatedResources from '@shell/components/RelatedResources'; -import { ExtensionPoint, TabLocation } from '@shell/core/types'; -import { getApplicableExtensionEnhancements } from '@shell/core/plugin-helpers'; import { isConditionReadyAndWaiting } from '@shell/plugins/dashboard-store/resource-class'; export default { @@ -77,7 +75,6 @@ export default { allEvents: [], selectedTab: this.defaultTab, didLoadEvents: false, - extensionTabs: getApplicableExtensionEnhancements(this, ExtensionPoint.TAB, TabLocation.RESOURCE_DETAIL, this.$route, this, this.extensionParams), inStore, showConditions: false, }; @@ -246,25 +243,5 @@ export default { direction="to" /> - - - - - diff --git a/shell/components/form/Taints.vue b/shell/components/form/Taints.vue index 343e0fa2913..209a5f3422d 100644 --- a/shell/components/form/Taints.vue +++ b/shell/components/form/Taints.vue @@ -71,7 +71,7 @@ export default { :preserve-keys="['effect']" :add-label="t('labels.addTaint')" :disabled="disabled" - @update:value="$emit('input', $event)" + @update:value="(e) => $emit('update:value', e)" >