diff --git a/.github/workflows/build-and-upload.yaml b/.github/workflows/build-and-upload.yaml index 90689841d1..9c0a61b96d 100644 --- a/.github/workflows/build-and-upload.yaml +++ b/.github/workflows/build-and-upload.yaml @@ -50,12 +50,12 @@ jobs: - name: Apply gcs auth # https://github.com/google-github-actions/auth - uses: 'google-github-actions/auth@v2' + uses: 'google-github-actions/auth@v3' with: credentials_json: "${{ env.GOOGLE_AUTH }}" - name: Upload build - uses: 'google-github-actions/upload-cloud-storage@v2' + uses: 'google-github-actions/upload-cloud-storage@v3' # https://github.com/google-github-actions/upload-cloud-storage with: path: ${{steps.build-hosted.outputs.BUILD_HOSTED_DIR}} @@ -95,12 +95,12 @@ jobs: - name: Apply gcs auth # https://github.com/google-github-actions/auth - uses: 'google-github-actions/auth@v2' + uses: 'google-github-actions/auth@v3' with: credentials_json: "${{ env.GOOGLE_AUTH }}" - name: Upload tar - uses: 'google-github-actions/upload-cloud-storage@v2' + uses: 'google-github-actions/upload-cloud-storage@v3' with: path: ${{steps.build-embedded.outputs.BUILD_EMBEDED_TGZ}} # Example - https://releases.rancher.com/ui/2.8.0.tar.gz diff --git a/cypress/e2e/blueprints/nav/fake-cluster.ts b/cypress/e2e/blueprints/nav/fake-cluster.ts index 2e4a578543..f616168423 100644 --- a/cypress/e2e/blueprints/nav/fake-cluster.ts +++ b/cypress/e2e/blueprints/nav/fake-cluster.ts @@ -2488,7 +2488,7 @@ export function generateFakeClusterDataAndIntercepts(fakeProvClusterId = 'some-p localCluster.metadata.annotations['field.cattle.io/description'] = longClusterDescription; } - res.body.data.unshift(fakeNavClusterData.provClusterObj); + res.body.data.push(fakeNavClusterData.provClusterObj); res.send(res.body); }); @@ -2506,7 +2506,7 @@ export function generateFakeClusterDataAndIntercepts(fakeProvClusterId = 'some-p localCluster.metadata.annotations['field.cattle.io/description'] = longClusterDescription; } - res.body.data.unshift(fakeNavClusterData.provClusterObj); + res.body.data.push(fakeNavClusterData.provClusterObj); res.send(res.body); }); @@ -2515,7 +2515,7 @@ export function generateFakeClusterDataAndIntercepts(fakeProvClusterId = 'some-p // add extra cluster to the nav list to test https://github.com/rancher/dashboard/issues/10452 cy.intercept('GET', `/v1/management.cattle.io.clusters?*`, (req) => { req.continue((res) => { - res.body.data.unshift(fakeNavClusterData.mgmtClusterObj); + res.body.data.push(fakeNavClusterData.mgmtClusterObj); res.send(res.body); }); }).as('mgmtClusters'); diff --git a/cypress/e2e/po/lists/jwt-authentication-list.po.ts b/cypress/e2e/po/lists/jwt-authentication-list.po.ts index fe2ed1346a..6596b5d908 100644 --- a/cypress/e2e/po/lists/jwt-authentication-list.po.ts +++ b/cypress/e2e/po/lists/jwt-authentication-list.po.ts @@ -1,28 +1,5 @@ import BaseResourceList from '@/cypress/e2e/po/lists/base-resource-list.po'; export default class JWTAuthenticationListPo extends BaseResourceList { - details(name: string, index: number) { - return this.resourceTable().sortableTable().rowWithName(name).column(index); - } - state(clusterName: string) { - return this.resourceTable().sortableTable().rowWithName(clusterName).column(1); - } - - activate() { - return cy.getId('sortable-table-activate').click(); - } - - deactivate() { - return cy.getId('sortable-table-deactivate').click(); - } - - clickRowActionMenuItem(name: string, itemLabel:string) { - return this.resourceTable().sortableTable().rowActionMenuOpen(name).getMenuItem(itemLabel) - .click(); - } - - getRowActionMenuItem(name: string, itemLabel:string) { - return this.resourceTable().sortableTable().rowActionMenuOpen(name).getMenuItem(itemLabel); - } } diff --git a/cypress/e2e/po/pages/cluster-manager/jwt-authentication.po.ts b/cypress/e2e/po/pages/cluster-manager/jwt-authentication.po.ts index ac49ec19e1..e4c8b9bdea 100644 --- a/cypress/e2e/po/pages/cluster-manager/jwt-authentication.po.ts +++ b/cypress/e2e/po/pages/cluster-manager/jwt-authentication.po.ts @@ -1,9 +1,9 @@ -import PagePo from '@/cypress/e2e/po/pages/page.po'; +import { BaseListPagePo } from '@/cypress/e2e/po/pages/base/base-list-page.po'; import JWTAuthenticationListPo from '@/cypress/e2e/po/lists/jwt-authentication-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 default class JWTAuthenticationPagePo extends PagePo { +export default class JWTAuthenticationPagePo extends BaseListPagePo { private static createPath(clusterId: string) { return `/c/${ clusterId }/manager/jwt.authentication`; } @@ -24,11 +24,7 @@ export default class JWTAuthenticationPagePo extends PagePo { sideNav.navToSideMenuEntryByLabel('JWT Authentication'); } - title() { - return cy.contains('.title > h1', 'JWT Authentication'); - } - list(): JWTAuthenticationListPo { - return new JWTAuthenticationListPo(this.self().find('[data-testid="jwt-authentication-list"]')); + return new JWTAuthenticationListPo('[data-testid="jwt-authentication-list"]'); } } diff --git a/cypress/e2e/po/pages/extensions.po.ts b/cypress/e2e/po/pages/extensions.po.ts index b9204b5fa8..e650b55727 100644 --- a/cypress/e2e/po/pages/extensions.po.ts +++ b/cypress/e2e/po/pages/extensions.po.ts @@ -150,12 +150,12 @@ export default class ExtensionsPagePo extends PagePo { return this.clickAction(extensionTitle, 'Install'); } - extensionCardUpdateClick(extensionTitle: string): Cypress.Chainable { - return this.clickAction(extensionTitle, 'Update'); + extensionCardUpgradeClick(extensionTitle: string): Cypress.Chainable { + return this.clickAction(extensionTitle, 'Upgrade'); } - extensionCardRollbackClick(extensionTitle: string): Cypress.Chainable { - return this.clickAction(extensionTitle, 'Rollback'); + extensionCardDowngradeClick(extensionTitle: string): Cypress.Chainable { + return this.clickAction(extensionTitle, 'Downgrade'); } extensionCardUninstallClick(extensionTitle: string): Cypress.Chainable { @@ -227,19 +227,11 @@ export default class ExtensionsPagePo extends PagePo { // ------------------ extension tabs ------------------ extensionTabInstalledClick(): Cypress.Chainable { - return this.extensionTabs.clickNthTab(1); + return this.extensionTabs.clickTabWithName('installed'); } extensionTabAvailableClick(): Cypress.Chainable { - return this.extensionTabs.clickNthTab(2); - } - - extensionTabUpdatesClick(): Cypress.Chainable { - return this.extensionTabs.clickNthTab(3); - } - - extensionTabAllClick(): Cypress.Chainable { - return this.extensionTabs.clickTabWithName('all'); + return this.extensionTabs.clickTabWithName('available'); } extensionTabBuiltinClick(): Cypress.Chainable { diff --git a/cypress/e2e/tests/accessibility/shell.spec.ts b/cypress/e2e/tests/accessibility/shell.spec.ts index b983259814..77920f31d1 100644 --- a/cypress/e2e/tests/accessibility/shell.spec.ts +++ b/cypress/e2e/tests/accessibility/shell.spec.ts @@ -693,8 +693,8 @@ describe('Shell a11y testing', { tags: ['@adminUser', '@accessibility'] }, () => extensionsPo.goTo(); extensionsPo.waitForPage(null, 'available'); extensionsPo.loading().should('not.exist'); - extensionsPo.extensionTabAllClick(); - extensionsPo.waitForPage(null, 'all'); + extensionsPo.extensionTabBuiltinClick(); + extensionsPo.waitForPage(null, 'builtin'); extensionsPo.extensionCard('AKS Provisioning').checkVisible(); cy.injectAxe(); diff --git a/cypress/e2e/tests/pages/extensions/extensions.spec.ts b/cypress/e2e/tests/pages/extensions/extensions.spec.ts index 0c597c6b88..6679b0af0e 100644 --- a/cypress/e2e/tests/pages/extensions/extensions.spec.ts +++ b/cypress/e2e/tests/pages/extensions/extensions.spec.ts @@ -25,11 +25,22 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => { cy.login(); }); - it('should go to the available tab by default', () => { + it('should go to the available tab by default and preserve active tab on reload', () => { const extensionsPo = new ExtensionsPagePo(); + // With no extensions installed, should default to "Available" extensionsPo.goTo(); extensionsPo.waitForPage(null, 'available'); + + // Preserve active tab on reload + cy.setUserPreference({ 'plugin-developer': true }); + extensionsPo.goTo(); // reload to get pref + extensionsPo.waitForPage(null, 'available'); + extensionsPo.extensionTabBuiltinClick(); + extensionsPo.waitForPage(null, 'builtin'); + cy.reload(); + extensionsPo.waitForPage(null, 'builtin'); + cy.setUserPreference({ 'plugin-developer': false }); }); it('should show built-in extensions only when configured', () => { @@ -91,7 +102,7 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => { extensionsPo.goTo(); extensionsPo.waitForPage(); - extensionsPo.extensionTabInstalledClick(); // Avoid nav guard failures that probably auto move user to this tab + extensionsPo.extensionTabAvailableClick(); // 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', GIT_REPO_NAME).then(() => { @@ -303,9 +314,10 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => { extensionsPo.extensionReloadBanner().should('be.visible'); extensionsPo.extensionReloadClick(); - // make sure extension card is in the installed tab - extensionsPo.extensionTabInstalledClick(); + // make sure we land on the installed tab by default extensionsPo.waitForPage(null, 'installed'); + + // make sure extension card is in the installed tab extensionsPo.extensionCardClick(EXTENSION_NAME); extensionsPo.extensionDetailsTitle().should('contain', EXTENSION_NAME); extensionsPo.extensionDetailsCloseClick(); @@ -327,7 +339,7 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => { cy.contains(`[data-testid="extension-card-${ EXTENSION_NAME }"]`).should('not.exist'); }); - it('Should update an extension version', () => { + it('Should upgrade an extension version', () => { cy.intercept('POST', `${ CLUSTER_REPOS_BASE_URL }/${ GIT_REPO_NAME }?action=upgrade`).as('upgradeExtension'); const extensionsPo = new ExtensionsPagePo(); @@ -338,7 +350,7 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => { extensionsPo.waitForPage(null, 'installed'); // click on update button on card - extensionsPo.extensionCardUpdateClick(EXTENSION_NAME); + extensionsPo.extensionCardUpgradeClick(EXTENSION_NAME); extensionsPo.installModalInstallClick(); cy.wait('@upgradeExtension').its('response.statusCode').should('eq', 201); @@ -346,14 +358,14 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => { extensionsPo.extensionReloadBanner().should('be.visible'); extensionsPo.extensionReloadClick(); - // make sure extension card is not available anymore on the updates tab + // make sure extension card is still on the installed tab // since we installed the latest version - extensionsPo.extensionTabUpdatesClick(); - extensionsPo.waitForPage(null, 'updates'); - cy.contains(`[data-testid="extension-card-${ EXTENSION_NAME }"]`).should('not.exist'); + extensionsPo.extensionTabInstalledClick(); + extensionsPo.waitForPage(null, 'installed'); + extensionsPo.extensionCard(EXTENSION_NAME).checkVisible(); }); - it('Should rollback an extension version', () => { + it('Should downgrade an extension version', () => { const extensionsPo = new ExtensionsPagePo(); extensionsPo.goTo(); @@ -362,18 +374,18 @@ describe('Extensions page', { tags: ['@extensions', '@adminUser'] }, () => { extensionsPo.extensionTabInstalledClick(); extensionsPo.waitForPage(null, 'installed'); - // click on the rollback button on card - // this will rollback to the immediate previous version - extensionsPo.extensionCardRollbackClick(EXTENSION_NAME); + // click on the downgrade button on card + // this will downgrade to the immediate previous version + extensionsPo.extensionCardDowngradeClick(EXTENSION_NAME); extensionsPo.installModalInstallClick(); // let's check the extension reload banner and reload the page extensionsPo.extensionReloadBanner().should('be.visible'); extensionsPo.extensionReloadClick(); - // make sure extension card is on the updates tab - extensionsPo.extensionTabUpdatesClick(); - extensionsPo.waitForPage(null, 'updates'); + // make sure extension card is on the installed tab and is visible + extensionsPo.extensionTabInstalledClick(); + extensionsPo.waitForPage(null, 'installed'); extensionsPo.extensionCard(EXTENSION_NAME).checkVisible(); }); diff --git a/cypress/e2e/tests/pages/generic/home.spec.ts b/cypress/e2e/tests/pages/generic/home.spec.ts index 8456bd9124..dbb3fc4315 100644 --- a/cypress/e2e/tests/pages/generic/home.spec.ts +++ b/cypress/e2e/tests/pages/generic/home.spec.ts @@ -62,7 +62,7 @@ describe('Home Page', () => { cy.percySnapshot('Home Page'); }); - it('Can see that cluster details match those in Cluster Manangement page', { tags: ['@generic', '@adminUser'] }, () => { + it('Can see that cluster details match those in Cluster Management page', { tags: ['@generic', '@adminUser'] }, () => { /** * Get cluster details from the Home page * Verify that the cluster details match those on the Cluster Management page @@ -120,7 +120,15 @@ describe('Home Page', () => { // since I wasn't able to fully mock a list of clusters // the next best thing is to add a description to the current local cluster // testing https://github.com/rancher/dashboard/issues/10441 - cy.intercept('GET', `/v1/provisioning.cattle.io.clusters?*`, (req) => { + + const homePageWithLocalPagination = '/v1/provisioning.cattle.io.clusters?*'; + + // Why the long intercept url? + // There are two requests to fetch clusters (side nav + cluster list). In theory "cy.intercept('GET', `/v1/provisioning.cattle.io.clusters?*`" should intercept them both + // how is not, only the first one for the side nav, and not the second for the list. + // const homePageWithSSP = `/v1/provisioning.cattle.io.clusters?page=1&pagesize=100&sort=metadata.annotations[provisioning.cattle.io/management-cluster-display-name]&filter=metadata.labels[provider.cattle.io]!=harvester&filter=status.provider!=harvester&exclude=metadata.managedFields`; + + cy.intercept('GET', homePageWithLocalPagination, (req) => { req.continue((res) => { const localIndex = res.body.data.findIndex((item) => item.id.includes('/local')); diff --git a/cypress/e2e/tests/pages/manager/jwt-authentication.spec.ts b/cypress/e2e/tests/pages/manager/jwt-authentication.spec.ts index 22be15c684..9b3fe800d6 100644 --- a/cypress/e2e/tests/pages/manager/jwt-authentication.spec.ts +++ b/cypress/e2e/tests/pages/manager/jwt-authentication.spec.ts @@ -4,9 +4,23 @@ import ProductNavPo from '@/cypress/e2e/po/side-bars/product-side-nav.po'; import ClusterManagerListPagePo from '@/cypress/e2e/po/pages/cluster-manager/cluster-manager-list.po'; import JWTAuthenticationPagePo from '@/cypress/e2e/po/pages/cluster-manager/jwt-authentication.po'; -describe('JWT Authentication', { testIsolation: 'off', tags: ['@manager', '@adminUser', '@jenkins'] }, () => { - const jwtAuthenticationPage = new JWTAuthenticationPagePo(); +const jwtAuthenticationPage = new JWTAuthenticationPagePo(); +// Go the JWT Authentication page and ensure the page is fully loaded +function goToJWTAuthenticationPageAndSettle() { + cy.intercept('GET', '/v1/management.cattle.io.clusterproxyconfigs?*').as('fetchJWTAuthentication'); + jwtAuthenticationPage.goTo(); + jwtAuthenticationPage.waitForPage(); + cy.wait('@fetchJWTAuthentication'); + + // Wait for the jwt table to load and filter so there are no rows + jwtAuthenticationPage.list().resourceTable().sortableTable().filter('random text', 200); + jwtAuthenticationPage.list().resourceTable().sortableTable().rowElements() + .should((el) => expect(el).to.contain.text('There are no rows which match your search query.')); + jwtAuthenticationPage.list().resourceTable().sortableTable().resetFilter(); +} + +describe('JWT Authentication', { testIsolation: 'off', tags: ['@manager', '@adminUser', '@jenkins'] }, () => { let instance0 = ''; let instance1 = ''; let removeCluster0 = false; @@ -61,80 +75,84 @@ describe('JWT Authentication', { testIsolation: 'off', tags: ['@manager', '@admi }); it('should show the JWT Authentication list page', () => { - JWTAuthenticationPagePo.navTo(); - jwtAuthenticationPage.waitForPage(); - jwtAuthenticationPage.title().should('be.visible'); + goToJWTAuthenticationPageAndSettle(); + jwtAuthenticationPage.list().masthead().title().should('contain', 'JWT Authentication'); jwtAuthenticationPage.list().resourceTable().sortableTable().checkVisible(); jwtAuthenticationPage.list().resourceTable().sortableTable().checkLoadingIndicatorNotVisible(); - jwtAuthenticationPage.list().state(instance0).should('contain', 'Disabled'); - jwtAuthenticationPage.list().state(instance1).should('contain', 'Disabled'); + jwtAuthenticationPage.list().resourceTable().resourceTableDetails(instance0, 1).should('contain', 'Disabled'); + jwtAuthenticationPage.list().resourceTable().resourceTableDetails(instance1, 1).should('contain', 'Disabled'); }); + it('should be able to enable JWT Authentication for a cluster', () => { - JWTAuthenticationPagePo.navTo(); - jwtAuthenticationPage.waitForPage(); + goToJWTAuthenticationPageAndSettle(); cy.intercept('POST', `/v1/management.cattle.io.clusterproxyconfigs`).as('enableJWT'); - jwtAuthenticationPage.list().clickRowActionMenuItem(instance0, 'Enable'); + jwtAuthenticationPage.list().resourceTable().sortableTable().rowActionMenuOpen(instance0) + .getMenuItem('Enable') + .click(); cy.wait('@enableJWT', { requestTimeout: 10000 }).then(({ request, response }) => { expect(response?.statusCode).to.eq(201); expect(request.body.enabled).to.equal(true); }); - jwtAuthenticationPage.list().state(instance0).should('contain', 'Enabled'); - jwtAuthenticationPage.list().state(instance1).should('contain', 'Disabled'); + jwtAuthenticationPage.list().resourceTable().resourceTableDetails(instance0, 1).should('contain', 'Enabled'); + jwtAuthenticationPage.list().resourceTable().resourceTableDetails(instance1, 1).should('contain', 'Disabled'); }); + it('should be able to disable JWT Authentication for a cluster', () => { - JWTAuthenticationPagePo.navTo(); - jwtAuthenticationPage.waitForPage(); + goToJWTAuthenticationPageAndSettle(); cy.intercept('PUT', `/v1/management.cattle.io.clusterproxyconfigs/**`).as('disableJWT'); - jwtAuthenticationPage.list().clickRowActionMenuItem(instance0, 'Disable'); + jwtAuthenticationPage.list().resourceTable().sortableTable().rowActionMenuOpen(instance0) + .getMenuItem('Disable') + .click(); cy.wait('@disableJWT', { requestTimeout: 10000 }).then(({ request, response }) => { expect(response?.statusCode).to.eq(200); expect(request.body.enabled).to.equal(false); }); - jwtAuthenticationPage.list().state(instance0).should('contain', 'Disabled'); - jwtAuthenticationPage.list().state(instance1).should('contain', 'Disabled'); + jwtAuthenticationPage.list().resourceTable().resourceTableDetails(instance0, 1).should('contain', 'Disabled'); + jwtAuthenticationPage.list().resourceTable().resourceTableDetails(instance1, 1).should('contain', 'Disabled'); }); + it('should be able to enable JWT Authentication in bulk', () => { - JWTAuthenticationPagePo.navTo(); - jwtAuthenticationPage.waitForPage(); - cy.intercept('PUT', `/v1/management.cattle.io.clusterproxyconfigs/**`).as('enableJWT'); + goToJWTAuthenticationPageAndSettle(); + cy.intercept('POST', `/v1/management.cattle.io.clusterproxyconfigs`).as('enableJWT'); jwtAuthenticationPage.list().resourceTable().sortableTable().rowSelectCtlWithName(instance0) .set(); jwtAuthenticationPage.list().resourceTable().sortableTable().rowSelectCtlWithName(instance1) .set(); - jwtAuthenticationPage.list().activate(); + jwtAuthenticationPage.list().resourceTable().sortableTable().bulkActionButton('Enable') + .click(); cy.wait('@enableJWT', { requestTimeout: 10000 }).then(({ request, response }) => { - expect(response?.statusCode).to.eq(200); + expect(response?.statusCode).to.eq(201); expect(request.body.enabled).to.equal(true); }); - jwtAuthenticationPage.list().state(instance0).should('contain', 'Enabled'); - jwtAuthenticationPage.list().state(instance1).should('contain', 'Enabled'); + jwtAuthenticationPage.list().resourceTable().resourceTableDetails(instance0, 1).should('contain', 'Enabled'); + jwtAuthenticationPage.list().resourceTable().resourceTableDetails(instance1, 1).should('contain', 'Enabled'); }); it('should be able to disable JWT Authentication in bulk', () => { - JWTAuthenticationPagePo.navTo(); - jwtAuthenticationPage.waitForPage(); + goToJWTAuthenticationPageAndSettle(); cy.intercept('PUT', `/v1/management.cattle.io.clusterproxyconfigs/**`).as('disableJWT'); jwtAuthenticationPage.list().resourceTable().sortableTable().rowSelectCtlWithName(instance0) .set(); jwtAuthenticationPage.list().resourceTable().sortableTable().rowSelectCtlWithName(instance1) .set(); - jwtAuthenticationPage.list().deactivate(); + jwtAuthenticationPage.list().resourceTable().sortableTable().bulkActionButton('Disable') + .click(); cy.wait('@disableJWT', { requestTimeout: 10000 }).then(({ request, response }) => { expect(response?.statusCode).to.eq(200); expect(request.body.enabled).to.equal(false); }); - jwtAuthenticationPage.list().state(instance0).should('contain', 'Disabled'); - jwtAuthenticationPage.list().state(instance1).should('contain', 'Disabled'); + jwtAuthenticationPage.list().resourceTable().resourceTableDetails(instance0, 1).should('contain', 'Disabled'); + jwtAuthenticationPage.list().resourceTable().resourceTableDetails(instance1, 1).should('contain', 'Disabled'); }); after('clean up', () => { diff --git a/cypress/e2e/tests/pages/manager/kontainer-drivers.spec.ts b/cypress/e2e/tests/pages/manager/kontainer-drivers.spec.ts index 6ba154c17c..6bad6f3997 100644 --- a/cypress/e2e/tests/pages/manager/kontainer-drivers.spec.ts +++ b/cypress/e2e/tests/pages/manager/kontainer-drivers.spec.ts @@ -5,7 +5,7 @@ import DeactivateDriverDialogPo from '@/cypress/e2e/po/prompts/deactivateDriverD import ClusterManagerListPagePo from '@/cypress/e2e/po/pages/cluster-manager/cluster-manager-list.po'; import ClusterManagerCreatePagePo from '@/cypress/e2e/po/edit/provisioning.cattle.io.cluster/create/cluster-create.po'; import PromptRemove from '@/cypress/e2e/po/prompts/promptRemove.po'; -import { LONG_TIMEOUT_OPT } from '@/cypress/support/utils/timeouts'; +import { EXTRA_LONG_TIMEOUT_OPT } from '@/cypress/support/utils/timeouts'; describe('Kontainer Drivers', { testIsolation: 'off', tags: ['@manager', '@adminUser'] }, () => { const driversPage = new KontainerDriversPagePo(); @@ -42,7 +42,7 @@ describe('Kontainer Drivers', { testIsolation: 'off', tags: ['@manager', '@admin driversPage.waitForPage(); cy.intercept('POST', '/v3/kontainerdrivers?action=refresh').as('refresh'); driversPage.refreshKubMetadata().click({ force: true }); - cy.wait('@refresh', LONG_TIMEOUT_OPT).its('response.statusCode').should('eq', 200); + cy.wait('@refresh', EXTRA_LONG_TIMEOUT_OPT).its('response.statusCode').should('eq', 200); }); it('can create new driver', () => { diff --git a/cypress/e2e/tests/pages/virtualization-mgmt/harvester.spec.ts b/cypress/e2e/tests/pages/virtualization-mgmt/harvester.spec.ts index fd5858d13b..71b5bca95d 100644 --- a/cypress/e2e/tests/pages/virtualization-mgmt/harvester.spec.ts +++ b/cypress/e2e/tests/pages/virtualization-mgmt/harvester.spec.ts @@ -210,8 +210,8 @@ describe('Harvester', { tags: ['@virtualizationMgmt', '@adminUser'] }, () => { extensionsPo.extensionReloadClick(); extensionsPo.loading().should('not.exist'); - // check harvester version on card - should not be older version - extensionsPo.extensionCardVersion(harvesterTitle).should('contain', versions[1]); + // check harvester version on card - should be the latest available version + extensionsPo.extensionCardVersion(harvesterTitle).should('contain', versions[0]); harvesterPo.goTo(); harvesterPo.waitForPage(); diff --git a/cypress/jenkins/Jenkinsfile_multi b/cypress/jenkins/Jenkinsfile_multi index ee65149c1f..01ece9187e 100644 --- a/cypress/jenkins/Jenkinsfile_multi +++ b/cypress/jenkins/Jenkinsfile_multi @@ -1,67 +1,13 @@ #!groovy def branch = "master" -def test_tags_raw = "${env.TEST_TAGS}".split(',') -def test_tags = [] -for (String tag : test_tags_raw) { - test_tags.add(tag.trim()) -} +def test_tags = "${env.TEST_TAGS}".split(',') +def k8s_versions = "${env.K3S_KUBERNETES_VERSIONS}".split(',') def all_cypress_tags = "${env.CYPRESS_TAGS}".split('\\|') -// Function to get KDM branch for a Rancher version -def getKdmBranch(rancherVersion, testTags) { - if (rancherVersion == "head") { - // Find the highest version from test tags and increment it - def latestVersion = "" - for (String tag : testTags) { - def versionMatch = tag =~ /v(\d+\.\d+)-head/ - if (versionMatch) { - def currentVersion = versionMatch[0][1] - if (currentVersion > latestVersion) { - latestVersion = currentVersion - } - } - } - // Increment minor version - if (latestVersion.isEmpty()) { - error("Cannot determine KDM branch for 'head' image. Please provide at least one version tag (e.g., v2.12-head) in TEST_TAGS.") - } - def parts = latestVersion.split('\\.') - def minor = (parts[1] as Integer) + 1 - return "dev-v${parts[0]}.${minor}" - } - - // Extract version from image tags (e.g."v2.12-head") - def versionMatch = rancherVersion =~ /v(\d+\.\d+)-head/ - if (versionMatch) { - def majorMinor = versionMatch[0][1] - return "dev-v${majorMinor}" - } - - error("Unable to determine KDM branch for Rancher version: ${rancherVersion}") +if (test_tags.length != k8s_versions.length) { + error("Number of Rancher versions (${test_tags.length}) must match number of K8s versions (${k8s_versions.length}). TEST_TAGS and K3S_KUBERNETES_VERSIONS must correspond positionally - first version pairs with first tag, second with second, etc.") } - -// Function to fetch latest K3s version from KDM -def getLatestK3sVersion(rancherVersion, testTags) { - def kdmBranch = getKdmBranch(rancherVersion, testTags) - def kdmUrl = "https://raw.githubusercontent.com/rancher/kontainer-driver-metadata/${kdmBranch}/data/data.json" - - try { - def kdmData = sh( - script: "curl -s '${kdmUrl}' | grep '\"version\":' | grep '+k3s' | sed 's/.*\"version\": \"\\([^\"]*+k3s[0-9]*\\)\".*/\\1/' | tail -1", - returnStdout: true - ).trim() - - if (!kdmData || kdmData.isEmpty()) { - error("Failed to fetch K3s version from KDM for ${rancherVersion}") - } - - return kdmData - } catch (Exception e) { - error("Error fetching KDM data for ${rancherVersion}: ${e.getMessage()}") - } -} - if ("${env.branch}" != "null" && "${env.branch}" != "") { branch = "${env.branch}" } @@ -91,21 +37,12 @@ node { userRemoteConfigs: scm.userRemoteConfigs ]) } - - // Generate K8s versions dynamically from KDM - def k8s_versions = [] - for (String rancherVersion : test_tags) { - def k8sVersion = getLatestK3sVersion(rancherVersion, test_tags) - k8s_versions.add(k8sVersion) - } - try { stage('Run Tests') { jobs = [:] test_tags.eachWithIndex { String rancher_version, int i -> String k8s_version = k8s_versions[i] - all_cypress_tags.each { String ct -> - echo "RANCHER_TAG: ${rancher_version}, K8S_VERSION: ${k8s_version}, CYPRESS_TAGS: ${ct}" + for ( String ct : all_cypress_tags ) { params = null params = [ string(name: 'JOB_TYPE', value: "${JOB_TYPE}"), string(name: 'CORRAL_PACKAGES_BRANCH', value: "${CORRAL_PACKAGES_BRANCH}"), @@ -141,8 +78,10 @@ node { string(name: 'QASE_PROJECT', value: "${QASE_PROJECT}"), string(name: 'QASE_REPORT', value: "${QASE_REPORT}"), string(name: 'RANCHER_IMAGE_TAG', value: rancher_version)] + echo "RANCHER_TAG: ${rancher_version}, K8S_VERSION: ${k8s_version}, CYPRESS_TAGS: ${ct}" build (job: 'ui-automation-job', parameters: params, propagate: false, wait: false) } + } } } catch (err) { @@ -153,4 +92,4 @@ node { } } } -} +} \ No newline at end of file diff --git a/docusaurus/extensions/catalog/partner/kamaji.yaml b/docusaurus/extensions/catalog/partner/kamaji.yaml deleted file mode 100644 index 7e681c7fa8..0000000000 --- a/docusaurus/extensions/catalog/partner/kamaji.yaml +++ /dev/null @@ -1,5 +0,0 @@ -name: Kamaji -author: Clastix -description: Deploys and operates Kubernetes Control Plane at scale with a fraction of the operational burden -url: https://kamaji.clastix.io/ -icon: https://kamaji.clastix.io/images/logo.png diff --git a/pkg/harvester-manager/list/harvesterhci.io.management.cluster.vue b/pkg/harvester-manager/list/harvesterhci.io.management.cluster.vue index 9beaefdf81..e74f2b3cd2 100644 --- a/pkg/harvester-manager/list/harvesterhci.io.management.cluster.vue +++ b/pkg/harvester-manager/list/harvesterhci.io.management.cluster.vue @@ -67,6 +67,8 @@ export default { this.mgmtClusters = hash.mgmtClusters; this.harvesterRepository = await this.getHarvesterRepository(); + + this.kubeVersion = this.$store.getters['management/byId'](MANAGEMENT.CLUSTER, 'local')?.kubernetesVersionBase || ''; }, data() { @@ -82,7 +84,7 @@ export default { hciClusters: [], mgmtClusters: [], rancherVersion: getVersionData()?.Version || '', - kubeVersion: this.$store.getters['management/byId'](MANAGEMENT.CLUSTER, 'local')?.kubernetesVersionBase || '', + kubeVersion: null, harvesterRepository: null, harvesterInstallVersion: true, harvesterUpdateVersion: null, diff --git a/scripts/update-e2e-rancher-version.sh b/scripts/update-e2e-rancher-version.sh index 65af5e846c..fdfd36fb2d 100755 --- a/scripts/update-e2e-rancher-version.sh +++ b/scripts/update-e2e-rancher-version.sh @@ -18,6 +18,10 @@ if [[ $BRANCH_NAME =~ ^release-(.*)$ ]]; then # Update scripts/e2e-docker-start sed -i "s|RANCHER_IMG_VERSION=head|RANCHER_IMG_VERSION=${NEW_TAG}|g" scripts/e2e-docker-start echo "Updated scripts/e2e-docker-start" + + # Update scripts/build-e2e + sed -i "s|ui/latest2/index.html|ui/${RELEASE_VERSION}/index.html|g" scripts/build-e2e + echo "Updated scripts/build-e2e" else echo "Not a release branch, no changes made." fi diff --git a/shell/assets/translations/en-us.yaml b/shell/assets/translations/en-us.yaml index fbd08b1a67..0924f30271 100644 --- a/shell/assets/translations/en-us.yaml +++ b/shell/assets/translations/en-us.yaml @@ -723,6 +723,17 @@ authConfig: tokenEndpoint: Token Endpoint userInfoEndpoint: User Info Endpoint acrValue: Authorization Context Reference + customClaims: + label: Custom Claims + enable: + label: Add custom claims + tooltip: Manually map OIDC claims when your provider doesn’t use standard claim names in tokens. + nameClaim: + label: Custom Name Claim + groupsClaim: + label: Custom Groups Claim + emailClaim: + label: Custom Email Claim cognitoIssuer: Issuer URL cognitoHelp: "You will need to create an application client in Cognito of type Traditional web application with the Return URL set to the URL shown below." scope: @@ -941,10 +952,6 @@ asyncButton: success: Updated waiting: Updating… upgrade: - action: Upgrade - success: Upgrading - waiting: Starting… - upgradeVersion: action: Upgrade success: Upgraded waiting: Upgrading… @@ -1262,7 +1269,6 @@ catalog: upgrade { upgrade } downgrade { upgrade } editVersion { update } - upgradeVersion { upgrade } } the {existing, select, true { app} false { chart} @@ -5128,16 +5134,17 @@ plugins: closePluginPanel: Close plugin description panel viewVersionDetails: View extension {name} version {version} details/Readme labels: - builtin: Built-in + builtin: Built-In experimental: Experimental third-party: Third-Party image: Image installing: Installing ... uninstalling: Uninstalling ... - updating: Updating ... - rollingBack: Rolling back ... + upgrading: Upgrading ... + downgrading: Downgrading ... menu: Extensions menu reloadRancher: Reload Rancher + current: current descriptions: experimental: This Extension is marked as experimental third-party: This Extension is provided by a Third-Party @@ -5166,6 +5173,8 @@ plugins: name: Extension module name persist: Persist extension by creating custom resource info: + actions: Actions + noActions: No actions available detail: Detail versions: Versions versionError: Could not load version information @@ -5229,7 +5238,7 @@ plugins: tabs: all: All available: Available - builtin: Built-in + builtin: Built-In images: Images installed: Installed updates: Updates @@ -5237,22 +5246,22 @@ plugins: version: "Version: {version}" install: label: Install - title: Install Extension {name} - prompt: "Are you sure that you want to install this Extension?" + title: Install extension {name} + prompt: "Are you sure that you want to install this extension?" version: Version warnNotCertified: Please ensure that you are aware of the risks of installing Extensions from untrusted authors - update: - label: Update - title: Update Extension {name} - prompt: "Are you sure that you want to update this Extension?" - rollback: - label: Rollback - title: Rollback Extension {name} - prompt: "Are you sure that you want to rollback this Extension?" + upgrade: + label: Upgrade + title: Upgrade extension {name} + prompt: "Are you sure that you want to upgrade this extension?" + downgrade: + label: Downgrade + title: Downgrade extension {name} + prompt: "Are you sure that you want to downgrade this extension?" uninstall: label: Uninstall - title: "Uninstall Extension: {name}" - prompt: "Are you sure that you want to uninstall this Extension?" + title: Uninstall extension {name} + prompt: "Are you sure that you want to uninstall this extension?" catalog: "Are you sure that you want to uninstall this Extension Catalog Image? This will also remove any Extensions provided by this image." upgradeAvailable: A newer version of this Extension is available reload: Extensions changed - reload required diff --git a/shell/components/ResourceDetail/index.vue b/shell/components/ResourceDetail/index.vue index c0329003d8..961d4fdaf8 100644 --- a/shell/components/ResourceDetail/index.vue +++ b/shell/components/ResourceDetail/index.vue @@ -181,6 +181,7 @@ export default { if (e.status === 404 || e.status === 403) { store.dispatch('loadingError', new Error(this.t('nav.failWhale.resourceIdNotFound', { resource: resourceType, fqid }, true))); } + console.debug(`Could not find '${ resourceType }' with id '${ id }''`, e); // eslint-disable-line no-console liveModel = {}; notFound = fqid; } diff --git a/shell/components/nav/TopLevelMenu.helper.ts b/shell/components/nav/TopLevelMenu.helper.ts index bf24fc56fa..13746ae8f7 100644 --- a/shell/components/nav/TopLevelMenu.helper.ts +++ b/shell/components/nav/TopLevelMenu.helper.ts @@ -139,12 +139,6 @@ export abstract class BaseTopLevelMenuHelper { this.$store = $store; this.hasProvCluster = this.$store.getters[`management/schemaFor`](CAPI.RANCHER_CLUSTER); - - // Reduce flicker when component is recreated on a different layout - const { clustersPinned = [], clustersOthers = [] } = this.$store.getters['sideNavCache'] || {}; - - this.clustersPinned.push(...clustersPinned); - this.clustersOthers.push(...clustersOthers); } protected convertToCluster(mgmtCluster: MgmtCluster, provCluster: ProvCluster): TopLevelMenuCluster { @@ -163,10 +157,6 @@ export abstract class BaseTopLevelMenuHelper { clusterRoute: { name: 'c-cluster-explorer', params: { cluster: mgmtCluster.id } } }; } - - protected cacheClusters() { - this.$store.dispatch('setSideNavCache', { clustersPinned: this.clustersPinned, clustersOthers: this.clustersOthers }); - } } /** @@ -202,9 +192,9 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme this.clustersOthersWrapper = new PaginationWrapper({ $store, id: 'tlm-unpinned-clusters', - onChange: () => { + onChange: async() => { if (this.args) { - this.update(this.args); + await this.update(this.args); } }, enabledFor: { @@ -220,9 +210,9 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme this.provClusterWrapper = new PaginationWrapper({ $store, id: 'tlm-prov-clusters', - onChange: () => { + onChange: async() => { if (this.args) { - this.update(this.args); + await this.update(this.args); } }, enabledFor: { @@ -276,8 +266,6 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme this.clustersPinned.push(..._clustersPinned); this.clustersOthers.push(..._clustersNotPinned); - - this.cacheClusters(); } async destroy() { @@ -390,7 +378,6 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme private async updateProvCluster(notPinned: MgmtCluster[], pinned: MgmtCluster[]): Promise { return this.provClusterWrapper.request({ pagination: { - filters: [ PaginationParamFilter.createMultipleFields( [...notPinned, ...pinned] @@ -399,7 +386,6 @@ export class TopLevelMenuHelperPagination extends BaseTopLevelMenuHelper impleme })) ) ], - page: 1, sort: [], projectsOrNamespaces: [] @@ -432,8 +418,6 @@ export class TopLevelMenuHelperLegacy extends BaseTopLevelMenuHelper implements this.clustersPinned.push(..._clustersPinned); this.clustersOthers.push(..._clustersNotPinned); - - this.cacheClusters(); } async destroy() { @@ -581,3 +565,46 @@ export class TopLevelMenuHelperLegacy extends BaseTopLevelMenuHelper implements return sorted; } } + +/** + * Retain state of the side nav, no matter when the TopLevelMenu component is created/deleted (on layout change) + * + * This means there's no flickering when the user changes pages and the side nav component re-renders + * + * Also it means we're not unwatching then watching the clusters + */ +class TopLevelMenuHelperService { + private _helper?: TopLevelMenuHelper; + public init($store: VuexStore) { + if (this._helper) { + return; + } + + const canPagination = $store.getters[`management/paginationEnabled`]({ + id: MANAGEMENT.CLUSTER, + context: 'side-bar', + }) && $store.getters[`management/paginationEnabled`]({ + id: CAPI.RANCHER_CLUSTER, + context: 'side-bar', + }); + + this._helper = canPagination ? new TopLevelMenuHelperPagination({ $store }) : new TopLevelMenuHelperLegacy({ $store }); + } + + public async reset() { + await this._helper?.destroy(); + delete this._helper; + } + + get helper(): TopLevelMenuHelper { + if (!this._helper) { + throw new Error('Unable to use the side nav cluster helper (not initialised)'); + } + + return this._helper; + } +} + +const instance = new TopLevelMenuHelperService(); + +export default instance; diff --git a/shell/components/nav/TopLevelMenu.vue b/shell/components/nav/TopLevelMenu.vue index 6bd758b2f5..29381a2f7b 100644 --- a/shell/components/nav/TopLevelMenu.vue +++ b/shell/components/nav/TopLevelMenu.vue @@ -14,7 +14,7 @@ import { SETTING } from '@shell/config/settings'; import { getProductFromRoute } from '@shell/utils/router'; import { isRancherPrime } from '@shell/config/version'; import Pinned from '@shell/components/nav/Pinned'; -import { TopLevelMenuHelperPagination, TopLevelMenuHelperLegacy } from '@shell/components/nav/TopLevelMenu.helper'; +import sideNavService from '@shell/components/nav/TopLevelMenu.helper'; import { debounce } from 'lodash'; import { sameContents } from '@shell/utils/array'; @@ -27,6 +27,8 @@ export default { }, data() { + sideNavService.init(this.$store); + const { displayVersion, fullVersion } = getVersionInfo(this.$store); const hasProvCluster = this.$store.getters[`management/schemaFor`](CAPI.RANCHER_CLUSTER); @@ -37,7 +39,7 @@ export default { id: CAPI.RANCHER_CLUSTER, context: 'side-bar', }); - const helper = canPagination ? new TopLevelMenuHelperPagination({ $store: this.$store }) : new TopLevelMenuHelperLegacy({ $store: this.$store }); + const helper = sideNavService.helper; const provClusters = !canPagination && hasProvCluster ? this.$store.getters[`management/all`](CAPI.RANCHER_CLUSTER) : []; const mgmtClusters = !canPagination ? this.$store.getters[`management/all`](MANAGEMENT.CLUSTER) : []; @@ -327,7 +329,6 @@ export default { beforeUnmount() { document.removeEventListener('keyup', this.handler); - this.helper?.destroy(); }, methods: { diff --git a/shell/components/nav/__tests__/TopLevelMenu.test.ts b/shell/components/nav/__tests__/TopLevelMenu.test.ts index b3465598f6..d78ef746fa 100644 --- a/shell/components/nav/__tests__/TopLevelMenu.test.ts +++ b/shell/components/nav/__tests__/TopLevelMenu.test.ts @@ -3,6 +3,7 @@ import { mount, Wrapper } from '@vue/test-utils'; import { CAPI, COUNT, MANAGEMENT } from '@shell/config/types'; import { PINNED_CLUSTERS } from '@shell/store/prefs'; import { nextTick } from 'vue'; +import sideNavService from '@shell/components/nav/TopLevelMenu.helper'; /** * `clusters` doubles up as both mgmt and prov clusters (don't shoot the messenger) @@ -53,6 +54,7 @@ const waitForIt = async() => { describe('topLevelMenu', () => { beforeEach(() => { jest.useFakeTimers(); + sideNavService.reset(); }); afterEach(() => { diff --git a/shell/config/uiplugins.js b/shell/config/uiplugins.js index 0f24ad0d1f..9966077a71 100644 --- a/shell/config/uiplugins.js +++ b/shell/config/uiplugins.js @@ -107,7 +107,7 @@ export function uiPluginAnnotation(chart, name) { /** * Parse the Rancher version string */ -function parseRancherVersion(v) { +export function parseRancherVersion(v) { let parsedVersion = semver.coerce(v)?.version; const splitArr = parsedVersion?.split('.'); diff --git a/shell/detail/provisioning.cattle.io.cluster.vue b/shell/detail/provisioning.cattle.io.cluster.vue index a4cfede671..dbe8baa24a 100644 --- a/shell/detail/provisioning.cattle.io.cluster.vue +++ b/shell/detail/provisioning.cattle.io.cluster.vue @@ -89,6 +89,7 @@ export default { }, async fetch() { + await this.$store.dispatch(`management/find`, { type: MANAGEMENT.CLUSTER, id: this.value.mgmtClusterId }); await this.value.waitForProvisioner(); // Support for the 'provisioner' extension diff --git a/shell/dialog/InstallExtensionDialog.vue b/shell/dialog/InstallExtensionDialog.vue index 493245ec18..abd79007ec 100644 --- a/shell/dialog/InstallExtensionDialog.vue +++ b/shell/dialog/InstallExtensionDialog.vue @@ -3,12 +3,12 @@ import AsyncButton from '@shell/components/AsyncButton'; import LabeledSelect from '@shell/components/form/LabeledSelect'; import { CATALOG, MANAGEMENT } from '@shell/config/types'; import { CATALOG as CATALOG_ANNOTATIONS } from '@shell/config/labels-annotations'; -import { UI_PLUGIN_NAMESPACE } from '@shell/config/uiplugins'; +import { UI_PLUGIN_NAMESPACE, isChartVersionHigher } from '@shell/config/uiplugins'; import Banner from '@components/Banner/Banner.vue'; import { SETTING } from '@shell/config/settings'; -import { getPluginChartVersion, getPluginChartVersionLabel } from '@shell/utils/uiplugins'; +import { getPluginChartVersionLabel } from '@shell/utils/uiplugins'; -// Note: This dialog handles installation and update of a plugin +// Note: This dialog handles installation, upgrade and downgrade of a plugin export default { emits: ['close'], @@ -29,7 +29,14 @@ export default { required: true }, /** - * The action to perform (install, update, rollback) + * The pre-selected version in the dropdown + */ + initialVersion: { + type: String, + default: null + }, + /** + * The action to perform (install, upgrade, downgrade) */ action: { type: String, @@ -63,33 +70,32 @@ export default { }, async fetch() { - const chartVersion = getPluginChartVersion(this.plugin); - - // Default to latest version on install (this is default on the plugin) - this.version = chartVersion; - - if (this.action === 'update') { - this.currentVersion = chartVersion; - - // Update to latest version, so take the first version - if (this.plugin?.installableVersions?.length > 0) { - this.version = this.plugin?.installableVersions?.[0]?.version; - } - } else if (this.action === 'rollback') { - // Find the newest version once we remove the current version - const versionNames = this.plugin.installableVersions.filter((v) => v.version !== chartVersion); - - this.currentVersion = chartVersion; - - if (versionNames.length > 0) { - this.version = versionNames[0].version; - } + // Determine the currently installed version, if any + if (this.plugin.installed) { + this.currentVersion = this.plugin.installedVersion; } - // Make sure we have the version available - const versionChart = this.plugin?.installableVersions?.find((v) => v.version === this.version); + // Determine the initial version to select in the dropdown + if (this.initialVersion) { + this.version = this.initialVersion; + } else if (this.action === 'upgrade') { + // Upgrade to the latest version, so take the first version + this.version = this.plugin?.installableVersions?.[0]?.version; + } else if (this.action === 'downgrade') { + const versions = this.plugin.installableVersions; + const currentIndex = versions.findIndex((v) => v.version === this.currentVersion); - if (!versionChart) { + if (currentIndex !== -1 && currentIndex < versions.length - 1) { + // Select the version just below the current version + this.version = versions[currentIndex + 1].version; + } + } else { + // Default to the latest installable version for new installs + this.version = this.plugin?.installableVersions?.[0]?.version; + } + + // Fallback if no version could be determined + if (!this.version) { this.version = this.plugin?.installableVersions?.[0]?.version; } @@ -119,37 +125,39 @@ export default { }, versionOptions() { - if (!this.plugin) { + if (!this.plugin?.installableVersions) { return []; } - // Don't allow update/rollback to current version - const versions = this.plugin?.installableVersions?.filter((v) => { - if (this.currentVersion) { - return v.version !== this.currentVersion; - } + // Don't allow upgrade/downgrade to current version by disabling the option + return this.plugin.installableVersions.map((v) => { + const isCurrent = v.version === this.currentVersion; - return true; - }); - - return versions.map((version) => { return { - label: getPluginChartVersionLabel(version), - value: version.version, + label: getPluginChartVersionLabel(v) + (isCurrent ? ` (${ this.t('plugins.labels.current') })` : ''), + value: v.version, + disabled: isCurrent, }; }); }, buttonMode() { - if (this.action === 'rollback') { - return 'rollback'; + if (this.action === 'install') { + return 'install'; } - if (this.action === 'update') { - return 'update'; + if (this.currentVersion && this.version) { + if (isChartVersionHigher(this.version, this.currentVersion)) { + return 'upgrade'; + } + + if (isChartVersionHigher(this.currentVersion, this.version)) { + return 'downgrade'; + } } - return 'install'; + // Fallback for safety, though should not be reached if version is selected + return this.action; }, chartVersionLoadsWithoutAuth() { @@ -158,6 +166,23 @@ export default { returnFocusSelector() { return `[data-testid="extension-card-${ this.action }-btn-${ this.plugin?.name }"]`; + }, + + buttonIcon() { + if (this.busy) { + return ''; + } + + switch (this.buttonMode) { + case 'install': + return 'icon-plus'; + case 'upgrade': + return 'icon-upgrade-alt'; + case 'downgrade': + return 'icon-downgrade-alt'; + default: + return ''; + } } }, @@ -295,12 +320,12 @@ export default {