mirror of https://github.com/rancher/dashboard.git
Merge remote-tracking branch 'upstream/master' into pagination-extensions-resource-enable
This commit is contained in:
commit
4305bcb0ba
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"]');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 <b>Traditional web application</b> 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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ProvCluster[]> {
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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('.');
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
<template>
|
||||
<div class="plugin-install-dialog">
|
||||
<h4 class="mt-10">
|
||||
{{ t(`plugins.${ action }.title`, { name: plugin?.label }) }}
|
||||
{{ t(`plugins.${ buttonMode }.title`, { name: `"${plugin?.label}"` }, true) }}
|
||||
</h4>
|
||||
<div class="custom mt-10">
|
||||
<div class="dialog-panel">
|
||||
<p>
|
||||
{{ t(`plugins.${ action }.prompt`) }}
|
||||
{{ t(`plugins.${ buttonMode }.prompt`) }}
|
||||
</p>
|
||||
<Banner
|
||||
v-if="chartVersionLoadsWithoutAuth"
|
||||
|
|
@ -335,6 +360,7 @@ export default {
|
|||
</button>
|
||||
<AsyncButton
|
||||
:mode="buttonMode"
|
||||
:icon="buttonIcon"
|
||||
data-testid="install-ext-modal-install-btn"
|
||||
@click="install"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ export default {
|
|||
<template>
|
||||
<div class="plugin-install-dialog">
|
||||
<h4 class="mt-10">
|
||||
{{ t('plugins.uninstall.title', { name: plugin?.label }) }}
|
||||
{{ t('plugins.uninstall.title', { name: `"${plugin?.label}"` }, true) }}
|
||||
</h4>
|
||||
<div class="mt-10 dialog-panel">
|
||||
<div class="dialog-info">
|
||||
|
|
@ -122,6 +122,7 @@ export default {
|
|||
</button>
|
||||
<AsyncButton
|
||||
mode="uninstall"
|
||||
:icon="busy ? '' : 'icon-delete'"
|
||||
data-testid="uninstall-ext-modal-uninstall-btn"
|
||||
@click="uninstall()"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,111 @@
|
|||
import { shallowMount, VueWrapper } from '@vue/test-utils';
|
||||
import InstallExtensionDialog from '@shell/dialog/InstallExtensionDialog.vue';
|
||||
|
||||
jest.mock('@shell/config/uiplugins', () => ({
|
||||
...jest.requireActual('@shell/config/uiplugins'),
|
||||
isChartVersionHigher: jest.fn((v1: string, v2: string) => v1 > v2),
|
||||
}));
|
||||
|
||||
const t = (key: string): string => key;
|
||||
|
||||
describe('component: InstallExtensionDialog', () => {
|
||||
let wrapper: VueWrapper<any>;
|
||||
|
||||
const mountComponent = (propsData = {}) => {
|
||||
const store = { dispatch: () => Promise.resolve() };
|
||||
|
||||
const defaultProps = {
|
||||
plugin: { installableVersions: [] },
|
||||
action: 'install',
|
||||
updateStatus: jest.fn(),
|
||||
closed: jest.fn(),
|
||||
};
|
||||
|
||||
return shallowMount(InstallExtensionDialog, {
|
||||
propsData: {
|
||||
...defaultProps,
|
||||
...propsData,
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$store: store,
|
||||
t,
|
||||
},
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
describe('fetch', () => {
|
||||
it('should set currentVersion if plugin is installed', async() => {
|
||||
wrapper = mountComponent({ plugin: { installed: true, installedVersion: '1.0.0' } });
|
||||
await wrapper.vm.$options.fetch.call(wrapper.vm);
|
||||
expect(wrapper.vm.currentVersion).toBe('1.0.0');
|
||||
});
|
||||
|
||||
it('should set version from initialVersion if provided', async() => {
|
||||
wrapper = mountComponent({ initialVersion: '1.2.3', plugin: { installed: false } });
|
||||
await wrapper.vm.$options.fetch.call(wrapper.vm);
|
||||
expect(wrapper.vm.version).toBe('1.2.3');
|
||||
});
|
||||
|
||||
it('should set version to latest for upgrade action', async() => {
|
||||
const plugin = { installableVersions: [{ version: '1.1.0' }, { version: '1.0.0' }] };
|
||||
|
||||
wrapper = mountComponent({ plugin, action: 'upgrade' });
|
||||
await wrapper.vm.$options.fetch.call(wrapper.vm);
|
||||
expect(wrapper.vm.version).toBe('1.1.0');
|
||||
});
|
||||
|
||||
it('should set version to next oldest for downgrade action', async() => {
|
||||
const plugin = {
|
||||
installed: true,
|
||||
installedVersion: '1.1.0',
|
||||
installableVersions: [{ version: '1.1.0' }, { version: '1.0.0' }]
|
||||
};
|
||||
|
||||
wrapper = mountComponent({ plugin, action: 'downgrade' });
|
||||
await wrapper.vm.$options.fetch.call(wrapper.vm);
|
||||
expect(wrapper.vm.version).toBe('1.0.0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('versionOptions', () => {
|
||||
it('should include and disable the current version', () => {
|
||||
const plugin = {
|
||||
installableVersions: [
|
||||
{ version: '1.1.0' },
|
||||
{ version: '1.0.0' },
|
||||
]
|
||||
};
|
||||
|
||||
wrapper = mountComponent({ plugin });
|
||||
wrapper.vm.currentVersion = '1.0.0';
|
||||
|
||||
const options = wrapper.vm.versionOptions;
|
||||
const currentOption = options.find((o: any) => o.value === '1.0.0');
|
||||
|
||||
expect(currentOption).toBeDefined();
|
||||
expect(currentOption.disabled).toBe(true);
|
||||
expect(currentOption.label).toContain('(plugins.labels.current)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buttonMode', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = mountComponent({ action: 'upgrade' });
|
||||
wrapper.vm.currentVersion = '1.0.0';
|
||||
});
|
||||
|
||||
it('should be "upgrade" if selected version is higher', async() => {
|
||||
wrapper.vm.version = '1.1.0';
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.vm.buttonMode).toBe('upgrade');
|
||||
});
|
||||
|
||||
it('should be "downgrade" if selected version is lower', async() => {
|
||||
wrapper.vm.version = '0.9.0';
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.vm.buttonMode).toBe('downgrade');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -59,11 +59,16 @@ export default {
|
|||
userInfoEndpoint: null,
|
||||
},
|
||||
// TODO #13457: this is duplicated due wrong format
|
||||
oidcScope: [],
|
||||
SLO_OPTION_VALUES
|
||||
oidcScope: [],
|
||||
SLO_OPTION_VALUES,
|
||||
addCustomClaims: false,
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.registerBeforeHook(this.willSave, 'willSave');
|
||||
},
|
||||
|
||||
computed: {
|
||||
tArgs() {
|
||||
return {
|
||||
|
|
@ -140,6 +145,10 @@ export default {
|
|||
return this.model?.id === 'cognito';
|
||||
},
|
||||
|
||||
isGenericOidc() {
|
||||
return this.model?.id === 'genericoidc';
|
||||
},
|
||||
|
||||
isLogoutAllSupported() {
|
||||
return this.model?.logoutAllSupported;
|
||||
},
|
||||
|
|
@ -220,6 +229,15 @@ export default {
|
|||
this.model.logoutAllForced = false;
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
model: {
|
||||
handler(newVal) {
|
||||
if (newVal?.nameClaim || newVal?.groupsClaim || newVal?.emailClaim) {
|
||||
this.addCustomClaims = true;
|
||||
}
|
||||
},
|
||||
once: true
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -248,6 +266,14 @@ export default {
|
|||
|
||||
updateScope() {
|
||||
this.model.scope = this.oidcScope.join(' ');
|
||||
},
|
||||
|
||||
willSave() {
|
||||
if (this.isGenericOidc && !this.addCustomClaims) {
|
||||
this.model.nameClaim = undefined;
|
||||
this.model.groupsClaim = undefined;
|
||||
this.model.emailClaim = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -390,21 +416,60 @@ export default {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Allow group search -->
|
||||
<div
|
||||
v-if="supportsGroupSearch"
|
||||
class="row mb-20"
|
||||
>
|
||||
<div class="col span-6">
|
||||
<Checkbox
|
||||
v-model:value="model.groupSearchEnabled"
|
||||
data-testid="input-group-search"
|
||||
:label="t('authConfig.oidc.groupSearch.label')"
|
||||
:tooltip="t('authConfig.oidc.groupSearch.tooltip')"
|
||||
:mode="mode"
|
||||
/>
|
||||
<template v-if="isGenericOidc || supportsGroupSearch">
|
||||
<div
|
||||
class="row mb-20"
|
||||
>
|
||||
<div class="col span-6 checkbox-flex">
|
||||
<!-- Allow group search -->
|
||||
<Checkbox
|
||||
v-if="supportsGroupSearch"
|
||||
v-model:value="model.groupSearchEnabled"
|
||||
data-testid="input-group-search"
|
||||
:label="t('authConfig.oidc.groupSearch.label')"
|
||||
:tooltip="t('authConfig.oidc.groupSearch.tooltip')"
|
||||
:mode="mode"
|
||||
/>
|
||||
<Checkbox
|
||||
v-if="isGenericOidc"
|
||||
v-model:value="addCustomClaims"
|
||||
data-testid="input-add-custom-claims"
|
||||
:label="t('authConfig.oidc.customClaims.enable.label')"
|
||||
:tooltip="t('authConfig.oidc.customClaims.enable.tooltip')"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="addCustomClaims">
|
||||
<h4>{{ t('authConfig.oidc.customClaims.label') }}</h4>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model:value="model.nameClaim"
|
||||
:label="t(`authConfig.oidc.customClaims.nameClaim.label`)"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model:value="model.groupsClaim"
|
||||
:label="t(`authConfig.oidc.customClaims.groupsClaim.label`)"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-20">
|
||||
<div class="col span-6">
|
||||
<LabeledInput
|
||||
v-model:value="model.emailClaim"
|
||||
:label="t(`authConfig.oidc.customClaims.emailClaim.label`)"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Scopes -->
|
||||
<div class="row mb-20">
|
||||
|
|
@ -608,4 +673,9 @@ export default {
|
|||
margin: 0 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-flex {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ describe('chartMixin', () => {
|
|||
});
|
||||
|
||||
expect(wrapper.vm.action).toStrictEqual({
|
||||
name: 'upgradeVersion',
|
||||
name: 'upgrade',
|
||||
tKey: 'upgrade',
|
||||
icon: 'icon-upgrade-alt',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@ export default {
|
|||
|
||||
if (compare(this.currentVersion, this.targetVersion) < 0) {
|
||||
return {
|
||||
name: 'upgradeVersion', tKey: 'upgrade', icon: 'icon-upgrade-alt'
|
||||
name: 'upgrade', tKey: 'upgrade', icon: 'icon-upgrade-alt'
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -391,6 +391,15 @@ export default class ProvCluster extends SteveModel {
|
|||
const pCluster = this.$rootGetters['management/byId'](CAPI.RANCHER_CLUSTER, this.id);
|
||||
const name = this.status?.clusterName || pCluster?.status?.clusterName;
|
||||
|
||||
try {
|
||||
if (name) {
|
||||
// Just in case we're not generically watching all mgmt clusters and...
|
||||
// thus won't receive new mgmt cluster over socket...
|
||||
// fire and forget a request to fetch it (this won't make multiple requests if one is already running)
|
||||
this.$dispatch('find', { type: MANAGEMENT.CLUSTER, id: name });
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return name && !!this.$rootGetters['management/byId'](MANAGEMENT.CLUSTER, name);
|
||||
}, this.$rootGetters['i18n/t']('cluster.managementTimeout'), timeout, interval);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@rancher/shell",
|
||||
"version": "3.0.5",
|
||||
"version": "3.0.6",
|
||||
"description": "Rancher Dashboard Shell",
|
||||
"repository": "https://github.com/rancherlabs/dashboard",
|
||||
"license": "Apache-2.0",
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import ChartReadme from '@shell/components/ChartReadme';
|
||||
import { Banner } from '@components/Banner';
|
||||
import LazyImage from '@shell/components/LazyImage';
|
||||
import { MANAGEMENT } from '@shell/config/types';
|
||||
import { SETTING } from '@shell/config/settings';
|
||||
import { useWatcherBasedSetupFocusTrapWithDestroyIncluded } from '@shell/composables/focusTrap';
|
||||
import { getPluginChartVersionLabel, getPluginChartVersion } from '@shell/utils/uiplugins';
|
||||
import { isChartVersionHigher } from '@shell/config/uiplugins';
|
||||
import RcButton from '@components/RcButton/RcButton.vue';
|
||||
import AppChartCardFooter from '@shell/pages/c/_cluster/apps/charts/AppChartCardFooter.vue';
|
||||
|
||||
export default {
|
||||
emits: ['action'],
|
||||
|
||||
async fetch() {
|
||||
const bannerSetting = await this.$store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.BANNERS);
|
||||
const { showHeader, bannerHeader } = JSON.parse(bannerSetting.value);
|
||||
|
|
@ -20,9 +24,10 @@ export default {
|
|||
}
|
||||
},
|
||||
components: {
|
||||
Banner,
|
||||
ChartReadme,
|
||||
LazyImage
|
||||
LazyImage,
|
||||
RcButton,
|
||||
AppChartCardFooter
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -49,6 +54,60 @@ export default {
|
|||
|
||||
return {};
|
||||
},
|
||||
|
||||
panelActions() {
|
||||
const actions = [];
|
||||
|
||||
if (!this.info) {
|
||||
return actions;
|
||||
}
|
||||
|
||||
const selectedVersion = this.infoVersion;
|
||||
const installedVersion = this.info.installedVersion;
|
||||
|
||||
if (!this.info.installed) {
|
||||
if (this.info.installableVersions?.length) {
|
||||
actions.push({
|
||||
label: this.t('catalog.chart.chartButton.action.install'),
|
||||
action: 'install',
|
||||
role: 'primary',
|
||||
version: selectedVersion,
|
||||
icon: 'icon-plus'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (selectedVersion && installedVersion && selectedVersion !== installedVersion) {
|
||||
if (isChartVersionHigher(selectedVersion, installedVersion)) {
|
||||
actions.push({
|
||||
label: this.t('catalog.chart.chartButton.action.upgrade'),
|
||||
action: 'upgrade',
|
||||
role: 'primary',
|
||||
version: selectedVersion,
|
||||
icon: 'icon-upgrade-alt'
|
||||
});
|
||||
} else {
|
||||
actions.push({
|
||||
label: this.t('catalog.chart.chartButton.action.downgrade'),
|
||||
action: 'downgrade',
|
||||
role: 'primary',
|
||||
version: selectedVersion,
|
||||
icon: 'icon-downgrade-alt'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.info.builtin) {
|
||||
actions.push({
|
||||
label: this.t('plugins.uninstall.label'),
|
||||
action: 'uninstall',
|
||||
role: 'secondary',
|
||||
icon: 'icon-delete'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
showSlideIn: {
|
||||
|
|
@ -65,12 +124,18 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
onButtonClick(button) {
|
||||
this.$emit('action', { ...button, plugin: this.info });
|
||||
this.hide();
|
||||
},
|
||||
|
||||
show(info) {
|
||||
this.info = info;
|
||||
this.showSlideIn = true;
|
||||
this.version = null;
|
||||
this.versionInfo = null;
|
||||
this.versionError = null;
|
||||
this.infoVersion = undefined;
|
||||
|
||||
this.loadPluginVersionInfo();
|
||||
},
|
||||
|
|
@ -153,7 +218,13 @@ export default {
|
|||
},
|
||||
|
||||
getVersionLabel(version) {
|
||||
return getPluginChartVersionLabel(version);
|
||||
const label = getPluginChartVersionLabel(version);
|
||||
|
||||
if (this.info.installed && version.version === this.info.installedVersion) {
|
||||
return `${ label } (${ this.t('plugins.labels.current') })`;
|
||||
}
|
||||
|
||||
return label;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -230,49 +301,66 @@ export default {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Banner
|
||||
v-if="info.builtin"
|
||||
color="warning"
|
||||
:label="t('plugins.descriptions.built-in')"
|
||||
class="mt-10"
|
||||
/>
|
||||
<template v-else>
|
||||
<Banner
|
||||
v-if="!info.certified"
|
||||
color="warning"
|
||||
:label="t('plugins.descriptions.third-party')"
|
||||
class="mt-10"
|
||||
/>
|
||||
<Banner
|
||||
v-if="info.experimental"
|
||||
color="warning"
|
||||
:label="t('plugins.descriptions.experimental')"
|
||||
class="mt-10"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<AppChartCardFooter
|
||||
v-if="info.tags && info.tags.length"
|
||||
:items="info.tags"
|
||||
class="plugin-tags-container"
|
||||
/>
|
||||
|
||||
<h3 v-if="info.versions.length">
|
||||
{{ t('plugins.info.versions') }}
|
||||
</h3>
|
||||
<div class="plugin-versions mb-10">
|
||||
<div class="plugin-versions-container">
|
||||
<h3>
|
||||
{{ t('plugins.info.versions') }}
|
||||
</h3>
|
||||
<div v-if="!info.versions.length">
|
||||
<div class="version-link version-active version-builtin">
|
||||
{{ info.displayVersion }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="v in info.versions"
|
||||
:key="`${v.name}-${v.version}`"
|
||||
v-else
|
||||
class="plugin-versions"
|
||||
>
|
||||
<a
|
||||
v-clean-tooltip="handleVersionBtnTooltip(v)"
|
||||
class="version-link"
|
||||
:class="handleVersionBtnClass(v)"
|
||||
:tabindex="!v.isVersionCompatible ? -1 : 0"
|
||||
role="button"
|
||||
:aria-label="t('plugins.viewVersionDetails', {name: v.name, version: v.version})"
|
||||
@click="loadPluginVersionInfo(v.version)"
|
||||
@keyup.enter.space="loadPluginVersionInfo(v.version)"
|
||||
<div
|
||||
v-for="v in info.versions"
|
||||
:key="`${v.name}-${v.version}`"
|
||||
>
|
||||
{{ getVersionLabel(v) }}
|
||||
</a>
|
||||
<a
|
||||
v-clean-tooltip="handleVersionBtnTooltip(v)"
|
||||
class="version-link"
|
||||
:class="handleVersionBtnClass(v)"
|
||||
:tabindex="!v.isVersionCompatible ? -1 : 0"
|
||||
role="button"
|
||||
:aria-label="t('plugins.viewVersionDetails', {name: v.name, version: v.version})"
|
||||
@click="loadPluginVersionInfo(v.version)"
|
||||
@keyup.enter.space="loadPluginVersionInfo(v.version)"
|
||||
>
|
||||
{{ getVersionLabel(v) }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="plugin-actions-container">
|
||||
<h3>
|
||||
{{ t('plugins.info.actions') }}
|
||||
</h3>
|
||||
<div class="plugin-actions">
|
||||
<template v-if="panelActions.length">
|
||||
<RcButton
|
||||
v-for="btn in panelActions"
|
||||
:key="btn.action"
|
||||
class="mmr-3 mmb-3"
|
||||
:[btn.role]="true"
|
||||
@click="onButtonClick(btn)"
|
||||
>
|
||||
<i :class="['icon', btn.icon, 'mmr-2']" />{{ btn.label }}
|
||||
</RcButton>
|
||||
</template>
|
||||
<div
|
||||
v-else
|
||||
class="no-actions"
|
||||
>
|
||||
{{ t('plugins.info.noActions') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -291,14 +379,6 @@ export default {
|
|||
:version-info="versionInfo"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="!info.versions.length">
|
||||
<h3>
|
||||
{{ t('plugins.info.versions') }}
|
||||
</h3>
|
||||
<div class="version-link version-active version-builtin">
|
||||
{{ info.displayVersion }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</transition>
|
||||
|
|
@ -312,8 +392,6 @@ export default {
|
|||
z-index: 1;
|
||||
|
||||
$slideout-width: 35%;
|
||||
$title-height: 50px;
|
||||
$padding: 5px;
|
||||
$slideout-width: 35%;
|
||||
--banner-top-offset: 0;
|
||||
$header-height: calc(54px + var(--banner-top-offset));
|
||||
|
|
@ -339,7 +417,7 @@ export default {
|
|||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
padding: 12px;
|
||||
|
||||
&.active {
|
||||
right: 0;
|
||||
|
|
@ -373,6 +451,10 @@ export default {
|
|||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.banner.warning {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.plugin-info-detail {
|
||||
overflow: auto;
|
||||
}
|
||||
|
|
@ -380,15 +462,16 @@ export default {
|
|||
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
margin: 15px 0 10px 0;
|
||||
opacity: 0.7;
|
||||
margin-bottom: 12px;
|
||||
color: var(--disabled-text);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.plugin-header {
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
padding-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.plugin-title {
|
||||
flex: 1;
|
||||
|
|
@ -397,8 +480,7 @@ export default {
|
|||
|
||||
.plugin-icon {
|
||||
font-size: 40px;
|
||||
margin-right:10px;
|
||||
color: #888;
|
||||
margin-right: 12px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
|
||||
|
|
@ -419,11 +501,26 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
.plugin-versions {
|
||||
.plugin-tags-container {
|
||||
margin-top: -8px;
|
||||
}
|
||||
|
||||
.plugin-tags-container,
|
||||
.plugin-versions-container,
|
||||
.plugin-actions-container {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.plugin-versions,
|
||||
.plugin-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.no-actions {
|
||||
color: var(--disabled-text);
|
||||
}
|
||||
|
||||
.plugin-description {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
|
@ -434,7 +531,7 @@ export default {
|
|||
padding: 2px 8px;
|
||||
border-radius: 5px;
|
||||
user-select: none;
|
||||
margin: 0 5px 5px 0;
|
||||
margin: 0 4px 4px 0;
|
||||
display: block;
|
||||
|
||||
&.version-active {
|
||||
|
|
@ -498,7 +595,7 @@ export default {
|
|||
|
||||
height: 0;
|
||||
|
||||
padding-bottom: 10px;
|
||||
padding-bottom: 12px;
|
||||
|
||||
:deep() .chart-readmes {
|
||||
flex: 1;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
import { shallowMount, VueWrapper } from '@vue/test-utils';
|
||||
import PluginInfoPanel from '@shell/pages/c/_cluster/uiplugins/PluginInfoPanel.vue';
|
||||
|
||||
jest.mock('@shell/config/uiplugins', () => ({
|
||||
...jest.requireActual('@shell/config/uiplugins'),
|
||||
isChartVersionHigher: jest.fn((v1: string, v2: string) => v1 > v2),
|
||||
}));
|
||||
|
||||
jest.mock('@shell/utils/uiplugins', () => ({
|
||||
getPluginChartVersionLabel: jest.fn((v) => v.version),
|
||||
getPluginChartVersion: jest.fn(),
|
||||
}));
|
||||
|
||||
const t = (key: string): string => key;
|
||||
|
||||
describe('component: PluginInfoPanel', () => {
|
||||
let wrapper: VueWrapper<any>;
|
||||
|
||||
const mountComponent = () => {
|
||||
return shallowMount(PluginInfoPanel, { global: { mocks: { t } } });
|
||||
};
|
||||
|
||||
describe('panelActions', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = mountComponent();
|
||||
});
|
||||
|
||||
it('should be empty if no info is provided', () => {
|
||||
expect(wrapper.vm.panelActions).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it('should show install action if not installed and installable versions exist', () => {
|
||||
wrapper.vm.info = { installed: false, installableVersions: [{ version: '1.0.0' }] };
|
||||
wrapper.vm.infoVersion = '1.0.0';
|
||||
const actions = wrapper.vm.panelActions;
|
||||
|
||||
expect(actions).toHaveLength(1);
|
||||
expect(actions[0].action).toBe('install');
|
||||
});
|
||||
|
||||
it('should be empty if not installed and no installable versions exist', () => {
|
||||
wrapper.vm.info = { installed: false, installableVersions: [] };
|
||||
const actions = wrapper.vm.panelActions;
|
||||
|
||||
expect(actions).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should show uninstall action if installed and not builtin', () => {
|
||||
wrapper.vm.info = { installed: true, builtin: false };
|
||||
const actions = wrapper.vm.panelActions;
|
||||
|
||||
expect(actions.some((action: any) => action.action === 'uninstall')).toBe(true);
|
||||
});
|
||||
|
||||
it('should show upgrade action for a higher active version', () => {
|
||||
wrapper.vm.info = { installed: true, installedVersion: '1.0.0' };
|
||||
wrapper.vm.infoVersion = '1.1.0';
|
||||
const actions = wrapper.vm.panelActions;
|
||||
|
||||
expect(actions.some((action: any) => action.action === 'upgrade')).toBe(true);
|
||||
});
|
||||
|
||||
it('should show downgrade action for a lower active version', () => {
|
||||
wrapper.vm.info = { installed: true, installedVersion: '1.0.0' };
|
||||
wrapper.vm.infoVersion = '0.9.0';
|
||||
const actions = wrapper.vm.panelActions;
|
||||
|
||||
expect(actions.some((action: any) => action.action === 'downgrade')).toBe(true);
|
||||
});
|
||||
|
||||
it('should not show upgrade/downgrade if active version is same as installed', () => {
|
||||
wrapper.vm.info = { installed: true, installedVersion: '1.0.0' };
|
||||
wrapper.vm.infoVersion = '1.0.0';
|
||||
const actions = wrapper.vm.panelActions;
|
||||
|
||||
expect(actions.some((action: any) => action.action === 'upgrade')).toBe(false);
|
||||
expect(actions.some((action: any) => action.action === 'downgrade')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVersionLabel', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = mountComponent();
|
||||
});
|
||||
|
||||
it('should return the version label', () => {
|
||||
wrapper.vm.info = { installed: false };
|
||||
const version = { version: '1.0.0' };
|
||||
const label = wrapper.vm.getVersionLabel(version);
|
||||
|
||||
expect(label).toBe('1.0.0');
|
||||
});
|
||||
|
||||
it('should append (current) for the installed version', () => {
|
||||
wrapper.vm.info = { installed: true, installedVersion: '1.0.0' };
|
||||
const version = { version: '1.0.0' };
|
||||
const label = wrapper.vm.getVersionLabel(version);
|
||||
|
||||
expect(label).toBe('1.0.0 (plugins.labels.current)');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { shallowMount, VueWrapper } from '@vue/test-utils';
|
||||
import UiPluginsPage from '@shell/pages/c/_cluster/uiplugins/index.vue';
|
||||
import { UI_PLUGIN_NAMESPACE } from '@shell/config/uiplugins';
|
||||
|
||||
const t = (key: string, args: Object) => {
|
||||
if (args) {
|
||||
|
|
@ -10,11 +11,16 @@ const t = (key: string, args: Object) => {
|
|||
};
|
||||
|
||||
describe('page: UI plugins/Extensions', () => {
|
||||
let wrapper;
|
||||
let wrapper: VueWrapper<any>;
|
||||
|
||||
const mountComponent = (mocks = {}) => {
|
||||
const store = {
|
||||
getters: { 'prefs/get': jest.fn() },
|
||||
getters: {
|
||||
'prefs/get': jest.fn(),
|
||||
'catalog/rawCharts': [],
|
||||
'uiplugins/plugins': [],
|
||||
'uiplugins/errors': {},
|
||||
},
|
||||
dispatch: () => Promise.resolve(),
|
||||
};
|
||||
|
||||
|
|
@ -57,8 +63,7 @@ describe('page: UI plugins/Extensions', () => {
|
|||
};
|
||||
const actions = wrapper.vm.getPluginActions(plugin);
|
||||
|
||||
expect(actions).toHaveLength(1);
|
||||
expect(actions[0].action).toBe('uninstall');
|
||||
expect(actions.find((action: any) => action.action === 'uninstall')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should not return uninstall action for a builtin plugin', () => {
|
||||
|
|
@ -70,10 +75,10 @@ describe('page: UI plugins/Extensions', () => {
|
|||
};
|
||||
const actions = wrapper.vm.getPluginActions(plugin);
|
||||
|
||||
expect(actions.some((a) => a.action === 'uninstall')).toBe(false);
|
||||
expect(actions.some((action: any) => action.action === 'uninstall')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return update action for an installed plugin with an upgrade', () => {
|
||||
it('should return upgrade action for an installed plugin with an upgrade', () => {
|
||||
const plugin = {
|
||||
installed: true,
|
||||
installableVersions: [],
|
||||
|
|
@ -82,33 +87,35 @@ describe('page: UI plugins/Extensions', () => {
|
|||
};
|
||||
const actions = wrapper.vm.getPluginActions(plugin);
|
||||
|
||||
expect(actions.some((a) => a.action === 'update')).toBe(true);
|
||||
expect(actions.some((action: any) => action.action === 'upgrade')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return rollback action for an installed plugin with multiple installable versions and no upgrade', () => {
|
||||
it('should return downgrade action for an installed plugin with older installable versions', () => {
|
||||
const plugin = {
|
||||
installed: true,
|
||||
installableVersions: [{ version: '1.0.0' }, { version: '0.9.0' }],
|
||||
builtin: false,
|
||||
upgrade: null,
|
||||
installedVersion: '1.0.0',
|
||||
};
|
||||
const actions = wrapper.vm.getPluginActions(plugin);
|
||||
|
||||
expect(actions.some((a) => a.action === 'rollback')).toBe(true);
|
||||
expect(actions.some((action: any) => action.action === 'downgrade')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return all applicable actions', () => {
|
||||
it('should return all applicable actions (upgrade, downgrade, uninstall)', () => {
|
||||
const plugin = {
|
||||
installed: true,
|
||||
installableVersions: [{ version: '1.1.0' }, { version: '1.0.0' }],
|
||||
installableVersions: [{ version: '1.1.0' }, { version: '1.0.0' }, { version: '0.9.0' }],
|
||||
builtin: false,
|
||||
upgrade: '1.1.0',
|
||||
installedVersion: '1.0.0',
|
||||
};
|
||||
const actions = wrapper.vm.getPluginActions(plugin);
|
||||
|
||||
expect(actions.map((a) => a.action)).toContain('uninstall');
|
||||
expect(actions.map((a) => a.action)).toContain('update');
|
||||
expect(actions.map((a) => a.action)).not.toContain('rollback');
|
||||
expect(actions.map((action: any) => action.action)).toContain('uninstall');
|
||||
expect(actions.map((action: any) => action.action)).toContain('upgrade');
|
||||
expect(actions.map((action: any) => action.action)).toContain('downgrade');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -120,43 +127,39 @@ describe('page: UI plugins/Extensions', () => {
|
|||
expect(items[0].label).toBe('v1.0.0');
|
||||
});
|
||||
|
||||
it('should include upgrade availability in tooltip', () => {
|
||||
const plugin = { displayVersionLabel: 'v1.0.0', upgrade: 'v1.1.0' };
|
||||
const items = wrapper.vm.getSubHeaderItems(plugin);
|
||||
|
||||
expect(items[0].labelTooltip).toBe('plugins.upgradeAvailableTooltip with {"version":"v1.1.0"}');
|
||||
});
|
||||
|
||||
it('should show installing status', () => {
|
||||
const plugin = { displayVersionLabel: 'v1.0.0', installing: 'install' };
|
||||
const items = wrapper.vm.getSubHeaderItems(plugin);
|
||||
|
||||
expect(items).toHaveLength(2);
|
||||
expect(items[1].label).toBe('plugins.labels.installing');
|
||||
expect(items.find((item: any) => item.label === 'plugins.labels.installing')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should show uninstalling status', () => {
|
||||
const plugin = { displayVersionLabel: 'v1.0.0', installing: 'uninstall' };
|
||||
const items = wrapper.vm.getSubHeaderItems(plugin);
|
||||
|
||||
expect(items).toHaveLength(2);
|
||||
expect(items[1].label).toBe('plugins.labels.uninstalling');
|
||||
expect(items.find((item: any) => item.label === 'plugins.labels.uninstalling')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should show updating status', () => {
|
||||
const plugin = { displayVersionLabel: 'v1.0.0', installing: 'update' };
|
||||
it('should show upgrading status', () => {
|
||||
const plugin = { displayVersionLabel: 'v1.0.0', installing: 'upgrade' };
|
||||
const items = wrapper.vm.getSubHeaderItems(plugin);
|
||||
|
||||
expect(items).toHaveLength(2);
|
||||
expect(items[1].label).toBe('plugins.labels.updating');
|
||||
expect(items.find((item: any) => item.label === 'plugins.labels.upgrading')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should show rolling back status', () => {
|
||||
const plugin = { displayVersionLabel: 'v1.0.0', installing: 'rollback' };
|
||||
it('should show downgrading status', () => {
|
||||
const plugin = { displayVersionLabel: 'v1.0.0', installing: 'downgrade' };
|
||||
const items = wrapper.vm.getSubHeaderItems(plugin);
|
||||
|
||||
expect(items).toHaveLength(2);
|
||||
expect(items[1].label).toBe('plugins.labels.rollingBack');
|
||||
expect(items.find((item: any) => item.label === 'plugins.labels.downgrading')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should include date info', () => {
|
||||
const plugin = { created: '2023-01-01T00:00:00Z' };
|
||||
const items = wrapper.vm.getSubHeaderItems(plugin);
|
||||
|
||||
expect(items.find((item: any) => item.icon === 'icon-refresh-alt')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -200,13 +203,16 @@ describe('page: UI plugins/Extensions', () => {
|
|||
});
|
||||
|
||||
describe('getStatuses', () => {
|
||||
it('should return "installed" status for installed, non-builtin plugins', () => {
|
||||
it('should return "installed" status with version for installed, non-builtin plugins', () => {
|
||||
const plugin = {
|
||||
installed: true, builtin: false, installing: false
|
||||
installed: true,
|
||||
builtin: false,
|
||||
installing: false,
|
||||
installedVersion: '1.2.3',
|
||||
};
|
||||
const statuses = wrapper.vm.getStatuses(plugin);
|
||||
|
||||
expect(statuses[0].tooltip.key).toBe('generic.installed');
|
||||
expect(statuses[0].tooltip.text).toBe('generic.installed (1.2.3)');
|
||||
});
|
||||
|
||||
it('should return "upgradeable" status for plugins with an upgrade', () => {
|
||||
|
|
@ -250,14 +256,15 @@ describe('page: UI plugins/Extensions', () => {
|
|||
it('should combine deprecated and other errors in tooltip', () => {
|
||||
const plugin = { chart: { deprecated: true }, helmError: true };
|
||||
const statuses = wrapper.vm.getStatuses(plugin);
|
||||
const warningStatus = statuses.find((s) => s.icon === 'icon-alert-alt');
|
||||
const warningStatus = statuses.find((status: any) => status.icon === 'icon-alert-alt');
|
||||
|
||||
expect(warningStatus.tooltip.text).toBe('generic.deprecated. generic.error: plugins.helmError');
|
||||
});
|
||||
});
|
||||
|
||||
describe('watch: helmOps', () => {
|
||||
let wrapper;
|
||||
let wrapper: VueWrapper<any>;
|
||||
let updatePluginInstallStatusMock: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
const store = {
|
||||
|
|
@ -265,9 +272,10 @@ describe('page: UI plugins/Extensions', () => {
|
|||
'prefs/get': jest.fn(),
|
||||
'catalog/rawCharts': {},
|
||||
'uiplugins/plugins': [],
|
||||
'uiplugins/errors': {}
|
||||
'uiplugins/errors': {},
|
||||
'features/get': () => true,
|
||||
},
|
||||
dispatch: () => Promise.resolve(),
|
||||
dispatch: () => Promise.resolve(true),
|
||||
};
|
||||
|
||||
wrapper = shallowMount(UiPluginsPage, {
|
||||
|
|
@ -277,42 +285,100 @@ describe('page: UI plugins/Extensions', () => {
|
|||
t,
|
||||
},
|
||||
stubs: { ActionMenu: { template: '<div />' } }
|
||||
},
|
||||
computed: {
|
||||
// Override the computed property for this test suite
|
||||
available: () => [
|
||||
{ name: 'plugin1' },
|
||||
{ name: 'plugin2' },
|
||||
{ name: 'plugin3' },
|
||||
{ name: 'plugin4' },
|
||||
],
|
||||
hasMenuActions: () => true,
|
||||
menuActions: () => []
|
||||
}
|
||||
});
|
||||
|
||||
updatePluginInstallStatusMock = jest.fn();
|
||||
wrapper.vm.updatePluginInstallStatus = updatePluginInstallStatusMock;
|
||||
|
||||
// Set the 'installing' status on the component instance
|
||||
wrapper.vm.installing = {
|
||||
plugin1: 'install',
|
||||
plugin2: 'downgrade',
|
||||
plugin3: 'uninstall',
|
||||
};
|
||||
|
||||
// Reset errors
|
||||
wrapper.vm.errors = {};
|
||||
});
|
||||
|
||||
it('should set status to "update" for an upgrade operation', async() => {
|
||||
const plugin = { name: 'my-plugin' };
|
||||
|
||||
wrapper.vm.available = [plugin];
|
||||
wrapper.vm.installing['my-plugin'] = 'update';
|
||||
|
||||
it('should update status for an active operation', async() => {
|
||||
const helmOps = [{
|
||||
metadata: { state: { transitioning: true } },
|
||||
status: { releaseName: 'my-plugin', action: 'upgrade' }
|
||||
namespace: UI_PLUGIN_NAMESPACE,
|
||||
status: { releaseName: 'plugin1', action: 'install' },
|
||||
metadata: { creationTimestamp: '1', state: { transitioning: true } }
|
||||
}];
|
||||
|
||||
wrapper.vm.helmOps = helmOps;
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.vm.installing['my-plugin']).toBe('update');
|
||||
expect(updatePluginInstallStatusMock).toHaveBeenCalledWith('plugin1', 'install');
|
||||
});
|
||||
|
||||
it('should keep status as "rollback" during an upgrade operation if it was already rolling back', async() => {
|
||||
const plugin = { name: 'my-plugin' };
|
||||
|
||||
wrapper.vm.available = [plugin];
|
||||
wrapper.vm.installing['my-plugin'] = 'rollback';
|
||||
|
||||
it('should not update status for an upgrade op when a downgrade was initiated', async() => {
|
||||
const helmOps = [{
|
||||
metadata: { state: { transitioning: true } },
|
||||
status: { releaseName: 'my-plugin', action: 'upgrade' }
|
||||
namespace: UI_PLUGIN_NAMESPACE,
|
||||
status: { releaseName: 'plugin2', action: 'upgrade' },
|
||||
metadata: { creationTimestamp: '1', state: { transitioning: true } }
|
||||
}];
|
||||
|
||||
wrapper.vm.helmOps = helmOps;
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.vm.installing['my-plugin']).toBe('rollback');
|
||||
// It should not be called with 'upgrade' for plugin2
|
||||
expect(updatePluginInstallStatusMock).not.toHaveBeenCalledWith('plugin2', 'upgrade');
|
||||
});
|
||||
|
||||
it('should clear status for a completed uninstall operation', async() => {
|
||||
const helmOps = [{
|
||||
namespace: UI_PLUGIN_NAMESPACE,
|
||||
status: { releaseName: 'plugin3', action: 'uninstall' },
|
||||
metadata: { creationTimestamp: '1', state: { transitioning: false } }
|
||||
}];
|
||||
|
||||
wrapper.vm.helmOps = helmOps;
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(updatePluginInstallStatusMock).toHaveBeenCalledWith('plugin3', false);
|
||||
});
|
||||
|
||||
it('should set error and clear status for a failed operation', async() => {
|
||||
const helmOps = [{
|
||||
namespace: UI_PLUGIN_NAMESPACE,
|
||||
status: { releaseName: 'plugin1', action: 'install' },
|
||||
metadata: { creationTimestamp: '1', state: { transitioning: false, error: true } }
|
||||
}];
|
||||
|
||||
wrapper.vm.helmOps = helmOps;
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.vm.errors.plugin1).toBe(true);
|
||||
expect(updatePluginInstallStatusMock).toHaveBeenCalledWith('plugin1', false);
|
||||
});
|
||||
|
||||
it('should clear status for plugins with no active operation', async() => {
|
||||
const helmOps = [{
|
||||
namespace: UI_PLUGIN_NAMESPACE,
|
||||
status: { releaseName: 'plugin1', action: 'install' },
|
||||
metadata: { creationTimestamp: '1', state: { transitioning: true } }
|
||||
}];
|
||||
|
||||
wrapper.vm.helmOps = helmOps;
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
// plugin4 has no op, so its status should be cleared
|
||||
expect(updatePluginInstallStatusMock).toHaveBeenCalledWith('plugin4', false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import day from 'dayjs';
|
||||
import { mapPref, PLUGIN_DEVELOPER } from '@shell/store/prefs';
|
||||
import { sortBy } from '@shell/utils/sort';
|
||||
import { allHash } from '@shell/utils/promise';
|
||||
import { CATALOG, UI_PLUGIN, MANAGEMENT } from '@shell/config/types';
|
||||
import { CATALOG, UI_PLUGIN, MANAGEMENT, ZERO_TIME } from '@shell/config/types';
|
||||
import { SETTING } from '@shell/config/settings';
|
||||
import { fetchOrCreateSetting } from '@shell/utils/settings';
|
||||
import { getVersionData, isRancherPrime } from '@shell/config/version';
|
||||
|
|
@ -40,10 +41,8 @@ const MAX_DESCRIPTION_LENGTH = 200;
|
|||
|
||||
const TABS_VALUES = {
|
||||
INSTALLED: 'installed',
|
||||
UPDATES: 'updates',
|
||||
AVAILABLE: 'available',
|
||||
BUILTIN: 'builtin',
|
||||
ALL: 'all',
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
@ -67,7 +66,7 @@ export default {
|
|||
return {
|
||||
TABS_VALUES,
|
||||
kubeVersion: null,
|
||||
view: '',
|
||||
activeTab: '',
|
||||
installing: {},
|
||||
errors: {},
|
||||
plugins: [], // The installed plugins
|
||||
|
|
@ -122,11 +121,6 @@ export default {
|
|||
|
||||
this.addExtensionReposBannerSetting = await fetchOrCreateSetting(this.$store, SETTING.ADD_EXTENSION_REPOS_BANNER_DISPLAY, 'true', true) || {};
|
||||
|
||||
// If there are no plugins installed, default to the catalog view
|
||||
if (this.plugins.length === 0) {
|
||||
this.$refs.tabs?.select(TABS_VALUES.AVAILABLE);
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
|
||||
|
|
@ -207,12 +201,10 @@ export default {
|
|||
// If not an extension developer, then don't include built-in extensions
|
||||
const all = this.pluginDeveloper ? this.available : this.available.filter((p) => !p.builtin);
|
||||
|
||||
switch (this.view) {
|
||||
switch (this.activeTab) {
|
||||
case TABS_VALUES.INSTALLED:
|
||||
// We never show built-in extensions as installed - installed are just the ones the user has installed
|
||||
return all.filter((p) => !p.builtin && (!!p.installed || !!p.installing));
|
||||
case TABS_VALUES.UPDATES:
|
||||
return this.updates;
|
||||
return all.filter((p) => !p.builtin && (!!p.installed || !!p.installing) && p.installableVersions?.length > 0);
|
||||
case TABS_VALUES.AVAILABLE:
|
||||
return all.filter((p) => !p.installed);
|
||||
case TABS_VALUES.BUILTIN:
|
||||
|
|
@ -226,14 +218,14 @@ export default {
|
|||
return this.menuActions?.length > 0;
|
||||
},
|
||||
|
||||
// Message to display when the tab view is empty (depends on the tab)
|
||||
// Message to display when the active tab is empty (depends on the tab)
|
||||
emptyMessage() {
|
||||
// i18n-uses plugins.empty.*
|
||||
return this.t(`plugins.empty.${ this.view }`);
|
||||
return this.t(`plugins.empty.${ this.activeTab }`);
|
||||
},
|
||||
|
||||
updates() {
|
||||
return this.available.filter((plugin) => !!plugin.upgrade);
|
||||
installed() {
|
||||
return this.available.filter((p) => !p.builtin && (!!p.installed || !!p.installing));
|
||||
},
|
||||
|
||||
pluginCards() {
|
||||
|
|
@ -298,6 +290,7 @@ export default {
|
|||
item.displayVersion = latestCompatible.version;
|
||||
item.displayVersionLabel = getPluginChartVersionLabel(latestCompatible);
|
||||
item.icon = latestCompatible.icon;
|
||||
item.created = latestCompatible.created;
|
||||
} else {
|
||||
item.experimental = uiPluginHasAnnotation(chart, CATALOG_ANNOTATIONS.EXPERIMENTAL, 'true');
|
||||
item.certified = uiPluginHasAnnotation(chart, CATALOG_ANNOTATIONS.CERTIFIED, CATALOG_ANNOTATIONS._RANCHER);
|
||||
|
|
@ -305,6 +298,7 @@ export default {
|
|||
item.displayVersion = item.versions?.[0]?.version;
|
||||
item.displayVersionLabel = getPluginChartVersionLabel(item.versions?.[0]);
|
||||
item.icon = chart.icon || latestCompatible?.annotations?.['catalog.cattle.io/ui-icon'];
|
||||
item.created = item.versions?.[0]?.created;
|
||||
}
|
||||
|
||||
// add message of extension card if there's a newer version of the extension, but it's not available to be installed
|
||||
|
|
@ -353,6 +347,7 @@ export default {
|
|||
displayVersion: p.metadata?.version,
|
||||
displayVersionLabel: p.metadata?.version || '-',
|
||||
installed: true,
|
||||
installedVersion: p.metadata?.version,
|
||||
builtin: !!p.builtin,
|
||||
experimental: rancher?.annotations?.[CATALOG_ANNOTATIONS.EXPERIMENTAL] === 'true',
|
||||
certified: rancher?.annotations?.[CATALOG_ANNOTATIONS.CERTIFIED] === CATALOG_ANNOTATIONS._RANCHER
|
||||
|
|
@ -373,8 +368,7 @@ export default {
|
|||
if (chart) {
|
||||
chart.installed = true;
|
||||
chart.uiplugin = p;
|
||||
chart.displayVersion = p.version;
|
||||
let displayVersionLabel = p.version;
|
||||
chart.installedVersion = p.version;
|
||||
|
||||
// Can't do this here
|
||||
chart.installing = this.installing[chart.name];
|
||||
|
|
@ -389,13 +383,10 @@ export default {
|
|||
if (installedVersion) {
|
||||
chart.experimental = installedVersion?.annotations?.[CATALOG_ANNOTATIONS.EXPERIMENTAL] === 'true';
|
||||
chart.certified = installedVersion?.annotations?.[CATALOG_ANNOTATIONS.CERTIFIED] === CATALOG_ANNOTATIONS._RANCHER;
|
||||
displayVersionLabel = getPluginChartVersionLabel(installedVersion);
|
||||
}
|
||||
|
||||
chart.upgrade = getPluginChartVersionLabel(latestInstallableVersion);
|
||||
}
|
||||
|
||||
chart.displayVersionLabel = displayVersionLabel;
|
||||
} else {
|
||||
// No chart, so add a card for the plugin based on its Custom resource being present
|
||||
const item = {
|
||||
|
|
@ -474,10 +465,10 @@ export default {
|
|||
|
||||
if (active) {
|
||||
// Can use the status directly, apart from upgrade, which maps to update
|
||||
const status = op.status.action === 'upgrade' ? 'update' : op.status.action;
|
||||
const status = op.status.action;
|
||||
|
||||
if (status === 'update' && this.installing[plugin.name] === 'rollback') {
|
||||
// Helm op is an upgrade, but we initiated a rollback, so keep the 'rollback' status
|
||||
if (status === 'upgrade' && this.installing[plugin.name] === 'downgrade') {
|
||||
// Helm op is an upgrade, but we initiated a downgrade, so keep the 'downgrade' status
|
||||
} else {
|
||||
this.updatePluginInstallStatus(plugin.name, status);
|
||||
}
|
||||
|
|
@ -535,6 +526,16 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
handlePanelAction(button) {
|
||||
const { action, plugin, version } = button;
|
||||
|
||||
if (action === 'uninstall') {
|
||||
this.showUninstallDialog(plugin, {});
|
||||
} else {
|
||||
this.showInstallDialog(plugin, action, {}, version);
|
||||
}
|
||||
},
|
||||
|
||||
async refreshCharts(forceChartsUpdate = false) {
|
||||
// we might need to force the request, so that we know at all times if what's the status of the offical and partners repos (installed or not)
|
||||
// tied to the SetupUIPlugins, AddExtensionRepos checkboxes
|
||||
|
|
@ -560,8 +561,8 @@ export default {
|
|||
return hasFeatureFlag;
|
||||
},
|
||||
|
||||
filterChanged(f) {
|
||||
this.view = f.selectedName;
|
||||
tabChanged(f) {
|
||||
this.activeTab = f.selectedName;
|
||||
},
|
||||
|
||||
// Developer Load is in the action menu
|
||||
|
|
@ -623,10 +624,10 @@ export default {
|
|||
});
|
||||
},
|
||||
|
||||
showInstallDialog(plugin, action, ev) {
|
||||
ev.target?.blur();
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
showInstallDialog(plugin, action, ev, initialVersion = null) {
|
||||
ev?.target?.blur();
|
||||
ev?.preventDefault?.();
|
||||
ev?.stopPropagation?.();
|
||||
|
||||
this.$store.dispatch('management/promptModal', {
|
||||
component: 'InstallExtensionDialog',
|
||||
|
|
@ -636,6 +637,7 @@ export default {
|
|||
componentProps: {
|
||||
plugin,
|
||||
action,
|
||||
initialVersion,
|
||||
updateStatus: (pluginName, type) => {
|
||||
this.updatePluginInstallStatus(pluginName, type);
|
||||
},
|
||||
|
|
@ -647,9 +649,9 @@ export default {
|
|||
},
|
||||
|
||||
showUninstallDialog(plugin, ev) {
|
||||
ev.target?.blur();
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
ev?.target?.blur();
|
||||
ev?.preventDefault?.();
|
||||
ev?.stopPropagation?.();
|
||||
|
||||
this.$store.dispatch('management/promptModal', {
|
||||
component: 'UninstallExtensionDialog',
|
||||
|
|
@ -683,7 +685,7 @@ export default {
|
|||
|
||||
didInstall(plugin) {
|
||||
if (plugin) {
|
||||
// Change the view to installed if we started installing a plugin
|
||||
// Change the activeTab to installed if we started installing a plugin
|
||||
this.$refs.tabs?.select(TABS_VALUES.INSTALLED);
|
||||
|
||||
// Clear the load error, if there was one previously
|
||||
|
|
@ -692,7 +694,9 @@ export default {
|
|||
},
|
||||
|
||||
showPluginDetail(plugin) {
|
||||
this.$refs.infoPanel.show(plugin);
|
||||
const tags = this.getFooterItems(plugin);
|
||||
|
||||
this.$refs.infoPanel.show({ ...plugin, tags });
|
||||
},
|
||||
|
||||
updatePluginInstallStatus(name, status) {
|
||||
|
|
@ -741,42 +745,46 @@ export default {
|
|||
getPluginActions(plugin) {
|
||||
const actions = [];
|
||||
|
||||
const canInstall = !plugin.installed && plugin.installableVersions?.length;
|
||||
const canUninstall = plugin.installed && !plugin.builtin;
|
||||
const canUpdate = plugin.installed && plugin.upgrade;
|
||||
const canRollback = plugin.installed && !plugin.upgrade && plugin.installableVersions?.length > 1;
|
||||
if (plugin.installed) {
|
||||
const canUpdate = !!plugin.upgrade;
|
||||
const canDowngrade = plugin.installableVersions?.some((v) => isChartVersionHigher(plugin.installedVersion, v.version));
|
||||
const canUninstall = !plugin.builtin;
|
||||
|
||||
if (canUninstall) {
|
||||
actions.push({
|
||||
label: this.t('plugins.uninstall.label'),
|
||||
action: 'uninstall',
|
||||
icon: 'icon-delete',
|
||||
});
|
||||
}
|
||||
if (canUpdate) {
|
||||
actions.push({
|
||||
label: this.t('plugins.upgrade.label'),
|
||||
action: 'upgrade',
|
||||
icon: 'icon-upgrade-alt',
|
||||
});
|
||||
}
|
||||
|
||||
if (canUpdate) {
|
||||
actions.push({
|
||||
label: this.t('plugins.update.label'),
|
||||
action: 'update',
|
||||
icon: 'icon-edit',
|
||||
});
|
||||
}
|
||||
if (canDowngrade) {
|
||||
actions.push({
|
||||
label: this.t('plugins.downgrade.label'),
|
||||
action: 'downgrade',
|
||||
icon: 'icon-downgrade-alt',
|
||||
});
|
||||
}
|
||||
|
||||
if (canRollback) {
|
||||
actions.push({
|
||||
label: this.t('plugins.rollback.label'),
|
||||
action: 'rollback',
|
||||
icon: 'icon-history',
|
||||
});
|
||||
}
|
||||
|
||||
if (canInstall) {
|
||||
actions.push({
|
||||
label: this.t('plugins.install.label'),
|
||||
action: 'install',
|
||||
icon: 'icon-plus',
|
||||
enabled: true,
|
||||
});
|
||||
if (canUninstall) {
|
||||
if (actions.length > 0) {
|
||||
actions.push({ divider: true });
|
||||
}
|
||||
actions.push({
|
||||
label: this.t('plugins.uninstall.label'),
|
||||
action: 'uninstall',
|
||||
icon: 'icon-delete',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (plugin.installableVersions?.length) {
|
||||
actions.push({
|
||||
label: this.t('plugins.install.label'),
|
||||
action: 'install',
|
||||
icon: 'icon-plus',
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return actions;
|
||||
|
|
@ -784,12 +792,25 @@ export default {
|
|||
|
||||
getSubHeaderItems(plugin) {
|
||||
const items = [{
|
||||
icon: 'icon-version-alt',
|
||||
iconTooltip: { key: 'tableHeaders.version' },
|
||||
label: plugin.displayVersionLabel,
|
||||
labelTooltip: plugin.upgrade ? this.t('plugins.upgradeAvailableTooltip', { version: plugin.upgrade }) : ''
|
||||
icon: 'icon-version-alt',
|
||||
iconTooltip: { key: 'tableHeaders.version' },
|
||||
label: plugin.displayVersionLabel,
|
||||
}];
|
||||
|
||||
if (plugin.created) {
|
||||
const hasZeroTime = plugin.created === ZERO_TIME;
|
||||
const lastUpdatedItem = {
|
||||
icon: 'icon-refresh-alt',
|
||||
iconTooltip: { key: 'tableHeaders.lastUpdated' },
|
||||
label: hasZeroTime ? this.t('generic.na') : day(plugin.created).format('MMM D, YYYY')
|
||||
};
|
||||
|
||||
if (hasZeroTime) {
|
||||
lastUpdatedItem.labelTooltip = this.t('catalog.charts.appChartCard.subHeaderItem.missingVersionDate');
|
||||
}
|
||||
items.push(lastUpdatedItem);
|
||||
}
|
||||
|
||||
if (plugin.installing) {
|
||||
let label;
|
||||
|
||||
|
|
@ -800,11 +821,11 @@ export default {
|
|||
case 'uninstall':
|
||||
label = this.t('plugins.labels.uninstalling');
|
||||
break;
|
||||
case 'update':
|
||||
label = this.t('plugins.labels.updating');
|
||||
case 'upgrade':
|
||||
label = this.t('plugins.labels.upgrading');
|
||||
break;
|
||||
case 'rollback':
|
||||
label = this.t('plugins.labels.rollingBack');
|
||||
case 'downgrade':
|
||||
label = this.t('plugins.labels.downgrading');
|
||||
break;
|
||||
default:
|
||||
label = this.t('plugins.labels.installing');
|
||||
|
|
@ -874,7 +895,7 @@ export default {
|
|||
|
||||
if (plugin.installed && !plugin.builtin && !plugin.installing) {
|
||||
statuses.push({
|
||||
icon: 'icon-confirmation-alt', color: 'success', tooltip: { key: 'generic.installed' }
|
||||
icon: 'icon-confirmation-alt', color: 'success', tooltip: { text: `${ this.t('generic.installed') } (${ plugin.installedVersion })` }
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -958,7 +979,10 @@ export default {
|
|||
</div>
|
||||
|
||||
<!-- extensions slide-in panel -->
|
||||
<PluginInfoPanel ref="infoPanel" />
|
||||
<PluginInfoPanel
|
||||
ref="infoPanel"
|
||||
@action="handlePanelAction"
|
||||
/>
|
||||
|
||||
<!-- extensions not enabled by feature flag -->
|
||||
<div v-if="!hasFeatureFlag">
|
||||
|
|
@ -1008,15 +1032,18 @@ export default {
|
|||
</Banner>
|
||||
|
||||
<Tabbed
|
||||
v-if="!loading"
|
||||
ref="tabs"
|
||||
:tabs-only="true"
|
||||
data-testid="extension-tabs"
|
||||
@changed="filterChanged"
|
||||
@changed="tabChanged"
|
||||
>
|
||||
<Tab
|
||||
v-if="installed.length"
|
||||
:name="TABS_VALUES.INSTALLED"
|
||||
data-testid="extension-tab-installed"
|
||||
label-key="plugins.tabs.installed"
|
||||
:badge="installed.length"
|
||||
:weight="20"
|
||||
/>
|
||||
<Tab
|
||||
|
|
@ -1025,23 +1052,12 @@ export default {
|
|||
label-key="plugins.tabs.available"
|
||||
:weight="19"
|
||||
/>
|
||||
<Tab
|
||||
:name="TABS_VALUES.UPDATES"
|
||||
label-key="plugins.tabs.updates"
|
||||
:weight="18"
|
||||
:badge="updates.length"
|
||||
/>
|
||||
<Tab
|
||||
v-if="pluginDeveloper"
|
||||
:name="TABS_VALUES.BUILTIN"
|
||||
label-key="plugins.tabs.builtin"
|
||||
:weight="17"
|
||||
/>
|
||||
<Tab
|
||||
:name="TABS_VALUES.ALL"
|
||||
label-key="plugins.tabs.all"
|
||||
:weight="16"
|
||||
/>
|
||||
</Tabbed>
|
||||
<div
|
||||
v-if="loading"
|
||||
|
|
@ -1079,8 +1095,8 @@ export default {
|
|||
:clickable="true"
|
||||
@card-click="showPluginDetail(card.plugin)"
|
||||
@uninstall="({event}) => showUninstallDialog(card.plugin, event)"
|
||||
@update="({event}) => showInstallDialog(card.plugin, 'update', event)"
|
||||
@rollback="({event}) => showInstallDialog(card.plugin, 'rollback', event)"
|
||||
@upgrade="({event}) => showInstallDialog(card.plugin, 'upgrade', event)"
|
||||
@downgrade="({event}) => showInstallDialog(card.plugin, 'downgrade', event)"
|
||||
@install="({event}) => showInstallDialog(card.plugin, 'install', event)"
|
||||
>
|
||||
<template #item-card-sub-header>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,194 @@
|
|||
import { SteveWatchEventListenerManager } from '@shell/plugins/subscribe-events';
|
||||
import { STEVE_WATCH_EVENT_TYPES } from '@shell/types/store/subscribe.types';
|
||||
|
||||
// Mock function to create a consistent key for testing
|
||||
const mockKeyForSubscribe = jest.fn(({
|
||||
params: {
|
||||
type, name, id, selector, mode
|
||||
}
|
||||
}) => {
|
||||
return `${ type }-${ name }-${ id }-${ selector }-${ mode }`;
|
||||
});
|
||||
|
||||
// Mock parameters and callbacks
|
||||
const mockParams1 = {
|
||||
type: 'pods', name: 'my-pod', id: 'abc-123', selector: 'app=test'
|
||||
};
|
||||
const mockCallback1 = jest.fn();
|
||||
const mockCallback2 = jest.fn();
|
||||
|
||||
// The class under test
|
||||
let manager: SteveWatchEventListenerManager;
|
||||
|
||||
describe('steveWatchEventListenerManager', () => {
|
||||
beforeEach(() => {
|
||||
// Reset the manager and mocks before each test
|
||||
manager = new SteveWatchEventListenerManager();
|
||||
jest.clearAllMocks();
|
||||
// Replace the internal keyForSubscribe with our mock
|
||||
(manager as any).keyForSubscribe = mockKeyForSubscribe;
|
||||
});
|
||||
|
||||
describe('initialization and Properties', () => {
|
||||
it('should be created successfully', () => {
|
||||
expect(manager).toBeInstanceOf(SteveWatchEventListenerManager);
|
||||
});
|
||||
|
||||
it('should have a supportedEventTypes array with STEVE_WATCH_EVENT_TYPES.CHANGES', () => {
|
||||
expect(manager.supportedEventTypes).toStrictEqual([STEVE_WATCH_EVENT_TYPES.CHANGES]);
|
||||
});
|
||||
|
||||
it('should correctly identify a supported event type', () => {
|
||||
const isSupported = manager.isSupportedEventType(STEVE_WATCH_EVENT_TYPES.CHANGES);
|
||||
|
||||
expect(isSupported).toBe(true);
|
||||
});
|
||||
|
||||
it('should correctly identify an unsupported event type', () => {
|
||||
const isSupported = manager.isSupportedEventType('some.other.event' as STEVE_WATCH_EVENT_TYPES);
|
||||
|
||||
expect(isSupported).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('watch Management', () => {
|
||||
it('should return undefined when getting a non-existent watch', () => {
|
||||
const watch = manager.getWatch({ params: mockParams1 });
|
||||
|
||||
expect(watch).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should create a watch when setStandardWatch is called with standardWatch true and no watch exists', () => {
|
||||
manager.setStandardWatch({ standardWatch: true, args: { params: mockParams1 } });
|
||||
const watch = (manager as any).watches[mockKeyForSubscribe({ params: mockParams1 })];
|
||||
|
||||
expect(watch).toBeDefined();
|
||||
expect(watch.hasStandardWatch).toBe(true);
|
||||
expect(watch.listeners).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it('should not create a watch when setStandardWatch is called with standardWatch false and no watch exists', () => {
|
||||
manager.setStandardWatch({ standardWatch: false, args: { params: mockParams1 } });
|
||||
const watch = (manager as any).watches[mockKeyForSubscribe({ params: mockParams1 })];
|
||||
|
||||
expect(watch).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should delete a watch when hasStandardWatch becomes false and there are no listeners', () => {
|
||||
manager.setStandardWatch({ standardWatch: true, args: { params: mockParams1 } });
|
||||
manager.setStandardWatch({ standardWatch: false, args: { params: mockParams1 } });
|
||||
const watch = (manager as any).watches[mockKeyForSubscribe({ params: mockParams1 })];
|
||||
|
||||
expect(watch).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('listener and Callback Management', () => {
|
||||
it('should add a new listener and a callback', () => {
|
||||
const listener = manager.addEventListenerCallback({
|
||||
callback: mockCallback1,
|
||||
args: {
|
||||
event: STEVE_WATCH_EVENT_TYPES.CHANGES,
|
||||
params: mockParams1,
|
||||
id: 'cb-1'
|
||||
}
|
||||
});
|
||||
|
||||
expect(listener).toBeDefined();
|
||||
expect(listener.event).toBe(STEVE_WATCH_EVENT_TYPES.CHANGES);
|
||||
expect(listener.callbacks['cb-1']).toBe(mockCallback1);
|
||||
const watch = manager.getWatch({ params: mockParams1 });
|
||||
|
||||
expect(watch?.listeners.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should add a second callback to an existing listener', () => {
|
||||
manager.addEventListenerCallback({
|
||||
callback: mockCallback1,
|
||||
args: {
|
||||
event: STEVE_WATCH_EVENT_TYPES.CHANGES,
|
||||
params: mockParams1,
|
||||
id: 'cb-1'
|
||||
}
|
||||
});
|
||||
manager.addEventListenerCallback({
|
||||
callback: mockCallback2,
|
||||
args: {
|
||||
event: STEVE_WATCH_EVENT_TYPES.CHANGES,
|
||||
params: mockParams1,
|
||||
id: 'cb-2'
|
||||
}
|
||||
});
|
||||
|
||||
const listener = manager.getEventListener({ args: { event: STEVE_WATCH_EVENT_TYPES.CHANGES, params: mockParams1 } });
|
||||
|
||||
expect(Object.keys(listener?.callbacks || {})).toHaveLength(2);
|
||||
expect(listener?.callbacks['cb-2']).toBe(mockCallback2);
|
||||
});
|
||||
|
||||
it('should trigger a specific event listener and its callbacks', () => {
|
||||
manager.addEventListenerCallback({
|
||||
callback: mockCallback1,
|
||||
args: {
|
||||
event: STEVE_WATCH_EVENT_TYPES.CHANGES,
|
||||
params: mockParams1,
|
||||
id: 'cb-1'
|
||||
}
|
||||
});
|
||||
manager.addEventListenerCallback({
|
||||
callback: mockCallback2,
|
||||
args: {
|
||||
event: STEVE_WATCH_EVENT_TYPES.CHANGES,
|
||||
params: mockParams1,
|
||||
id: 'cb-2'
|
||||
}
|
||||
});
|
||||
|
||||
manager.triggerEventListener({ event: STEVE_WATCH_EVENT_TYPES.CHANGES, params: mockParams1 });
|
||||
expect(mockCallback1).toHaveBeenCalledTimes(1);
|
||||
expect(mockCallback2).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should remove a specific callback from a listener', () => {
|
||||
manager.addEventListenerCallback({
|
||||
callback: mockCallback1,
|
||||
args: {
|
||||
event: STEVE_WATCH_EVENT_TYPES.CHANGES,
|
||||
params: mockParams1,
|
||||
id: 'cb-1'
|
||||
}
|
||||
});
|
||||
manager.removeEventListenerCallback({
|
||||
event: STEVE_WATCH_EVENT_TYPES.CHANGES,
|
||||
params: mockParams1,
|
||||
id: 'cb-1'
|
||||
});
|
||||
const listener = manager.getEventListener({ args: { event: STEVE_WATCH_EVENT_TYPES.CHANGES, params: mockParams1 } });
|
||||
|
||||
expect(listener?.callbacks['cb-1']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should trigger all callbacks for a given watch', () => {
|
||||
manager.addEventListenerCallback({
|
||||
callback: mockCallback1,
|
||||
args: {
|
||||
event: STEVE_WATCH_EVENT_TYPES.CHANGES,
|
||||
params: mockParams1,
|
||||
id: 'cb-1'
|
||||
}
|
||||
});
|
||||
manager.addEventListenerCallback({
|
||||
callback: mockCallback2,
|
||||
args: {
|
||||
event: 'another.event' as STEVE_WATCH_EVENT_TYPES,
|
||||
params: mockParams1,
|
||||
id: 'cb-2'
|
||||
}
|
||||
});
|
||||
|
||||
manager.triggerAllEventListeners({ params: mockParams1 });
|
||||
expect(mockCallback1).toHaveBeenCalledTimes(1);
|
||||
expect(mockCallback2).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -220,6 +220,9 @@ export default {
|
|||
)
|
||||
) {
|
||||
if (opt.watch !== false ) {
|
||||
// Note - Empty revision here seems broken
|
||||
// - list page (watch all) --> detail page (stop watch all, watch one) --> list page (watch all - no revision)
|
||||
// - the missing revision means watch start from now... instead of the point the clusters were last monitored (cache contains stale data)
|
||||
const args = {
|
||||
type,
|
||||
revision: '',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import { NORMAN_NAME } from '@shell/config/labels-annotations';
|
||||
import { getVersionData } from '@shell/config/version';
|
||||
import { parseRancherVersion } from '@shell/config/uiplugins';
|
||||
import semver from 'semver';
|
||||
import {
|
||||
_CLONE,
|
||||
_CONFIG,
|
||||
|
|
@ -59,7 +62,7 @@ const DEFAULT_COLOR = 'warning';
|
|||
const DEFAULT_ICON = 'x';
|
||||
|
||||
const DEFAULT_WAIT_INTERVAL = 1000;
|
||||
const DEFAULT_WAIT_TMIMEOUT = 30000;
|
||||
const DEFAULT_WAIT_TIMEOUT = 30000;
|
||||
|
||||
export const STATES_ENUM = {
|
||||
IN_USE: 'in-use',
|
||||
|
|
@ -803,7 +806,7 @@ export default class Resource {
|
|||
// ------------------------------------------------------------------
|
||||
|
||||
waitForTestFn(fn, msg, timeoutMs, intervalMs) {
|
||||
return waitFor(() => fn.apply(this), msg, timeoutMs || DEFAULT_WAIT_TMIMEOUT, intervalMs || DEFAULT_WAIT_INTERVAL, true);
|
||||
return waitFor(() => fn.apply(this), msg, timeoutMs || DEFAULT_WAIT_TIMEOUT, intervalMs || DEFAULT_WAIT_INTERVAL, true);
|
||||
}
|
||||
|
||||
waitForState(state, timeout, interval) {
|
||||
|
|
@ -852,7 +855,7 @@ export default class Resource {
|
|||
return (entry.status || '').toLowerCase() === `${ withStatus }`.toLowerCase();
|
||||
}
|
||||
|
||||
waitForCondition(name, withStatus = 'True', timeoutMs = DEFAULT_WAIT_TMIMEOUT, intervalMs = DEFAULT_WAIT_INTERVAL) {
|
||||
waitForCondition(name, withStatus = 'True', timeoutMs = DEFAULT_WAIT_TIMEOUT, intervalMs = DEFAULT_WAIT_INTERVAL) {
|
||||
return this.waitForTestFn(() => {
|
||||
return this.isCondition(name, withStatus);
|
||||
}, `condition ${ name }=${ withStatus }`, timeoutMs, intervalMs);
|
||||
|
|
@ -929,12 +932,20 @@ export default class Resource {
|
|||
const currentRoute = this.currentRouter().currentRoute.value;
|
||||
const extensionMenuActions = getApplicableExtensionEnhancements(this.$rootState, ExtensionPoint.ACTION, ActionLocation.TABLE, currentRoute, this);
|
||||
|
||||
const currRancherVersionData = getVersionData();
|
||||
const parsedRancherVersion = parseRancherVersion(currRancherVersionData.Version);
|
||||
|
||||
// "showConfiguration" table action is only compatible with Rancher 2.13 and onwards
|
||||
// defence against extension issue https://github.com/rancher/dashboard/issues/15564
|
||||
// where mostly likely extension CRD model is extending from resource-class
|
||||
const isResourceDetailDrawerCompatibleWithRancherSystem = semver.satisfies(parsedRancherVersion, '>= 2.13.0');
|
||||
|
||||
const all = [
|
||||
{
|
||||
action: 'showConfiguration',
|
||||
label: this.t('action.showConfiguration'),
|
||||
icon: 'icon icon-document',
|
||||
enabled: this.disableResourceDetailDrawer !== true && (this.canCustomEdit || this.canYaml), // If the resource can't show an edit or a yaml we don't want to show the configuration drawer
|
||||
enabled: isResourceDetailDrawerCompatibleWithRancherSystem && this.disableResourceDetailDrawer !== true && (this.canCustomEdit || this.canYaml), // If the resource can't show an edit or a yaml we don't want to show the configuration drawer
|
||||
},
|
||||
{ divider: true },
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
import { actions, getters, mutations } from '../subscribe';
|
||||
import { REVISION_TOO_OLD } from '../../../utils/socket';
|
||||
import { STEVE_WATCH_EVENT } from '../../../types/store/subscribe.types';
|
||||
import { STEVE_WATCH_MODE } from '../../../types/store/subscribe.types';
|
||||
import backOff from '../../../utils/back-off';
|
||||
import { SteveWatchEventListenerManager } from '../../subscribe-events';
|
||||
|
||||
describe('steve: subscribe', () => {
|
||||
describe('actions', () => {
|
||||
describe('watch', () => {
|
||||
const state = {};
|
||||
const state = { listenerManager: new SteveWatchEventListenerManager() };
|
||||
const getters = {
|
||||
normalizeType: (type: string) => type,
|
||||
schemaFor: () => null,
|
||||
inError: () => false,
|
||||
watchStarted: () => false,
|
||||
normalizeType: (type: string) => type,
|
||||
schemaFor: () => null,
|
||||
inError: () => false,
|
||||
watchStarted: () => false,
|
||||
listenerManager: state.listenerManager
|
||||
};
|
||||
const rootGetters = {
|
||||
'type-map/isSpoofed': () => false,
|
||||
|
|
@ -211,15 +213,11 @@ describe('steve: subscribe', () => {
|
|||
}, {
|
||||
...obj,
|
||||
revision,
|
||||
mode: STEVE_WATCH_EVENT.CHANGES,
|
||||
mode: STEVE_WATCH_MODE.RESOURCE_CHANGES,
|
||||
force: true,
|
||||
});
|
||||
|
||||
expect(dispatch).toHaveBeenNthCalledWith(1, 'unwatchIncompatible', {
|
||||
id: undefined, mode: STEVE_WATCH_EVENT.CHANGES, namespace: undefined, selector: undefined, type: obj.type
|
||||
});
|
||||
|
||||
expect(dispatch).toHaveBeenNthCalledWith(2, 'send', {
|
||||
expect(dispatch).toHaveBeenNthCalledWith(1, 'send', {
|
||||
debounceMs: 4000,
|
||||
mode: 'resource.changes',
|
||||
resourceType: obj.type,
|
||||
|
|
@ -231,7 +229,7 @@ describe('steve: subscribe', () => {
|
|||
state, dispatch, getters, commit
|
||||
}, { ...msg });
|
||||
|
||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
dispatch.mockClear();
|
||||
};
|
||||
|
||||
|
|
@ -252,7 +250,7 @@ describe('steve: subscribe', () => {
|
|||
});
|
||||
expect(state.inError).toStrictEqual(
|
||||
{
|
||||
'type=abc,namespace=,id=,selector=': {
|
||||
'type=abc,namespace=,id=,selector=,mode=resource.changes': {
|
||||
obj: {
|
||||
type: msg.resourceType,
|
||||
mode: msg.mode,
|
||||
|
|
@ -268,7 +266,7 @@ describe('steve: subscribe', () => {
|
|||
}, { ...msg });
|
||||
// stop tries to watch again, however we're in error so will be ignored
|
||||
expect(dispatch).toHaveBeenNthCalledWith(1, 'watch', {
|
||||
id: undefined, mode: STEVE_WATCH_EVENT.CHANGES, namespace: undefined, selector: undefined, type: obj.type
|
||||
id: undefined, mode: STEVE_WATCH_MODE.RESOURCE_CHANGES, namespace: undefined, selector: undefined, standardWatch: true, type: obj.type
|
||||
});
|
||||
|
||||
dispatch.mockClear();
|
||||
|
|
@ -349,19 +347,24 @@ describe('steve: subscribe', () => {
|
|||
const obj = { type: 'abc' };
|
||||
const msg = {
|
||||
resourceType: obj.type,
|
||||
mode: STEVE_WATCH_EVENT.CHANGES,
|
||||
mode: STEVE_WATCH_MODE.RESOURCE_CHANGES,
|
||||
};
|
||||
|
||||
const initStore = () => {
|
||||
const state = { started: [], inError: {} };
|
||||
const state = {
|
||||
started: [],
|
||||
inError: {},
|
||||
listenerManager: new SteveWatchEventListenerManager()
|
||||
};
|
||||
const _getters = {
|
||||
normalizeType: (type: string) => type,
|
||||
schemaFor: () => ({}),
|
||||
storeName: 'test',
|
||||
inError: (...args) => getters.inError(state)(...args),
|
||||
watchStarted: (...args) => getters.watchStarted(state)(...args),
|
||||
backOffId: (...args) => getters.backOffId()(...args),
|
||||
canBackoff: () => true,
|
||||
normalizeType: (type: string) => type,
|
||||
schemaFor: () => ({}),
|
||||
storeName: 'test',
|
||||
inError: (...args) => getters.inError(state)(...args),
|
||||
watchStarted: (...args) => getters.watchStarted(state)(...args),
|
||||
backOffId: (...args) => getters.backOffId()(...args),
|
||||
canBackoff: () => true,
|
||||
listenerManager: state.listenerManager
|
||||
};
|
||||
const commit = (type, ...args) => mutations[type](state, ...args);
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import {
|
|||
import getters, { STEVE_MODEL_TYPES } from './getters';
|
||||
import mutations from './mutations';
|
||||
import actions from './actions';
|
||||
import { SteveWatchEventListenerManager } from '@shell/plugins/subscribe-events';
|
||||
import { markRaw } from 'vue';
|
||||
|
||||
export function SteveFactory(namespace, baseUrl) {
|
||||
return {
|
||||
|
|
@ -17,16 +19,22 @@ export function SteveFactory(namespace, baseUrl) {
|
|||
state() {
|
||||
return {
|
||||
...coreStoreState(namespace, baseUrl),
|
||||
socket: null,
|
||||
queue: [], // For change event coalescing
|
||||
wantSocket: false,
|
||||
debugSocket: false,
|
||||
allowStreaming: true,
|
||||
pendingFrames: [],
|
||||
deferredRequests: {},
|
||||
started: [],
|
||||
inError: {},
|
||||
podsByNamespace: {}, // Cache of pods by namespace
|
||||
socket: null,
|
||||
queue: [], // For change event coalescing
|
||||
wantSocket: false,
|
||||
debugSocket: false,
|
||||
allowStreaming: true,
|
||||
pendingFrames: [],
|
||||
deferredRequests: {},
|
||||
started: [],
|
||||
inError: {},
|
||||
/**
|
||||
* Socket listener manager for this store
|
||||
*
|
||||
* Instance of @SteveWatchEventListenerManager . See it's description for more info
|
||||
*/
|
||||
socketListenerManager: markRaw(new SteveWatchEventListenerManager()),
|
||||
podsByNamespace: {}, // Cache of pods by namespace
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -164,10 +164,10 @@ export default {
|
|||
},
|
||||
|
||||
reset(state) {
|
||||
// Reset generic store things.... then steve specific things
|
||||
|
||||
// 1. Reset generic store things
|
||||
resetStore(state, this.commit);
|
||||
|
||||
// 2. Reset steve specific store things
|
||||
this.commit(`${ state.config.namespace }/resetSubscriptions`);
|
||||
|
||||
// Clear the podsByNamespace cache
|
||||
|
|
|
|||
|
|
@ -40,10 +40,10 @@ export const WATCH_STATUSES = {
|
|||
* Create a unique key for a specific resource watch's params
|
||||
*/
|
||||
export const keyForSubscribe = ({
|
||||
resourceType, type, namespace, id, selector
|
||||
resourceType, type, namespace, id, selector, mode
|
||||
} = {}) => {
|
||||
const keyMap = {
|
||||
type: resourceType || type, namespace, id, selector
|
||||
type: resourceType || type, namespace, id, selector, mode
|
||||
};
|
||||
|
||||
return Object.entries(keyMap)
|
||||
|
|
|
|||
|
|
@ -615,7 +615,6 @@ class StevePaginationUtils extends NamespaceProjectFilters {
|
|||
res.push(`filter=!${ labelKey }`);
|
||||
break;
|
||||
case 'Gt':
|
||||
// Currently broken - see https://github.com/rancher/rancher/issues/50057
|
||||
// Only applicable to node affinity (atm) - https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#operators
|
||||
|
||||
if (typeof exp.values !== 'string') {
|
||||
|
|
@ -628,7 +627,6 @@ class StevePaginationUtils extends NamespaceProjectFilters {
|
|||
res.push(`filter=${ labelKey } > (${ exp.values })`);
|
||||
break;
|
||||
case 'Lt':
|
||||
// Currently broken - see https://github.com/rancher/rancher/issues/50057
|
||||
// Only applicable to node affinity (atm) - https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#operators
|
||||
if (typeof exp.values !== 'string') {
|
||||
console.error(`Skipping labelSelector to API filter param conversion for ${ exp.key }(Lt) as no value was supplied`); // eslint-disable-line no-console
|
||||
|
|
@ -673,8 +671,8 @@ export const PAGINATION_SETTINGS_STORE_DEFAULTS: PaginationSettingsStores = {
|
|||
enableAll: false,
|
||||
enableSome: {
|
||||
enabled: [
|
||||
// { resource: CAPI.RANCHER_CLUSTER, context: ['home', 'side-bar'] }, // Disabled due to https://github.com/rancher/dashboard/issues/14493
|
||||
// { resource: MANAGEMENT.CLUSTER, context: ['side-bar'] }, // Disabled due to https://github.com/rancher/dashboard/issues/14493
|
||||
// { resource: CAPI.RANCHER_CLUSTER, context: ['home', 'side-bar'] },
|
||||
// { resource: MANAGEMENT.CLUSTER, context: ['side-bar'] },
|
||||
{ resource: CATALOG.APP, context: ['branding'] },
|
||||
SECRET
|
||||
],
|
||||
|
|
|
|||
|
|
@ -23,11 +23,13 @@
|
|||
* Successfully flow - watch
|
||||
* 1. UI --> Rancher: _watch_ request
|
||||
* 2. Rancher --> UI: `resource.start`. UI sets watch as started
|
||||
* ...
|
||||
* 3. Rancher --> UI: `resource.change` (contains data). UI caches data
|
||||
*
|
||||
* Successful flow - watch - new mode
|
||||
* 1. UI --> Rancher: _watch_ request
|
||||
* 2. Rancher --> UI: `resource.start`. UI sets watch as started
|
||||
* ...
|
||||
* 3. Rancher --> UI: `resource.changes` (contains no data). UI makes a HTTP request to fetch data
|
||||
*
|
||||
* Successful flow - unwatch
|
||||
|
|
@ -84,9 +86,10 @@ import { WORKER_MODES } from './worker';
|
|||
import acceptOrRejectSocketMessage from './accept-or-reject-socket-message';
|
||||
import { BLANK_CLUSTER, STORE } from '@shell/store/store-types.js';
|
||||
import { _MERGE } from '@shell/plugins/dashboard-store/actions';
|
||||
import { STEVE_WATCH_EVENT, STEVE_WATCH_MODE } from '@shell/types/store/subscribe.types';
|
||||
import { STEVE_WATCH_EVENT_TYPES, STEVE_WATCH_MODE } from '@shell/types/store/subscribe.types';
|
||||
import paginationUtils from '@shell/utils/pagination-utils';
|
||||
import backOff from '@shell/utils/back-off';
|
||||
import { SteveWatchEventListenerManager } from '@shell/plugins/subscribe-events';
|
||||
|
||||
// minimum length of time a disconnect notification is shown
|
||||
const MINIMUM_TIME_NOTIFIED = 3000;
|
||||
|
|
@ -312,24 +315,6 @@ function growlsDisabled(rootGetters) {
|
|||
return getPerformanceSetting(rootGetters)?.disableWebsocketNotification;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supported events are listed
|
||||
*
|
||||
* of type { [key: STEVE_WATCH_EVENT]: STEVE_WATCH_EVENT_LISTENER[]}
|
||||
*/
|
||||
const listeners = { [STEVE_WATCH_EVENT.CHANGES]: [] };
|
||||
|
||||
/**
|
||||
* Given a started or error entry, is it compatible with the given change in mode?
|
||||
*/
|
||||
const shouldUnwatchIncompatible = (messageMeta, mode) => {
|
||||
if (messageMeta.mode === STEVE_WATCH_EVENT.CHANGES) {
|
||||
return mode !== STEVE_WATCH_EVENT.CHANGES;
|
||||
}
|
||||
|
||||
return mode === STEVE_WATCH_EVENT.CHANGES;
|
||||
};
|
||||
|
||||
/**
|
||||
* clear the provided error, but also ensure any backoff request associated with it is cleared as well
|
||||
*/
|
||||
|
|
@ -452,7 +437,7 @@ const sharedActions = {
|
|||
* @param {STEVE_WATCH_EVENT_PARAMS} event
|
||||
*/
|
||||
watchEvent(ctx, {
|
||||
event = STEVE_WATCH_EVENT.CHANGES,
|
||||
event = STEVE_WATCH_EVENT_TYPES.CHANGES,
|
||||
id,
|
||||
callback,
|
||||
/**
|
||||
|
|
@ -460,26 +445,27 @@ const sharedActions = {
|
|||
*/
|
||||
params
|
||||
}) {
|
||||
if (!listeners[event]) {
|
||||
console.error(`Unknown event type "${ event }", only ${ Object.keys(listeners).join(',') } are supported`); // eslint-disable-line no-console
|
||||
if (!ctx.getters.listenerManager.isSupportedEventType(event)) {
|
||||
console.error(`Unknown event type "${ event }", only ${ Object.keys(ctx.getters.listenerManager.supportedEventTypes).join(',') } are supported`); // eslint-disable-line no-console
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// STEVE_WATCH_EVENT_LISTENER | undefined
|
||||
let listener = listeners[event].find((l) => equivalentWatch(l.params, params));
|
||||
ctx.getters.listenerManager.addEventListenerCallback({
|
||||
callback,
|
||||
args: {
|
||||
event, params, id
|
||||
}
|
||||
});
|
||||
|
||||
if (!listener) {
|
||||
listener = {
|
||||
params,
|
||||
callbacks: { }
|
||||
};
|
||||
listeners[event].push(listener);
|
||||
}
|
||||
const hasStandardWatch = ctx.getters.listenerManager.hasStandardWatch({ params });
|
||||
|
||||
if (!listener.callbacks[id]) {
|
||||
listener.callbacks[id] = callback;
|
||||
ctx.dispatch('watch', params);
|
||||
if (!hasStandardWatch) {
|
||||
// If there's nothing to piggy back on... start a watch to do so.
|
||||
ctx.dispatch('watch', {
|
||||
...params,
|
||||
standardWatch: false // Ensure that we don't treat this as a standard watch
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -488,24 +474,26 @@ const sharedActions = {
|
|||
* @param {STEVE_UNWATCH_EVENT_PARAMS} event
|
||||
*/
|
||||
unwatchEvent(ctx, {
|
||||
event = STEVE_WATCH_EVENT.CHANGES,
|
||||
event = STEVE_WATCH_EVENT_TYPES.CHANGES,
|
||||
id,
|
||||
/**
|
||||
* of type @STEVE_WATCH_PARAMS
|
||||
*/
|
||||
params
|
||||
}) {
|
||||
if (!listeners[event]) {
|
||||
if (!ctx.getters.listenerManager.isSupportedEventType(event)) {
|
||||
console.info(`Attempted to unwatch for an event "${ event }" but it had no watchers`); // eslint-disable-line no-console
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const existing = listeners[event].find((l) => equivalentWatch(l.params, params));
|
||||
ctx.getters.listenerManager.removeEventListenerCallback({
|
||||
event, params, id
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
delete existing.callbacks[id];
|
||||
}
|
||||
// Unwatch the underlying standard watch
|
||||
// Note - If we were piggybacking on a watch that previously existed we won't unwatch it
|
||||
ctx.dispatch('unwatch', params);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -517,7 +505,7 @@ const sharedActions = {
|
|||
state.debugSocket && console.info(`Watch Request [${ getters.storeName }]`, JSON.stringify(params)); // eslint-disable-line no-console
|
||||
let {
|
||||
// eslint-disable-next-line prefer-const
|
||||
type, selector, id, revision, namespace, stop, force, mode
|
||||
type, selector, id, revision, namespace, stop, force, mode, standardWatch = true
|
||||
} = params;
|
||||
|
||||
namespace = acceptOrRejectSocketMessage.subscribeNamespace(namespace);
|
||||
|
|
@ -562,10 +550,6 @@ const sharedActions = {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!stop) {
|
||||
dispatch('unwatchIncompatible', messageMeta);
|
||||
}
|
||||
|
||||
// Watch errors mean we make a http request to get latest revision (which is still missing) and try to re-watch with it...
|
||||
// etc
|
||||
if (typeof revision === 'undefined') {
|
||||
|
|
@ -618,6 +602,12 @@ const sharedActions = {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!stop && standardWatch) {
|
||||
// Track that this watch is just a normal one, not one kicked off by listeners
|
||||
// This helps us keep the watch going (for listeners) instead of in unwatch just stopping it
|
||||
getters.listenerManager.setStandardWatch({ standardWatch: true, args: { event: msg.mode, params: msg } });
|
||||
}
|
||||
|
||||
return dispatch('send', msg);
|
||||
},
|
||||
|
||||
|
|
@ -642,6 +632,21 @@ const sharedActions = {
|
|||
};
|
||||
|
||||
const unwatch = (obj) => {
|
||||
// Has this normal watch got listeners? If so
|
||||
const hasStandardWatch = ctx.getters.listenerManager.hasStandardWatch({ params: obj });
|
||||
const watchHasListeners = ctx.getters.listenerManager.hasEventListeners({ params: obj });
|
||||
|
||||
if (hasStandardWatch) {
|
||||
// If we have listeners for this watch... make sure it knows there's now no root standard watch
|
||||
ctx.getters.listenerManager.setStandardWatch({ standardWatch: false, args: { params: obj } });
|
||||
}
|
||||
|
||||
if (watchHasListeners) {
|
||||
// Does this watch have listeners? if so we shouldn't stop it (they still need it)
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (getters['watchStarted'](obj)) {
|
||||
// Set that we don't want to watch this type
|
||||
// Otherwise, the dispatch to unwatch below will just cause a re-watch when we
|
||||
|
|
@ -651,45 +656,28 @@ const sharedActions = {
|
|||
// Make sure anything in the pending queue for the type is removed, since we've now removed the type
|
||||
commit('clearFromQueue', type);
|
||||
}
|
||||
// Ensure anything pinging in the background is stopped
|
||||
backOff.resetPrefix(getters.backOffId(obj));
|
||||
};
|
||||
|
||||
const objKey = keyForSubscribe(obj);
|
||||
const reset = [];
|
||||
|
||||
if (isAdvancedWorker(ctx)) {
|
||||
dispatch('watch', obj); // Ask the backend to stop watching the type
|
||||
} else if (all) {
|
||||
getters['watchesOfType'](type).forEach((obj) => {
|
||||
unwatch({ ...obj, stop: true });
|
||||
});
|
||||
reset.push(...getters['watchesOfType'](type));
|
||||
} else if (getters['watchStarted'](obj)) {
|
||||
unwatch(obj);
|
||||
reset.push(obj);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Unwatch watches that are incompatible with the new type
|
||||
*/
|
||||
unwatchIncompatible({
|
||||
state, dispatch, getters, commit
|
||||
}, messageMeta) {
|
||||
// Step 1 - Clear incompatible watches that have STARTED
|
||||
const watchesOfType = getters.watchesOfType(messageMeta.type);
|
||||
|
||||
watchesOfType
|
||||
.filter((entry) => shouldUnwatchIncompatible(messageMeta, entry.mode))
|
||||
.forEach((entry) => {
|
||||
dispatch('unwatch', entry);
|
||||
reset.forEach((obj) => {
|
||||
unwatch(obj);
|
||||
// Ensure anything pinging in the background is stopped
|
||||
dispatch('resetWatchBackOff', {
|
||||
type,
|
||||
compareWatches: (entry) => objKey === keyForSubscribe(entry)
|
||||
});
|
||||
});
|
||||
|
||||
// Step 2 - Clear inError state for incompatible watches (these won't appear in watchesOfType / state.started)
|
||||
// (important for the backoff case... for example backoff request to find would overwrite findPage res if executed after nav from detail to list)
|
||||
const inErrorOfType = Object.values(state.inError || {})
|
||||
.filter((error) => error.obj.type === messageMeta.type);
|
||||
|
||||
inErrorOfType
|
||||
.filter((error) => shouldUnwatchIncompatible(messageMeta, error.obj.mode))
|
||||
.forEach((error) => clearInError({ getters, commit }, error));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -704,7 +692,7 @@ const sharedActions = {
|
|||
if (resetStarted && state.started?.length) {
|
||||
let entries = state.started;
|
||||
|
||||
if (type) { // Filter out ones for types we're no interested in
|
||||
if (type || compareWatches) { // Filter out ones for types we're no interested in
|
||||
entries = entries
|
||||
.filter((obj) => compareWatches ? compareWatches(obj) : obj.type === type);
|
||||
}
|
||||
|
|
@ -718,7 +706,7 @@ const sharedActions = {
|
|||
// however resource.stop clears `started` and we need the settings to persist over start-->error-->stop-->start cycles
|
||||
let entries = Object.values(state.inError || {});
|
||||
|
||||
if (type) { // Filter out ones for types we're no interested in
|
||||
if (type || compareWatches) { // Filter out ones for types we're no interested in
|
||||
entries = entries
|
||||
.filter((error) => compareWatches ? compareWatches(error.obj) : error.obj.type === type);
|
||||
}
|
||||
|
|
@ -817,6 +805,7 @@ const defaultActions = {
|
|||
|
||||
if ( getters.schemaFor(entry.type) ) {
|
||||
commit('setWatchStopped', entry);
|
||||
// Delete the cached socket revision, forcing the watch to get latest revision from cached resources instead
|
||||
delete entry.revision;
|
||||
promises.push(dispatch('watch', entry));
|
||||
}
|
||||
|
|
@ -901,11 +890,7 @@ const defaultActions = {
|
|||
}
|
||||
|
||||
// Should any listeners be notified of this request for them to kick off their own event handling?
|
||||
const listener = listeners[STEVE_WATCH_MODE.RESOURCE_CHANGES].find((sl) => equivalentWatch(sl.params, params));
|
||||
|
||||
if (listener) {
|
||||
Object.values(listener.callbacks).forEach((cb) => cb());
|
||||
}
|
||||
getters.listenerManager.triggerEventListener({ event: STEVE_WATCH_MODE.RESOURCE_CHANGES, params });
|
||||
} else {
|
||||
have = getters['all'](resourceType).slice();
|
||||
|
||||
|
|
@ -1086,10 +1071,16 @@ const defaultActions = {
|
|||
mode: msg.mode,
|
||||
};
|
||||
|
||||
// Unwatch watches that are incompatible with the new type
|
||||
// This is mainly to prevent the cache being polluted with resources that aren't compatible with it's aim
|
||||
// For instance if the store/cache for pods contains a namespace X and we watch another namespace Y... we don't want ns X resources added to cache
|
||||
|
||||
// Unwatch incompatible watches
|
||||
state.started.filter((entry) => {
|
||||
if (
|
||||
entry.type === newWatch.type &&
|
||||
entry.namespace !== newWatch.namespace
|
||||
(entry.type === newWatch.type) &&
|
||||
(entry.namespace !== newWatch.namespace) &&
|
||||
(!entry.mode && !newWatch.mode) // mode watches will be handled when they become an issue
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1178,7 +1169,25 @@ const defaultActions = {
|
|||
commit('setWatchStopped', obj);
|
||||
}
|
||||
|
||||
dispatch('watch', obj);
|
||||
// Now re-watch
|
||||
const hasEventListeners = getters.listenerManager.hasEventListeners({ params: obj });
|
||||
const hasStandardWatch = getters.listenerManager.hasStandardWatch({ params: obj });
|
||||
|
||||
dispatch('watch', {
|
||||
...obj,
|
||||
// hasEventListeners && !hasStandardWatch ? false : true
|
||||
// if this watch isn't associated with a normal watch... (there are no listeners, or there are listeners but also a normal watch)
|
||||
standardWatch: !(hasEventListeners && !hasStandardWatch)
|
||||
});
|
||||
|
||||
if (hasEventListeners) {
|
||||
// If there's event listeners always kick them off
|
||||
// - The re-watch associated with normal watches will watch from a revision from it's own cache
|
||||
// - The revision in that cache might be ahead of the state the listeners have, so the watch won't ping something for the listeners to trigger on
|
||||
// - so to work around this whenever we start the watches again trigger off the changes for it
|
||||
// Improvement - we only do one event here (currently the only one supported), could expand to others
|
||||
getters.listenerManager.triggerEventListener({ event: STEVE_WATCH_EVENT_TYPES.CHANGES, params: obj });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -1210,6 +1219,15 @@ const defaultActions = {
|
|||
}
|
||||
}
|
||||
|
||||
const havePage = ctx.getters['havePage'](type);
|
||||
|
||||
if (havePage) {
|
||||
console.warn(`Prevented watch \`resource.change\` data from polluting the cache for type "${ type }" (currently represents a page). To prevent any further issues the watch has been stopped.`, data); // eslint-disable-line no-console
|
||||
ctx.dispatch('unwatch', data);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
queueChange(ctx, msg, true, 'Change');
|
||||
|
||||
const typeOption = ctx.rootGetters['type-map/optionsFor'](type);
|
||||
|
|
@ -1344,6 +1362,7 @@ const defaultMutations = {
|
|||
clearTimeout(state.queueTimer);
|
||||
state.deferredRequests = {};
|
||||
state.queueTimer = null;
|
||||
state.socketListenerManager = new SteveWatchEventListenerManager(state.config.namespace);
|
||||
},
|
||||
|
||||
clearFromQueue(state, type) {
|
||||
|
|
@ -1444,6 +1463,15 @@ const defaultGetters = {
|
|||
|
||||
return revision || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the watch listener manager for this store
|
||||
*
|
||||
* Instance of @SteveWatchEventListenerManager . See it's description for more info
|
||||
*/
|
||||
listenerManager: (state) => {
|
||||
return state.socketListenerManager;
|
||||
},
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,211 @@
|
|||
import { keyForSubscribe } from '@shell/plugins/steve/resourceWatcher';
|
||||
import {
|
||||
SubscribeEventListener, SubscribeEventCallbackArgs, SubscribeEventListenerArgs, SubscribeEventWatch, SubscribeEventWatchArgs,
|
||||
STEVE_WATCH_EVENT_LISTENER_CALLBACK
|
||||
} from '@shell/types/store/subscribe-events.types';
|
||||
import { STEVE_WATCH_EVENT_TYPES, STEVE_WATCH_PARAMS } from '@shell/types/store/subscribe.types';
|
||||
|
||||
type SubscribeEventWatches = { [socketId: string]: SubscribeEventWatch};
|
||||
|
||||
/**
|
||||
* For a specific resource watch, listen for a specific event type and trigger callback when received
|
||||
*
|
||||
* For example, listen for provisioning.cattle.io clusters messages of type resource.changes and trigger callback when received
|
||||
*
|
||||
* Watch - UI is watching a resource type restricted by nothing/id/namespace/selector. For example
|
||||
* - watch all pods
|
||||
* - watch specific pod
|
||||
* - watch pods with specific labels
|
||||
* Event - Rancher socket messages TO the ui. For example
|
||||
* - resource.started
|
||||
* - resource.change
|
||||
* - resource.changes
|
||||
* Listener - listen to events, trigger when received. For example
|
||||
* - listen for resource.changes messages for the all pods watch
|
||||
* Callback - triggered when a listener has heard something
|
||||
* - watch for all pods receives a resource.changes message, it has a listener, listener executes it's callback
|
||||
*
|
||||
* Watch 0:M Events 0:M Listeners 0:M Callbacks
|
||||
*/
|
||||
export class SteveWatchEventListenerManager {
|
||||
private keyForSubscribe({ params }: {params: STEVE_WATCH_PARAMS}): string {
|
||||
return keyForSubscribe(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* collection of ui --> rancher watches. we keep state specific to this class here
|
||||
*/
|
||||
private watches: SubscribeEventWatches = {};
|
||||
|
||||
/**
|
||||
* Not all event types can be listened to are supported, only these
|
||||
*/
|
||||
public readonly supportedEventTypes: STEVE_WATCH_EVENT_TYPES[] = [STEVE_WATCH_EVENT_TYPES.CHANGES];
|
||||
|
||||
/**
|
||||
* Not all event types can be listened to are supported, check if one is
|
||||
*/
|
||||
public isSupportedEventType(type: STEVE_WATCH_EVENT_TYPES): boolean {
|
||||
return !!this.supportedEventTypes.includes(type);
|
||||
}
|
||||
|
||||
/** **** Watches ***********************/
|
||||
|
||||
public getWatch({ params } : SubscribeEventWatchArgs): SubscribeEventWatch {
|
||||
const socketId = this.keyForSubscribe({ params });
|
||||
|
||||
return this.watches[socketId];
|
||||
}
|
||||
|
||||
private initialiseWatch({ params }: SubscribeEventWatchArgs): SubscribeEventWatch {
|
||||
const socketId = this.keyForSubscribe({ params });
|
||||
|
||||
this.watches[socketId] = {
|
||||
hasStandardWatch: false,
|
||||
listeners: []
|
||||
};
|
||||
|
||||
return this.watches[socketId];
|
||||
}
|
||||
|
||||
/**
|
||||
* This is just tidying the entry
|
||||
*
|
||||
* All watches associated with this type should be unwatched
|
||||
*/
|
||||
private deleteWatch({ params } : SubscribeEventWatchArgs) {
|
||||
const socketId = this.keyForSubscribe({ params });
|
||||
|
||||
delete this.watches[socketId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Is there a standard non-listener watch for this this type
|
||||
*/
|
||||
public hasStandardWatch({ params } : SubscribeEventWatchArgs): boolean {
|
||||
const socketId = this.keyForSubscribe({ params });
|
||||
|
||||
return this.watches[socketId]?.hasStandardWatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set if this type has a standard non-listener watch associated with it
|
||||
*/
|
||||
public setStandardWatch({ standardWatch, args }: { standardWatch: boolean, args: SubscribeEventWatchArgs}) {
|
||||
const { params } = args;
|
||||
|
||||
let watch = this.getWatch({ params });
|
||||
|
||||
if (!watch) {
|
||||
if (!standardWatch) {
|
||||
// no point setting a non-existent watch as not started
|
||||
return;
|
||||
}
|
||||
watch = this.initialiseWatch({ params });
|
||||
}
|
||||
|
||||
watch.hasStandardWatch = standardWatch;
|
||||
|
||||
// if we've just set this to false and there's no listeners, tidy up the entry
|
||||
if (!watch.hasStandardWatch && watch.listeners.length === 0) {
|
||||
this.deleteWatch({ params });
|
||||
}
|
||||
}
|
||||
|
||||
/** **** Listeners ***********************/
|
||||
|
||||
public hasEventListeners({ params }: SubscribeEventWatchArgs): boolean {
|
||||
const socketId = this.keyForSubscribe({ params });
|
||||
const watch = this.watches[socketId];
|
||||
const listener = watch?.listeners.find((l) => Object.values(l.callbacks).length > 0);
|
||||
|
||||
return !!listener;
|
||||
}
|
||||
|
||||
public getEventListener({ entryOnly, args }: { entryOnly?: boolean, args: SubscribeEventListenerArgs}): SubscribeEventListener | null {
|
||||
const { params, event } = args;
|
||||
const socketId = this.keyForSubscribe({ params });
|
||||
const watch = this.watches[socketId];
|
||||
|
||||
if (watch) {
|
||||
const listener = watch.listeners.find((w) => w.event === event);
|
||||
|
||||
if (listener && (entryOnly || !!Object.keys(listener?.callbacks || {}).length)) {
|
||||
return listener;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public addEventListener({ event, params }: SubscribeEventListenerArgs): SubscribeEventListener {
|
||||
if (!event) {
|
||||
throw new Error(`Cannot add a socket watch event listener if there's no event to listen to`);
|
||||
}
|
||||
|
||||
let watch = this.getWatch({ params });
|
||||
|
||||
if (!watch) {
|
||||
watch = this.initialiseWatch({ params });
|
||||
}
|
||||
|
||||
let listener = this.getEventListener({ entryOnly: true, args: { event, params } });
|
||||
|
||||
if (!listener) {
|
||||
listener = {
|
||||
event,
|
||||
callbacks: { },
|
||||
};
|
||||
watch.listeners.push(listener);
|
||||
}
|
||||
|
||||
return listener;
|
||||
}
|
||||
|
||||
public triggerEventListener({ event, params }: SubscribeEventListenerArgs) {
|
||||
const eventWatcher = this.getEventListener({ entryOnly: false, args: { event, params } });
|
||||
|
||||
if (eventWatcher) {
|
||||
Object.values(eventWatcher.callbacks).forEach((cb) => {
|
||||
cb();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public triggerAllEventListeners({ params }: SubscribeEventWatchArgs) {
|
||||
const watch = this.getWatch({ params });
|
||||
|
||||
watch.listeners.forEach((l) => {
|
||||
Object.values(l.callbacks || {}).forEach((cb) => cb());
|
||||
});
|
||||
}
|
||||
|
||||
/** **** Callbacks ***********************/
|
||||
|
||||
public addEventListenerCallback({ callback, args }: {
|
||||
callback: STEVE_WATCH_EVENT_LISTENER_CALLBACK,
|
||||
args: SubscribeEventCallbackArgs
|
||||
}): SubscribeEventListener {
|
||||
const { params, event, id } = args;
|
||||
const eventWatcher = this.addEventListener({ event, params });
|
||||
|
||||
if (!eventWatcher.callbacks[id]) {
|
||||
eventWatcher.callbacks[id] = callback;
|
||||
}
|
||||
|
||||
return eventWatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is just tidying the entry
|
||||
*
|
||||
* All watches associated with this type should be unwatched
|
||||
*/
|
||||
public removeEventListenerCallback({ event, params, id }: SubscribeEventCallbackArgs) {
|
||||
const existing = this.getEventListener({ args: { event, params } });
|
||||
|
||||
if (existing) {
|
||||
delete existing.callbacks[id];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -40,6 +40,7 @@ import { isDevBuild } from '@shell/utils/version';
|
|||
import { markRaw } from 'vue';
|
||||
import paginationUtils from '@shell/utils/pagination-utils';
|
||||
import { addReleaseNotesNotification } from '@shell/utils/release-notes';
|
||||
import sideNavService from '@shell/components/nav/TopLevelMenu.helper';
|
||||
|
||||
// Disables strict mode for all store instances to prevent warning about changing state outside of mutations
|
||||
// because it's more efficient to do that sometimes.
|
||||
|
|
@ -260,11 +261,8 @@ export const state = () => {
|
|||
$router: markRaw({}),
|
||||
$route: markRaw({}),
|
||||
$plugin: markRaw({}),
|
||||
/**
|
||||
* Cache state of side nav clusters. This avoids flickering when the user changes pages and the side nav component re-renders
|
||||
*/
|
||||
sideNavCache: undefined,
|
||||
showWorkspaceSwitcher: true,
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -629,10 +627,6 @@ export const getters = {
|
|||
return `${ base }/latest`;
|
||||
},
|
||||
|
||||
sideNavCache(state) {
|
||||
return state.sideNavCache;
|
||||
},
|
||||
|
||||
...gcGetters
|
||||
};
|
||||
|
||||
|
|
@ -773,10 +767,6 @@ export const mutations = {
|
|||
state.$plugin = markRaw(pluginDefinition || {});
|
||||
},
|
||||
|
||||
setSideNavCache(state, sideNavCache) {
|
||||
state.sideNavCache = sideNavCache;
|
||||
},
|
||||
|
||||
showWorkspaceSwitcher(state, value) {
|
||||
state.showWorkspaceSwitcher = value;
|
||||
},
|
||||
|
|
@ -1189,6 +1179,8 @@ export const actions = {
|
|||
}
|
||||
});
|
||||
|
||||
sideNavService.reset();
|
||||
|
||||
await dispatch('management/unsubscribe');
|
||||
commit('managementChanged', { ready: false });
|
||||
commit('management/reset');
|
||||
|
|
@ -1310,10 +1302,6 @@ export const actions = {
|
|||
});
|
||||
},
|
||||
|
||||
setSideNavCache({ commit }, sideNavCache) {
|
||||
commit('setSideNavCache', sideNavCache);
|
||||
},
|
||||
|
||||
showWorkspaceSwitcher({ commit }, value) {
|
||||
commit('showWorkspaceSwitcher', value);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
import { STEVE_WATCH_EVENT_TYPES, STEVE_WATCH_EVENT_TYPES_NAMES, STEVE_WATCH_PARAMS } from '@shell/types/store/subscribe.types';
|
||||
|
||||
/**
|
||||
* Common params used when a watcher adds or removes a listener to a watch
|
||||
*/
|
||||
export interface STEVE_WATCH_EVENT_PARAMS_COMMON {
|
||||
event: STEVE_WATCH_EVENT_TYPES,
|
||||
id: string,
|
||||
/**
|
||||
* of type @STEVE_WATCH_PARAMS
|
||||
*/
|
||||
params: STEVE_WATCH_PARAMS,
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes when a watch event has a listener and it's triggered
|
||||
*/
|
||||
export type STEVE_WATCH_EVENT_LISTENER_CALLBACK = () => void
|
||||
|
||||
/**
|
||||
* Common params used when a watcher adds a listener to a watch
|
||||
*/
|
||||
export interface STEVE_WATCH_EVENT_PARAMS extends STEVE_WATCH_EVENT_PARAMS_COMMON {
|
||||
callback: STEVE_WATCH_EVENT_LISTENER_CALLBACK,
|
||||
}
|
||||
|
||||
/**
|
||||
* Common params used when a watcher removes a listener from a watch
|
||||
*/
|
||||
export type STEVE_UNWATCH_EVENT_PARAMS = STEVE_WATCH_EVENT_PARAMS_COMMON
|
||||
|
||||
/**
|
||||
* Common properties for identifying a subscribe watcher
|
||||
*/
|
||||
export interface SubscribeEventWatchArgs {
|
||||
params: STEVE_WATCH_PARAMS,
|
||||
}
|
||||
|
||||
/**
|
||||
* Common properties for identifying a subscribe watcher's listeners
|
||||
*/
|
||||
export interface SubscribeEventListenerArgs extends SubscribeEventWatchArgs {
|
||||
event: STEVE_WATCH_EVENT_TYPES_NAMES,
|
||||
}
|
||||
|
||||
/**
|
||||
* Common properties for identifying a subscribe watcher's listener
|
||||
*/
|
||||
export interface SubscribeEventCallbackArgs extends SubscribeEventListenerArgs {
|
||||
id: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a subscribe watcher listener
|
||||
*/
|
||||
export interface SubscribeEventListener {
|
||||
event: STEVE_WATCH_EVENT_TYPES_NAMES,
|
||||
callbacks: { [id: string]: STEVE_WATCH_EVENT_LISTENER_CALLBACK},
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a subscribe watcher
|
||||
*/
|
||||
export type SubscribeEventWatch = {
|
||||
/**
|
||||
* is there a standard non-listener watch for this type (i.e. vanilla watch)
|
||||
*/
|
||||
hasStandardWatch: boolean,
|
||||
listeners: SubscribeEventListener[],
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ export enum STEVE_WATCH_MODE {
|
|||
/* eslint-enable no-unused-vars */
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
export enum STEVE_WATCH_EVENT {
|
||||
export enum STEVE_WATCH_EVENT_TYPES {
|
||||
START = 'resource.start',
|
||||
CREATE = 'resource.create',
|
||||
CHANGE = 'resource.change',
|
||||
|
|
@ -17,6 +17,11 @@ export enum STEVE_WATCH_EVENT {
|
|||
}
|
||||
/* eslint-enable no-unused-vars */
|
||||
|
||||
export type STEVE_WATCH_EVENT_TYPES_NAMES = `${ STEVE_WATCH_EVENT_TYPES }`;
|
||||
|
||||
/**
|
||||
* The content of the web socket messages sent (and partially received back from) steve
|
||||
*/
|
||||
export interface STEVE_WATCH_PARAMS {
|
||||
type: string,
|
||||
selector?: string,
|
||||
|
|
@ -27,24 +32,3 @@ export interface STEVE_WATCH_PARAMS {
|
|||
force?: boolean,
|
||||
mode?: STEVE_WATCH_MODE
|
||||
}
|
||||
|
||||
export type STEVE_WATCH_EVENT_LISTENER_CALLBACK = () => void
|
||||
export interface STEVE_WATCH_EVENT_LISTENER {
|
||||
params: STEVE_WATCH_PARAMS,
|
||||
callbacks: { [id: string]: STEVE_WATCH_EVENT_LISTENER_CALLBACK},
|
||||
}
|
||||
|
||||
export interface STEVE_WATCH_EVENT_PARAMS_COMMON {
|
||||
event: STEVE_WATCH_EVENT,
|
||||
id: string,
|
||||
/**
|
||||
* of type @STEVE_WATCH_PARAMS
|
||||
*/
|
||||
params: STEVE_WATCH_PARAMS,
|
||||
}
|
||||
|
||||
export interface STEVE_WATCH_EVENT_PARAMS extends STEVE_WATCH_EVENT_PARAMS_COMMON {
|
||||
callback: STEVE_WATCH_EVENT_LISTENER_CALLBACK,
|
||||
}
|
||||
|
||||
export type STEVE_UNWATCH_EVENT_PARAMS = STEVE_WATCH_EVENT_PARAMS_COMMON
|
||||
|
|
|
|||
|
|
@ -2,10 +2,9 @@ import paginationUtils from '@shell/utils/pagination-utils';
|
|||
import { PaginationArgs, PaginationResourceContext } from '@shell/types/store/pagination.types';
|
||||
import { VuexStore } from '@shell/types/store/vuex';
|
||||
import { ActionFindPageArgs, ActionFindPageTransientResult } from '@shell/types/store/dashboard-store.types';
|
||||
import {
|
||||
STEVE_WATCH_EVENT_LISTENER_CALLBACK, STEVE_UNWATCH_EVENT_PARAMS, STEVE_WATCH_EVENT, STEVE_WATCH_EVENT_PARAMS, STEVE_WATCH_EVENT_PARAMS_COMMON, STEVE_WATCH_MODE
|
||||
} from '@shell/types/store/subscribe.types';
|
||||
import { STEVE_WATCH_EVENT_TYPES, STEVE_WATCH_MODE } from '@shell/types/store/subscribe.types';
|
||||
import { Reactive, reactive } from 'vue';
|
||||
import { STEVE_UNWATCH_EVENT_PARAMS, STEVE_WATCH_EVENT_LISTENER_CALLBACK, STEVE_WATCH_EVENT_PARAMS, STEVE_WATCH_EVENT_PARAMS_COMMON } from '@shell/types/store/subscribe-events.types';
|
||||
|
||||
interface Args {
|
||||
$store: VuexStore,
|
||||
|
|
@ -92,7 +91,7 @@ class PaginationWrapper<T extends object> {
|
|||
// Watch
|
||||
if (this.onChange && !this.steveWatchParams) {
|
||||
this.steveWatchParams = {
|
||||
event: STEVE_WATCH_EVENT.CHANGES,
|
||||
event: STEVE_WATCH_EVENT_TYPES.CHANGES,
|
||||
id: this.id,
|
||||
params: {
|
||||
type: this.enabledFor.resource?.id as string,
|
||||
|
|
@ -126,7 +125,7 @@ class PaginationWrapper<T extends object> {
|
|||
}
|
||||
const watchParams: STEVE_WATCH_EVENT_PARAMS = {
|
||||
...this.steveWatchParams,
|
||||
callback: this.onChange as STEVE_WATCH_EVENT_LISTENER_CALLBACK, // we must have it by now
|
||||
callback: this.onChange as STEVE_WATCH_EVENT_LISTENER_CALLBACK, // we must have onChange by now
|
||||
};
|
||||
|
||||
await this.$store.dispatch(`${ this.enabledFor.store }/watchEvent`, watchParams);
|
||||
|
|
@ -134,8 +133,7 @@ class PaginationWrapper<T extends object> {
|
|||
|
||||
private async unWatch() {
|
||||
if (!this.steveWatchParams) {
|
||||
console.error('Calling unWatch but no watch params created'); // eslint-disable-line no-console
|
||||
|
||||
// We're unwatching before we've made the initial request
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue