@rancher/shell 3.0.8-rc.8 → 3.0.8

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 (260) hide show
  1. package/apis/impl/apis.ts +61 -0
  2. package/apis/index.ts +40 -0
  3. package/apis/intf/modal.ts +90 -0
  4. package/apis/intf/shell.ts +36 -0
  5. package/apis/intf/slide-in.ts +98 -0
  6. package/apis/intf/system.ts +41 -0
  7. package/apis/shell/__tests__/modal.test.ts +80 -0
  8. package/apis/shell/__tests__/notifications.test.ts +71 -0
  9. package/apis/shell/__tests__/slide-in.test.ts +54 -0
  10. package/apis/shell/__tests__/system.test.ts +129 -0
  11. package/apis/shell/index.ts +38 -0
  12. package/apis/shell/modal.ts +41 -0
  13. package/apis/shell/notifications.ts +65 -0
  14. package/apis/shell/slide-in.ts +33 -0
  15. package/apis/shell/system.ts +65 -0
  16. package/apis/vue-shim.d.ts +11 -0
  17. package/assets/brand/suse/dark/rancher-logo.svg +1 -64
  18. package/assets/styles/global/_tooltip.scss +6 -1
  19. package/assets/translations/en-us.yaml +14 -1
  20. package/components/ActionMenuShell.vue +3 -1
  21. package/components/BackLink.vue +8 -0
  22. package/components/BannerGraphic.vue +1 -5
  23. package/components/BrandImage.vue +17 -6
  24. package/components/Cron/CronExpressionEditor.vue +1 -1
  25. package/components/Cron/CronExpressionEditorModal.vue +1 -1
  26. package/components/CruResource.vue +8 -1
  27. package/components/Drawer/ResourceDetailDrawer/ConfigTab.vue +1 -0
  28. package/components/Drawer/ResourceDetailDrawer/__tests__/composables.test.ts +50 -1
  29. package/components/Drawer/ResourceDetailDrawer/composables.ts +19 -0
  30. package/components/Drawer/ResourceDetailDrawer/index.vue +4 -1
  31. package/components/Drawer/ResourceDetailDrawer/types.ts +2 -1
  32. package/components/LocaleSelector.vue +2 -2
  33. package/components/ModalManager.vue +11 -1
  34. package/components/Questions/__tests__/Yaml.test.ts +1 -1
  35. package/components/Questions/__tests__/index.test.ts +159 -0
  36. package/components/RelatedResources.vue +5 -0
  37. package/components/Resource/Detail/Metadata/Annotations/index.vue +2 -2
  38. package/components/Resource/Detail/Metadata/Labels/index.vue +2 -2
  39. package/components/Resource/Detail/Metadata/index.vue +3 -3
  40. package/components/Resource/Detail/ResourcePopover/index.vue +5 -1
  41. package/components/Resource/Detail/composables.ts +2 -2
  42. package/components/ResourceDetail/Masthead/latest.vue +23 -21
  43. package/components/ResourceDetail/index.vue +3 -0
  44. package/components/ResourceTable.vue +54 -21
  45. package/components/SlideInPanelManager.vue +16 -11
  46. package/components/SortableTable/THead.vue +2 -1
  47. package/components/SortableTable/index.vue +20 -2
  48. package/components/Tabbed/__tests__/index.test.ts +86 -0
  49. package/components/Tabbed/index.vue +37 -2
  50. package/components/__tests__/NamespaceFilter.test.ts +49 -0
  51. package/components/auth/SelectPrincipal.vue +28 -6
  52. package/components/auth/__tests__/SelectPrincipal.test.ts +119 -0
  53. package/components/auth/login/ldap.vue +3 -3
  54. package/components/fleet/FleetSecretSelector.vue +1 -1
  55. package/components/form/KeyValue.vue +1 -1
  56. package/components/form/NameNsDescription.vue +1 -1
  57. package/components/form/NodeScheduling.vue +2 -2
  58. package/components/form/ResourceTabs/composable.ts +2 -2
  59. package/components/form/ResourceTabs/index.vue +0 -2
  60. package/components/form/__tests__/NameNsDescription.test.ts +42 -0
  61. package/components/formatter/InternalExternalIP.vue +4 -1
  62. package/components/formatter/LinkName.vue +5 -0
  63. package/components/formatter/__tests__/InternalExternalIP.test.ts +1 -1
  64. package/components/nav/Group.vue +25 -7
  65. package/components/nav/Header.vue +1 -1
  66. package/components/nav/NamespaceFilter.vue +1 -0
  67. package/components/nav/Type.vue +17 -6
  68. package/components/nav/WindowManager/panels/TabBodyContainer.vue +1 -1
  69. package/components/nav/__tests__/Type.test.ts +59 -0
  70. package/components/templates/standalone.vue +1 -1
  71. package/composables/cruResource.ts +27 -0
  72. package/composables/focusTrap.ts +3 -1
  73. package/composables/resourceDetail.ts +15 -0
  74. package/composables/useI18n.ts +10 -1
  75. package/composables/useLabeledFormElement.ts +3 -4
  76. package/config/__test__/uiplugins.test.ts +309 -0
  77. package/config/labels-annotations.js +1 -0
  78. package/config/product/explorer.js +3 -1
  79. package/config/product/fleet.js +1 -1
  80. package/config/router/navigation-guards/clusters.js +3 -3
  81. package/config/router/navigation-guards/products.js +1 -1
  82. package/config/router/routes.js +7 -7
  83. package/config/types.js +7 -0
  84. package/config/uiplugins.js +46 -2
  85. package/core/__tests__/extension-manager-impl.test.js +437 -0
  86. package/core/extension-manager-impl.js +21 -25
  87. package/core/plugin-helpers.ts +2 -2
  88. package/core/plugin.ts +9 -1
  89. package/core/plugins-loader.js +2 -2
  90. package/core/types-provisioning.ts +5 -1
  91. package/core/types.ts +35 -0
  92. package/detail/provisioning.cattle.io.cluster.vue +9 -6
  93. package/dialog/DeveloperLoadExtensionDialog.vue +13 -4
  94. package/dialog/MoveNamespaceDialog.vue +20 -4
  95. package/dialog/RollbackWorkloadDialog.vue +2 -5
  96. package/dialog/SearchDialog.vue +1 -0
  97. package/dialog/__tests__/MoveNamespaceDialog.test.ts +249 -0
  98. package/directives/__tests__/clean-tooltip.test.ts +298 -0
  99. package/directives/clean-tooltip.ts +234 -0
  100. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +2 -2
  101. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +100 -3
  102. package/edit/autoscaling.horizontalpodautoscaler/index.vue +1 -0
  103. package/edit/configmap.vue +1 -0
  104. package/edit/constraints.gatekeeper.sh.constraint/index.vue +1 -0
  105. package/edit/fleet.cattle.io.helmop.vue +11 -6
  106. package/edit/helm.cattle.io.projecthelmchart.vue +1 -0
  107. package/edit/k8s.cni.cncf.io.networkattachmentdefinition.vue +1 -0
  108. package/edit/logging-flow/index.vue +1 -0
  109. package/edit/logging.banzaicloud.io.output/index.vue +1 -0
  110. package/edit/management.cattle.io.fleetworkspace.vue +1 -1
  111. package/edit/management.cattle.io.project.vue +1 -0
  112. package/edit/monitoring.coreos.com.alertmanagerconfig/index.vue +4 -1
  113. package/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig.vue +2 -1
  114. package/edit/monitoring.coreos.com.prometheusrule/index.vue +1 -0
  115. package/edit/monitoring.coreos.com.receiver/index.vue +2 -1
  116. package/edit/monitoring.coreos.com.route.vue +1 -1
  117. package/edit/namespace.vue +1 -0
  118. package/edit/networking.istio.io.destinationrule/index.vue +1 -0
  119. package/edit/networking.k8s.io.ingress/index.vue +1 -0
  120. package/edit/networking.k8s.io.networkpolicy/PolicyRules.vue +1 -0
  121. package/edit/networking.k8s.io.networkpolicy/index.vue +1 -0
  122. package/edit/node.vue +1 -0
  123. package/edit/persistentvolume/index.vue +27 -22
  124. package/edit/persistentvolume/plugins/awsElasticBlockStore.vue +13 -14
  125. package/edit/persistentvolume/plugins/azureDisk.vue +49 -48
  126. package/edit/persistentvolume/plugins/azureFile.vue +15 -14
  127. package/edit/persistentvolume/plugins/cephfs.vue +15 -14
  128. package/edit/persistentvolume/plugins/cinder.vue +15 -14
  129. package/edit/persistentvolume/plugins/csi.vue +18 -16
  130. package/edit/persistentvolume/plugins/fc.vue +13 -14
  131. package/edit/persistentvolume/plugins/flexVolume.vue +15 -14
  132. package/edit/persistentvolume/plugins/flocker.vue +1 -3
  133. package/edit/persistentvolume/plugins/gcePersistentDisk.vue +13 -14
  134. package/edit/persistentvolume/plugins/glusterfs.vue +15 -14
  135. package/edit/persistentvolume/plugins/hostPath.vue +40 -39
  136. package/edit/persistentvolume/plugins/iscsi.vue +13 -14
  137. package/edit/persistentvolume/plugins/local.vue +1 -3
  138. package/edit/persistentvolume/plugins/longhorn.vue +23 -22
  139. package/edit/persistentvolume/plugins/nfs.vue +15 -14
  140. package/edit/persistentvolume/plugins/photonPersistentDisk.vue +1 -14
  141. package/edit/persistentvolume/plugins/portworxVolume.vue +15 -14
  142. package/edit/persistentvolume/plugins/quobyte.vue +15 -14
  143. package/edit/persistentvolume/plugins/rbd.vue +15 -14
  144. package/edit/persistentvolume/plugins/scaleIO.vue +15 -14
  145. package/edit/persistentvolume/plugins/storageos.vue +15 -14
  146. package/edit/persistentvolume/plugins/vsphereVolume.vue +1 -3
  147. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +21 -21
  148. package/edit/provisioning.cattle.io.cluster/index.vue +5 -5
  149. package/edit/provisioning.cattle.io.cluster/rke2.vue +9 -8
  150. package/edit/resources.cattle.io.restore.vue +1 -1
  151. package/edit/secret/index.vue +1 -1
  152. package/edit/service.vue +1 -0
  153. package/edit/serviceaccount.vue +1 -0
  154. package/edit/storage.k8s.io.storageclass/index.vue +1 -0
  155. package/edit/workload/Job.vue +2 -2
  156. package/edit/workload/index.vue +2 -1
  157. package/edit/workload/mixins/workload.js +1 -1
  158. package/initialize/App.vue +4 -4
  159. package/initialize/install-plugins.js +19 -5
  160. package/machine-config/azure.vue +1 -1
  161. package/machine-config/components/GCEImage.vue +1 -1
  162. package/mixins/__tests__/brand.spec.ts +2 -2
  163. package/mixins/brand.js +1 -7
  164. package/mixins/create-edit-view/index.js +5 -0
  165. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +128 -5
  166. package/models/chart.js +70 -74
  167. package/models/management.cattle.io.cluster.js +21 -3
  168. package/models/provisioning.cattle.io.cluster.js +31 -11
  169. package/package.json +11 -10
  170. package/pages/auth/login.vue +4 -6
  171. package/pages/auth/setup.vue +1 -1
  172. package/pages/auth/verify.vue +3 -3
  173. package/pages/c/_cluster/apps/charts/__tests__/chart.test.ts +135 -0
  174. package/pages/c/_cluster/apps/charts/chart.vue +33 -15
  175. package/pages/c/_cluster/apps/charts/index.vue +122 -24
  176. package/pages/c/_cluster/apps/charts/install.vue +33 -0
  177. package/pages/c/_cluster/explorer/__tests__/index.test.ts +1 -1
  178. package/pages/c/_cluster/explorer/index.vue +8 -6
  179. package/pages/c/_cluster/fleet/index.vue +4 -7
  180. package/pages/c/_cluster/manager/hostedprovider/index.vue +12 -6
  181. package/pages/c/_cluster/settings/brand.vue +1 -1
  182. package/pages/c/_cluster/settings/index.vue +5 -0
  183. package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +7 -0
  184. package/pages/c/_cluster/uiplugins/catalogs.vue +147 -0
  185. package/pages/c/_cluster/uiplugins/index.vue +126 -184
  186. package/pkg/auto-import.js +3 -3
  187. package/pkg/dynamic-importer.lib.js +1 -1
  188. package/pkg/import.js +1 -1
  189. package/plugins/__tests__/mutations.tests.ts +179 -0
  190. package/plugins/dashboard-client-init.js +3 -0
  191. package/plugins/dashboard-store/getters.js +19 -2
  192. package/plugins/dashboard-store/model-loader.js +1 -1
  193. package/plugins/dashboard-store/mutations.js +23 -2
  194. package/plugins/dashboard-store/resource-class.js +11 -5
  195. package/plugins/i18n.js +8 -0
  196. package/plugins/plugin.js +2 -2
  197. package/plugins/steve/__tests__/steve-pagination-utils.test.ts +506 -0
  198. package/plugins/steve/steve-class.js +1 -1
  199. package/plugins/steve/steve-pagination-utils.ts +131 -47
  200. package/rancher-components/Form/Checkbox/Checkbox.vue +1 -1
  201. package/rancher-components/Form/LabeledInput/LabeledInput.vue +1 -1
  202. package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +6 -42
  203. package/rancher-components/Pill/RcStatusBadge/index.ts +0 -1
  204. package/rancher-components/Pill/RcStatusBadge/types.ts +1 -1
  205. package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.vue +5 -28
  206. package/rancher-components/Pill/RcStatusIndicator/types.ts +2 -1
  207. package/rancher-components/Pill/types.ts +0 -1
  208. package/rancher-components/RcDropdown/useDropdownContext.ts +2 -4
  209. package/rancher-components/RcIcon/RcIcon.test.ts +51 -0
  210. package/rancher-components/RcIcon/RcIcon.vue +46 -0
  211. package/rancher-components/RcIcon/index.ts +1 -0
  212. package/rancher-components/RcIcon/types.ts +160 -0
  213. package/rancher-components/RcItemCard/RcItemCard.vue +1 -1
  214. package/rancher-components/utils/status.test.ts +67 -0
  215. package/rancher-components/utils/status.ts +77 -0
  216. package/scripts/publish-shell.sh +25 -0
  217. package/scripts/typegen.sh +1 -0
  218. package/store/__tests__/catalog.test.ts +1 -1
  219. package/store/__tests__/type-map.test.ts +164 -2
  220. package/store/action-menu.js +8 -0
  221. package/store/auth.js +25 -13
  222. package/store/catalog.js +6 -0
  223. package/store/i18n.js +3 -3
  224. package/store/index.js +8 -6
  225. package/store/notifications.ts +2 -0
  226. package/store/prefs.js +6 -7
  227. package/store/type-map.js +17 -7
  228. package/store/wm.ts +4 -4
  229. package/types/internal-api/shell/modal.d.ts +6 -6
  230. package/types/notifications/index.ts +126 -15
  231. package/types/rancher/index.d.ts +9 -0
  232. package/types/shell/index.d.ts +54 -3
  233. package/types/store/__tests__/pagination.types.spec.ts +137 -0
  234. package/types/store/pagination.types.ts +157 -9
  235. package/types/vue-shim.d.ts +5 -4
  236. package/utils/__tests__/provider.test.ts +98 -0
  237. package/utils/__tests__/router.test.js +238 -0
  238. package/utils/__tests__/selector-typed.test.ts +263 -0
  239. package/utils/cluster.js +4 -1
  240. package/utils/color.js +1 -1
  241. package/utils/dynamic-content/__tests__/info.test.ts +6 -0
  242. package/utils/dynamic-content/info.ts +43 -0
  243. package/utils/favicon.js +4 -4
  244. package/utils/fleet.ts +8 -1
  245. package/utils/pagination-utils.ts +2 -2
  246. package/utils/pagination-wrapper.ts +1 -1
  247. package/utils/provider.ts +14 -0
  248. package/utils/router.js +50 -0
  249. package/utils/selector-typed.ts +6 -2
  250. package/utils/unit-tests/pagination-utils.spec.ts +8 -8
  251. package/vue.config.js +3 -3
  252. package/composables/useExtensionManager.ts +0 -17
  253. package/core/plugins.js +0 -38
  254. package/directives/clean-tooltip.js +0 -32
  255. package/plugins/internal-api/index.ts +0 -37
  256. package/plugins/internal-api/shared/base-api.ts +0 -13
  257. package/plugins/internal-api/shell/shell.api.ts +0 -108
  258. package/plugins/nuxt-client-init.js +0 -3
  259. package/types/internal-api/shell/growl.d.ts +0 -25
  260. package/types/internal-api/shell/slideIn.d.ts +0 -15
@@ -0,0 +1,437 @@
1
+ import { DEVELOPER_LOAD_NAME_SUFFIX, createExtensionManager } from '@shell/core/extension-manager-impl';
2
+
3
+ // Mock external dependencies
4
+ jest.mock('@shell/store/type-map', () => ({ productsLoaded: jest.fn().mockReturnValue(true) }));
5
+
6
+ jest.mock('@shell/plugins/dashboard-store/model-loader', () => ({ clearModelCache: jest.fn() }));
7
+
8
+ jest.mock('@shell/config/uiplugins', () => ({ UI_PLUGIN_BASE_URL: '/api/v1/uiplugins' }));
9
+
10
+ jest.mock('@shell/plugins/clean-html', () => ({
11
+ addLinkInterceptor: jest.fn(),
12
+ removeLinkInterceptor: jest.fn(),
13
+ }));
14
+
15
+ // Mock the Plugin class
16
+ jest.mock('@shell/core/plugin', () => {
17
+ return {
18
+ Plugin: jest.fn().mockImplementation((id) => ({
19
+ id,
20
+ name: id,
21
+ builtin: false,
22
+ types: {},
23
+ uiConfig: {},
24
+ l10n: {},
25
+ modelExtensions: {},
26
+ stores: [],
27
+ locales: [],
28
+ routes: [],
29
+ validators: {},
30
+ uninstallHooks: [],
31
+ productNames: [],
32
+ products: [],
33
+ })),
34
+ EXT_IDS: {
35
+ MODELS: 'models',
36
+ MODEL_EXTENSION: 'model-extension'
37
+ }
38
+ };
39
+ });
40
+
41
+ // Mock ExtensionPoint
42
+ jest.mock('@shell/core/types', () => ({ ExtensionPoint: { EDIT_YAML: 'edit-yaml' } }));
43
+
44
+ // Mock PluginRoutes
45
+ jest.mock('@shell/core/plugin-routes', () => {
46
+ return { PluginRoutes: jest.fn().mockImplementation(() => ({ addRoutes: jest.fn() })) };
47
+ });
48
+
49
+ describe('extension Manager', () => {
50
+ let mockStore;
51
+ let mockApp;
52
+ let context;
53
+ let manager;
54
+
55
+ beforeEach(() => {
56
+ jest.clearAllMocks();
57
+
58
+ // Setup Mock Context
59
+ mockStore = {
60
+ getters: { 'i18n/t': jest.fn() },
61
+ dispatch: jest.fn(),
62
+ commit: jest.fn(),
63
+ };
64
+
65
+ mockApp = { router: {} };
66
+
67
+ context = {
68
+ app: mockApp,
69
+ store: mockStore,
70
+ $axios: {},
71
+ redirect: jest.fn(),
72
+ };
73
+
74
+ // Clean up DOM from previous tests
75
+ document.head.innerHTML = '';
76
+ document.body.innerHTML = '';
77
+
78
+ // Create a fresh extension manager for each test
79
+ manager = createExtensionManager(context);
80
+ });
81
+
82
+ describe('factory Pattern', () => {
83
+ it('creates independent manager instances', () => {
84
+ const instance1 = createExtensionManager(context);
85
+ const instance2 = createExtensionManager(context);
86
+
87
+ expect(instance1).toBeDefined();
88
+ expect(instance2).toBeDefined();
89
+ expect(instance1).not.toBe(instance2);
90
+ });
91
+
92
+ it('provides access to internal context', () => {
93
+ const internal = manager.internal();
94
+
95
+ expect(internal).toBeDefined();
96
+ expect(internal.app).toBe(mockApp);
97
+ expect(internal.store).toBe(mockStore);
98
+ expect(internal.$axios).toBeDefined();
99
+ expect(internal.redirect).toBeDefined();
100
+ expect(internal.plugins).toBe(manager);
101
+ });
102
+ });
103
+
104
+ describe('registration (Dynamic)', () => {
105
+ it('registers and retrieves a dynamic component', () => {
106
+ const mockFn = jest.fn();
107
+
108
+ manager.register('component', 'my-component', mockFn);
109
+
110
+ const retrieved = manager.getDynamic('component', 'my-component');
111
+
112
+ expect(retrieved).toBe(mockFn);
113
+ });
114
+
115
+ it('unregisters a dynamic component', () => {
116
+ const mockFn = jest.fn();
117
+
118
+ manager.register('component', 'my-component', mockFn);
119
+ manager.unregister('component', 'my-component');
120
+
121
+ const retrieved = manager.getDynamic('component', 'my-component');
122
+
123
+ expect(retrieved).toBeUndefined();
124
+ });
125
+
126
+ it('accumulates l10n resources', () => {
127
+ const mockFn1 = jest.fn();
128
+ const mockFn2 = jest.fn();
129
+
130
+ manager.register('l10n', 'en-us', mockFn1);
131
+ manager.register('l10n', 'en-us', mockFn2);
132
+
133
+ const retrieved = manager.getDynamic('l10n', 'en-us');
134
+
135
+ expect(Array.isArray(retrieved)).toBe(true);
136
+ expect(retrieved).toHaveLength(2);
137
+ expect(retrieved).toContain(mockFn1);
138
+ expect(retrieved).toContain(mockFn2);
139
+ });
140
+
141
+ it('lists dynamic registrations by type', () => {
142
+ manager.register('component', 'comp1', jest.fn());
143
+ manager.register('component', 'comp2', jest.fn());
144
+ manager.register('edit', 'edit1', jest.fn());
145
+
146
+ const components = manager.listDynamic('component');
147
+ const edits = manager.listDynamic('edit');
148
+
149
+ expect(components).toHaveLength(2);
150
+ expect(components).toContain('comp1');
151
+ expect(components).toContain('comp2');
152
+ expect(edits).toHaveLength(1);
153
+ expect(edits).toContain('edit1');
154
+ });
155
+ });
156
+
157
+ describe('loadPluginAsync (URL Generation)', () => {
158
+ beforeEach(() => {
159
+ // Mock the internal loadAsync so we only test URL generation here
160
+ jest.spyOn(manager, 'loadAsync').mockImplementation().mockResolvedValue();
161
+ });
162
+
163
+ it('generates correct URL for standard plugin', async() => {
164
+ const pluginData = { name: 'elemental', version: '1.0.0' };
165
+ const expectedId = 'elemental-1.0.0';
166
+ const expectedUrl = `/api/v1/uiplugins/elemental/1.0.0/plugin/elemental-1.0.0.umd.min.js`;
167
+
168
+ await manager.loadPluginAsync(pluginData);
169
+
170
+ expect(manager.loadAsync).toHaveBeenCalledWith(expectedId, expectedUrl);
171
+ });
172
+
173
+ it('handles custom main file from metadata', async() => {
174
+ const pluginData = {
175
+ name: 'custom-plugin',
176
+ version: '2.0.0',
177
+ metadata: { main: 'custom.js' }
178
+ };
179
+ const expectedUrl = `/api/v1/uiplugins/custom-plugin/2.0.0/plugin/custom.js`;
180
+
181
+ await manager.loadPluginAsync(pluginData);
182
+
183
+ expect(manager.loadAsync).toHaveBeenCalledWith('custom-plugin-2.0.0', expectedUrl);
184
+ });
185
+
186
+ it('handles "direct" metadata plugins', async() => {
187
+ const pluginData = {
188
+ name: 'direct-plugin',
189
+ version: '1.0.0',
190
+ endpoint: 'http://localhost:8000/plugin.js',
191
+ metadata: { direct: 'true' }
192
+ };
193
+
194
+ await manager.loadPluginAsync(pluginData);
195
+
196
+ expect(manager.loadAsync).toHaveBeenCalledWith('direct-plugin-1.0.0', 'http://localhost:8000/plugin.js');
197
+ });
198
+
199
+ it('removes developer suffix from ID', async() => {
200
+ const pluginData = {
201
+ name: `my-plugin${ DEVELOPER_LOAD_NAME_SUFFIX }`,
202
+ version: '1.0.0'
203
+ };
204
+
205
+ await manager.loadPluginAsync(pluginData);
206
+
207
+ const expectedIdWithoutSuffix = 'my-plugin-1.0.0';
208
+
209
+ expect(manager.loadAsync).toHaveBeenCalledWith(
210
+ expectedIdWithoutSuffix,
211
+ expect.any(String)
212
+ );
213
+ });
214
+ });
215
+
216
+ describe('loadAsync (Script Injection)', () => {
217
+ it('resolves immediately if element already exists', async() => {
218
+ const id = 'existing-plugin';
219
+ const script = document.createElement('script');
220
+
221
+ script.id = id;
222
+ document.body.appendChild(script);
223
+
224
+ await expect(manager.loadAsync(id, 'url.js')).resolves.toBeUndefined();
225
+
226
+ document.body.removeChild(script);
227
+ });
228
+
229
+ it('injects script tag and initializes plugin on load', async() => {
230
+ const pluginId = 'test-plugin';
231
+ const pluginUrl = 'http://test.com/plugin.js';
232
+
233
+ // Mock the window object to simulate the plugin loading into global scope
234
+ const mockPluginInit = jest.fn();
235
+
236
+ window[pluginId] = { default: mockPluginInit };
237
+
238
+ // Start the load
239
+ const loadPromise = manager.loadAsync(pluginId, pluginUrl);
240
+
241
+ // Find the injected script tag in the DOM
242
+ const script = document.head.querySelector(`script[id="${ pluginId }"]`);
243
+
244
+ expect(script).toBeTruthy();
245
+ expect(script.src).toBe(pluginUrl);
246
+ expect(script.dataset.purpose).toBe('extension');
247
+
248
+ // Manually trigger the onload event
249
+ script.onload();
250
+
251
+ // Await the promise
252
+ await loadPromise;
253
+
254
+ // Assertions
255
+ expect(mockPluginInit).toHaveBeenCalledWith(
256
+ expect.objectContaining({ id: pluginId }),
257
+ expect.objectContaining({
258
+ app: mockApp,
259
+ store: mockStore,
260
+ $axios: {},
261
+ redirect: expect.any(Function),
262
+ plugins: manager
263
+ })
264
+ );
265
+ expect(mockStore.dispatch).toHaveBeenCalledWith('uiplugins/addPlugin', expect.objectContaining({ id: pluginId }));
266
+
267
+ // Cleanup
268
+ delete window[pluginId];
269
+ });
270
+
271
+ it('rejects if plugin code is not available', async() => {
272
+ const pluginId = 'missing-plugin';
273
+ const loadPromise = manager.loadAsync(pluginId, 'test.js');
274
+
275
+ const script = document.head.querySelector(`script[id="${ pluginId }"]`);
276
+
277
+ // Don't set window[pluginId], simulate missing plugin
278
+ script.onload();
279
+
280
+ await expect(loadPromise).rejects.toThrow('Could not load plugin code');
281
+ });
282
+
283
+ it('rejects if plugin initialization fails', async() => {
284
+ const pluginId = 'error-plugin';
285
+ const mockPluginInit = jest.fn().mockImplementation(() => {
286
+ throw new Error('Init error');
287
+ });
288
+
289
+ window[pluginId] = { default: mockPluginInit };
290
+
291
+ const loadPromise = manager.loadAsync(pluginId, 'test.js');
292
+
293
+ const script = document.head.querySelector(`script[id="${ pluginId }"]`);
294
+
295
+ script.onload();
296
+
297
+ await expect(loadPromise).rejects.toThrow('Could not initialize plugin');
298
+
299
+ delete window[pluginId];
300
+ });
301
+
302
+ it('rejects if script load fails', async() => {
303
+ const pluginId = 'fail-plugin';
304
+ const loadPromise = manager.loadAsync(pluginId, 'bad-url.js');
305
+
306
+ const script = document.head.querySelector(`script[id="${ pluginId }"]`);
307
+
308
+ // Trigger error
309
+ script.onerror({ target: { src: 'bad-url.js' } });
310
+
311
+ await expect(loadPromise).rejects.toThrow('Failed to load script');
312
+ });
313
+ });
314
+
315
+ describe('builtin extensions', () => {
316
+ it('registers and loads builtin extensions', () => {
317
+ const mockModule = {
318
+ default: jest.fn().mockImplementation((plugin) => {
319
+ plugin.types.models = { TestModel: jest.fn() };
320
+ })
321
+ };
322
+
323
+ manager.registerBuiltinExtension('builtin-test', mockModule);
324
+ manager.loadBuiltinExtensions();
325
+
326
+ expect(mockModule.default).toHaveBeenCalledWith(expect.objectContaining({ builtin: true }), expect.any(Object));
327
+ expect(mockStore.dispatch).toHaveBeenCalledWith('uiplugins/addPlugin', expect.objectContaining({
328
+ id: 'builtin-test',
329
+ builtin: true
330
+ }));
331
+ });
332
+
333
+ it('handles builtin extension that returns false', () => {
334
+ const mockModule = { default: jest.fn().mockReturnValue(false) };
335
+
336
+ manager.registerBuiltinExtension('skip-test', mockModule);
337
+ manager.loadBuiltinExtensions();
338
+
339
+ expect(mockModule.default).toHaveBeenCalledWith(expect.objectContaining({ builtin: true }), expect.any(Object));
340
+ expect(mockStore.dispatch).not.toHaveBeenCalledWith('uiplugins/addPlugin', expect.anything());
341
+ });
342
+
343
+ it('handles errors in builtin extension initialization', () => {
344
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
345
+ const mockModule = {
346
+ default: jest.fn().mockImplementation(() => {
347
+ throw new Error('Init failed');
348
+ })
349
+ };
350
+
351
+ manager.registerBuiltinExtension('error-builtin', mockModule);
352
+ manager.loadBuiltinExtensions();
353
+
354
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.objectContaining({}));
355
+ expect(mockStore.dispatch).not.toHaveBeenCalledWith('uiplugins/addPlugin', expect.anything());
356
+
357
+ consoleErrorSpy.mockRestore();
358
+ });
359
+ });
360
+
361
+ describe('getValidator', () => {
362
+ it('retrieves registered validator', () => {
363
+ const mockValidator = jest.fn();
364
+
365
+ manager.register('validator', 'test-validator', mockValidator);
366
+
367
+ // Validators are stored separately, simulate applyPlugin behavior
368
+ const plugin = {
369
+ types: {},
370
+ uiConfig: {},
371
+ l10n: {},
372
+ modelExtensions: {},
373
+ stores: [],
374
+ locales: [],
375
+ routes: [],
376
+ validators: { 'test-validator': mockValidator },
377
+ productNames: [],
378
+ };
379
+
380
+ manager.applyPlugin(plugin);
381
+
382
+ const validator = manager.getValidator('test-validator');
383
+
384
+ expect(validator).toBe(mockValidator);
385
+ });
386
+ });
387
+
388
+ describe('lastLoad timestamp', () => {
389
+ it('updates lastLoad after plugin load', async() => {
390
+ const initialLastLoad = manager.lastLoad;
391
+ const pluginId = 'timestamp-test';
392
+
393
+ window[pluginId] = { default: jest.fn() };
394
+
395
+ const loadPromise = manager.loadAsync(pluginId, 'test.js');
396
+ const script = document.head.querySelector(`script[id="${ pluginId }"]`);
397
+
398
+ script.onload();
399
+ await loadPromise;
400
+
401
+ expect(manager.lastLoad).toBeGreaterThan(initialLastLoad);
402
+
403
+ delete window[pluginId];
404
+ });
405
+ });
406
+
407
+ describe('getUIConfig', () => {
408
+ it('returns UI config for specific type and area', () => {
409
+ const mockAction = { label: 'Test Action' };
410
+ const plugin = {
411
+ types: {},
412
+ uiConfig: { 'edit-yaml': { header: [mockAction] } },
413
+ l10n: {},
414
+ modelExtensions: {},
415
+ stores: [],
416
+ locales: [],
417
+ routes: [],
418
+ validators: {},
419
+ productNames: [],
420
+ };
421
+
422
+ manager.applyPlugin(plugin);
423
+
424
+ const config = manager.getUIConfig('edit-yaml', 'header');
425
+
426
+ expect(config).toHaveLength(1);
427
+ expect(config[0]).toBe(mockAction);
428
+ });
429
+
430
+ it('returns empty array for non-existent area in valid type', () => {
431
+ // Use a valid ExtensionPoint type that exists in uiConfig
432
+ const config = manager.getUIConfig('edit-yaml', 'non-existent-area');
433
+
434
+ expect(config).toStrictEqual([]);
435
+ });
436
+ });
437
+ });
@@ -6,9 +6,9 @@ import { UI_PLUGIN_BASE_URL } from '@shell/config/uiplugins';
6
6
  import { ExtensionPoint } from './types';
7
7
  import { addLinkInterceptor, removeLinkInterceptor } from '@shell/plugins/clean-html';
8
8
 
9
- let extensionManagerInstance;
9
+ export const DEVELOPER_LOAD_NAME_SUFFIX = '-developer-load';
10
10
 
11
- const createExtensionManager = (context) => {
11
+ export const createExtensionManager = (context) => {
12
12
  const {
13
13
  app, store, $axios, redirect
14
14
  } = context;
@@ -33,13 +33,13 @@ const createExtensionManager = (context) => {
33
33
  /**
34
34
  * When an extension adds a model extension, it provides the class - we will instantiate that class and store and use that
35
35
  */
36
- function instantiateModelExtension($plugin, clz) {
36
+ function instantiateModelExtension($extension, clz) {
37
37
  const context = {
38
38
  dispatch: store.dispatch,
39
39
  getters: store.getters,
40
40
  t: store.getters['i18n/t'],
41
41
  $axios,
42
- $plugin,
42
+ $extension,
43
43
  };
44
44
 
45
45
  return new clz(context);
@@ -63,9 +63,18 @@ const createExtensionManager = (context) => {
63
63
  // Load a plugin from a UI package
64
64
  loadPluginAsync(plugin) {
65
65
  const { name, version } = plugin;
66
- const id = `${ name }-${ version }`;
66
+ let id = `${ name }-${ version }`;
67
67
  let url;
68
68
 
69
+ // for a developer load, we need to remove the suffix applied
70
+ // otherwise the extension won't load correctly
71
+ // but with this at least we won't hit developer loaded cards find charts
72
+ // when they aren't supposed to
73
+ if (id.includes(DEVELOPER_LOAD_NAME_SUFFIX)) {
74
+ id = id.replace(DEVELOPER_LOAD_NAME_SUFFIX, '');
75
+ }
76
+
77
+ // this is where a developer load hits (direct=true, developer=true)
69
78
  if (plugin?.metadata?.direct === 'true') {
70
79
  url = plugin.endpoint;
71
80
  } else {
@@ -455,7 +464,13 @@ const createExtensionManager = (context) => {
455
464
  try {
456
465
  const provisioner = context.$extension.getDynamic('provisioner', name);
457
466
 
458
- return new provisioner({ ...context });
467
+ const defaults = {
468
+ isCreate: false,
469
+ isEdit: false,
470
+ isView: false
471
+ };
472
+
473
+ return new provisioner({ ...defaults, ...context });
459
474
  } catch (e) {
460
475
  console.error('Error loading provisioner(s) from extensions', e); // eslint-disable-line no-console
461
476
  }
@@ -497,22 +512,3 @@ const createExtensionManager = (context) => {
497
512
  },
498
513
  };
499
514
  };
500
-
501
- /**
502
- * Initializes a new extension manager if one does not exist.
503
- * @param {*} context The Rancher Dashboard context object
504
- * @returns The extension manager instance
505
- */
506
- export const initExtensionManager = (context) => {
507
- if (!extensionManagerInstance) {
508
- extensionManagerInstance = createExtensionManager(context);
509
- }
510
-
511
- return extensionManagerInstance;
512
- };
513
-
514
- /**
515
- * Gets the extension manager instance.
516
- * @returns The extension manager instance
517
- */
518
- export const getExtensionManager = () => extensionManagerInstance;
@@ -147,8 +147,8 @@ export function getApplicableExtensionEnhancements<T>(
147
147
  const extensionEnhancements: T[] = [];
148
148
 
149
149
  // gate it so that we prevent errors on older versions of dashboard
150
- if (pluginCtx.$plugin?.getUIConfig) {
151
- const actions = pluginCtx.$plugin.getUIConfig(actionType, uiArea);
150
+ if (pluginCtx.$extension?.getUIConfig) {
151
+ const actions = pluginCtx.$extension.getUIConfig(actionType, uiArea);
152
152
 
153
153
  actions.forEach((action: any, i: number) => {
154
154
  if (checkExtensionRouteBinding(currRoute, action.locationConfig, context || {})) {
package/core/plugin.ts CHANGED
@@ -18,7 +18,8 @@ import {
18
18
  NavHooks, OnNavToPackage, OnNavAwayFromPackage, OnLogIn, OnLogOut,
19
19
  PaginationTableColumn,
20
20
  ExtensionEnvironment,
21
- ServerSidePaginationExtensionConfig
21
+ ServerSidePaginationExtensionConfig,
22
+ TableAction
22
23
  } from './types';
23
24
  import coreStore, { coreStoreModule, coreStoreState } from '@shell/plugins/dashboard-store';
24
25
  import { defineAsyncComponent, markRaw, Component } from 'vue';
@@ -272,6 +273,13 @@ export class Plugin implements IPlugin {
272
273
  });
273
274
  }
274
275
 
276
+ /**
277
+ * Adds an action/button to the UI
278
+ */
279
+ addTableHook(where: string, when: LocationConfig | string, action: TableAction): void {
280
+ this._addUIConfig(ExtensionPoint.TABLE, where, when, action);
281
+ }
282
+
275
283
  setHomePage(component: any) {
276
284
  this.addRoute({
277
285
  name: 'home',
@@ -13,10 +13,10 @@ export default function({
13
13
  store,
14
14
  $axios,
15
15
  redirect,
16
- $plugin,
16
+ $extension,
17
17
  }, inject) {
18
18
  if (dynamicLoader) {
19
- dynamicLoader.default($plugin);
19
+ dynamicLoader.default($extension);
20
20
  }
21
21
 
22
22
  // The libraries we build have Vue externalised, so we need to expose Vue as a global for
@@ -61,9 +61,13 @@ export interface ClusterProvisionerContext {
61
61
  */
62
62
  axios: any,
63
63
  /**
64
- * Definition of the extension
64
+ * [Deprecated] Definition of the extension
65
65
  */
66
66
  $plugin: any,
67
+ /**
68
+ * Definition of the extension
69
+ */
70
+ $extension: any,
67
71
  /**
68
72
  * Function to retrieve a localised string
69
73
  */
package/core/types.ts CHANGED
@@ -60,6 +60,7 @@ export enum ExtensionPoint {
60
60
  PANEL = 'Panel', // eslint-disable-line no-unused-vars
61
61
  CARD = 'Card', // eslint-disable-line no-unused-vars
62
62
  TABLE_COL = 'TableColumn', // eslint-disable-line no-unused-vars
63
+ TABLE = 'Table', // eslint-disable-line no-unused-vars
63
64
  }
64
65
 
65
66
  /** Enum regarding action locations that are extensible in the UI */
@@ -79,6 +80,11 @@ export enum PanelLocation {
79
80
  /** Enum regarding tab locations that are extensible in the UI */
80
81
  export enum TabLocation {
81
82
  RESOURCE_DETAIL = 'tab', // eslint-disable-line no-unused-vars
83
+ OTHER = 'other-tab-locations', // eslint-disable-line no-unused-vars
84
+ RESOURCE_DETAIL_PAGE = 'resource-detail-page', // eslint-disable-line no-unused-vars
85
+ RESOURCE_CREATE_PAGE = 'resource-create-page', // eslint-disable-line no-unused-vars
86
+ RESOURCE_EDIT_PAGE = 'resource-edit-page', // eslint-disable-line no-unused-vars
87
+ RESOURCE_SHOW_CONFIGURATION = 'resource-show-configuration', // eslint-disable-line no-unused-vars
82
88
  CLUSTER_CREATE_RKE2 = 'cluster-create-rke2', // eslint-disable-line no-unused-vars
83
89
  }
84
90
 
@@ -92,6 +98,16 @@ export enum TableColumnLocation {
92
98
  RESOURCE = 'resource-list', // eslint-disable-line no-unused-vars
93
99
  }
94
100
 
101
+ /** Enum regarding table locations that are extensible in the UI */
102
+ export enum TableLocation {
103
+ RESOURCE = 'resource-list', // eslint-disable-line no-unused-vars
104
+ }
105
+
106
+ /** Definition of a Table extension hook */
107
+ export type TableAction = {
108
+ tableHook: Function
109
+ };
110
+
95
111
  /** Definition of the shortcut object (keyboard shortcuts) */
96
112
  export type ShortCutKey = {
97
113
  windows?: string[];
@@ -234,6 +250,11 @@ export interface ProductOptions {
234
250
  */
235
251
  ifHaveType?: string | RegExp;
236
252
 
253
+ /**
254
+ * Hide the product if the type is present (opposite of ifHaveType)
255
+ */
256
+ ifNotHaveType?: string | RegExp;
257
+
237
258
  /**
238
259
  * The vuex store that this product should use by default i.e. 'management'
239
260
  */
@@ -283,6 +304,11 @@ export interface ProductOptions {
283
304
  * Configuration required to show a header in a ResourceTable
284
305
  */
285
306
  export interface HeaderOptions {
307
+ /**
308
+ * Order/position of the table column added by an extension
309
+ */
310
+ weight?: number;
311
+
286
312
  /**
287
313
  * Name of the header. This should be unique.
288
314
  */
@@ -654,6 +680,15 @@ export interface IPlugin {
654
680
  */
655
681
  addTableColumn(where: TableColumnLocation | string, when: LocationConfig | string, column: TableColumn, paginationColumn?: TableColumn): void;
656
682
 
683
+ /**
684
+ * Adds to Table events hook on ResourceTable
685
+ *
686
+ * @param where
687
+ * @param when
688
+ * @param action
689
+ */
690
+ addTableHook(where: TableLocation | string, when: LocationConfig | string, action: TableAction): void;
691
+
657
692
  /**
658
693
  * Set the component to use for the landing home page
659
694
  * @param component Home page component