@rancher/shell 3.0.8-rc.8 → 3.0.8-rc.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/assets/brand/suse/dark/rancher-logo.svg +1 -64
  2. package/assets/translations/en-us.yaml +9 -1
  3. package/components/BackLink.vue +8 -0
  4. package/components/BannerGraphic.vue +1 -5
  5. package/components/BrandImage.vue +17 -6
  6. package/components/Cron/CronExpressionEditor.vue +1 -1
  7. package/components/Cron/CronExpressionEditorModal.vue +1 -1
  8. package/components/Drawer/ResourceDetailDrawer/ConfigTab.vue +1 -0
  9. package/components/Drawer/ResourceDetailDrawer/index.vue +1 -0
  10. package/components/Drawer/ResourceDetailDrawer/types.ts +2 -1
  11. package/components/Questions/__tests__/index.test.ts +159 -0
  12. package/components/Resource/Detail/Metadata/Annotations/index.vue +2 -2
  13. package/components/Resource/Detail/Metadata/Labels/index.vue +2 -2
  14. package/components/Resource/Detail/Metadata/index.vue +3 -3
  15. package/components/Resource/Detail/composables.ts +2 -2
  16. package/components/Tabbed/__tests__/index.test.ts +86 -0
  17. package/components/auth/SelectPrincipal.vue +24 -6
  18. package/components/auth/__tests__/SelectPrincipal.test.ts +119 -0
  19. package/components/formatter/InternalExternalIP.vue +4 -1
  20. package/components/formatter/__tests__/InternalExternalIP.test.ts +1 -1
  21. package/components/templates/standalone.vue +1 -1
  22. package/composables/useI18n.ts +10 -1
  23. package/config/__test__/uiplugins.test.ts +309 -0
  24. package/config/labels-annotations.js +1 -0
  25. package/config/product/explorer.js +3 -1
  26. package/config/router/routes.js +6 -2
  27. package/config/types.js +7 -0
  28. package/config/uiplugins.js +46 -2
  29. package/core/__test__/extension-manager-impl.test.js +236 -0
  30. package/core/extension-manager-impl.js +23 -6
  31. package/core/types-provisioning.ts +1 -1
  32. package/detail/provisioning.cattle.io.cluster.vue +1 -0
  33. package/dialog/DeveloperLoadExtensionDialog.vue +12 -3
  34. package/dialog/RollbackWorkloadDialog.vue +2 -5
  35. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +2 -2
  36. package/edit/autoscaling.horizontalpodautoscaler/index.vue +1 -0
  37. package/edit/configmap.vue +1 -0
  38. package/edit/constraints.gatekeeper.sh.constraint/index.vue +1 -0
  39. package/edit/fleet.cattle.io.helmop.vue +6 -6
  40. package/edit/helm.cattle.io.projecthelmchart.vue +1 -0
  41. package/edit/k8s.cni.cncf.io.networkattachmentdefinition.vue +1 -0
  42. package/edit/logging-flow/index.vue +1 -0
  43. package/edit/logging.banzaicloud.io.output/index.vue +1 -0
  44. package/edit/management.cattle.io.fleetworkspace.vue +1 -1
  45. package/edit/management.cattle.io.project.vue +1 -0
  46. package/edit/monitoring.coreos.com.alertmanagerconfig/index.vue +4 -1
  47. package/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig.vue +2 -1
  48. package/edit/monitoring.coreos.com.prometheusrule/index.vue +1 -0
  49. package/edit/monitoring.coreos.com.receiver/index.vue +2 -1
  50. package/edit/monitoring.coreos.com.route.vue +1 -1
  51. package/edit/namespace.vue +1 -0
  52. package/edit/networking.istio.io.destinationrule/index.vue +1 -0
  53. package/edit/networking.k8s.io.ingress/index.vue +1 -0
  54. package/edit/networking.k8s.io.networkpolicy/PolicyRules.vue +1 -0
  55. package/edit/networking.k8s.io.networkpolicy/index.vue +1 -0
  56. package/edit/node.vue +1 -0
  57. package/edit/persistentvolume/index.vue +27 -22
  58. package/edit/persistentvolume/plugins/awsElasticBlockStore.vue +13 -14
  59. package/edit/persistentvolume/plugins/azureDisk.vue +49 -48
  60. package/edit/persistentvolume/plugins/azureFile.vue +15 -14
  61. package/edit/persistentvolume/plugins/cephfs.vue +15 -14
  62. package/edit/persistentvolume/plugins/cinder.vue +15 -14
  63. package/edit/persistentvolume/plugins/csi.vue +18 -16
  64. package/edit/persistentvolume/plugins/fc.vue +13 -14
  65. package/edit/persistentvolume/plugins/flexVolume.vue +15 -14
  66. package/edit/persistentvolume/plugins/flocker.vue +1 -3
  67. package/edit/persistentvolume/plugins/gcePersistentDisk.vue +13 -14
  68. package/edit/persistentvolume/plugins/glusterfs.vue +15 -14
  69. package/edit/persistentvolume/plugins/hostPath.vue +40 -39
  70. package/edit/persistentvolume/plugins/iscsi.vue +13 -14
  71. package/edit/persistentvolume/plugins/local.vue +1 -3
  72. package/edit/persistentvolume/plugins/longhorn.vue +23 -22
  73. package/edit/persistentvolume/plugins/nfs.vue +15 -14
  74. package/edit/persistentvolume/plugins/photonPersistentDisk.vue +1 -14
  75. package/edit/persistentvolume/plugins/portworxVolume.vue +15 -14
  76. package/edit/persistentvolume/plugins/quobyte.vue +15 -14
  77. package/edit/persistentvolume/plugins/rbd.vue +15 -14
  78. package/edit/persistentvolume/plugins/scaleIO.vue +15 -14
  79. package/edit/persistentvolume/plugins/storageos.vue +15 -14
  80. package/edit/persistentvolume/plugins/vsphereVolume.vue +1 -3
  81. package/edit/provisioning.cattle.io.cluster/rke2.vue +1 -0
  82. package/edit/secret/index.vue +1 -1
  83. package/edit/service.vue +1 -0
  84. package/edit/serviceaccount.vue +1 -0
  85. package/edit/storage.k8s.io.storageclass/index.vue +1 -0
  86. package/edit/workload/index.vue +2 -1
  87. package/edit/workload/mixins/workload.js +1 -1
  88. package/initialize/App.vue +4 -4
  89. package/initialize/install-plugins.js +17 -2
  90. package/mixins/__tests__/brand.spec.ts +2 -2
  91. package/mixins/brand.js +1 -7
  92. package/mixins/create-edit-view/index.js +5 -0
  93. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +112 -5
  94. package/models/management.cattle.io.cluster.js +21 -3
  95. package/models/provisioning.cattle.io.cluster.js +21 -9
  96. package/package.json +5 -4
  97. package/pages/auth/login.vue +1 -3
  98. package/pages/c/_cluster/apps/charts/__tests__/chart.test.ts +135 -0
  99. package/pages/c/_cluster/apps/charts/chart.vue +33 -15
  100. package/pages/c/_cluster/explorer/index.vue +8 -6
  101. package/pages/c/_cluster/manager/hostedprovider/index.vue +12 -6
  102. package/pages/c/_cluster/settings/brand.vue +1 -1
  103. package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +7 -0
  104. package/pages/c/_cluster/uiplugins/catalogs.vue +147 -0
  105. package/pages/c/_cluster/uiplugins/index.vue +126 -184
  106. package/plugins/dashboard-client-init.js +3 -0
  107. package/plugins/dashboard-store/getters.js +18 -1
  108. package/plugins/dashboard-store/resource-class.js +3 -2
  109. package/plugins/i18n.js +8 -0
  110. package/plugins/steve/__tests__/steve-pagination-utils.test.ts +333 -0
  111. package/plugins/steve/steve-pagination-utils.ts +39 -20
  112. package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +6 -42
  113. package/rancher-components/Pill/RcStatusBadge/index.ts +0 -1
  114. package/rancher-components/Pill/RcStatusBadge/types.ts +1 -1
  115. package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.vue +5 -28
  116. package/rancher-components/Pill/RcStatusIndicator/types.ts +2 -1
  117. package/rancher-components/Pill/types.ts +0 -1
  118. package/rancher-components/RcIcon/RcIcon.test.ts +51 -0
  119. package/rancher-components/RcIcon/RcIcon.vue +46 -0
  120. package/rancher-components/RcIcon/index.ts +1 -0
  121. package/rancher-components/RcIcon/types.ts +160 -0
  122. package/rancher-components/utils/status.test.ts +67 -0
  123. package/rancher-components/utils/status.ts +77 -0
  124. package/scripts/typegen.sh +1 -0
  125. package/store/action-menu.js +8 -0
  126. package/store/auth.js +3 -3
  127. package/store/catalog.js +6 -0
  128. package/store/index.js +4 -4
  129. package/store/prefs.js +4 -5
  130. package/store/wm.ts +4 -4
  131. package/types/shell/index.d.ts +38 -2
  132. package/types/store/__tests__/pagination.types.spec.ts +137 -0
  133. package/types/store/pagination.types.ts +157 -9
  134. package/utils/__tests__/provider.test.ts +98 -0
  135. package/utils/__tests__/selector-typed.test.ts +263 -0
  136. package/utils/color.js +1 -1
  137. package/utils/dynamic-content/__tests__/info.test.ts +6 -0
  138. package/utils/dynamic-content/info.ts +43 -0
  139. package/utils/favicon.js +4 -4
  140. package/utils/provider.ts +14 -0
  141. package/utils/selector-typed.ts +6 -2
  142. package/plugins/nuxt-client-init.js +0 -3
@@ -56,11 +56,13 @@ export default {
56
56
 
57
57
  data() {
58
58
  return {
59
- principals: null,
60
- searchStr: '',
61
- options: [],
62
- newValue: '',
63
- tooltipContent: null,
59
+ principals: null,
60
+ searchStr: '',
61
+ options: [],
62
+ newValue: '',
63
+ tooltipContent: null,
64
+ hasSearchTooShort: false,
65
+ minSearchLength: 2,
64
66
  };
65
67
  },
66
68
 
@@ -133,9 +135,20 @@ export default {
133
135
  this.searchStr = str;
134
136
 
135
137
  if ( str ) {
138
+ // Backend requires minimum 2 characters for search
139
+ if (str.length < this.minSearchLength) {
140
+ this.hasSearchTooShort = true;
141
+ this.options = [];
142
+ loading(false);
143
+
144
+ return;
145
+ }
146
+
147
+ this.hasSearchTooShort = false;
136
148
  loading(true);
137
149
  this.debouncedSearch(str, loading);
138
150
  } else {
151
+ this.hasSearchTooShort = false;
139
152
  this.search(null, loading);
140
153
  }
141
154
  },
@@ -196,7 +209,12 @@ export default {
196
209
  @on-close="setTooltipContent()"
197
210
  >
198
211
  <template v-slot:no-options="{ searching }">
199
- <template v-if="searching">
212
+ <template v-if="hasSearchTooShort">
213
+ <span class="search-slot">
214
+ {{ t('cluster.memberRoles.addClusterMember.minCharacters', { count: minSearchLength }) }}
215
+ </span>
216
+ </template>
217
+ <template v-else-if="searching">
200
218
  <span class="search-slot">
201
219
  {{ t('cluster.memberRoles.addClusterMember.noResults') }}
202
220
  </span>
@@ -0,0 +1,119 @@
1
+ import { shallowMount, type VueWrapper } from '@vue/test-utils';
2
+ import SelectPrincipal from '@shell/components/auth/SelectPrincipal.vue';
3
+
4
+ describe('component: SelectPrincipal', () => {
5
+ const mockStore = { dispatch: jest.fn().mockResolvedValue([]) };
6
+
7
+ const defaultMountOptions = {
8
+ global: {
9
+ mocks: {
10
+ $fetchState: { pending: false },
11
+ $store: mockStore,
12
+ t: (key: string, opts?: any) => opts?.count ? `${ key } ${ opts.count }` : key,
13
+ },
14
+ stubs: {
15
+ LabeledSelect: {
16
+ template: '<div class="labeled-select-stub"><slot name="no-options" :searching="searching" /></div>',
17
+ props: ['options', 'searchable', 'filterable'],
18
+ data() {
19
+ return { searching: false };
20
+ }
21
+ },
22
+ Principal: true,
23
+ },
24
+ },
25
+ };
26
+
27
+ beforeEach(() => {
28
+ jest.clearAllMocks();
29
+ mockStore.dispatch.mockResolvedValue([]);
30
+ });
31
+
32
+ describe('onSearch', () => {
33
+ it('should set hasSearchTooShort to true when search string is less than minSearchLength', async() => {
34
+ const wrapper: VueWrapper<any> = shallowMount(SelectPrincipal, defaultMountOptions);
35
+
36
+ // Set principals to an empty array to avoid null errors
37
+ wrapper.vm.principals = [];
38
+ await wrapper.vm.$nextTick();
39
+
40
+ const loadingFn = jest.fn();
41
+
42
+ wrapper.vm.onSearch('a', loadingFn);
43
+
44
+ expect(wrapper.vm.hasSearchTooShort).toBe(true);
45
+ expect(wrapper.vm.options).toStrictEqual([]);
46
+ expect(loadingFn).toHaveBeenCalledWith(false);
47
+ });
48
+
49
+ it('should set hasSearchTooShort to false when search string meets minSearchLength', async() => {
50
+ const wrapper: VueWrapper<any> = shallowMount(SelectPrincipal, defaultMountOptions);
51
+
52
+ wrapper.vm.principals = [];
53
+ await wrapper.vm.$nextTick();
54
+
55
+ const loadingFn = jest.fn();
56
+
57
+ wrapper.vm.onSearch('ab', loadingFn);
58
+
59
+ expect(wrapper.vm.hasSearchTooShort).toBe(false);
60
+ expect(loadingFn).toHaveBeenCalledWith(true);
61
+ });
62
+
63
+ it('should set hasSearchTooShort to false when search string is empty', async() => {
64
+ const wrapper: VueWrapper<any> = shallowMount(SelectPrincipal, defaultMountOptions);
65
+
66
+ wrapper.vm.principals = [];
67
+ await wrapper.vm.$nextTick();
68
+
69
+ // First set hasSearchTooShort to true
70
+ wrapper.vm.hasSearchTooShort = true;
71
+
72
+ const loadingFn = jest.fn();
73
+
74
+ wrapper.vm.onSearch('', loadingFn);
75
+
76
+ expect(wrapper.vm.hasSearchTooShort).toBe(false);
77
+ });
78
+
79
+ it('should not call debouncedSearch when search string is too short', async() => {
80
+ const wrapper: VueWrapper<any> = shallowMount(SelectPrincipal, defaultMountOptions);
81
+
82
+ wrapper.vm.principals = [];
83
+ await wrapper.vm.$nextTick();
84
+
85
+ // Spy on the debounced search
86
+ const debouncedSearchSpy = jest.spyOn(wrapper.vm, 'debouncedSearch');
87
+ const loadingFn = jest.fn();
88
+
89
+ wrapper.vm.onSearch('x', loadingFn);
90
+
91
+ expect(debouncedSearchSpy).not.toHaveBeenCalled();
92
+ });
93
+
94
+ it('should call debouncedSearch when search string meets minimum length', async() => {
95
+ const wrapper: VueWrapper<any> = shallowMount(SelectPrincipal, defaultMountOptions);
96
+
97
+ wrapper.vm.principals = [];
98
+ await wrapper.vm.$nextTick();
99
+
100
+ const debouncedSearchSpy = jest.spyOn(wrapper.vm, 'debouncedSearch');
101
+ const loadingFn = jest.fn();
102
+
103
+ wrapper.vm.onSearch('xy', loadingFn);
104
+
105
+ expect(debouncedSearchSpy).toHaveBeenCalledWith('xy', loadingFn);
106
+ });
107
+ });
108
+
109
+ describe('minSearchLength', () => {
110
+ it('should have a default minSearchLength of 2', async() => {
111
+ const wrapper: VueWrapper<any> = shallowMount(SelectPrincipal, defaultMountOptions);
112
+
113
+ wrapper.vm.principals = [];
114
+ await wrapper.vm.$nextTick();
115
+
116
+ expect(wrapper.vm.minSearchLength).toBe(2);
117
+ });
118
+ });
119
+ });
@@ -108,10 +108,13 @@ export default {
108
108
  >
109
109
  <template #default>
110
110
  <RcStatusBadge
111
- v-clean-tooltip="tooltipContent"
111
+ v-clean-tooltip="{content: tooltipContent, triggers: ['hover', 'focus']}"
112
+ :aria-label="t('generic.plusMore', {n: remainingIpCount})"
113
+ tabindex="0"
112
114
  status="info"
113
115
  data-testid="plus-more"
114
116
  @click.stop
117
+ @keyup.enter.space="$refs.dropdown.show()"
115
118
  >
116
119
  {{ t('generic.plusMore', {n: remainingIpCount}) }}
117
120
  </RcStatusBadge>
@@ -24,7 +24,7 @@ describe('component: InternalExternalIP', () => {
24
24
  components: { 'v-dropdown': { name: 'v-dropdown', template: '<div><slot /><slot name="popper" /></div>' } },
25
25
  directives: {
26
26
  'clean-tooltip': (el, binding) => {
27
- el.setAttribute('v-clean-tooltip', binding.value);
27
+ el.setAttribute('v-clean-tooltip', binding.value.content);
28
28
  }
29
29
  },
30
30
  mocks: { $store: { getters: mockGetters } }
@@ -3,7 +3,7 @@
3
3
  </template>
4
4
 
5
5
  <style lang="scss">
6
- body, #__nuxt, #__layout {
6
+ body, #__root, #__layout {
7
7
  height: 100%;
8
8
  }
9
9
  </style>
@@ -11,7 +11,16 @@ let store: Store<any> | null = null;
11
11
  * @returns A translated string or the raw value if the raw parameter is set to true.
12
12
  */
13
13
  const t = (key: string, args?: unknown, raw?: boolean): string => {
14
- return stringFor(store, key, args, raw);
14
+ if (!store) {
15
+ if (!!process.env.dev) {
16
+ // eslint-disable-next-line no-console
17
+ console.warn('useI18n: store not available');
18
+ }
19
+
20
+ return key;
21
+ }
22
+
23
+ return stringFor(store, key, args as any, raw);
15
24
  };
16
25
 
17
26
  export type I18n = { t: typeof t };
@@ -0,0 +1,309 @@
1
+ import {
2
+ UI_PLUGIN_ANNOTATION,
3
+ UI_PLUGIN_CHART_ANNOTATIONS,
4
+ EXTENSIONS_INCOMPATIBILITY_TYPES,
5
+ isUIPlugin,
6
+ uiPluginHasAnnotation,
7
+ parseRancherVersion,
8
+ // shouldNotLoadPlugin, this is required per test so that we can mock semver.coerce
9
+ isSupportedChartVersion,
10
+ isChartVersionHigher
11
+ } from '@shell/config/uiplugins';
12
+
13
+ import * as VersionModule from '@shell/config/version';
14
+
15
+ let semver;
16
+ let originalCoerce: any;
17
+ const MOCK_API_VERSION = '33.33.33';
18
+
19
+ describe('uiPlugins Config methods', () => {
20
+ describe('fx: isUIPlugin', () => {
21
+ const pluginChart = { versions: [{ annotations: { [UI_PLUGIN_ANNOTATION.NAME]: UI_PLUGIN_ANNOTATION.VALUE } }, { annotations: {} }] };
22
+
23
+ it('should return true if any version has the UI plugin annotation', () => {
24
+ expect(isUIPlugin(pluginChart)).toBe(true);
25
+ });
26
+ it('should return false if chart is null/undefined', () => {
27
+ expect(isUIPlugin(null)).toBe(false);
28
+ });
29
+ });
30
+
31
+ describe('fx: uiPluginHasAnnotation', () => {
32
+ const version1 = { annotations: { [UI_PLUGIN_ANNOTATION.NAME]: UI_PLUGIN_ANNOTATION.VALUE } };
33
+ const chart = { versions: [version1] };
34
+
35
+ it('should return true if it finds version with annotation and its corresponding value', () => {
36
+ expect(uiPluginHasAnnotation(chart, UI_PLUGIN_ANNOTATION.NAME, UI_PLUGIN_ANNOTATION.VALUE)).toBe(true);
37
+ });
38
+ it('should return false if chart has no version with a given pair of annotation/value', () => {
39
+ expect(uiPluginHasAnnotation(chart, 'bananas', UI_PLUGIN_ANNOTATION.VALUE)).toBe(false);
40
+ });
41
+ });
42
+
43
+ describe('fx: parseRancherVersion', () => {
44
+ it('should handle standard version strings', () => {
45
+ expect(parseRancherVersion('v2.8.1')).toBe('2.8.1');
46
+ });
47
+ it('should apply .999 patch for RC versions', () => {
48
+ expect(parseRancherVersion('v2.8.1-rc1')).toBe('2.8.999');
49
+ });
50
+ it('should apply .999 patch for "head" versions', () => {
51
+ expect(parseRancherVersion('2.8.0-head')).toBe('2.8.999');
52
+ });
53
+ });
54
+
55
+ const PLUGIN_NAME = 'test-plugin';
56
+
57
+ const basePluginResource = {
58
+ name: PLUGIN_NAME,
59
+ version: '1.0.0',
60
+ endpoint: '/some/path',
61
+ metadata: { [UI_PLUGIN_CHART_ANNOTATIONS.EXTENSIONS_VERSION]: '>=1.0.0' }
62
+ };
63
+ const compatibleEnv = {
64
+ rancherVersion: '2.7.5',
65
+ kubeVersion: 'v1.25.10',
66
+ };
67
+
68
+ describe('fx: shouldNotLoadPlugin', () => {
69
+ beforeEach(() => {
70
+ jest.resetModules();
71
+ process.env.UI_EXTENSIONS_API_VERSION = MOCK_API_VERSION;
72
+
73
+ semver = require('semver');
74
+ originalCoerce = semver.coerce;
75
+
76
+ jest.spyOn(semver, 'coerce').mockImplementation((v) => {
77
+ if (v === MOCK_API_VERSION) {
78
+ return { version: '3.0.0' };
79
+ }
80
+
81
+ // Use the original for all other inputs (Rancher, Kube)
82
+ return originalCoerce(v);
83
+ });
84
+ });
85
+
86
+ afterEach(() => {
87
+ jest.restoreAllMocks();
88
+ });
89
+
90
+ it('should return false when compatible', () => {
91
+ const { shouldNotLoadPlugin } = require('./../uiplugins');
92
+
93
+ expect(shouldNotLoadPlugin(basePluginResource, compatibleEnv, [])).toBe(false);
94
+ });
95
+
96
+ it('should return "plugins.error.api" if Extensions API version is incompatible', () => {
97
+ const { shouldNotLoadPlugin } = require('./../uiplugins');
98
+
99
+ const incompatibleApiPlugin = {
100
+ ...basePluginResource,
101
+ metadata: { [UI_PLUGIN_CHART_ANNOTATIONS.EXTENSIONS_VERSION]: '>=4.0.0' }
102
+ };
103
+
104
+ expect(shouldNotLoadPlugin(incompatibleApiPlugin, compatibleEnv, [])).toBe('plugins.error.api');
105
+ });
106
+
107
+ it('should return "plugins.error.primeOnly" if extension is Prime-only and Rancher is not Prime', () => {
108
+ const { shouldNotLoadPlugin } = require('./../uiplugins');
109
+
110
+ jest.spyOn(VersionModule, 'isRancherPrime').mockReturnValue(false);
111
+
112
+ const incompatibleApiPlugin = {
113
+ ...basePluginResource,
114
+ metadata: {
115
+ [UI_PLUGIN_CHART_ANNOTATIONS.EXTENSIONS_VERSION]: '>=1.0.0',
116
+ [UI_PLUGIN_CHART_ANNOTATIONS.PRIME_ONLY]: 'true'
117
+ }
118
+ };
119
+
120
+ expect(shouldNotLoadPlugin(incompatibleApiPlugin, compatibleEnv, [])).toBe('plugins.error.primeOnly');
121
+ });
122
+
123
+ it('should return "plugins.error.host" if HOST application value is incompatible', () => {
124
+ const { shouldNotLoadPlugin } = require('./../uiplugins');
125
+
126
+ const incompatibleApiPlugin = {
127
+ ...basePluginResource,
128
+ metadata: {
129
+ [UI_PLUGIN_CHART_ANNOTATIONS.EXTENSIONS_VERSION]: '>=1.0.0',
130
+ [UI_PLUGIN_CHART_ANNOTATIONS.EXTENSIONS_HOST]: 'rancher-dummy-host'
131
+ }
132
+ };
133
+
134
+ expect(shouldNotLoadPlugin(incompatibleApiPlugin, compatibleEnv, [])).toBe('plugins.error.host');
135
+ });
136
+
137
+ it('should return "plugins.error.kubeVersion" if incompatible', () => {
138
+ const { shouldNotLoadPlugin } = require('./../uiplugins');
139
+
140
+ const incompatibleApiPlugin = {
141
+ ...basePluginResource,
142
+ metadata: {
143
+ [UI_PLUGIN_CHART_ANNOTATIONS.EXTENSIONS_VERSION]: '>=1.0.0',
144
+ [UI_PLUGIN_CHART_ANNOTATIONS.KUBE_VERSION]: '>= 2.0.0'
145
+ }
146
+ };
147
+
148
+ expect(shouldNotLoadPlugin(incompatibleApiPlugin, compatibleEnv, [])).toBe('plugins.error.kubeVersion');
149
+ });
150
+
151
+ it('should return "plugins.error.version" if incompatible', () => {
152
+ const { shouldNotLoadPlugin } = require('./../uiplugins');
153
+
154
+ const incompatibleApiPlugin = {
155
+ ...basePluginResource,
156
+ metadata: {
157
+ [UI_PLUGIN_CHART_ANNOTATIONS.EXTENSIONS_VERSION]: '>=1.0.0',
158
+ [UI_PLUGIN_CHART_ANNOTATIONS.RANCHER_VERSION]: '>= 3.0.0'
159
+ }
160
+ };
161
+
162
+ expect(shouldNotLoadPlugin(incompatibleApiPlugin, compatibleEnv, [])).toBe('plugins.error.version');
163
+ });
164
+
165
+ it('should return "plugins.error.developerPkg" if a builtin extension with the same name was loaded first', () => {
166
+ const { shouldNotLoadPlugin } = require('./../uiplugins');
167
+
168
+ const incompatibleApiPlugin = {
169
+ ...basePluginResource,
170
+ metadata: { [UI_PLUGIN_CHART_ANNOTATIONS.EXTENSIONS_VERSION]: '>=1.0.0' }
171
+ };
172
+
173
+ expect(shouldNotLoadPlugin(incompatibleApiPlugin, compatibleEnv, [{ name: PLUGIN_NAME, builtin: true }])).toBe('plugins.error.developerPkg');
174
+ });
175
+ });
176
+
177
+ describe('fx: isSupportedChartVersion', () => {
178
+ beforeEach(() => {
179
+ jest.resetModules();
180
+ process.env.UI_EXTENSIONS_API_VERSION = MOCK_API_VERSION;
181
+
182
+ semver = require('semver');
183
+ originalCoerce = semver.coerce;
184
+
185
+ jest.spyOn(semver, 'coerce').mockImplementation((v) => {
186
+ if (v === MOCK_API_VERSION) {
187
+ return { version: '3.0.0' };
188
+ }
189
+
190
+ // Use the original for all other inputs (Rancher, Kube)
191
+ return originalCoerce(v);
192
+ });
193
+ });
194
+
195
+ afterEach(() => {
196
+ jest.restoreAllMocks();
197
+ });
198
+
199
+ const versionData = {
200
+ rancherVersion: 'v2.7.5',
201
+ kubeVersion: 'v1.25.10',
202
+ version: '1.0.0'
203
+ };
204
+
205
+ it('should return "isVersionCompatible" true when compatible', () => {
206
+ const { isSupportedChartVersion } = require('./../uiplugins');
207
+
208
+ const annotations = {
209
+ [UI_PLUGIN_CHART_ANNOTATIONS.KUBE_VERSION]: '>=1.0.0',
210
+ [UI_PLUGIN_CHART_ANNOTATIONS.RANCHER_VERSION]: '>=2.0.0',
211
+ [UI_PLUGIN_CHART_ANNOTATIONS.EXTENSIONS_VERSION]: '>=2.0.0'
212
+ };
213
+ const data = { ...versionData, version: { annotations } };
214
+ const result = isSupportedChartVersion(data, true);
215
+
216
+ expect(result.isVersionCompatible).toBe(true);
217
+ expect(result.versionIncompatibilityData).toStrictEqual({});
218
+ });
219
+
220
+ it('should return incompatibility object for kube version mismatch', () => {
221
+ const annotations = { [UI_PLUGIN_CHART_ANNOTATIONS.KUBE_VERSION]: '>=2.0.0' };
222
+ const data = { ...versionData, version: { annotations } };
223
+ const result = isSupportedChartVersion(data, true);
224
+
225
+ expect(result.isVersionCompatible).toBe(false);
226
+ expect(result.versionIncompatibilityData.type).toBe(EXTENSIONS_INCOMPATIBILITY_TYPES.KUBE);
227
+ expect(result.versionIncompatibilityData.required).toBe('>=2.0.0');
228
+ });
229
+
230
+ it('should return incompatibility object for Rancher version mismatch', () => {
231
+ const annotations = { [UI_PLUGIN_CHART_ANNOTATIONS.RANCHER_VERSION]: '>=3.0.0' };
232
+ const data = { ...versionData, version: { annotations } };
233
+ const result = isSupportedChartVersion(data, true);
234
+
235
+ expect(result.isVersionCompatible).toBe(false);
236
+ expect(result.versionIncompatibilityData.type).toBe(EXTENSIONS_INCOMPATIBILITY_TYPES.UI);
237
+ expect(result.versionIncompatibilityData.required).toBe('>=3.0.0');
238
+ });
239
+
240
+ it('should return incompatibility object for UI version mismatch', () => {
241
+ const annotations = { [UI_PLUGIN_CHART_ANNOTATIONS.UI_VERSION]: '>=3.0.0' };
242
+ const data = { ...versionData, version: { annotations } };
243
+ const result = isSupportedChartVersion(data, true);
244
+
245
+ expect(result.isVersionCompatible).toBe(false);
246
+ expect(result.versionIncompatibilityData.type).toBe(EXTENSIONS_INCOMPATIBILITY_TYPES.UI);
247
+ expect(result.versionIncompatibilityData.required).toBe('>=3.0.0');
248
+ });
249
+
250
+ it('should return incompatibility object for extensions api missing', () => {
251
+ const annotations = {};
252
+ const data = { ...versionData, version: { annotations } };
253
+ const result = isSupportedChartVersion(data, true);
254
+
255
+ expect(result.isVersionCompatible).toBe(false);
256
+ expect(result.versionIncompatibilityData.type).toBe(EXTENSIONS_INCOMPATIBILITY_TYPES.EXTENSIONS_API_MISSING);
257
+ expect(result.versionIncompatibilityData.required).toBeUndefined();
258
+ });
259
+
260
+ it('should return incompatibility object for extensions API version mismatch', () => {
261
+ const { isSupportedChartVersion } = require('./../uiplugins');
262
+
263
+ const annotations = { [UI_PLUGIN_CHART_ANNOTATIONS.EXTENSIONS_VERSION]: '>=4.0.0' };
264
+ const data = { ...versionData, version: { annotations } };
265
+ const result = isSupportedChartVersion(data, true);
266
+
267
+ expect(result.isVersionCompatible).toBe(false);
268
+ expect(result.versionIncompatibilityData.type).toBe(EXTENSIONS_INCOMPATIBILITY_TYPES.EXTENSIONS_API);
269
+ expect(result.versionIncompatibilityData.required).toBe('>=4.0.0');
270
+ });
271
+
272
+ it('should return incompatibility object for HOST_APP mismatch', () => {
273
+ const { isSupportedChartVersion } = require('./../uiplugins');
274
+
275
+ const annotations = { [UI_PLUGIN_CHART_ANNOTATIONS.EXTENSIONS_VERSION]: '>=1.0.0 < 5.0.0', [UI_PLUGIN_CHART_ANNOTATIONS.EXTENSIONS_HOST]: 'rancher-dummy-host' };
276
+ const data = { ...versionData, version: { annotations } };
277
+ const result = isSupportedChartVersion(data, true);
278
+
279
+ expect(result.isVersionCompatible).toBe(false);
280
+ expect(result.versionIncompatibilityData.type).toBe(EXTENSIONS_INCOMPATIBILITY_TYPES.HOST);
281
+ expect(result.versionIncompatibilityData.required).toBe('rancher-dummy-host');
282
+ });
283
+
284
+ it('should return incompatibility object for Prime only extension running in non-prime env', () => {
285
+ const { isSupportedChartVersion } = require('./../uiplugins');
286
+
287
+ jest.spyOn(VersionModule, 'isRancherPrime').mockReturnValue(false);
288
+
289
+ const annotations = { [UI_PLUGIN_CHART_ANNOTATIONS.EXTENSIONS_VERSION]: '>=1.0.0 < 5.0.0', [UI_PLUGIN_CHART_ANNOTATIONS.PRIME_ONLY]: 'true' };
290
+ const data = { ...versionData, version: { annotations } };
291
+ const result = isSupportedChartVersion(data, true);
292
+
293
+ expect(result.isVersionCompatible).toBe(false);
294
+ expect(result.versionIncompatibilityData.type).toBe(EXTENSIONS_INCOMPATIBILITY_TYPES.PRIME_ONLY);
295
+ expect(result.versionIncompatibilityData.required).toBe(true);
296
+ });
297
+ });
298
+
299
+ describe('fx: isChartVersionHigher', () => {
300
+ // Tests now rely on the actual semver.gt implementation
301
+ it('should return true when version A is higher', () => {
302
+ expect(isChartVersionHigher('2.1.0', '2.0.0')).toBe(true);
303
+ });
304
+ it('should return false when version A is lower or equal', () => {
305
+ expect(isChartVersionHigher('2.0.0', '2.1.0')).toBe(false);
306
+ expect(isChartVersionHigher('2.0.0', '2.0.0')).toBe(false);
307
+ });
308
+ });
309
+ });
@@ -85,6 +85,7 @@ export const CATALOG = {
85
85
  _PARTNER: 'partner',
86
86
  _OTHER: 'other',
87
87
 
88
+ PRIME_ONLY: 'catalog.cattle.io/prime-only',
88
89
  EXPERIMENTAL: 'catalog.cattle.io/experimental',
89
90
  NAMESPACE: 'catalog.cattle.io/namespace',
90
91
  RELEASE_NAME: 'catalog.cattle.io/release-name',
@@ -180,7 +180,8 @@ export function init(store) {
180
180
  mapGroup(/^(.*\.)?(scc)\.cattle\.io$/, 'SCC');
181
181
 
182
182
  const dePaginateBindings = configureConditionalDepaginate({ maxResourceCount: 5000 });
183
- const dePaginateNormanBindings = configureConditionalDepaginate({ maxResourceCount: 5000, isNorman: true }) ;
183
+ const dePaginateNormanBindings = configureConditionalDepaginate({ maxResourceCount: 5000, isNorman: true });
184
+ const dePaginateNormanUsers = configureConditionalDepaginate({ maxResourceCount: 5000, isNorman: true });
184
185
 
185
186
  configureType(NODE, { isCreatable: false, isEditable: true });
186
187
  configureType(WORKLOAD_TYPES.JOB, { isEditable: false, match: WORKLOAD_TYPES.JOB });
@@ -189,6 +190,7 @@ export function init(store) {
189
190
  configureType(MANAGEMENT.PROJECT, { displayName: store.getters['i18n/t']('namespace.project.label') });
190
191
  configureType(NORMAN.CLUSTER_ROLE_TEMPLATE_BINDING, { depaginate: dePaginateNormanBindings });
191
192
  configureType(NORMAN.PROJECT_ROLE_TEMPLATE_BINDING, { depaginate: dePaginateNormanBindings });
193
+ configureType(NORMAN.USER, { depaginate: dePaginateNormanUsers });
192
194
  configureType(SNAPSHOT, { depaginate: true });
193
195
 
194
196
  configureType(SECRET, { showListMasthead: false });
@@ -81,9 +81,13 @@ export default [
81
81
  {
82
82
  path: '/c/:cluster/uiplugins',
83
83
  name: 'c-cluster-uiplugins',
84
- component: () => interopDefault(import('@shell/pages/c/_cluster/uiplugins/index.vue')),
84
+ component: () => interopDefault(import('@shell/pages/c/_cluster/uiplugins/index.vue'))
85
+ },
86
+ {
87
+ path: '/c/:cluster/uiplugins/catalogs',
88
+ component: () => interopDefault(import('@shell/pages/c/_cluster/uiplugins/catalogs.vue')),
89
+ name: 'c-cluster-uiplugins-catalogs'
85
90
  },
86
-
87
91
  {
88
92
  path: '/diagnostic',
89
93
  component: () => interopDefault(import('@shell/pages/diagnostic.vue')),
package/config/types.js CHANGED
@@ -222,6 +222,13 @@ export const MANAGEMENT = {
222
222
  OIDC_CLIENT: 'management.cattle.io.oidcclient'
223
223
  };
224
224
 
225
+ export const BRAND = {
226
+ SUSE: 'suse',
227
+ CSP: 'csp',
228
+ FEDERAL: 'federal',
229
+ RGS: 'rgs',
230
+ };
231
+
225
232
  export const EXT = { USER_ACTIVITY: 'ext.cattle.io.useractivity' };
226
233
 
227
234
  export const CAPI = {