diff --git a/shell/mixins/__tests__/brand.spec.ts b/shell/mixins/__tests__/brand.spec.ts new file mode 100644 index 0000000000..05e6b2dec2 --- /dev/null +++ b/shell/mixins/__tests__/brand.spec.ts @@ -0,0 +1,170 @@ +import { mount } from '@vue/test-utils'; +import { CATALOG, MANAGEMENT } from '@shell/config/types'; +import Brand from '@shell/mixins/brand'; + +describe('brandMixin', () => { + const createWrapper = (vaiOn = false) => { + const Component = { + template: '
', + mixins: [Brand], + }; + + const data = { + apps: null, haveAppsAndSettings: null, canPaginate: false + }; + + const store = { + dispatch: (action, ...args) => { + switch (action) { + case 'management/findAll': + if (args[0] === MANAGEMENT.SETTING) { + return []; + } + if (args[0] === CATALOG.APP) { + return []; + } + break; + } + }, + getters: { + 'auth/loggedIn': () => true, + 'auth/fromHeader': () => false, + 'management/byId': () => undefined, + 'management/canList': () => () => true, + 'management/schemaFor': (type: string) => { + switch (type) { + case MANAGEMENT.SETTING: + return { linkFor: () => undefined }; + } + }, + 'management/generation': () => undefined, + 'management/paginationEnabled': () => vaiOn, + 'management/all': (type: string) => { + switch (type) { + case MANAGEMENT.SETTING: + return []; + } + }, + } + }; + + const wrapper = mount( + Component, + { + data: () => data, + global: { mocks: { $store: store } } + }); + const spyManagementFindAll = jest.spyOn(store, 'dispatch'); + + return { + wrapper, + store, + spyManagementFindAll, + }; + }; + + describe('should make correct requests', () => { + it('vai off', async() => { + const { wrapper, spyManagementFindAll } = createWrapper(false); + + // NOTE - wrapper.vm.$options.fetch() doesn't work + await wrapper.vm.$options.fetch.apply(wrapper.vm); + + // wrapper.vm.$nextTick(); + expect(spyManagementFindAll).toHaveBeenNthCalledWith(1, 'management/findAll', { + type: MANAGEMENT.SETTING, + opt: { + load: 'multi', redirectUnauthorized: false, url: `/v1/${ MANAGEMENT.SETTING }s` + } + }); + expect(spyManagementFindAll).toHaveBeenNthCalledWith(2, 'management/findAll', { type: CATALOG.APP }); + }); + + it('vai on', async() => { + const { wrapper, spyManagementFindAll } = createWrapper(true); + + // NOTE - wrapper.vm.$options.fetch() doesn't work + await wrapper.vm.$options.fetch.apply(wrapper.vm); + + expect(spyManagementFindAll).toHaveBeenNthCalledWith(1, 'management/findAll', { + type: MANAGEMENT.SETTING, + opt: { + load: 'multi', url: `/v1/${ MANAGEMENT.SETTING }s`, redirectUnauthorized: false + } + }); + expect(spyManagementFindAll).toHaveBeenNthCalledWith(2, 'management/findPage', { + type: CATALOG.APP, + opt: { + pagination: { + filters: [{ + equals: true, + fields: [{ + equals: true, exact: true, field: 'metadata.name', value: 'rancher-csp-adapter' + }, { + equals: true, exact: true, field: 'metadata.name', value: 'rancher-csp-billing-adapter' + }], + param: 'filter' + }], + labelSelector: undefined, + page: null, + pageSize: null, + projectsOrNamespaces: [], + sort: [] + } + } + }); + }); + }); + + describe('cspAdapter', () => { + it('should have correct csp values (off)', async() => { + const { wrapper, store } = createWrapper(); + + const spyManagementFindAll = jest.spyOn(store, 'dispatch').mockImplementation((_, options) => { + const { type } = options as any; + + if (type === MANAGEMENT.SETTING) { + return Promise.resolve([]); + } + if (type === CATALOG.APP) { + return Promise.resolve([]); + } + + return Promise.reject(new Error('reason')); + }); + + // NOTE - wrapper.vm.$options.fetch() doesn't work + await wrapper.vm.$options.fetch.apply(wrapper.vm, []); + + expect(spyManagementFindAll).toHaveBeenCalledTimes(2); + + expect(wrapper.vm.canCalcCspAdapter).toBeTruthy(); + expect(wrapper.vm.cspAdapter).toBeFalsy(); + }); + + it.each(['rancher-csp-adapter', 'rancher-csp-billing-adapter'])('should have correct csp values (on - %p )', async(chartName) => { + const { wrapper, store } = createWrapper(); + + const spyManagementFindAll = jest.spyOn(store, 'dispatch').mockImplementation((_, options) => { + const { type } = options as any; + + if (type === MANAGEMENT.SETTING) { + return Promise.resolve([]); + } + if (type === CATALOG.APP) { + return Promise.resolve([{ metadata: { name: chartName } }]); + } + + return Promise.reject(new Error('reason')); + }); + + // NOTE - wrapper.vm.$options.fetch() doesn't work + await wrapper.vm.$options.fetch.apply(wrapper.vm, []); + + expect(spyManagementFindAll).toHaveBeenCalledTimes(2); + + expect(wrapper.vm.canCalcCspAdapter).toBeTruthy(); + expect(wrapper.vm.cspAdapter).toBeTruthy(); + }); + }); +}); diff --git a/shell/mixins/brand.js b/shell/mixins/brand.js index 87455c4a53..38adf7576c 100644 --- a/shell/mixins/brand.js +++ b/shell/mixins/brand.js @@ -1,39 +1,38 @@ import { mapGetters } from 'vuex'; -import { CATALOG, MANAGEMENT } from '@shell/config/types'; +import { MANAGEMENT } from '@shell/config/types'; import { SETTING } from '@shell/config/settings'; import { createCssVars } from '@shell/utils/color'; import { setTitle } from '@shell/config/private-label'; import { setFavIcon, haveSetFavIcon } from '@shell/utils/favicon'; - -const cspAdaptorApp = ['rancher-csp-adapter', 'rancher-csp-billing-adapter']; - -export const hasCspAdapter = (apps) => { - return apps?.find((a) => cspAdaptorApp.includes(a.metadata?.name)); -}; +import { allHash } from '@shell/utils/promise'; +import { fetchInitialSettings } from '@shell/utils/settings'; +import CspAdapterUtils from '@shell/utils/cspAdaptor'; export default { async fetch() { - // For the login page, the schemas won't be loaded - we don't need the apps in this case try { - if (this.$store.getters['management/canList'](CATALOG.APP)) { - this.apps = await this.$store.dispatch('management/findAll', { type: CATALOG.APP }); - } - } catch (e) {} + const res = await allHash({ + // Ensure we read the settings even when we are not authenticated + globalSettings: fetchInitialSettings(this.$store), + apps: CspAdapterUtils.fetchCspAdaptorApp(this.$store), + }); - // Ensure we read the settings even when we are not authenticated - try { // The favicon is implicitly dependent on the initial settings having already been fetched if (!haveSetFavIcon()) { setFavIcon(this.$store); } - } catch (e) {} + + this.apps = res.apps; + } catch (e) { } // Setting this up front will remove `computed` churn, and we only care that we've initialised them this.haveAppsAndSettings = !!this.apps && !!this.globalSettings; }, data() { - return { apps: null, haveAppsAndSettings: null }; + return { + apps: null, haveAppsAndSettings: null, canPaginate: false + }; }, computed: { @@ -87,7 +86,7 @@ export default { // Note! this used to be `findBy(this.app)` however for that case we lost reactivity on the collection // (computed fires before fetch, fetch happens and update apps, computed would not fire again - even with vue.set) // So use `.find` in method instead - return hasCspAdapter(this.apps); + return CspAdapterUtils.hasCspAdapter({ $store: this.$store, apps: this.apps }); }, canCalcCspAdapter() { diff --git a/shell/pages/support/index.vue b/shell/pages/support/index.vue index 7c486ee7db..792324e5fa 100644 --- a/shell/pages/support/index.vue +++ b/shell/pages/support/index.vue @@ -2,13 +2,13 @@ import BannerGraphic from '@shell/components/BannerGraphic'; import IndentedPanel from '@shell/components/IndentedPanel'; import CommunityLinks from '@shell/components/CommunityLinks'; -import { CATALOG, MANAGEMENT } from '@shell/config/types'; +import { MANAGEMENT } from '@shell/config/types'; import { getVendor } from '@shell/config/private-label'; import { SETTING } from '@shell/config/settings'; import { addParam } from '@shell/utils/url'; import { isRancherPrime } from '@shell/config/version'; -import { hasCspAdapter } from '@shell/mixins/brand'; import TabTitle from '@shell/components/TabTitle'; +import CspAdapterUtils from '@shell/utils/cspAdaptor'; export default { @@ -42,9 +42,7 @@ export default { return setting; }; - if ( this.$store.getters['management/canList'](CATALOG.APP) ) { - this.apps = await this.$store.dispatch('management/findAll', { type: CATALOG.APP }); - } + this.apps = await CspAdapterUtils.fetchCspAdaptorApp(this.$store); this.brandSetting = await fetchOrCreateSetting(SETTING.BRAND, ''); this.serverUrlSetting = await fetchOrCreateSetting(SETTING.SERVER_URL, ''); this.uiIssuesSetting = await this.$store.dispatch('management/find', { type: MANAGEMENT.SETTING, id: SETTING.ISSUES }); @@ -75,7 +73,7 @@ export default { computed: { cspAdapter() { - return hasCspAdapter(this.apps); + return CspAdapterUtils.hasCspAdapter({ $store: this.$store, apps: this.apps }); }, hasSupport() { diff --git a/shell/plugins/steve/steve-pagination-utils.ts b/shell/plugins/steve/steve-pagination-utils.ts index c16d4b9dd4..8f429e6b30 100644 --- a/shell/plugins/steve/steve-pagination-utils.ts +++ b/shell/plugins/steve/steve-pagination-utils.ts @@ -667,19 +667,19 @@ export const PAGINATION_SETTINGS_STORE_DEFAULTS: PaginationSettingsStore = { } } }, - // Disabled due to https://github.com/rancher/dashboard/issues/14493 - // management: { - // resources: { - // enableAll: false, - // enableSome: { - // enabled: [ - // { resource: CAPI.RANCHER_CLUSTER, context: ['home', 'side-bar'] }, - // { resource: MANAGEMENT.CLUSTER, context: ['side-bar'] }, - // ], - // generic: false, - // } - // } - // } + management: { + resources: { + 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: CATALOG.APP, context: ['branding'] }, + ], + generic: false, + } + } + } }; export default new StevePaginationUtils(); diff --git a/shell/utils/cspAdaptor.ts b/shell/utils/cspAdaptor.ts new file mode 100644 index 0000000000..6bbffb3cb1 --- /dev/null +++ b/shell/utils/cspAdaptor.ts @@ -0,0 +1,51 @@ +// For testing these could be changed to something like... + +import { CATALOG } from '@shell/config/types'; +import { FilterArgs, PaginationFilterField, PaginationParamFilter } from '@shell/types/store/pagination.types'; +import { VuexStore } from '@shell/types/store/vuex'; + +const CSP_ADAPTER_APPS = ['rancher-csp-adapter', 'rancher-csp-billing-adapter']; +// For testing above line could be replaced with below line... +// const cspAdaptorApp = ['rancher-webhooka', 'rancher-webhook']; + +/** + * Helpers in order to + */ +class CspAdapterUtils { + static canPagination($store: VuexStore): boolean { + return $store.getters[`management/paginationEnabled`]({ id: CATALOG.APP, context: 'branding' }); + } + + static fetchCspAdaptorApp($store: VuexStore): Promise { + // For the login page, the schemas won't be loaded - we don't need the apps in this case + if ($store.getters['management/canList'](CATALOG.APP)) { + if (CspAdapterUtils.canPagination($store)) { + // Restrict the amount of apps we need to fetch + return $store.dispatch('management/findPage', { + type: CATALOG.APP, + opt: { // Of type ActionFindPageArgs + pagination: new FilterArgs({ + filters: PaginationParamFilter.createMultipleFields(CSP_ADAPTER_APPS.map( + (t) => new PaginationFilterField({ + field: 'metadata.name', + value: t, + }) + )), + }) + } + }); + } + + return $store.dispatch('management/findAll', { type: CATALOG.APP }); + } + + return Promise.resolve([]); + } + + static hasCspAdapter({ $store, apps }: { $store: VuexStore, apps: any[]}): Object { + // In theory this should contain the filtered apps when pagination is on, and all apps when off. Keep filtering though in both cases just in case + return apps?.find((a) => CSP_ADAPTER_APPS.includes(a.metadata?.name)); + } +} + +export default CspAdapterUtils;