@rancher/shell 3.0.12-rc.5 → 3.0.12-rc.6

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 (89) hide show
  1. package/apis/intf/resources-api/resource-base.ts +42 -2
  2. package/apis/intf/resources-api/resource-instance.ts +101 -0
  3. package/apis/intf/resources-api/resources-api.ts +177 -23
  4. package/apis/intf/resources.ts +8 -6
  5. package/apis/resources/__tests__/resources-api-class.test.ts +309 -28
  6. package/apis/resources/resources-api-class.ts +232 -43
  7. package/assets/images/providers/traefik.png +0 -0
  8. package/assets/translations/en-us.yaml +10 -4
  9. package/chart/__tests__/rancher-monitoring-dashboards-index.test.ts +269 -0
  10. package/chart/rancher-monitoring-dashboards/index.vue +155 -0
  11. package/cloud-credential/__tests__/generic.test.ts +132 -0
  12. package/cloud-credential/generic.vue +33 -3
  13. package/components/EtcdInfoBanner.vue +2 -12
  14. package/components/GrafanaDashboard.vue +2 -8
  15. package/components/Resource/Detail/Masthead/__tests__/index.test.ts +68 -14
  16. package/components/Resource/Detail/Masthead/index.vue +8 -3
  17. package/components/form/Security.vue +1 -1
  18. package/components/formatter/Translate.vue +22 -1
  19. package/components/nav/Group.vue +5 -10
  20. package/components/nav/TopLevelMenu.vue +1 -30
  21. package/components/nav/__tests__/Group.test.ts +61 -0
  22. package/config/labels-annotations.js +1 -0
  23. package/config/table-headers.js +5 -2
  24. package/core/__tests__/plugin-products-apply.test.ts +787 -0
  25. package/core/__tests__/plugin-products-extend.test.ts +189 -0
  26. package/core/__tests__/plugin-products-helpers.test.ts +81 -78
  27. package/core/__tests__/plugin-products-new.test.ts +389 -0
  28. package/core/__tests__/plugin-products-scenarios.test.ts +980 -0
  29. package/core/__tests__/plugin-products-side-menu.test.ts +1935 -0
  30. package/core/plugin-products-base.ts +201 -93
  31. package/core/plugin-products-extending.ts +1 -1
  32. package/core/plugin-products-external.ts +380 -0
  33. package/core/plugin-products-helpers.ts +28 -24
  34. package/core/plugin-products-internal.ts +207 -0
  35. package/core/plugin-products-top-level.ts +10 -5
  36. package/core/plugin-products-type-guards.ts +17 -6
  37. package/core/plugin-products.ts +5 -7
  38. package/core/plugin-types.ts +1 -389
  39. package/core/plugin.ts +10 -11
  40. package/core/types.ts +6 -202
  41. package/detail/__tests__/provisioning.cattle.io.cluster.test.ts +52 -0
  42. package/detail/pod.vue +1 -1
  43. package/detail/provisioning.cattle.io.cluster.vue +0 -2
  44. package/edit/auth/AuthProviderWarningBanners.vue +12 -0
  45. package/edit/auth/__tests__/AuthProviderWarningBanners.test.ts +10 -1
  46. package/edit/auth/__tests__/azuread.test.ts +1 -0
  47. package/edit/auth/__tests__/github.test.ts +1 -0
  48. package/edit/auth/__tests__/oidc.test.ts +1 -0
  49. package/edit/auth/__tests__/saml.test.ts +1 -0
  50. package/edit/auth/ldap/__tests__/index.test.ts +1 -0
  51. package/edit/provisioning.cattle.io.cluster/index.vue +1 -1
  52. package/edit/provisioning.cattle.io.cluster/rke2.vue +1 -1
  53. package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +2 -2
  54. package/list/management.cattle.io.user.vue +24 -5
  55. package/list/utils/management.cattle.io.cluster.utils.ts +1 -1
  56. package/machine-config/__tests__/generic.test.ts +163 -0
  57. package/machine-config/components/EC2Networking.vue +31 -7
  58. package/machine-config/components/__tests__/EC2Networking.test.ts +38 -15
  59. package/machine-config/generic.vue +32 -5
  60. package/mixins/resource-fetch.js +1 -1
  61. package/models/__tests__/rke-machine.cattle.io.vmwarevspheremachinetemplate.test.ts +22 -0
  62. package/models/pod.js +12 -10
  63. package/models/rke-machine.cattle.io.vmwarevspheremachinetemplate.js +9 -0
  64. package/package.json +1 -1
  65. package/pages/auth/login.vue +1 -1
  66. package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +110 -0
  67. package/pages/c/_cluster/apps/charts/install.vue +29 -0
  68. package/pages/c/_cluster/auth/config/index.vue +11 -4
  69. package/pages/c/_cluster/monitoring/index.vue +6 -1
  70. package/plugins/dashboard-store/resource-class.js +10 -3
  71. package/plugins/steve/__tests__/steve-class-resource-api.test.ts +147 -0
  72. package/plugins/steve/steve-class.js +43 -0
  73. package/plugins/steve/subscribe.js +11 -0
  74. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +115 -0
  75. package/rancher-components/Form/LabeledInput/LabeledInput.vue +83 -3
  76. package/store/__tests__/auth.test.ts +18 -0
  77. package/store/__tests__/notifications.test.ts +466 -1
  78. package/store/__tests__/ui-context.test.ts +255 -0
  79. package/store/auth.js +11 -1
  80. package/store/features.js +1 -0
  81. package/store/plugins.js +6 -0
  82. package/types/rancher/steve.api.ts +2 -2
  83. package/types/shell/index.d.ts +58 -0
  84. package/types/store/dashboard-store.types.ts +2 -2
  85. package/types/store/type-map.ts +262 -1
  86. package/utils/async.ts +2 -0
  87. package/utils/grafana.js +2 -17
  88. package/utils/monitoring.js +38 -1
  89. package/core/__tests__/plugin-products.test.ts +0 -4694
@@ -1,4694 +0,0 @@
1
- import { PluginProduct } from '@shell/core/plugin-products';
2
- import { Plugin } from '@shell/core/plugin';
3
- import {
4
- ProductMetadata, ProductSinglePage, ProductChildPage,
5
- ProductChildGroup, ProductChildCustomPage, ProductChildResourcePage,
6
- ProductChild, StandardProductNames
7
- } from '@shell/core/plugin-types';
8
- import { IExtension } from '@shell/core/types';
9
-
10
- // Mock the helper functions
11
- jest.mock('@shell/core/plugin-products-helpers', () => ({
12
- gatherChildrenOrdering: jest.fn((config) => config),
13
- generateTopLevelExtensionSimpleBaseRoute: jest.fn((name, opts) => ({
14
- name: `${ name }-simple`,
15
- path: opts?.omitPath ? '' : `/${ name }`,
16
- component: opts?.component,
17
- })),
18
- generateVirtualTypeRoute: jest.fn((parentName, page, opts) => ({
19
- name: page ? `${ parentName }-${ page.name }` : `${ parentName }-group`,
20
- path: opts?.omitPath ? '' : `/${ parentName }/${ page?.name || 'group' }`,
21
- component: opts?.component,
22
- })),
23
- generateConfigureTypeRoute: jest.fn((parentName, page, opts) => {
24
- const routeName = opts?.extendProduct ? `c-cluster-${ parentName }-resource` : `${ parentName }-c-cluster-resource`;
25
- const routePath = opts?.extendProduct ? `c/:cluster/${ parentName }/:resource` : `${ parentName }/c/:cluster/:resource`;
26
- const cluster = opts?.extendProduct ? undefined : '__BLANK_CLUSTER__';
27
-
28
- return {
29
- name: routeName,
30
- path: opts?.omitPath ? '' : routePath,
31
- params: cluster ? {
32
- product: parentName.replace(/-/g, ''),
33
- cluster,
34
- resource: page?.type,
35
- } : {
36
- product: parentName,
37
- resource: page?.type,
38
- },
39
- };
40
- }),
41
- generateResourceRoutes: jest.fn((parentName, child) => [
42
- {
43
- name: `${ parentName }-${ child.type }-list`,
44
- path: `/${ parentName }/${ child.type }`,
45
- },
46
- {
47
- name: `${ parentName }-${ child.type }-detail`,
48
- path: `/${ parentName }/${ child.type }/:id`,
49
- },
50
- ]),
51
- }));
52
-
53
- jest.mock('@shell/core/productDebugger', () => ({
54
- DSLRegistrationsPerProduct: jest.fn(),
55
- registeredRoutes: jest.fn(),
56
- }));
57
-
58
- // Create mock factories
59
- function createMockPlugin(): IExtension {
60
- return {
61
- _registerTopLevelProduct: jest.fn(),
62
- addRoute: jest.fn(),
63
- enableServerSidePagination: jest.fn(),
64
- DSL: jest.fn((store, productName) => ({
65
- basicType: jest.fn(),
66
- labelGroup: jest.fn(),
67
- setGroupDefaultType: jest.fn(),
68
- weightGroup: jest.fn(),
69
- virtualType: jest.fn(),
70
- configureType: jest.fn(),
71
- weightType: jest.fn(),
72
- product: jest.fn(),
73
- headers: jest.fn(),
74
- hideBulkActions: jest.fn(),
75
- mapGroup: jest.fn(),
76
- ignoreGroup: jest.fn(),
77
- mapType: jest.fn(),
78
- ignoreType: jest.fn(),
79
- moveType: jest.fn(),
80
- })),
81
- } as any;
82
- }
83
-
84
- function createMockStore(extendableProducts: string[] = Object.values(StandardProductNames), managementSchemas: string[] = []): any {
85
- return {
86
- getters: {
87
- 'type-map/productByName': (productName: string) => (extendableProducts.includes(productName) ? { name: productName, extendable: true } : undefined),
88
- 'management/schemaFor': (type: string) => (managementSchemas.includes(type) ? { id: type } : undefined),
89
- }
90
- };
91
- }
92
-
93
- describe('pluginProduct', () => {
94
- describe('new product scenarios', () => {
95
- it('should create a new product with config items', () => {
96
- const mockPlugin = createMockPlugin();
97
- const productMetadata: ProductMetadata = {
98
- name: 'test-product',
99
- label: 'Test Product',
100
- icon: 'icon-test',
101
- };
102
- const config: ProductChildPage[] = [
103
- {
104
- name: 'overview',
105
- label: 'Overview',
106
- component: { name: 'OverviewPage' },
107
- },
108
- {
109
- name: 'details',
110
- label: 'Details',
111
- component: { name: 'DetailsPage' },
112
- },
113
- ];
114
-
115
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
116
-
117
- expect(pluginProduct.newProduct).toBe(true);
118
- expect(mockPlugin._registerTopLevelProduct).toHaveBeenCalledTimes(1);
119
- expect(mockPlugin.addRoute).toHaveBeenCalledTimes(2);
120
- });
121
-
122
- it('should create a single page product', () => {
123
- const mockPlugin = createMockPlugin();
124
- const productSinglePage: ProductSinglePage = {
125
- name: 'single-page-product',
126
- label: 'Single Page',
127
- component: { name: 'SinglePageComponent' },
128
- };
129
-
130
- const pluginProduct = new PluginProduct(mockPlugin, productSinglePage, []);
131
-
132
- expect(pluginProduct.newProduct).toBe(true);
133
- expect(mockPlugin._registerTopLevelProduct).toHaveBeenCalledTimes(1);
134
- expect(mockPlugin.addRoute).toHaveBeenCalledTimes(1);
135
- });
136
-
137
- it('should handle product names with dashes by removing them', () => {
138
- const mockPlugin = createMockPlugin();
139
- const productMetadata: ProductMetadata = {
140
- name: 'test-product-name',
141
- label: 'Test',
142
- };
143
-
144
- new PluginProduct(mockPlugin, productMetadata, []);
145
-
146
- expect(mockPlugin._registerTopLevelProduct).toHaveBeenCalledTimes(1);
147
- });
148
-
149
- it('should create default empty page config when no config provided', () => {
150
- const mockPlugin = createMockPlugin();
151
- const productMetadata: ProductMetadata = {
152
- name: 'empty-product',
153
- label: 'Empty',
154
- };
155
-
156
- new PluginProduct(mockPlugin, productMetadata, []);
157
-
158
- expect(mockPlugin.addRoute).toHaveBeenCalledTimes(1);
159
- });
160
-
161
- it('should throw error when product object lacks name property', () => {
162
- const mockPlugin = createMockPlugin();
163
- const invalidProduct = { label: 'No Name' } as any;
164
-
165
- expect(() => {
166
- new PluginProduct(mockPlugin, invalidProduct, []);
167
- }).toThrow('Invalid product');
168
- });
169
-
170
- it('should throw error when product type is invalid', () => {
171
- const mockPlugin = createMockPlugin();
172
-
173
- expect(() => {
174
- new PluginProduct(mockPlugin, 123 as any, []);
175
- }).toThrow('Invalid product');
176
- });
177
- });
178
-
179
- describe('extending standard product', () => {
180
- it('should extend an existing standard product with valid name', () => {
181
- const mockPlugin = createMockPlugin();
182
- const config: ProductChildPage[] = [
183
- {
184
- name: 'custom-section',
185
- label: 'Custom',
186
- component: { name: 'CustomComponent' },
187
- },
188
- ];
189
-
190
- const validStandardProduct = StandardProductNames.EXPLORER;
191
-
192
- const pluginProduct = new PluginProduct(mockPlugin, validStandardProduct, config);
193
-
194
- expect(pluginProduct.newProduct).toBe(false);
195
- expect(mockPlugin.addRoute).toHaveBeenCalledTimes(1);
196
- });
197
-
198
- it('should accept any string as product name when extending', () => {
199
- const mockPlugin = createMockPlugin();
200
- const customProduct = 'custom-product';
201
-
202
- const pluginProduct = new PluginProduct(mockPlugin, customProduct, []);
203
-
204
- expect(pluginProduct.newProduct).toBe(false);
205
- });
206
-
207
- it('should throw error during apply when extending a product that is not registered', () => {
208
- const mockPlugin = createMockPlugin();
209
- const mockStore = createMockStore([]);
210
- const mockDSL = {
211
- product: jest.fn(),
212
- basicType: jest.fn(),
213
- labelGroup: jest.fn(),
214
- setGroupDefaultType: jest.fn(),
215
- weightGroup: jest.fn(),
216
- virtualType: jest.fn(),
217
- configureType: jest.fn(),
218
- weightType: jest.fn(),
219
- };
220
-
221
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
222
-
223
- const pluginProduct = new PluginProduct(mockPlugin, 'non-existent-product', []);
224
-
225
- expect(() => {
226
- pluginProduct.apply(mockPlugin, mockStore);
227
- }).toThrow('is not extendable');
228
- });
229
-
230
- it('should apply successfully when extending an extendable product', () => {
231
- const mockPlugin = createMockPlugin();
232
- const mockStore = createMockStore(['my-custom-builtin-product']);
233
- const mockDSL = {
234
- product: jest.fn(),
235
- basicType: jest.fn(),
236
- labelGroup: jest.fn(),
237
- setGroupDefaultType: jest.fn(),
238
- weightGroup: jest.fn(),
239
- virtualType: jest.fn(),
240
- configureType: jest.fn(),
241
- weightType: jest.fn(),
242
- };
243
-
244
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
245
-
246
- const pluginProduct = new PluginProduct(mockPlugin, 'my-custom-builtin-product', []);
247
-
248
- expect(() => {
249
- pluginProduct.apply(mockPlugin, mockStore);
250
- }).not.toThrow();
251
- });
252
-
253
- it('should throw error during apply when extending a registered product that is not extendable', () => {
254
- const mockPlugin = createMockPlugin();
255
- const mockStore = {
256
- getters: {
257
- 'type-map/productByName': (productName: string) => {
258
- if (productName === 'other-extension-product') {
259
- return { name: productName, extendable: false };
260
- }
261
-
262
- return undefined;
263
- },
264
- },
265
- };
266
- const mockDSL = {
267
- product: jest.fn(),
268
- basicType: jest.fn(),
269
- labelGroup: jest.fn(),
270
- setGroupDefaultType: jest.fn(),
271
- weightGroup: jest.fn(),
272
- virtualType: jest.fn(),
273
- configureType: jest.fn(),
274
- weightType: jest.fn(),
275
- };
276
-
277
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
278
-
279
- const pluginProduct = new PluginProduct(mockPlugin, 'other-extension-product', []);
280
-
281
- expect(() => {
282
- pluginProduct.apply(mockPlugin, mockStore);
283
- }).toThrow('is not extendable');
284
- });
285
-
286
- it('should not register new product when extending standard product', () => {
287
- const mockPlugin = createMockPlugin();
288
- const validStandardProduct = StandardProductNames.EXPLORER;
289
-
290
- new PluginProduct(mockPlugin, validStandardProduct, []);
291
-
292
- expect(mockPlugin._registerTopLevelProduct).not.toHaveBeenCalled();
293
- });
294
- });
295
-
296
- describe('apply stage - product registration', () => {
297
- it('should register new product via DSL during apply', () => {
298
- const mockPlugin = createMockPlugin();
299
- const mockStore = createMockStore();
300
- const mockDSL = {
301
- product: jest.fn(),
302
- basicType: jest.fn(),
303
- labelGroup: jest.fn(),
304
- setGroupDefaultType: jest.fn(),
305
- weightGroup: jest.fn(),
306
- virtualType: jest.fn(),
307
- configureType: jest.fn(),
308
- weightType: jest.fn(),
309
- };
310
-
311
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
312
-
313
- const productMetadata: ProductMetadata = {
314
- name: 'new-product',
315
- label: 'New Product',
316
- };
317
- const config: ProductChildPage[] = [
318
- {
319
- name: 'page1',
320
- label: 'Page 1',
321
- component: { name: 'Page1' },
322
- },
323
- ];
324
-
325
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
326
-
327
- pluginProduct.apply(mockPlugin, mockStore);
328
-
329
- expect(mockDSL.product).toHaveBeenCalledTimes(1);
330
- expect(mockDSL.product).toHaveBeenCalledWith(
331
- expect.objectContaining({
332
- name: 'newproduct',
333
- inStore: 'management',
334
- version: 2,
335
- showClusterSwitcher: false,
336
- category: 'global',
337
- })
338
- );
339
- });
340
-
341
- it('should not register product when extending standard product during apply', () => {
342
- const mockPlugin = createMockPlugin();
343
- const mockStore = createMockStore();
344
- const mockDSL = {
345
- product: jest.fn(),
346
- basicType: jest.fn(),
347
- labelGroup: jest.fn(),
348
- setGroupDefaultType: jest.fn(),
349
- weightGroup: jest.fn(),
350
- virtualType: jest.fn(),
351
- configureType: jest.fn(),
352
- weightType: jest.fn(),
353
- };
354
-
355
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
356
-
357
- const validStandardProduct = StandardProductNames.EXPLORER;
358
-
359
- const pluginProduct = new PluginProduct(mockPlugin, validStandardProduct, []);
360
-
361
- pluginProduct.apply(mockPlugin, mockStore);
362
-
363
- expect(mockDSL.product).not.toHaveBeenCalled();
364
- });
365
-
366
- it('should configure virtualType items during apply', () => {
367
- const mockPlugin = createMockPlugin();
368
- const mockStore = createMockStore();
369
- const mockDSL = {
370
- product: jest.fn(),
371
- basicType: jest.fn(),
372
- labelGroup: jest.fn(),
373
- setGroupDefaultType: jest.fn(),
374
- weightGroup: jest.fn(),
375
- virtualType: jest.fn(),
376
- configureType: jest.fn(),
377
- weightType: jest.fn(),
378
- };
379
-
380
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
381
-
382
- const productMetadata: ProductMetadata = {
383
- name: 'product-with-pages',
384
- label: 'Product',
385
- };
386
- const config: ProductChildPage[] = [
387
- {
388
- name: 'overview',
389
- label: 'Overview',
390
- component: { name: 'OverviewComponent' },
391
- weight: 10,
392
- },
393
- ];
394
-
395
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
396
-
397
- pluginProduct.apply(mockPlugin, mockStore);
398
-
399
- expect(mockDSL.virtualType).toHaveBeenCalledTimes(1);
400
- expect(mockDSL.virtualType).toHaveBeenCalledWith(
401
- expect.objectContaining({
402
- name: 'productwithpages-overview',
403
- label: 'Overview',
404
- weight: 10,
405
- })
406
- );
407
- });
408
-
409
- it('should configure configureType (resource) items during apply', () => {
410
- const mockPlugin = createMockPlugin();
411
- const mockStore = createMockStore();
412
- const mockDSL = {
413
- product: jest.fn(),
414
- basicType: jest.fn(),
415
- labelGroup: jest.fn(),
416
- setGroupDefaultType: jest.fn(),
417
- weightGroup: jest.fn(),
418
- virtualType: jest.fn(),
419
- configureType: jest.fn(),
420
- weightType: jest.fn(),
421
- };
422
-
423
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
424
-
425
- const productMetadata: ProductMetadata = {
426
- name: 'resource-product',
427
- label: 'Resources',
428
- };
429
- const config: ProductChildPage[] = [
430
- {
431
- type: 'custom.resource',
432
- weight: 5,
433
- },
434
- ];
435
-
436
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
437
-
438
- pluginProduct.apply(mockPlugin, mockStore);
439
-
440
- expect(mockDSL.configureType).toHaveBeenCalledWith(
441
- 'custom.resource',
442
- expect.objectContaining({
443
- isCreatable: true,
444
- isEditable: true,
445
- isRemovable: true,
446
- canYaml: true,
447
- })
448
- );
449
- expect(mockDSL.weightType).toHaveBeenCalledWith('custom.resource', 5, true);
450
- });
451
- });
452
-
453
- describe('grouped items', () => {
454
- it('should handle product with grouped items', () => {
455
- const mockPlugin = createMockPlugin();
456
- const mockStore = createMockStore();
457
- const mockDSL = {
458
- product: jest.fn(),
459
- basicType: jest.fn(),
460
- labelGroup: jest.fn(),
461
- setGroupDefaultType: jest.fn(),
462
- weightGroup: jest.fn(),
463
- virtualType: jest.fn(),
464
- configureType: jest.fn(),
465
- weightType: jest.fn(),
466
- };
467
-
468
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
469
-
470
- const productMetadata: ProductMetadata = {
471
- name: 'grouped-product',
472
- label: 'Grouped',
473
- };
474
- const groupedConfig: ProductChildGroup[] = [
475
- {
476
- name: 'settings',
477
- label: 'Settings',
478
- children: [
479
- {
480
- name: 'general',
481
- label: 'General',
482
- component: { name: 'GeneralSettings' },
483
- },
484
- {
485
- name: 'advanced',
486
- label: 'Advanced',
487
- component: { name: 'AdvancedSettings' },
488
- },
489
- ],
490
- },
491
- ];
492
-
493
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, groupedConfig);
494
-
495
- pluginProduct.apply(mockPlugin, mockStore);
496
-
497
- expect(mockDSL.basicType).toHaveBeenCalledTimes(2);
498
- expect(mockDSL.labelGroup).toHaveBeenCalledWith(
499
- expect.stringContaining('settings'),
500
- 'Settings',
501
- undefined
502
- );
503
- expect(mockDSL.virtualType).toHaveBeenCalledTimes(2);
504
- });
505
-
506
- it('should set group default type when group has no component', () => {
507
- const mockPlugin = createMockPlugin();
508
- const mockStore = createMockStore();
509
- const mockDSL = {
510
- product: jest.fn(),
511
- basicType: jest.fn(),
512
- labelGroup: jest.fn(),
513
- setGroupDefaultType: jest.fn(),
514
- weightGroup: jest.fn(),
515
- virtualType: jest.fn(),
516
- configureType: jest.fn(),
517
- weightType: jest.fn(),
518
- };
519
-
520
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
521
-
522
- const productMetadata: ProductMetadata = {
523
- name: 'group-no-component',
524
- label: 'Group No Component',
525
- };
526
- const config: ProductChildGroup[] = [
527
- {
528
- name: 'group',
529
- label: 'Group Without Component',
530
- children: [
531
- {
532
- name: 'child1',
533
- label: 'Child 1',
534
- component: { name: 'Child1Component' },
535
- },
536
- ],
537
- },
538
- ];
539
-
540
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
541
-
542
- pluginProduct.apply(mockPlugin, mockStore);
543
-
544
- expect(mockDSL.setGroupDefaultType).toHaveBeenCalledWith(
545
- expect.stringContaining('group'),
546
- expect.stringContaining('child1')
547
- );
548
- });
549
-
550
- it('should set group default type to itself when group has component', () => {
551
- const mockPlugin = createMockPlugin();
552
- const mockStore = createMockStore();
553
- const mockDSL = {
554
- product: jest.fn(),
555
- basicType: jest.fn(),
556
- labelGroup: jest.fn(),
557
- setGroupDefaultType: jest.fn(),
558
- weightGroup: jest.fn(),
559
- virtualType: jest.fn(),
560
- configureType: jest.fn(),
561
- weightType: jest.fn(),
562
- };
563
-
564
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
565
-
566
- const productMetadata: ProductMetadata = {
567
- name: 'group-with-component',
568
- label: 'Group With Component',
569
- };
570
- const config: ProductChildGroup[] = [
571
- {
572
- name: 'group',
573
- label: 'Group With Component',
574
- component: { name: 'GroupOverviewComponent' },
575
- children: [
576
- {
577
- name: 'child1',
578
- label: 'Child 1',
579
- component: { name: 'Child1Component' },
580
- },
581
- ],
582
- },
583
- ];
584
-
585
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
586
-
587
- pluginProduct.apply(mockPlugin, mockStore);
588
-
589
- // When a group has a component, setGroupDefaultType should be called with the group name itself
590
- // This ensures clicking the group in nav routes to the group's page, not bypassing to first child
591
- expect(mockDSL.setGroupDefaultType).toHaveBeenCalledWith(
592
- 'groupwithcomponent-group',
593
- 'groupwithcomponent-group'
594
- );
595
- });
596
-
597
- it('should apply group weight when specified', () => {
598
- const mockPlugin = createMockPlugin();
599
- const mockStore = createMockStore();
600
- const mockDSL = {
601
- product: jest.fn(),
602
- basicType: jest.fn(),
603
- labelGroup: jest.fn(),
604
- setGroupDefaultType: jest.fn(),
605
- weightGroup: jest.fn(),
606
- virtualType: jest.fn(),
607
- configureType: jest.fn(),
608
- weightType: jest.fn(),
609
- };
610
-
611
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
612
-
613
- const productMetadata: ProductMetadata = {
614
- name: 'weighted-group',
615
- label: 'Weighted',
616
- };
617
- const config: ProductChildGroup[] = [
618
- {
619
- name: 'group',
620
- label: 'Group',
621
- weight: 50,
622
- children: [
623
- {
624
- name: 'child',
625
- label: 'Child',
626
- component: { name: 'ChildComponent' },
627
- },
628
- ],
629
- },
630
- ];
631
-
632
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
633
-
634
- pluginProduct.apply(mockPlugin, mockStore);
635
-
636
- expect(mockDSL.weightGroup).toHaveBeenCalledWith(
637
- expect.stringContaining('group'),
638
- 50,
639
- true
640
- );
641
- });
642
- });
643
-
644
- describe('default route determination', () => {
645
- it('should use first config item as default route for new product', () => {
646
- const mockPlugin = createMockPlugin();
647
- const mockStore = createMockStore();
648
- const mockDSL = {
649
- product: jest.fn(),
650
- basicType: jest.fn(),
651
- labelGroup: jest.fn(),
652
- setGroupDefaultType: jest.fn(),
653
- weightGroup: jest.fn(),
654
- virtualType: jest.fn(),
655
- configureType: jest.fn(),
656
- weightType: jest.fn(),
657
- };
658
-
659
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
660
-
661
- const productMetadata: ProductMetadata = {
662
- name: 'default-route-product',
663
- label: 'Default Route',
664
- };
665
- const config: ProductChildPage[] = [
666
- {
667
- name: 'first',
668
- label: 'First',
669
- component: { name: 'FirstComponent' },
670
- },
671
- {
672
- name: 'second',
673
- label: 'Second',
674
- component: { name: 'SecondComponent' },
675
- },
676
- ];
677
-
678
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
679
-
680
- pluginProduct.apply(mockPlugin, mockStore);
681
-
682
- expect(mockDSL.product).toHaveBeenCalledWith(
683
- expect.objectContaining({ to: expect.objectContaining({ name: expect.stringContaining('first') }) })
684
- );
685
- });
686
-
687
- it('should use first group child as default route when first item is group', () => {
688
- const mockPlugin = createMockPlugin();
689
- const mockStore = createMockStore();
690
- const mockDSL = {
691
- product: jest.fn(),
692
- basicType: jest.fn(),
693
- labelGroup: jest.fn(),
694
- setGroupDefaultType: jest.fn(),
695
- weightGroup: jest.fn(),
696
- virtualType: jest.fn(),
697
- configureType: jest.fn(),
698
- weightType: jest.fn(),
699
- };
700
-
701
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
702
-
703
- const productMetadata: ProductMetadata = {
704
- name: 'group-default-route',
705
- label: 'Group Default',
706
- };
707
- const config: ProductChildGroup[] = [
708
- {
709
- name: 'settings',
710
- label: 'Settings',
711
- children: [
712
- {
713
- name: 'general',
714
- label: 'General',
715
- component: { name: 'GeneralComponent' },
716
- },
717
- ],
718
- },
719
- ];
720
-
721
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
722
-
723
- pluginProduct.apply(mockPlugin, mockStore);
724
-
725
- expect(mockDSL.product).toHaveBeenCalledWith(
726
- expect.objectContaining({ to: expect.objectContaining({ name: expect.stringContaining('general') }) })
727
- );
728
- });
729
-
730
- it('should use first group child with resource type as default route when first item is group', () => {
731
- const mockPlugin = createMockPlugin();
732
- const mockStore = createMockStore();
733
- const mockDSL = {
734
- product: jest.fn(),
735
- basicType: jest.fn(),
736
- labelGroup: jest.fn(),
737
- setGroupDefaultType: jest.fn(),
738
- weightGroup: jest.fn(),
739
- virtualType: jest.fn(),
740
- configureType: jest.fn(),
741
- weightType: jest.fn(),
742
- };
743
-
744
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
745
-
746
- const productMetadata: ProductMetadata = {
747
- name: 'group-resource-default',
748
- label: 'Group Resource Default',
749
- };
750
- const config: ProductChildGroup[] = [
751
- {
752
- name: 'resources',
753
- label: 'Resources',
754
- children: [
755
- { type: 'provisioning.cattle.io.cluster' },
756
- ],
757
- },
758
- ];
759
-
760
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
761
-
762
- pluginProduct.apply(mockPlugin, mockStore);
763
-
764
- expect(mockDSL.product).toHaveBeenCalledWith(
765
- expect.objectContaining({
766
- to: expect.objectContaining({
767
- name: 'groupresourcedefault-c-cluster-resource',
768
- params: expect.objectContaining({
769
- product: 'groupresourcedefault',
770
- cluster: '__BLANK_CLUSTER__',
771
- resource: 'provisioning.cattle.io.cluster',
772
- }),
773
- }),
774
- })
775
- );
776
- });
777
-
778
- it('should use resource type as default route when first item is configureType', () => {
779
- const mockPlugin = createMockPlugin();
780
- const mockStore = createMockStore();
781
- const mockDSL = {
782
- product: jest.fn(),
783
- basicType: jest.fn(),
784
- labelGroup: jest.fn(),
785
- setGroupDefaultType: jest.fn(),
786
- weightGroup: jest.fn(),
787
- virtualType: jest.fn(),
788
- configureType: jest.fn(),
789
- weightType: jest.fn(),
790
- };
791
-
792
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
793
-
794
- const productMetadata: ProductMetadata = {
795
- name: 'resource-first',
796
- label: 'Resource First',
797
- };
798
- const config: ProductChildPage[] = [
799
- { type: 'apps.deployment' },
800
- {
801
- name: 'overview',
802
- label: 'Overview',
803
- component: { name: 'OverviewComponent' },
804
- },
805
- ];
806
-
807
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
808
-
809
- pluginProduct.apply(mockPlugin, mockStore);
810
-
811
- expect(mockDSL.product).toHaveBeenCalledWith(
812
- expect.objectContaining({
813
- to: expect.objectContaining({
814
- name: 'resourcefirst-c-cluster-resource',
815
- params: expect.objectContaining({
816
- product: 'resourcefirst',
817
- cluster: '__BLANK_CLUSTER__',
818
- resource: 'apps.deployment',
819
- }),
820
- }),
821
- })
822
- );
823
- });
824
-
825
- it('should use group component as default route when group has component but no children', () => {
826
- const mockPlugin = createMockPlugin();
827
- const mockStore = createMockStore();
828
- const mockDSL = {
829
- product: jest.fn(),
830
- basicType: jest.fn(),
831
- labelGroup: jest.fn(),
832
- setGroupDefaultType: jest.fn(),
833
- weightGroup: jest.fn(),
834
- virtualType: jest.fn(),
835
- configureType: jest.fn(),
836
- weightType: jest.fn(),
837
- };
838
-
839
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
840
-
841
- const productMetadata: ProductMetadata = {
842
- name: 'empty-group',
843
- label: 'Empty Group',
844
- };
845
- const config: ProductChildGroup[] = [
846
- {
847
- name: 'empty-group-with-page',
848
- label: 'Empty Group With Page',
849
- component: { name: 'EmptyGroupComponent' },
850
- children: [],
851
- },
852
- ];
853
-
854
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
855
-
856
- pluginProduct.apply(mockPlugin, mockStore);
857
-
858
- // Verify default route points to the group's component page (not a child, since there are none)
859
- expect(mockDSL.product).toHaveBeenCalledWith(
860
- expect.objectContaining({ to: expect.objectContaining({ name: expect.stringContaining('emptygroup') }) })
861
- );
862
- });
863
-
864
- it('should use group component as default route when group has both component and children', () => {
865
- const mockPlugin = createMockPlugin();
866
- const mockStore = createMockStore();
867
- const mockDSL = {
868
- product: jest.fn(),
869
- basicType: jest.fn(),
870
- labelGroup: jest.fn(),
871
- setGroupDefaultType: jest.fn(),
872
- weightGroup: jest.fn(),
873
- virtualType: jest.fn(),
874
- configureType: jest.fn(),
875
- weightType: jest.fn(),
876
- };
877
-
878
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
879
-
880
- const productMetadata: ProductMetadata = {
881
- name: 'group-with-page',
882
- label: 'Group With Page',
883
- };
884
- const config: ProductChildGroup[] = [
885
- {
886
- name: 'settings',
887
- label: 'Settings',
888
- component: { name: 'SettingsOverviewComponent' },
889
- children: [
890
- {
891
- name: 'general',
892
- label: 'General Settings',
893
- component: { name: 'GeneralComponent' },
894
- },
895
- {
896
- name: 'advanced',
897
- label: 'Advanced Settings',
898
- component: { name: 'AdvancedComponent' },
899
- },
900
- ],
901
- },
902
- ];
903
-
904
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
905
-
906
- pluginProduct.apply(mockPlugin, mockStore);
907
-
908
- // Verify default route points to the group's component page (not first child)
909
- // When a group has a component, the route includes the group's name for proper side-menu highlighting
910
- expect(mockDSL.product).toHaveBeenCalledWith(
911
- expect.objectContaining({ to: expect.objectContaining({ name: 'groupwithpage-settings' }) })
912
- );
913
-
914
- // Verify virtualType was still created for the group component
915
- expect(mockDSL.virtualType).toHaveBeenCalledWith(
916
- expect.objectContaining({
917
- name: 'groupwithpage-settings',
918
- exact: true,
919
- overview: true,
920
- })
921
- );
922
- });
923
- });
924
-
925
- describe('mixed config types', () => {
926
- it('should handle product with mixed virtualType and configureType items', () => {
927
- const mockPlugin = createMockPlugin();
928
- const mockStore = createMockStore();
929
- const mockDSL = {
930
- product: jest.fn(),
931
- basicType: jest.fn(),
932
- labelGroup: jest.fn(),
933
- setGroupDefaultType: jest.fn(),
934
- weightGroup: jest.fn(),
935
- virtualType: jest.fn(),
936
- configureType: jest.fn(),
937
- weightType: jest.fn(),
938
- };
939
-
940
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
941
-
942
- const productMetadata: ProductMetadata = {
943
- name: 'mixed-product',
944
- label: 'Mixed Content',
945
- };
946
- const mixedConfig: ProductChildPage[] = [
947
- {
948
- name: 'overview',
949
- label: 'Overview',
950
- component: { name: 'OverviewComponent' },
951
- },
952
- { type: 'resources.io' },
953
- ];
954
-
955
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, mixedConfig);
956
-
957
- pluginProduct.apply(mockPlugin, mockStore);
958
-
959
- expect(mockDSL.virtualType).toHaveBeenCalledTimes(1);
960
- expect(mockDSL.configureType).toHaveBeenCalledWith('resources.io', expect.any(Object));
961
- });
962
- });
963
-
964
- describe('error handling', () => {
965
- it('should throw error when config children is not an array', () => {
966
- const mockPlugin = createMockPlugin();
967
- const productMetadata: ProductMetadata = {
968
- name: 'bad-group',
969
- label: 'Bad Group',
970
- };
971
- const badConfig: any[] = [
972
- {
973
- name: 'group',
974
- label: 'Group',
975
- children: 'not-an-array', // Invalid
976
- },
977
- ];
978
-
979
- expect(() => {
980
- new PluginProduct(mockPlugin, productMetadata, badConfig);
981
- }).toThrow('forEach');
982
- });
983
- });
984
-
985
- describe('state verification', () => {
986
- it('should set newProduct flag for new products', () => {
987
- const mockPlugin = createMockPlugin();
988
- const productMetadata: ProductMetadata = {
989
- name: 'new-prod',
990
- label: 'New',
991
- };
992
-
993
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, []);
994
-
995
- expect(pluginProduct.newProduct).toBe(true);
996
- });
997
-
998
- it('should not set newProduct flag for standard product extensions', () => {
999
- const mockPlugin = createMockPlugin();
1000
- const validStandardProduct = StandardProductNames.EXPLORER;
1001
-
1002
- const pluginProduct = new PluginProduct(mockPlugin, validStandardProduct, []);
1003
-
1004
- expect(pluginProduct.newProduct).toBe(false);
1005
- });
1006
- });
1007
-
1008
- describe('real-world scenarios from pkg/add-new-prod', () => {
1009
- describe('scenario 1: simple product with single page component (plain layout)', () => {
1010
- it('should create single page product with plain layout', () => {
1011
- const mockPlugin = createMockPlugin();
1012
- const productSinglePage: ProductSinglePage = {
1013
- name: 'alex-simple-one-page',
1014
- weight: -100,
1015
- label: 'Simple One Page (no sidebar)',
1016
- component: { name: 'TestComponent' },
1017
- };
1018
-
1019
- const pluginProduct = new PluginProduct(mockPlugin, productSinglePage, []);
1020
-
1021
- expect(pluginProduct.newProduct).toBe(true);
1022
- expect(mockPlugin.addRoute).toHaveBeenCalledTimes(1);
1023
- });
1024
- });
1025
-
1026
- describe('scenario 2: simple product without children', () => {
1027
- it('should create product with sidebar but no config children', () => {
1028
- const mockPlugin = createMockPlugin();
1029
- const productMetadata: ProductMetadata = {
1030
- name: 'alex-simple-top-level',
1031
- weight: -100,
1032
- label: 'Simple (with sidebar)',
1033
- };
1034
-
1035
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, []);
1036
-
1037
- expect(pluginProduct.newProduct).toBe(true);
1038
- expect(mockPlugin.addRoute).toHaveBeenCalledTimes(1);
1039
- });
1040
- });
1041
-
1042
- describe('scenario 3: simple product with simple children (virtualTypes)', () => {
1043
- it('should create product with multiple simple virtualType children', () => {
1044
- const mockPlugin = createMockPlugin();
1045
- const mockStore = createMockStore();
1046
- const mockDSL = {
1047
- product: jest.fn(),
1048
- basicType: jest.fn(),
1049
- labelGroup: jest.fn(),
1050
- setGroupDefaultType: jest.fn(),
1051
- weightGroup: jest.fn(),
1052
- virtualType: jest.fn(),
1053
- configureType: jest.fn(),
1054
- weightType: jest.fn(),
1055
- };
1056
-
1057
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
1058
-
1059
- const productMetadata: ProductMetadata = {
1060
- name: 'alex-simple-children',
1061
- weight: -100,
1062
- label: 'Simple with Children',
1063
- };
1064
- const config: ProductChildPage[] = [
1065
- {
1066
- name: 'page1',
1067
- label: 'My label for page 1',
1068
- component: { name: 'TestComponent' },
1069
- },
1070
- {
1071
- name: 'page2',
1072
- label: 'My label for page 2',
1073
- component: { name: 'TestComponent' },
1074
- },
1075
- ];
1076
-
1077
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
1078
-
1079
- pluginProduct.apply(mockPlugin, mockStore);
1080
-
1081
- expect(mockDSL.virtualType).toHaveBeenCalledTimes(2);
1082
- expect(mockDSL.product).toHaveBeenCalledTimes(1);
1083
- });
1084
- });
1085
-
1086
- describe('scenario 4: product with simple children (virtualTypes) + type (configureType)', () => {
1087
- it('should handle mix of virtualType pages and resource types', () => {
1088
- const mockPlugin = createMockPlugin();
1089
- const mockStore = createMockStore();
1090
- const mockDSL = {
1091
- product: jest.fn(),
1092
- basicType: jest.fn(),
1093
- labelGroup: jest.fn(),
1094
- setGroupDefaultType: jest.fn(),
1095
- weightGroup: jest.fn(),
1096
- virtualType: jest.fn(),
1097
- configureType: jest.fn(),
1098
- weightType: jest.fn(),
1099
- };
1100
-
1101
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
1102
-
1103
- const productMetadata: ProductMetadata = {
1104
- name: 'alex-simple-children',
1105
- weight: -100,
1106
- label: 'Simple with Children',
1107
- };
1108
- const config: ProductChildPage[] = [
1109
- {
1110
- name: 'page1',
1111
- label: 'My label for page 1',
1112
- component: { name: 'TestComponent' },
1113
- },
1114
- {
1115
- name: 'page2',
1116
- label: 'My label for page 2',
1117
- component: { name: 'TestComponent' },
1118
- },
1119
- { type: 'upgrade.cattle.io.plan' },
1120
- ];
1121
-
1122
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
1123
-
1124
- pluginProduct.apply(mockPlugin, mockStore);
1125
-
1126
- expect(mockDSL.virtualType).toHaveBeenCalledTimes(2);
1127
- expect(mockDSL.configureType).toHaveBeenCalledWith('upgrade.cattle.io.plan', expect.any(Object));
1128
- });
1129
- });
1130
-
1131
- describe('scenario 5: product with type first, then children with nested groups', () => {
1132
- it('should handle resource type first, then virtualType pages with nested children', () => {
1133
- const mockPlugin = createMockPlugin();
1134
- const mockStore = createMockStore();
1135
- const mockDSL = {
1136
- product: jest.fn(),
1137
- basicType: jest.fn(),
1138
- labelGroup: jest.fn(),
1139
- setGroupDefaultType: jest.fn(),
1140
- weightGroup: jest.fn(),
1141
- virtualType: jest.fn(),
1142
- configureType: jest.fn(),
1143
- weightType: jest.fn(),
1144
- };
1145
-
1146
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
1147
-
1148
- const productMetadata: ProductMetadata = {
1149
- name: 'alex-simple-children',
1150
- weight: -100,
1151
- label: 'Simple with Children',
1152
- };
1153
- const config: (ProductChildGroup | ProductChildPage)[] = [
1154
- { type: 'fleet.cattle.io.clustergroup' },
1155
- {
1156
- name: 'page1',
1157
- label: 'My label for page 1',
1158
- component: { name: 'TestComponent' },
1159
- children: [
1160
- {
1161
- name: 'hello0',
1162
- label: 'Testing 12',
1163
- labelKey: 'aks.label',
1164
- component: { name: 'TestComponent' },
1165
- } as any,
1166
- {
1167
- name: 'hello1',
1168
- label: 'Testing 1',
1169
- labelKey: 'aks.label',
1170
- component: { name: 'TestComponent' },
1171
- },
1172
- {
1173
- name: 'hello3',
1174
- labelKey: 'aks.label',
1175
- component: { name: 'TestComponent' },
1176
- },
1177
- {
1178
- name: 'hello2',
1179
- label: 'Testing 2',
1180
- component: { name: 'TestComponent' },
1181
- },
1182
- ],
1183
- },
1184
- { type: 'upgrade.cattle.io.plan' },
1185
- {
1186
- name: 'page2',
1187
- label: 'My label for page 2',
1188
- component: { name: 'TestComponent' },
1189
- },
1190
- ];
1191
-
1192
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
1193
-
1194
- pluginProduct.apply(mockPlugin, mockStore);
1195
-
1196
- expect(mockDSL.configureType).toHaveBeenCalledWith('fleet.cattle.io.clustergroup', expect.any(Object));
1197
- expect(mockDSL.virtualType).toHaveBeenCalledTimes(6);
1198
- expect(mockDSL.labelGroup).toHaveBeenCalledTimes(1);
1199
- expect(mockDSL.product).toHaveBeenCalledTimes(1);
1200
- });
1201
- });
1202
-
1203
- describe('scenario 6: extend standard product without configuring children', () => {
1204
- it('should extend existing standard product with empty config', () => {
1205
- const mockPlugin = createMockPlugin();
1206
- const validStandardProduct = StandardProductNames.EXPLORER;
1207
-
1208
- const pluginProduct = new PluginProduct(mockPlugin, validStandardProduct, []);
1209
-
1210
- expect(pluginProduct.newProduct).toBe(false);
1211
- expect(mockPlugin._registerTopLevelProduct).not.toHaveBeenCalled();
1212
- expect(mockPlugin.addRoute).toHaveBeenCalledTimes(1);
1213
- });
1214
- });
1215
-
1216
- describe('scenario 7: extend standard product with simple virtualType children', () => {
1217
- it('should extend standard product adding simple virtualType page', () => {
1218
- const mockPlugin = createMockPlugin();
1219
- const mockStore = createMockStore();
1220
- const mockDSL = {
1221
- product: jest.fn(),
1222
- basicType: jest.fn(),
1223
- labelGroup: jest.fn(),
1224
- setGroupDefaultType: jest.fn(),
1225
- weightGroup: jest.fn(),
1226
- virtualType: jest.fn(),
1227
- configureType: jest.fn(),
1228
- weightType: jest.fn(),
1229
- };
1230
-
1231
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
1232
-
1233
- const validStandardProduct = StandardProductNames.EXPLORER;
1234
- const config: ProductChildPage[] = [
1235
- {
1236
- name: 'mysettings',
1237
- label: 'Custom',
1238
- weight: 97,
1239
- component: { name: 'TestComponent' },
1240
- },
1241
- ];
1242
-
1243
- const pluginProduct = new PluginProduct(mockPlugin, validStandardProduct, config);
1244
-
1245
- pluginProduct.apply(mockPlugin, mockStore);
1246
-
1247
- expect(mockDSL.virtualType).toHaveBeenCalledTimes(1);
1248
- expect(mockDSL.product).not.toHaveBeenCalled();
1249
- });
1250
- });
1251
-
1252
- describe('scenario 8: extend standard product with mixed types and nested groups', () => {
1253
- it('should extend standard product adding mixed virtualTypes and resource types with nested groups', () => {
1254
- const mockPlugin = createMockPlugin();
1255
- const mockStore = createMockStore();
1256
- const mockDSL = {
1257
- product: jest.fn(),
1258
- basicType: jest.fn(),
1259
- labelGroup: jest.fn(),
1260
- setGroupDefaultType: jest.fn(),
1261
- weightGroup: jest.fn(),
1262
- virtualType: jest.fn(),
1263
- configureType: jest.fn(),
1264
- weightType: jest.fn(),
1265
- };
1266
-
1267
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
1268
-
1269
- const validStandardProduct = StandardProductNames.EXPLORER;
1270
- const config: (ProductChildGroup | ProductChildPage)[] = [
1271
- {
1272
- name: 'page1',
1273
- label: 'My label for page 1',
1274
- weight: -10,
1275
- children: [
1276
- {
1277
- name: 'hello0',
1278
- label: 'Testing 12',
1279
- labelKey: 'aks.label',
1280
- component: { name: 'TestComponent' },
1281
- } as any,
1282
- {
1283
- name: 'hello1',
1284
- label: 'Testing 1',
1285
- component: { name: 'TestComponent' },
1286
- },
1287
- {
1288
- name: 'hello3',
1289
- labelKey: 'generic.unified',
1290
- component: { name: 'TestComponent' },
1291
- },
1292
- {
1293
- name: 'hello2',
1294
- label: 'Testing 2',
1295
- component: { name: 'TestComponent' },
1296
- },
1297
- ],
1298
- },
1299
- { type: 'upgrade.cattle.io.plan' },
1300
- {
1301
- name: 'page2',
1302
- label: 'My label for page 2',
1303
- component: { name: 'TestComponent' },
1304
- },
1305
- ];
1306
-
1307
- const pluginProduct = new PluginProduct(mockPlugin, validStandardProduct, config);
1308
-
1309
- pluginProduct.apply(mockPlugin, mockStore);
1310
-
1311
- expect(mockDSL.virtualType).toHaveBeenCalledTimes(5);
1312
- expect(mockDSL.configureType).toHaveBeenCalledWith('upgrade.cattle.io.plan', expect.any(Object));
1313
- expect(mockDSL.labelGroup).toHaveBeenCalledTimes(1);
1314
- expect(mockDSL.product).not.toHaveBeenCalled();
1315
- });
1316
- });
1317
- });
1318
-
1319
- describe('side menu structure and ordering', () => {
1320
- describe('new product - virtualType ordering', () => {
1321
- it('should register virtualTypes in config array order when no weights specified', () => {
1322
- const mockPlugin = createMockPlugin();
1323
- const mockStore = createMockStore();
1324
- const virtualTypeCalls: any[] = [];
1325
- const mockDSL = {
1326
- product: jest.fn(),
1327
- basicType: jest.fn(),
1328
- labelGroup: jest.fn(),
1329
- setGroupDefaultType: jest.fn(),
1330
- weightGroup: jest.fn(),
1331
- virtualType: jest.fn((...args) => virtualTypeCalls.push(args)),
1332
- configureType: jest.fn(),
1333
- weightType: jest.fn(),
1334
- };
1335
-
1336
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
1337
-
1338
- const productMetadata: ProductMetadata = {
1339
- name: 'test-ordering',
1340
- label: 'Test Ordering',
1341
- };
1342
- const config: ProductChildPage[] = [
1343
- {
1344
- name: 'first-page',
1345
- label: 'First Page',
1346
- component: { name: 'FirstComponent' },
1347
- },
1348
- {
1349
- name: 'second-page',
1350
- label: 'Second Page',
1351
- component: { name: 'SecondComponent' },
1352
- },
1353
- {
1354
- name: 'third-page',
1355
- label: 'Third Page',
1356
- component: { name: 'ThirdComponent' },
1357
- },
1358
- ];
1359
-
1360
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
1361
-
1362
- pluginProduct.apply(mockPlugin, mockStore);
1363
-
1364
- // Verify order - virtualType calls should match config order
1365
- expect(virtualTypeCalls).toHaveLength(3);
1366
- expect(virtualTypeCalls[0][0]).toMatchObject({ name: 'testordering-first-page' });
1367
- expect(virtualTypeCalls[1][0]).toMatchObject({ name: 'testordering-second-page' });
1368
- expect(virtualTypeCalls[2][0]).toMatchObject({ name: 'testordering-third-page' });
1369
- });
1370
-
1371
- it('should register virtualTypes with weight parameter for menu ordering', () => {
1372
- const mockPlugin = createMockPlugin();
1373
- const mockStore = createMockStore();
1374
- const virtualTypeCalls: any[] = [];
1375
- const mockDSL = {
1376
- product: jest.fn(),
1377
- basicType: jest.fn(),
1378
- labelGroup: jest.fn(),
1379
- setGroupDefaultType: jest.fn(),
1380
- weightGroup: jest.fn(),
1381
- virtualType: jest.fn((...args) => virtualTypeCalls.push(args)),
1382
- configureType: jest.fn(),
1383
- weightType: jest.fn(),
1384
- };
1385
-
1386
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
1387
-
1388
- const productMetadata: ProductMetadata = {
1389
- name: 'test-weights',
1390
- label: 'Test Weights',
1391
- };
1392
- const config: ProductChildPage[] = [
1393
- {
1394
- name: 'low-priority',
1395
- label: 'Low Priority',
1396
- component: { name: 'Component1' },
1397
- weight: 100,
1398
- },
1399
- {
1400
- name: 'high-priority',
1401
- label: 'High Priority',
1402
- component: { name: 'Component2' },
1403
- weight: 1,
1404
- },
1405
- {
1406
- name: 'medium-priority',
1407
- label: 'Medium Priority',
1408
- component: { name: 'Component3' },
1409
- weight: 50,
1410
- },
1411
- ];
1412
-
1413
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
1414
-
1415
- pluginProduct.apply(mockPlugin, mockStore);
1416
-
1417
- // Verify weight is passed to virtualType
1418
- expect(virtualTypeCalls[0][0].weight).toBe(100);
1419
- expect(virtualTypeCalls[1][0].weight).toBe(1);
1420
- expect(virtualTypeCalls[2][0].weight).toBe(50);
1421
- });
1422
- });
1423
-
1424
- describe('new product - mixed types ordering', () => {
1425
- it('should maintain order of virtualTypes and configureTypes as specified in config', () => {
1426
- const mockPlugin = createMockPlugin();
1427
- const mockStore = createMockStore();
1428
- const dslCallOrder: string[] = [];
1429
- const mockDSL = {
1430
- product: jest.fn(),
1431
- basicType: jest.fn((...args) => dslCallOrder.push(`basicType:${ args[0] }`)),
1432
- labelGroup: jest.fn(),
1433
- setGroupDefaultType: jest.fn(),
1434
- weightGroup: jest.fn(),
1435
- virtualType: jest.fn((...args) => dslCallOrder.push(`virtualType:${ args[0].name }`)),
1436
- configureType: jest.fn((...args) => dslCallOrder.push(`configureType:${ args[0] }`)),
1437
- weightType: jest.fn(),
1438
- };
1439
-
1440
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
1441
-
1442
- const productMetadata: ProductMetadata = {
1443
- name: 'mixed-types',
1444
- label: 'Mixed Types Product',
1445
- };
1446
- const config: ProductChildPage[] = [
1447
- { type: 'fleet.cattle.io.clustergroup' },
1448
- {
1449
- name: 'custom-page',
1450
- label: 'Custom Page',
1451
- component: { name: 'CustomComponent' },
1452
- },
1453
- { type: 'upgrade.cattle.io.plan' },
1454
- ];
1455
-
1456
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
1457
-
1458
- pluginProduct.apply(mockPlugin, mockStore);
1459
-
1460
- // Verify DSL methods called in correct order
1461
- const relevantCalls = dslCallOrder.filter((call) => call.startsWith('configureType:') || call.startsWith('virtualType:')
1462
- );
1463
-
1464
- expect(relevantCalls).toStrictEqual([
1465
- 'configureType:fleet.cattle.io.clustergroup',
1466
- 'virtualType:mixedtypes-custom-page',
1467
- 'configureType:upgrade.cattle.io.plan',
1468
- ]);
1469
- });
1470
- });
1471
-
1472
- describe('new product - group menu structure', () => {
1473
- it('should create nested side menu structure with groups', () => {
1474
- const mockPlugin = createMockPlugin();
1475
- const mockStore = createMockStore();
1476
- const basicTypeCalls: any[] = [];
1477
- const labelGroupCalls: any[] = [];
1478
- const weightGroupCalls: any[] = [];
1479
- const virtualTypeCalls: any[] = [];
1480
-
1481
- const mockDSL = {
1482
- product: jest.fn(),
1483
- basicType: jest.fn((...args) => basicTypeCalls.push(args)),
1484
- labelGroup: jest.fn((...args) => labelGroupCalls.push(args)),
1485
- setGroupDefaultType: jest.fn(),
1486
- weightGroup: jest.fn((...args) => weightGroupCalls.push(args)),
1487
- virtualType: jest.fn((...args) => virtualTypeCalls.push(args)),
1488
- configureType: jest.fn(),
1489
- weightType: jest.fn(),
1490
- };
1491
-
1492
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
1493
-
1494
- const productMetadata: ProductMetadata = {
1495
- name: 'grouped-product',
1496
- label: 'Grouped Product',
1497
- };
1498
- const config: ProductChildGroup[] = [
1499
- {
1500
- name: 'settings-group',
1501
- label: 'Settings',
1502
- weight: 10,
1503
- children: [
1504
- {
1505
- name: 'general',
1506
- label: 'General',
1507
- component: { name: 'GeneralComponent' },
1508
- },
1509
- {
1510
- name: 'advanced',
1511
- label: 'Advanced',
1512
- component: { name: 'AdvancedComponent' },
1513
- },
1514
- ],
1515
- },
1516
- ];
1517
-
1518
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
1519
-
1520
- pluginProduct.apply(mockPlugin, mockStore);
1521
-
1522
- // Verify group label was set
1523
- expect(labelGroupCalls.length).toBeGreaterThan(0);
1524
- expect(labelGroupCalls[0]).toStrictEqual(['groupedproduct-settings-group', 'Settings', undefined]);
1525
-
1526
- // Verify group weight was set
1527
- expect(weightGroupCalls.length).toBeGreaterThan(0);
1528
- expect(weightGroupCalls[0]).toStrictEqual(['groupedproduct-settings-group', 10, true]);
1529
-
1530
- // Verify basicType was called for navigation with group children
1531
- // basicType is called twice: once for top-level items (excluding groups), once for group children
1532
- expect(basicTypeCalls.length).toBeGreaterThan(1);
1533
- // Check the second call which includes group children
1534
- expect(basicTypeCalls[1][0]).toStrictEqual(expect.arrayContaining([
1535
- expect.stringContaining('general'),
1536
- expect.stringContaining('advanced')
1537
- ]));
1538
-
1539
- // Verify nested virtualTypes were created
1540
- expect(virtualTypeCalls).toHaveLength(2);
1541
- });
1542
-
1543
- it('should set label for groups without explicit label via labelGroup', () => {
1544
- const mockPlugin = createMockPlugin();
1545
- const mockStore = createMockStore();
1546
- const labelGroupCalls: any[] = [];
1547
-
1548
- const mockDSL = {
1549
- product: jest.fn(),
1550
- basicType: jest.fn(),
1551
- labelGroup: jest.fn((...args) => labelGroupCalls.push(args)),
1552
- setGroupDefaultType: jest.fn(),
1553
- weightGroup: jest.fn(),
1554
- virtualType: jest.fn(),
1555
- configureType: jest.fn(),
1556
- weightType: jest.fn(),
1557
- };
1558
-
1559
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
1560
-
1561
- const productMetadata: ProductMetadata = {
1562
- name: 'product-with-groups',
1563
- label: 'Product',
1564
- };
1565
- const config: ProductChildGroup[] = [
1566
- {
1567
- name: 'my-group',
1568
- label: 'My Group Label',
1569
- children: [
1570
- {
1571
- name: 'child1',
1572
- label: 'Child 1',
1573
- component: { name: 'Child1Component' },
1574
- },
1575
- ],
1576
- },
1577
- ];
1578
-
1579
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
1580
-
1581
- pluginProduct.apply(mockPlugin, mockStore);
1582
-
1583
- // Verify labelGroup was called with the group's label
1584
- expect(labelGroupCalls[0]).toStrictEqual(['productwithgroups-my-group', 'My Group Label', undefined]);
1585
- });
1586
- });
1587
-
1588
- describe('extending product - side menu additions', () => {
1589
- it('should add virtualType to existing product navigation in order specified', () => {
1590
- const mockPlugin = createMockPlugin();
1591
- const mockStore = createMockStore();
1592
- const virtualTypeCalls: any[] = [];
1593
- const mockDSL = {
1594
- product: jest.fn(),
1595
- basicType: jest.fn(),
1596
- labelGroup: jest.fn(),
1597
- setGroupDefaultType: jest.fn(),
1598
- weightGroup: jest.fn(),
1599
- virtualType: jest.fn((...args) => virtualTypeCalls.push(args)),
1600
- configureType: jest.fn(),
1601
- weightType: jest.fn(),
1602
- };
1603
-
1604
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
1605
-
1606
- const validStandardProduct = StandardProductNames.EXPLORER;
1607
- const config: ProductChildPage[] = [
1608
- {
1609
- name: 'custom-section-1',
1610
- label: 'Custom Section 1',
1611
- weight: 99,
1612
- component: { name: 'CustomComponent1' },
1613
- },
1614
- {
1615
- name: 'custom-section-2',
1616
- label: 'Custom Section 2',
1617
- weight: 98,
1618
- component: { name: 'CustomComponent2' },
1619
- },
1620
- ];
1621
-
1622
- const pluginProduct = new PluginProduct(mockPlugin, validStandardProduct, config);
1623
-
1624
- pluginProduct.apply(mockPlugin, mockStore);
1625
-
1626
- // Verify virtualTypes added in order with weights
1627
- expect(virtualTypeCalls).toHaveLength(2);
1628
- expect(virtualTypeCalls[0][0]).toMatchObject({ name: 'explorer-custom-section-1' });
1629
- expect(virtualTypeCalls[0][0].weight).toBe(99);
1630
- expect(virtualTypeCalls[1][0]).toMatchObject({ name: 'explorer-custom-section-2' });
1631
- expect(virtualTypeCalls[1][0].weight).toBe(98);
1632
- });
1633
-
1634
- it('should add groups to existing product with proper navigation structure', () => {
1635
- const mockPlugin = createMockPlugin();
1636
- const mockStore = createMockStore();
1637
- const basicTypeCalls: any[] = [];
1638
- const labelGroupCalls: any[] = [];
1639
-
1640
- const mockDSL = {
1641
- product: jest.fn(),
1642
- basicType: jest.fn((...args) => basicTypeCalls.push(args)),
1643
- labelGroup: jest.fn((...args) => labelGroupCalls.push(args)),
1644
- setGroupDefaultType: jest.fn(),
1645
- weightGroup: jest.fn(),
1646
- virtualType: jest.fn(),
1647
- configureType: jest.fn(),
1648
- weightType: jest.fn(),
1649
- };
1650
-
1651
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
1652
-
1653
- const validStandardProduct = StandardProductNames.SETTINGS;
1654
- const config: ProductChildGroup[] = [
1655
- {
1656
- name: 'extension-settings',
1657
- label: 'Extension Settings',
1658
- weight: 5,
1659
- children: [
1660
- {
1661
- name: 'config-page',
1662
- label: 'Configuration',
1663
- component: { name: 'ConfigComponent' },
1664
- },
1665
- {
1666
- name: 'advanced-page',
1667
- label: 'Advanced',
1668
- component: { name: 'AdvancedComponent' },
1669
- },
1670
- ],
1671
- },
1672
- ];
1673
-
1674
- const pluginProduct = new PluginProduct(mockPlugin, validStandardProduct, config);
1675
-
1676
- pluginProduct.apply(mockPlugin, mockStore);
1677
-
1678
- // Verify group label was set for the extended product
1679
- expect(labelGroupCalls.length).toBeGreaterThan(0);
1680
- expect(labelGroupCalls[0]).toStrictEqual(['settings-extension-settings', 'Extension Settings', undefined]);
1681
-
1682
- // Verify basicType includes children for navigation
1683
- // basicType is called multiple times: first for top-level items, then for each group's children
1684
- expect(basicTypeCalls.length).toBeGreaterThan(0);
1685
- // Check the call that includes group children (should be in basicTypeCalls)
1686
- const groupChildrenCall = basicTypeCalls.find((call) => call[0] && Array.isArray(call[0]) && call[0].some((name: string) => name.includes('config-page'))
1687
- );
1688
-
1689
- expect(groupChildrenCall).toBeDefined();
1690
- expect(groupChildrenCall![0]).toStrictEqual(expect.arrayContaining([
1691
- expect.stringContaining('config-page'),
1692
- expect.stringContaining('advanced-page')
1693
- ]));
1694
- });
1695
- });
1696
-
1697
- describe('navigation and default route determination', () => {
1698
- it('should pass correct default route to product registration', () => {
1699
- const mockPlugin = createMockPlugin();
1700
- const mockStore = createMockStore();
1701
- let productConfig: any;
1702
- const mockDSL = {
1703
- product: jest.fn((config) => {
1704
- productConfig = config;
1705
- }),
1706
- basicType: jest.fn(),
1707
- labelGroup: jest.fn(),
1708
- setGroupDefaultType: jest.fn(),
1709
- weightGroup: jest.fn(),
1710
- virtualType: jest.fn(),
1711
- configureType: jest.fn(),
1712
- weightType: jest.fn(),
1713
- };
1714
-
1715
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
1716
-
1717
- const productMetadata: ProductMetadata = {
1718
- name: 'test-default-route',
1719
- label: 'Test Default Route',
1720
- };
1721
- const config: ProductChildPage[] = [
1722
- {
1723
- name: 'overview',
1724
- label: 'Overview',
1725
- component: { name: 'OverviewComponent' },
1726
- },
1727
- {
1728
- name: 'details',
1729
- label: 'Details',
1730
- component: { name: 'DetailsComponent' },
1731
- },
1732
- ];
1733
-
1734
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
1735
-
1736
- pluginProduct.apply(mockPlugin, mockStore);
1737
-
1738
- // Verify product registration includes default route (first config item)
1739
- expect(productConfig).toBeDefined();
1740
- expect(productConfig.to).toBeDefined();
1741
- });
1742
-
1743
- it('should use first group child as default route when first config item is a group', () => {
1744
- const mockPlugin = createMockPlugin();
1745
- const mockStore = createMockStore();
1746
- let productConfig: any;
1747
- const mockDSL = {
1748
- product: jest.fn((config) => {
1749
- productConfig = config;
1750
- }),
1751
- basicType: jest.fn(),
1752
- labelGroup: jest.fn(),
1753
- setGroupDefaultType: jest.fn(),
1754
- weightGroup: jest.fn(),
1755
- virtualType: jest.fn(),
1756
- configureType: jest.fn(),
1757
- weightType: jest.fn(),
1758
- };
1759
-
1760
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
1761
-
1762
- const productMetadata: ProductMetadata = {
1763
- name: 'test-group-default',
1764
- label: 'Test Group Default',
1765
- };
1766
- const config: ProductChildGroup[] = [
1767
- {
1768
- name: 'main-group',
1769
- label: 'Main Group',
1770
- children: [
1771
- {
1772
- name: 'first-child',
1773
- label: 'First Child',
1774
- component: { name: 'FirstChildComponent' },
1775
- },
1776
- {
1777
- name: 'second-child',
1778
- label: 'Second Child',
1779
- component: { name: 'SecondChildComponent' },
1780
- },
1781
- ],
1782
- },
1783
- ];
1784
-
1785
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
1786
-
1787
- pluginProduct.apply(mockPlugin, mockStore);
1788
-
1789
- // Verify default route points to first child of the group
1790
- expect(productConfig).toBeDefined();
1791
- expect(productConfig.to).toBeDefined();
1792
- });
1793
- });
1794
-
1795
- describe('comprehensive ordering scenario', () => {
1796
- it('should maintain complex menu structure with proper ordering of mixed items', () => {
1797
- const mockPlugin = createMockPlugin();
1798
- const mockStore = createMockStore();
1799
- const dslCallOrder: string[] = [];
1800
- const weightTypeCalls: any[] = [];
1801
-
1802
- const mockDSL = {
1803
- product: jest.fn(),
1804
- basicType: jest.fn((...args) => dslCallOrder.push(`basicType`)),
1805
- labelGroup: jest.fn((...args) => dslCallOrder.push(`labelGroup:${ args[0] }`)),
1806
- setGroupDefaultType: jest.fn(),
1807
- weightGroup: jest.fn((...args) => dslCallOrder.push(`weightGroup:${ args[0] }`)),
1808
- virtualType: jest.fn((...args) => dslCallOrder.push(`virtualType:${ args[0].name }`)),
1809
- configureType: jest.fn((...args) => dslCallOrder.push(`configureType:${ args[0] }`)),
1810
- weightType: jest.fn((...args) => weightTypeCalls.push(args)),
1811
- };
1812
-
1813
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
1814
-
1815
- const productMetadata: ProductMetadata = {
1816
- name: 'complex-product',
1817
- label: 'Complex Product',
1818
- };
1819
- const config: (ProductChildGroup | ProductChildPage)[] = [
1820
- { type: 'fleet.cattle.io.clustergroup' }, // Resource type first
1821
- {
1822
- name: 'overview',
1823
- label: 'Overview',
1824
- weight: 1,
1825
- component: { name: 'OverviewComponent' },
1826
- },
1827
- {
1828
- name: 'settings',
1829
- label: 'Settings',
1830
- weight: 50,
1831
- children: [
1832
- {
1833
- name: 'general',
1834
- label: 'General',
1835
- component: { name: 'GeneralComponent' },
1836
- },
1837
- {
1838
- name: 'advanced',
1839
- label: 'Advanced',
1840
- component: { name: 'AdvancedComponent' },
1841
- },
1842
- ],
1843
- },
1844
- { type: 'upgrade.cattle.io.plan' }, // Another resource type
1845
- {
1846
- name: 'monitoring',
1847
- label: 'Monitoring',
1848
- weight: 100,
1849
- component: { name: 'MonitoringComponent' },
1850
- },
1851
- ];
1852
-
1853
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
1854
-
1855
- pluginProduct.apply(mockPlugin, mockStore);
1856
-
1857
- // Verify configureTypes were registered
1858
- const configureTypeCalls = dslCallOrder.filter((call) => call.startsWith('configureType:'));
1859
-
1860
- expect(configureTypeCalls).toContain('configureType:fleet.cattle.io.clustergroup');
1861
- expect(configureTypeCalls).toContain('configureType:upgrade.cattle.io.plan');
1862
-
1863
- // Verify virtualTypes were registered
1864
- const virtualTypeCalls = dslCallOrder.filter((call) => call.startsWith('virtualType:'));
1865
-
1866
- expect(virtualTypeCalls).toContain('virtualType:complexproduct-overview');
1867
- expect(virtualTypeCalls).toContain('virtualType:complexproduct-monitoring');
1868
- expect(virtualTypeCalls).toContain('virtualType:complexproduct-settings-general');
1869
- expect(virtualTypeCalls).toContain('virtualType:complexproduct-settings-advanced');
1870
-
1871
- // Verify group was configured
1872
- const labelGroupCalls = dslCallOrder.filter((call) => call.startsWith('labelGroup:'));
1873
-
1874
- expect(labelGroupCalls).toContain('labelGroup:complexproduct-settings');
1875
- });
1876
- });
1877
- });
1878
-
1879
- describe('sideNav menu structure rendering', () => {
1880
- describe('new product - menu structure validation', () => {
1881
- it('should create correct menu structure with proper ordering and names for simple virtualTypes', () => {
1882
- const mockPlugin = createMockPlugin();
1883
- const mockStore = createMockStore();
1884
-
1885
- // Capture the menu structure that would be created
1886
- const menuStructure: any = {
1887
- basicTypes: [],
1888
- virtualTypes: [],
1889
- groupLabels: {},
1890
- groupWeights: {},
1891
- };
1892
-
1893
- const mockDSL = {
1894
- product: jest.fn(),
1895
- basicType: jest.fn((types, group) => {
1896
- menuStructure.basicTypes.push({ types, group });
1897
- }),
1898
- labelGroup: jest.fn((group, label, labelKey) => {
1899
- menuStructure.groupLabels[group] = { label, labelKey };
1900
- }),
1901
- setGroupDefaultType: jest.fn(),
1902
- weightGroup: jest.fn((group, weight) => {
1903
- menuStructure.groupWeights[group] = weight;
1904
- }),
1905
- virtualType: jest.fn((config) => {
1906
- menuStructure.virtualTypes.push(config);
1907
- }),
1908
- configureType: jest.fn(),
1909
- weightType: jest.fn(),
1910
- };
1911
-
1912
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
1913
-
1914
- const productMetadata: ProductMetadata = {
1915
- name: 'my-product',
1916
- label: 'My Product',
1917
- };
1918
- const config: ProductChildPage[] = [
1919
- {
1920
- name: 'overview',
1921
- label: 'Overview',
1922
- weight: 100,
1923
- component: { name: 'OverviewComponent' },
1924
- },
1925
- {
1926
- name: 'settings',
1927
- label: 'Settings',
1928
- weight: 50,
1929
- component: { name: 'SettingsComponent' },
1930
- },
1931
- {
1932
- name: 'monitoring',
1933
- label: 'Monitoring',
1934
- weight: 25,
1935
- component: { name: 'MonitoringComponent' },
1936
- },
1937
- ];
1938
-
1939
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
1940
-
1941
- pluginProduct.apply(mockPlugin, mockStore);
1942
-
1943
- // Verify menu structure ordering (virtualTypes are registered in config order but with weights)
1944
- expect(menuStructure.virtualTypes).toHaveLength(3);
1945
-
1946
- // Menu items should have concatenated product-page names
1947
- expect(menuStructure.virtualTypes[0].name).toBe('myproduct-overview');
1948
- expect(menuStructure.virtualTypes[0].label).toBe('Overview');
1949
- expect(menuStructure.virtualTypes[0].weight).toBe(100);
1950
-
1951
- expect(menuStructure.virtualTypes[1].name).toBe('myproduct-settings');
1952
- expect(menuStructure.virtualTypes[1].label).toBe('Settings');
1953
- expect(menuStructure.virtualTypes[1].weight).toBe(50);
1954
-
1955
- expect(menuStructure.virtualTypes[2].name).toBe('myproduct-monitoring');
1956
- expect(menuStructure.virtualTypes[2].label).toBe('Monitoring');
1957
- expect(menuStructure.virtualTypes[2].weight).toBe(25);
1958
-
1959
- // Verify basicType creates flat navigation structure (no groups for simple pages)
1960
- expect(menuStructure.basicTypes.length).toBeGreaterThan(0);
1961
- expect(menuStructure.basicTypes[0].types).toStrictEqual(expect.arrayContaining([
1962
- 'myproduct-overview',
1963
- 'myproduct-settings',
1964
- 'myproduct-monitoring',
1965
- ]));
1966
- });
1967
-
1968
- it('should create correct menu structure with groups and nested items', () => {
1969
- const mockPlugin = createMockPlugin();
1970
- const mockStore = createMockStore();
1971
-
1972
- const menuStructure: any = {
1973
- basicTypes: [],
1974
- virtualTypes: [],
1975
- groupLabels: {},
1976
- groupWeights: {},
1977
- };
1978
-
1979
- const mockDSL = {
1980
- product: jest.fn(),
1981
- basicType: jest.fn((types, group) => {
1982
- menuStructure.basicTypes.push({ types, group });
1983
- }),
1984
- labelGroup: jest.fn((group, label, labelKey) => {
1985
- menuStructure.groupLabels[group] = { label, labelKey };
1986
- }),
1987
- setGroupDefaultType: jest.fn(),
1988
- weightGroup: jest.fn((group, weight) => {
1989
- menuStructure.groupWeights[group] = weight;
1990
- }),
1991
- virtualType: jest.fn((config) => {
1992
- menuStructure.virtualTypes.push(config);
1993
- }),
1994
- configureType: jest.fn(),
1995
- weightType: jest.fn(),
1996
- };
1997
-
1998
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
1999
-
2000
- const productMetadata: ProductMetadata = {
2001
- name: 'grouped-product',
2002
- label: 'Grouped Product',
2003
- };
2004
- const config: (ProductChildPage | ProductChildGroup)[] = [
2005
- {
2006
- name: 'overview',
2007
- label: 'Overview',
2008
- weight: 100,
2009
- component: { name: 'OverviewComponent' },
2010
- },
2011
- {
2012
- name: 'admin',
2013
- label: 'Administration',
2014
- weight: 50,
2015
- children: [
2016
- {
2017
- name: 'users',
2018
- label: 'Users',
2019
- component: { name: 'UsersComponent' },
2020
- },
2021
- {
2022
- name: 'roles',
2023
- label: 'Roles',
2024
- component: { name: 'RolesComponent' },
2025
- },
2026
- ],
2027
- },
2028
- {
2029
- name: 'reports',
2030
- label: 'Reports',
2031
- weight: 25,
2032
- component: { name: 'ReportsComponent' },
2033
- },
2034
- ];
2035
-
2036
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
2037
-
2038
- pluginProduct.apply(mockPlugin, mockStore);
2039
-
2040
- // Verify group structure
2041
- expect(menuStructure.groupLabels['groupedproduct-admin']).toStrictEqual({
2042
- label: 'Administration',
2043
- labelKey: undefined,
2044
- });
2045
- expect(menuStructure.groupWeights['groupedproduct-admin']).toBe(50);
2046
-
2047
- // Verify nested items in group have correct names
2048
- const adminGroupItems = menuStructure.virtualTypes.filter((vt: any) => vt.name.includes('admin')
2049
- );
2050
-
2051
- expect(adminGroupItems).toHaveLength(2);
2052
- expect(adminGroupItems.some((item: any) => item.name === 'groupedproduct-admin-users')).toBe(true);
2053
- expect(adminGroupItems.some((item: any) => item.name === 'groupedproduct-admin-roles')).toBe(true);
2054
-
2055
- // Verify top-level items ordering via basicType
2056
- const topLevelCall = menuStructure.basicTypes.find((bt: any) => !bt.group);
2057
-
2058
- expect(topLevelCall).toBeDefined();
2059
- expect(topLevelCall.types).toContain('groupedproduct-overview');
2060
- expect(topLevelCall.types).toContain('groupedproduct-reports');
2061
- });
2062
-
2063
- it('should render mixed virtualTypes and configureTypes in correct order', () => {
2064
- const mockPlugin = createMockPlugin();
2065
- const mockStore = createMockStore();
2066
-
2067
- const menuStructure: any = {
2068
- basicTypes: [],
2069
- virtualTypes: [],
2070
- configureTypes: [],
2071
- };
2072
-
2073
- const mockDSL = {
2074
- product: jest.fn(),
2075
- basicType: jest.fn((types, group) => {
2076
- menuStructure.basicTypes.push({ types, group });
2077
- }),
2078
- labelGroup: jest.fn(),
2079
- setGroupDefaultType: jest.fn(),
2080
- weightGroup: jest.fn(),
2081
- virtualType: jest.fn((config) => {
2082
- menuStructure.virtualTypes.push({ name: config.name, weight: config.weight });
2083
- }),
2084
- configureType: jest.fn((type, config) => {
2085
- menuStructure.configureTypes.push({ type, config });
2086
- }),
2087
- weightType: jest.fn((type, weight) => {
2088
- // Find existing configureType and add weight
2089
- const existing = menuStructure.configureTypes.find((ct: any) => ct.type === type);
2090
-
2091
- if (existing) {
2092
- existing.weight = weight;
2093
- }
2094
- }),
2095
- };
2096
-
2097
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
2098
-
2099
- const productMetadata: ProductMetadata = {
2100
- name: 'mixed-product',
2101
- label: 'Mixed Product',
2102
- };
2103
- const config: ProductChildPage[] = [
2104
- { type: 'fleet.cattle.io.clustergroup', weight: 100 },
2105
- {
2106
- name: 'overview',
2107
- label: 'Overview',
2108
- weight: 75,
2109
- component: { name: 'OverviewComponent' },
2110
- },
2111
- { type: 'workload.io.deployment', weight: 50 },
2112
- {
2113
- name: 'settings',
2114
- label: 'Settings',
2115
- weight: 25,
2116
- component: { name: 'SettingsComponent' },
2117
- },
2118
- ];
2119
-
2120
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
2121
-
2122
- pluginProduct.apply(mockPlugin, mockStore);
2123
-
2124
- // In SideNav, items are sorted by weight descending
2125
- // Verify that the menu structure reflects the intended ordering
2126
-
2127
- // ConfigureTypes (resources)
2128
- expect(menuStructure.configureTypes[0]).toMatchObject({
2129
- type: 'fleet.cattle.io.clustergroup',
2130
- weight: 100,
2131
- });
2132
- expect(menuStructure.configureTypes[1]).toMatchObject({
2133
- type: 'workload.io.deployment',
2134
- weight: 50,
2135
- });
2136
-
2137
- // VirtualTypes (custom pages)
2138
- expect(menuStructure.virtualTypes[0]).toMatchObject({
2139
- name: 'mixedproduct-overview',
2140
- weight: 75,
2141
- });
2142
- expect(menuStructure.virtualTypes[1]).toMatchObject({
2143
- name: 'mixedproduct-settings',
2144
- weight: 25,
2145
- });
2146
-
2147
- // Verify basicType call includes all items for navigation
2148
- const allItems = menuStructure.basicTypes[0].types;
2149
-
2150
- expect(allItems).toContain('fleet.cattle.io.clustergroup');
2151
- expect(allItems).toContain('mixedproduct-overview');
2152
- expect(allItems).toContain('workload.io.deployment');
2153
- expect(allItems).toContain('mixedproduct-settings');
2154
- });
2155
- });
2156
-
2157
- describe('extending product - menu structure validation', () => {
2158
- it('should add items to existing product menu with correct naming', () => {
2159
- const mockPlugin = createMockPlugin();
2160
- const mockStore = createMockStore();
2161
-
2162
- const menuStructure: any = {
2163
- basicTypes: [],
2164
- virtualTypes: [],
2165
- };
2166
-
2167
- const mockDSL = {
2168
- product: jest.fn(),
2169
- basicType: jest.fn((types, group) => {
2170
- menuStructure.basicTypes.push({ types, group });
2171
- }),
2172
- labelGroup: jest.fn(),
2173
- setGroupDefaultType: jest.fn(),
2174
- weightGroup: jest.fn(),
2175
- virtualType: jest.fn((config) => {
2176
- menuStructure.virtualTypes.push({
2177
- name: config.name,
2178
- label: config.label,
2179
- weight: config.weight,
2180
- });
2181
- }),
2182
- configureType: jest.fn(),
2183
- weightType: jest.fn(),
2184
- };
2185
-
2186
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
2187
-
2188
- const validStandardProduct = StandardProductNames.EXPLORER;
2189
- const config: ProductChildPage[] = [
2190
- {
2191
- name: 'my-custom-page',
2192
- label: 'My Custom Page',
2193
- weight: 99,
2194
- component: { name: 'CustomComponent' },
2195
- },
2196
- {
2197
- name: 'another-page',
2198
- label: 'Another Page',
2199
- weight: 98,
2200
- component: { name: 'AnotherComponent' },
2201
- },
2202
- ];
2203
-
2204
- const pluginProduct = new PluginProduct(mockPlugin, validStandardProduct, config);
2205
-
2206
- pluginProduct.apply(mockPlugin, mockStore);
2207
-
2208
- // When extending EXPLORER, names are prefixed with product name
2209
- expect(menuStructure.virtualTypes[0]).toMatchObject({
2210
- name: 'explorer-my-custom-page',
2211
- label: 'My Custom Page',
2212
- weight: 99,
2213
- });
2214
- expect(menuStructure.virtualTypes[1]).toMatchObject({
2215
- name: 'explorer-another-page',
2216
- label: 'Another Page',
2217
- weight: 98,
2218
- });
2219
-
2220
- // These should be added to EXPLORER's existing navigation
2221
- const explorerItems = menuStructure.basicTypes[0].types;
2222
-
2223
- expect(explorerItems).toContain('explorer-my-custom-page');
2224
- expect(explorerItems).toContain('explorer-another-page');
2225
- });
2226
-
2227
- it('should add groups to existing product with correct hierarchy', () => {
2228
- const mockPlugin = createMockPlugin();
2229
- const mockStore = createMockStore();
2230
-
2231
- const menuStructure: any = {
2232
- basicTypes: [],
2233
- virtualTypes: [],
2234
- groupLabels: {},
2235
- groupWeights: {},
2236
- };
2237
-
2238
- const mockDSL = {
2239
- product: jest.fn(),
2240
- basicType: jest.fn((types, group) => {
2241
- menuStructure.basicTypes.push({ types, group });
2242
- }),
2243
- labelGroup: jest.fn((group, label, labelKey) => {
2244
- menuStructure.groupLabels[group] = { label, labelKey };
2245
- }),
2246
- setGroupDefaultType: jest.fn(),
2247
- weightGroup: jest.fn((group, weight) => {
2248
- menuStructure.groupWeights[group] = weight;
2249
- }),
2250
- virtualType: jest.fn((config) => {
2251
- menuStructure.virtualTypes.push({
2252
- name: config.name,
2253
- label: config.label,
2254
- weight: config.weight,
2255
- });
2256
- }),
2257
- configureType: jest.fn(),
2258
- weightType: jest.fn(),
2259
- };
2260
-
2261
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
2262
-
2263
- const validStandardProduct = StandardProductNames.SETTINGS;
2264
- const config: ProductChildGroup[] = [
2265
- {
2266
- name: 'extensions',
2267
- label: 'Extensions',
2268
- weight: 80,
2269
- children: [
2270
- {
2271
- name: 'marketplace',
2272
- label: 'Marketplace',
2273
- component: { name: 'MarketplaceComponent' },
2274
- },
2275
- {
2276
- name: 'installed',
2277
- label: 'Installed',
2278
- component: { name: 'InstalledComponent' },
2279
- },
2280
- ],
2281
- },
2282
- ];
2283
-
2284
- const pluginProduct = new PluginProduct(mockPlugin, validStandardProduct, config);
2285
-
2286
- pluginProduct.apply(mockPlugin, mockStore);
2287
-
2288
- // Verify group is added with correct naming (settings-extensions)
2289
- expect(menuStructure.groupLabels['settings-extensions']).toStrictEqual({
2290
- label: 'Extensions',
2291
- labelKey: undefined,
2292
- });
2293
- expect(menuStructure.groupWeights['settings-extensions']).toBe(80);
2294
-
2295
- // Verify child items have correct hierarchy in their names
2296
- const extensionItems = menuStructure.virtualTypes.filter((vt: any) => vt.name.includes('extensions')
2297
- );
2298
-
2299
- expect(extensionItems).toHaveLength(2);
2300
- expect(extensionItems.some((item: any) => item.name === 'settings-extensions-marketplace')).toBe(true);
2301
- expect(extensionItems.some((item: any) => item.name === 'settings-extensions-installed')).toBe(true);
2302
-
2303
- // Verify group navigation structure
2304
- const groupNavCall = menuStructure.basicTypes.find((bt: any) => bt.group === 'settings-extensions'
2305
- );
2306
-
2307
- expect(groupNavCall).toBeDefined();
2308
- expect(groupNavCall.types).toStrictEqual(expect.arrayContaining([
2309
- 'settings-extensions-marketplace',
2310
- 'settings-extensions-installed',
2311
- 'settings-extensions', // Group itself is also in the nav
2312
- ]));
2313
- });
2314
- });
2315
-
2316
- describe('comprehensive menu rendering scenario', () => {
2317
- it('should create complete menu structure matching SideNav expectations', () => {
2318
- const mockPlugin = createMockPlugin();
2319
- const mockStore = createMockStore();
2320
-
2321
- // Simulate the complete menu structure as SideNav would build it
2322
- const menuStructure: any = {
2323
- groups: [],
2324
- items: [],
2325
- };
2326
-
2327
- const mockDSL = {
2328
- product: jest.fn(),
2329
- basicType: jest.fn((types, group) => {
2330
- if (group) {
2331
- // This is a group with children
2332
- const existingGroup = menuStructure.groups.find((g: any) => g.name === group);
2333
-
2334
- if (existingGroup) {
2335
- existingGroup.children = types.filter((t: string) => t !== group);
2336
- } else {
2337
- menuStructure.groups.push({
2338
- name: group,
2339
- children: types.filter((t: string) => t !== group),
2340
- });
2341
- }
2342
- }
2343
- }),
2344
- labelGroup: jest.fn((group, label, labelKey) => {
2345
- const existingGroup = menuStructure.groups.find((g: any) => g.name === group);
2346
-
2347
- if (existingGroup) {
2348
- existingGroup.label = label;
2349
- existingGroup.labelKey = labelKey;
2350
- } else {
2351
- menuStructure.groups.push({
2352
- name: group,
2353
- label,
2354
- labelKey,
2355
- });
2356
- }
2357
- }),
2358
- setGroupDefaultType: jest.fn(),
2359
- weightGroup: jest.fn((group, weight) => {
2360
- const existingGroup = menuStructure.groups.find((g: any) => g.name === group);
2361
-
2362
- if (existingGroup) {
2363
- existingGroup.weight = weight;
2364
- }
2365
- }),
2366
- virtualType: jest.fn((config) => {
2367
- menuStructure.items.push({
2368
- name: config.name,
2369
- label: config.label,
2370
- weight: config.weight,
2371
- type: 'virtual',
2372
- });
2373
- }),
2374
- configureType: jest.fn((type) => {
2375
- menuStructure.items.push({
2376
- name: type,
2377
- type: 'configure',
2378
- weight: 0, // Will be set by weightType if provided
2379
- });
2380
- }),
2381
- weightType: jest.fn((type, weight) => {
2382
- const item = menuStructure.items.find((i: any) => i.name === type);
2383
-
2384
- if (item) {
2385
- item.weight = weight;
2386
- }
2387
- }),
2388
- };
2389
-
2390
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
2391
-
2392
- const productMetadata: ProductMetadata = {
2393
- name: 'complete-app',
2394
- label: 'Complete App',
2395
- };
2396
- const config: (ProductChildGroup | ProductChildPage)[] = [
2397
- {
2398
- name: 'dashboard',
2399
- label: 'Dashboard',
2400
- weight: 100,
2401
- component: { name: 'DashboardComponent' },
2402
- },
2403
- {
2404
- name: 'workloads',
2405
- label: 'Workloads',
2406
- weight: 90,
2407
- children: [
2408
- { type: 'workload.io.deployment', weight: 50 },
2409
- { type: 'workload.io.pod', weight: 45 },
2410
- {
2411
- name: 'jobs',
2412
- label: 'Jobs',
2413
- component: { name: 'JobsComponent' },
2414
- },
2415
- ],
2416
- },
2417
- { type: 'config.io.configmap', weight: 80 },
2418
- {
2419
- name: 'settings',
2420
- label: 'Settings',
2421
- weight: 70,
2422
- children: [
2423
- {
2424
- name: 'general',
2425
- label: 'General',
2426
- component: { name: 'GeneralComponent' },
2427
- },
2428
- {
2429
- name: 'advanced',
2430
- label: 'Advanced',
2431
- component: { name: 'AdvancedComponent' },
2432
- },
2433
- ],
2434
- },
2435
- ];
2436
-
2437
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
2438
-
2439
- pluginProduct.apply(mockPlugin, mockStore);
2440
-
2441
- // Verify groups were created with correct structure
2442
- const workloadsGroup = menuStructure.groups.find((g: any) => g.name === 'completeapp-workloads');
2443
-
2444
- expect(workloadsGroup).toBeDefined();
2445
- expect(workloadsGroup.label).toBe('Workloads');
2446
- expect(workloadsGroup.weight).toBe(90);
2447
- expect(workloadsGroup.children).toStrictEqual(expect.arrayContaining([
2448
- 'workload.io.deployment',
2449
- 'workload.io.pod',
2450
- 'completeapp-workloads-jobs',
2451
- ]));
2452
-
2453
- const settingsGroup = menuStructure.groups.find((g: any) => g.name === 'completeapp-settings');
2454
-
2455
- expect(settingsGroup).toBeDefined();
2456
- expect(settingsGroup.label).toBe('Settings');
2457
- expect(settingsGroup.weight).toBe(70);
2458
- expect(settingsGroup.children).toStrictEqual(expect.arrayContaining([
2459
- 'completeapp-settings-general',
2460
- 'completeapp-settings-advanced',
2461
- ]));
2462
-
2463
- // Verify all menu items were created with correct properties
2464
- const virtualItems = menuStructure.items.filter((i: any) => i.type === 'virtual');
2465
- const configureItems = menuStructure.items.filter((i: any) => i.type === 'configure');
2466
-
2467
- // Check dashboard (top-level virtual type)
2468
- const dashboardItem = virtualItems.find((i: any) => i.name === 'completeapp-dashboard');
2469
-
2470
- expect(dashboardItem).toBeDefined();
2471
- expect(dashboardItem.label).toBe('Dashboard');
2472
- expect(dashboardItem.weight).toBe(100);
2473
-
2474
- // Check configmap (top-level configure type)
2475
- const configMapItem = configureItems.find((i: any) => i.name === 'config.io.configmap');
2476
-
2477
- expect(configMapItem).toBeDefined();
2478
- expect(configMapItem.weight).toBe(80);
2479
-
2480
- // Verify group items
2481
- const jobsItem = virtualItems.find((i: any) => i.name === 'completeapp-workloads-jobs');
2482
-
2483
- expect(jobsItem).toBeDefined();
2484
- expect(jobsItem.label).toBe('Jobs');
2485
-
2486
- const generalItem = virtualItems.find((i: any) => i.name === 'completeapp-settings-general');
2487
-
2488
- expect(generalItem).toBeDefined();
2489
- expect(generalItem.label).toBe('General');
2490
-
2491
- const advancedItem = virtualItems.find((i: any) => i.name === 'completeapp-settings-advanced');
2492
-
2493
- expect(advancedItem).toBeDefined();
2494
- expect(advancedItem.label).toBe('Advanced');
2495
-
2496
- // Verify resource types in workloads group
2497
- const deploymentsItem = configureItems.find((i: any) => i.name === 'workload.io.deployment');
2498
-
2499
- expect(deploymentsItem).toBeDefined();
2500
- expect(deploymentsItem.weight).toBe(50);
2501
-
2502
- const podsItem = configureItems.find((i: any) => i.name === 'workload.io.pod');
2503
-
2504
- expect(podsItem).toBeDefined();
2505
- expect(podsItem.weight).toBe(45);
2506
-
2507
- // Verify total counts
2508
- expect(virtualItems.length).toBeGreaterThanOrEqual(4); // dashboard, jobs, general, advanced
2509
- expect(configureItems.length).toBeGreaterThanOrEqual(3); // configmap, deployment, pod
2510
- expect(menuStructure.groups).toHaveLength(2); // workloads, settings
2511
- });
2512
- });
2513
-
2514
- describe('deeply nested groups (groups within groups)', () => {
2515
- it('should handle 2-level nested groups with correct hierarchical paths', () => {
2516
- const mockPlugin = createMockPlugin();
2517
- const mockStore = createMockStore();
2518
- const basicTypeCalls: any[] = [];
2519
-
2520
- const mockDSL = {
2521
- product: jest.fn(),
2522
- basicType: jest.fn((...args) => basicTypeCalls.push(args)),
2523
- labelGroup: jest.fn(),
2524
- setGroupDefaultType: jest.fn(),
2525
- weightGroup: jest.fn(),
2526
- virtualType: jest.fn(),
2527
- configureType: jest.fn(),
2528
- weightType: jest.fn(),
2529
- };
2530
-
2531
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
2532
-
2533
- const productMetadata: ProductMetadata = {
2534
- name: 'nested-product',
2535
- label: 'Nested Product',
2536
- };
2537
- const config: ProductChildGroup[] = [
2538
- {
2539
- name: 'root-group',
2540
- label: 'Root Group',
2541
- weight: 100,
2542
- children: [
2543
- {
2544
- name: 'page1',
2545
- label: 'Page 1',
2546
- component: { name: 'Page1Component' },
2547
- },
2548
- {
2549
- name: 'nested-group',
2550
- label: 'Nested Group',
2551
- weight: 50,
2552
- children: [
2553
- {
2554
- name: 'nested-page1',
2555
- label: 'Nested Page 1',
2556
- component: { name: 'NestedPage1Component' },
2557
- },
2558
- {
2559
- name: 'nested-page2',
2560
- label: 'Nested Page 2',
2561
- component: { name: 'NestedPage2Component' },
2562
- },
2563
- ],
2564
- },
2565
- {
2566
- name: 'page2',
2567
- label: 'Page 2',
2568
- component: { name: 'Page2Component' },
2569
- },
2570
- ],
2571
- },
2572
- ];
2573
-
2574
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
2575
-
2576
- pluginProduct.apply(mockPlugin, mockStore);
2577
-
2578
- // Verify basicType calls include hierarchical paths with :: separators
2579
- // First call should be for root group items
2580
- const rootGroupCall = basicTypeCalls.find((call) => call[1] === 'nestedproduct-root-group');
2581
-
2582
- expect(rootGroupCall).toBeDefined();
2583
- expect(rootGroupCall[0]).toContain('nestedproduct-root-group-page1');
2584
- expect(rootGroupCall[0]).toContain('nestedproduct-root-group-page2');
2585
- expect(rootGroupCall[0]).toContain('nestedproduct-root-group-nested-group');
2586
- expect(rootGroupCall[0]).toContain('nestedproduct-root-group'); // Root group itself
2587
-
2588
- // Second call should be for nested group with hierarchical path
2589
- const nestedGroupCall = basicTypeCalls.find((call) => call[1] === 'nestedproduct-root-group::nestedproduct-root-group-nested-group');
2590
-
2591
- expect(nestedGroupCall).toBeDefined();
2592
- expect(nestedGroupCall[0]).toContain('nestedproduct-root-group-nested-group-nested-page1');
2593
- expect(nestedGroupCall[0]).toContain('nestedproduct-root-group-nested-group-nested-page2');
2594
- });
2595
-
2596
- it('should handle 3-level nested groups with correct hierarchical paths', () => {
2597
- const mockPlugin = createMockPlugin();
2598
- const mockStore = createMockStore();
2599
- const basicTypeCalls: any[] = [];
2600
-
2601
- const mockDSL = {
2602
- product: jest.fn(),
2603
- basicType: jest.fn((...args) => basicTypeCalls.push(args)),
2604
- labelGroup: jest.fn(),
2605
- setGroupDefaultType: jest.fn(),
2606
- weightGroup: jest.fn(),
2607
- virtualType: jest.fn(),
2608
- configureType: jest.fn(),
2609
- weightType: jest.fn(),
2610
- };
2611
-
2612
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
2613
-
2614
- const productMetadata: ProductMetadata = {
2615
- name: 'deep-nested',
2616
- label: 'Deep Nested',
2617
- };
2618
- const config: ProductChildGroup[] = [
2619
- {
2620
- name: 'level1',
2621
- label: 'Level 1',
2622
- children: [
2623
- {
2624
- name: 'level2',
2625
- label: 'Level 2',
2626
- children: [
2627
- {
2628
- name: 'level3',
2629
- label: 'Level 3',
2630
- children: [
2631
- {
2632
- name: 'deep-page',
2633
- label: 'Deep Page',
2634
- component: { name: 'DeepPageComponent' },
2635
- },
2636
- ],
2637
- },
2638
- ],
2639
- },
2640
- ],
2641
- },
2642
- ];
2643
-
2644
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
2645
-
2646
- pluginProduct.apply(mockPlugin, mockStore);
2647
-
2648
- // Verify level 1 (root)
2649
- const level1Call = basicTypeCalls.find((call) => call[1] === 'deepnested-level1');
2650
-
2651
- expect(level1Call).toBeDefined();
2652
- expect(level1Call[0]).toContain('deepnested-level1-level2');
2653
-
2654
- // Verify level 2 (nested in level1)
2655
- const level2Call = basicTypeCalls.find((call) => call[1] === 'deepnested-level1::deepnested-level1-level2');
2656
-
2657
- expect(level2Call).toBeDefined();
2658
- expect(level2Call[0]).toContain('deepnested-level1-level2-level3');
2659
-
2660
- // Verify level 3 (nested in level2)
2661
- const level3Call = basicTypeCalls.find((call) => call[1] === 'deepnested-level1::deepnested-level1-level2::deepnested-level1-level2-level3');
2662
-
2663
- expect(level3Call).toBeDefined();
2664
- expect(level3Call[0]).toContain('deepnested-level1-level2-level3-deep-page');
2665
- });
2666
-
2667
- it('should handle mixed nested groups and pages in standard product extension', () => {
2668
- const mockPlugin = createMockPlugin();
2669
- const mockStore = createMockStore();
2670
- const basicTypeCalls: any[] = [];
2671
-
2672
- const mockDSL = {
2673
- product: jest.fn(),
2674
- basicType: jest.fn((...args) => basicTypeCalls.push(args)),
2675
- labelGroup: jest.fn(),
2676
- setGroupDefaultType: jest.fn(),
2677
- weightGroup: jest.fn(),
2678
- virtualType: jest.fn(),
2679
- configureType: jest.fn(),
2680
- weightType: jest.fn(),
2681
- };
2682
-
2683
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
2684
-
2685
- const validStandardProduct = StandardProductNames.EXPLORER;
2686
- const config: (ProductChildGroup | ProductChildPage)[] = [
2687
- {
2688
- name: 'top-page',
2689
- label: 'Top Page',
2690
- component: { name: 'TopPageComponent' },
2691
- },
2692
- {
2693
- name: 'parent-group',
2694
- label: 'Parent Group',
2695
- weight: 90,
2696
- children: [
2697
- {
2698
- name: 'sibling-page',
2699
- label: 'Sibling Page',
2700
- component: { name: 'SiblingPageComponent' },
2701
- },
2702
- {
2703
- name: 'child-group',
2704
- label: 'Child Group',
2705
- weight: 80,
2706
- children: [
2707
- {
2708
- name: 'nested-page',
2709
- label: 'Nested Page',
2710
- component: { name: 'NestedPageComponent' },
2711
- },
2712
- ],
2713
- },
2714
- ],
2715
- },
2716
- ];
2717
-
2718
- const pluginProduct = new PluginProduct(mockPlugin, validStandardProduct, config);
2719
-
2720
- pluginProduct.apply(mockPlugin, mockStore);
2721
-
2722
- // Verify parent group has both pages and nested groups
2723
- const parentGroupCall = basicTypeCalls.find((call) => call[1] === 'explorer-parent-group');
2724
-
2725
- expect(parentGroupCall).toBeDefined();
2726
- expect(parentGroupCall[0]).toContain('explorer-parent-group-sibling-page');
2727
- expect(parentGroupCall[0]).toContain('explorer-parent-group-child-group');
2728
-
2729
- // Verify child group uses hierarchical path
2730
- const childGroupCall = basicTypeCalls.find((call) => call[1] === 'explorer-parent-group::explorer-parent-group-child-group');
2731
-
2732
- expect(childGroupCall).toBeDefined();
2733
- expect(childGroupCall[0]).toContain('explorer-parent-group-child-group-nested-page');
2734
- });
2735
-
2736
- it('should only add root-level groups to their own basicType list', () => {
2737
- const mockPlugin = createMockPlugin();
2738
- const mockStore = createMockStore();
2739
- const basicTypeCalls: any[] = [];
2740
-
2741
- const mockDSL = {
2742
- product: jest.fn(),
2743
- basicType: jest.fn((...args) => basicTypeCalls.push(args)),
2744
- labelGroup: jest.fn(),
2745
- setGroupDefaultType: jest.fn(),
2746
- weightGroup: jest.fn(),
2747
- virtualType: jest.fn(),
2748
- configureType: jest.fn(),
2749
- weightType: jest.fn(),
2750
- };
2751
-
2752
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
2753
-
2754
- const productMetadata: ProductMetadata = {
2755
- name: 'test-self-ref',
2756
- label: 'Test Self Reference',
2757
- };
2758
- const config: ProductChildGroup[] = [
2759
- {
2760
- name: 'root',
2761
- label: 'Root',
2762
- children: [
2763
- {
2764
- name: 'nested',
2765
- label: 'Nested',
2766
- children: [
2767
- {
2768
- name: 'page',
2769
- label: 'Page',
2770
- component: { name: 'PageComponent' },
2771
- },
2772
- ],
2773
- },
2774
- ],
2775
- },
2776
- ];
2777
-
2778
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
2779
-
2780
- pluginProduct.apply(mockPlugin, mockStore);
2781
-
2782
- // Root group should include itself in its basicType call
2783
- const rootCall = basicTypeCalls.find((call) => call[1] === 'testselfref-root');
2784
-
2785
- expect(rootCall).toBeDefined();
2786
- expect(rootCall[0]).toContain('testselfref-root'); // Self-reference
2787
-
2788
- // Nested group should NOT include itself (would create wrong hierarchy)
2789
- const nestedCall = basicTypeCalls.find((call) => call[1] === 'testselfref-root::testselfref-root-nested');
2790
-
2791
- expect(nestedCall).toBeDefined();
2792
- expect(nestedCall[0]).not.toContain('testselfref-root-nested'); // No self-reference for nested
2793
- expect(nestedCall[0]).toContain('testselfref-root-nested-page'); // Contains its child page
2794
- });
2795
-
2796
- it('should handle multiple nested groups at the same level', () => {
2797
- const mockPlugin = createMockPlugin();
2798
- const mockStore = createMockStore();
2799
- const basicTypeCalls: any[] = [];
2800
-
2801
- const mockDSL = {
2802
- product: jest.fn(),
2803
- basicType: jest.fn((...args) => basicTypeCalls.push(args)),
2804
- labelGroup: jest.fn(),
2805
- setGroupDefaultType: jest.fn(),
2806
- weightGroup: jest.fn(),
2807
- virtualType: jest.fn(),
2808
- configureType: jest.fn(),
2809
- weightType: jest.fn(),
2810
- };
2811
-
2812
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
2813
-
2814
- const productMetadata: ProductMetadata = {
2815
- name: 'multi-nested',
2816
- label: 'Multi Nested',
2817
- };
2818
- const config: ProductChildGroup[] = [
2819
- {
2820
- name: 'parent',
2821
- label: 'Parent',
2822
- children: [
2823
- {
2824
- name: 'child1',
2825
- label: 'Child 1',
2826
- children: [
2827
- {
2828
- name: 'page1',
2829
- label: 'Page 1',
2830
- component: { name: 'Page1Component' },
2831
- },
2832
- ],
2833
- },
2834
- {
2835
- name: 'child2',
2836
- label: 'Child 2',
2837
- children: [
2838
- {
2839
- name: 'page2',
2840
- label: 'Page 2',
2841
- component: { name: 'Page2Component' },
2842
- },
2843
- ],
2844
- },
2845
- ],
2846
- },
2847
- ];
2848
-
2849
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
2850
-
2851
- pluginProduct.apply(mockPlugin, mockStore);
2852
-
2853
- // Verify both child groups have correct hierarchical paths
2854
- const child1Call = basicTypeCalls.find((call) => call[1] === 'multinested-parent::multinested-parent-child1');
2855
-
2856
- expect(child1Call).toBeDefined();
2857
- expect(child1Call[0]).toContain('multinested-parent-child1-page1');
2858
-
2859
- const child2Call = basicTypeCalls.find((call) => call[1] === 'multinested-parent::multinested-parent-child2');
2860
-
2861
- expect(child2Call).toBeDefined();
2862
- expect(child2Call[0]).toContain('multinested-parent-child2-page2');
2863
-
2864
- // Verify parent includes both child groups
2865
- const parentCall = basicTypeCalls.find((call) => call[1] === 'multinested-parent');
2866
-
2867
- expect(parentCall).toBeDefined();
2868
- expect(parentCall[0]).toContain('multinested-parent-child1');
2869
- expect(parentCall[0]).toContain('multinested-parent-child2');
2870
- });
2871
- });
2872
-
2873
- describe('group default type behavior with components', () => {
2874
- it('should set correct default types for mixed groups (with and without components)', () => {
2875
- const mockPlugin = createMockPlugin();
2876
- const mockStore = createMockStore();
2877
- const setGroupDefaultTypeCalls: any[] = [];
2878
-
2879
- const mockDSL = {
2880
- product: jest.fn(),
2881
- basicType: jest.fn(),
2882
- labelGroup: jest.fn(),
2883
- setGroupDefaultType: jest.fn((...args) => setGroupDefaultTypeCalls.push(args)),
2884
- weightGroup: jest.fn(),
2885
- virtualType: jest.fn(),
2886
- configureType: jest.fn(),
2887
- weightType: jest.fn(),
2888
- };
2889
-
2890
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
2891
-
2892
- const productMetadata: ProductMetadata = {
2893
- name: 'mixed-groups',
2894
- label: 'Mixed Groups',
2895
- };
2896
- const config: ProductChildGroup[] = [
2897
- {
2898
- name: 'group-with-page',
2899
- label: 'Group With Page',
2900
- component: { name: 'GroupPageComponent' },
2901
- children: [
2902
- {
2903
- name: 'child1',
2904
- label: 'Child 1',
2905
- component: { name: 'Child1Component' },
2906
- },
2907
- ],
2908
- },
2909
- {
2910
- name: 'group-without-page',
2911
- label: 'Group Without Page',
2912
- children: [
2913
- {
2914
- name: 'child2',
2915
- label: 'Child 2',
2916
- component: { name: 'Child2Component' },
2917
- },
2918
- ],
2919
- },
2920
- ];
2921
-
2922
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
2923
-
2924
- pluginProduct.apply(mockPlugin, mockStore);
2925
-
2926
- // Group WITH component should have defaultType pointing to itself
2927
- const groupWithPageCall = setGroupDefaultTypeCalls.find((call) => call[0] === 'mixedgroups-group-with-page');
2928
-
2929
- expect(groupWithPageCall).toBeDefined();
2930
- expect(groupWithPageCall[0]).toBe('mixedgroups-group-with-page');
2931
- expect(groupWithPageCall[1]).toBe('mixedgroups-group-with-page'); // Points to itself
2932
-
2933
- // Group WITHOUT component should have defaultType pointing to first child
2934
- const groupWithoutPageCall = setGroupDefaultTypeCalls.find((call) => call[0] === 'mixedgroups-group-without-page');
2935
-
2936
- expect(groupWithoutPageCall).toBeDefined();
2937
- expect(groupWithoutPageCall[0]).toBe('mixedgroups-group-without-page');
2938
- expect(groupWithoutPageCall[1]).toBe('mixedgroups-group-without-page-child2'); // Points to first child
2939
- });
2940
-
2941
- it('should handle nested groups where both parent and child have components', () => {
2942
- const mockPlugin = createMockPlugin();
2943
- const mockStore = createMockStore();
2944
- const setGroupDefaultTypeCalls: any[] = [];
2945
-
2946
- const mockDSL = {
2947
- product: jest.fn(),
2948
- basicType: jest.fn(),
2949
- labelGroup: jest.fn(),
2950
- setGroupDefaultType: jest.fn((...args) => setGroupDefaultTypeCalls.push(args)),
2951
- weightGroup: jest.fn(),
2952
- virtualType: jest.fn(),
2953
- configureType: jest.fn(),
2954
- weightType: jest.fn(),
2955
- };
2956
-
2957
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
2958
-
2959
- const productMetadata: ProductMetadata = {
2960
- name: 'nested-with-pages',
2961
- label: 'Nested With Pages',
2962
- };
2963
- const config: ProductChildGroup[] = [
2964
- {
2965
- name: 'parent',
2966
- label: 'Parent',
2967
- component: { name: 'ParentComponent' },
2968
- children: [
2969
- {
2970
- name: 'child-group',
2971
- label: 'Child Group',
2972
- component: { name: 'ChildGroupComponent' },
2973
- children: [
2974
- {
2975
- name: 'grandchild',
2976
- label: 'Grandchild',
2977
- component: { name: 'GrandchildComponent' },
2978
- },
2979
- ],
2980
- },
2981
- ],
2982
- },
2983
- ];
2984
-
2985
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
2986
-
2987
- pluginProduct.apply(mockPlugin, mockStore);
2988
-
2989
- // Parent group with component should point to itself
2990
- const parentCall = setGroupDefaultTypeCalls.find((call) => call[0] === 'nestedwithpages-parent');
2991
-
2992
- expect(parentCall).toBeDefined();
2993
- expect(parentCall[1]).toBe('nestedwithpages-parent');
2994
-
2995
- // Nested child group with component should also point to itself
2996
- const childCall = setGroupDefaultTypeCalls.find((call) => call[0] === 'nestedwithpages-parent-child-group');
2997
-
2998
- expect(childCall).toBeDefined();
2999
- expect(childCall[1]).toBe('nestedwithpages-parent-child-group');
3000
- });
3001
-
3002
- it('should reproduce user bug: group with component and children routes to group page not first child', () => {
3003
- const mockPlugin = createMockPlugin();
3004
- const mockStore = createMockStore();
3005
- const setGroupDefaultTypeCalls: any[] = [];
3006
- const virtualTypeCalls: any[] = [];
3007
-
3008
- const mockDSL = {
3009
- product: jest.fn(),
3010
- basicType: jest.fn(),
3011
- labelGroup: jest.fn(),
3012
- setGroupDefaultType: jest.fn((...args) => setGroupDefaultTypeCalls.push(args)),
3013
- weightGroup: jest.fn(),
3014
- virtualType: jest.fn((...args) => virtualTypeCalls.push(args)),
3015
- configureType: jest.fn(),
3016
- weightType: jest.fn(),
3017
- };
3018
-
3019
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
3020
-
3021
- // Exact scenario from user's bug report
3022
- const productMetadata: ProductMetadata = {
3023
- name: 'group-with-page',
3024
- label: 'Group With Page',
3025
- };
3026
- const config: (ProductChildGroup | ProductChildPage)[] = [
3027
- {
3028
- name: 'general1',
3029
- label: 'General Settings1',
3030
- component: { name: 'GeneralComponent' },
3031
- },
3032
- {
3033
- name: 'settings',
3034
- label: 'Settings',
3035
- component: { name: 'SettingsOverviewComponent' },
3036
- children: [
3037
- {
3038
- name: 'general',
3039
- label: 'General Settings',
3040
- component: { name: 'GeneralComponent' },
3041
- },
3042
- {
3043
- name: 'advanced',
3044
- label: 'Advanced Settings',
3045
- component: { name: 'AdvancedComponent' },
3046
- },
3047
- ],
3048
- },
3049
- {
3050
- name: 'general2',
3051
- label: 'General Settings2',
3052
- component: { name: 'GeneralComponent' },
3053
- },
3054
- ];
3055
-
3056
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
3057
-
3058
- pluginProduct.apply(mockPlugin, mockStore);
3059
-
3060
- // Verify the settings group (which has component + children) has defaultType pointing to itself
3061
- const settingsCall = setGroupDefaultTypeCalls.find((call) => call[0] === 'groupwithpage-settings');
3062
-
3063
- expect(settingsCall).toBeDefined();
3064
- expect(settingsCall[0]).toBe('groupwithpage-settings');
3065
- expect(settingsCall[1]).toBe('groupwithpage-settings'); // Should point to itself, NOT 'groupwithpage-settings-general'
3066
-
3067
- // Verify the settings virtualType was created with exact + overview flags
3068
- const settingsVirtualType = virtualTypeCalls.find((call) => call[0].name === 'groupwithpage-settings');
3069
-
3070
- expect(settingsVirtualType).toBeDefined();
3071
- expect(settingsVirtualType[0].exact).toBe(true);
3072
- expect(settingsVirtualType[0].overview).toBe(true);
3073
- });
3074
-
3075
- it('should handle empty children array with component (group page, no children)', () => {
3076
- const mockPlugin = createMockPlugin();
3077
- const mockStore = createMockStore();
3078
- const setGroupDefaultTypeCalls: any[] = [];
3079
-
3080
- const mockDSL = {
3081
- product: jest.fn(),
3082
- basicType: jest.fn(),
3083
- labelGroup: jest.fn(),
3084
- setGroupDefaultType: jest.fn((...args) => setGroupDefaultTypeCalls.push(args)),
3085
- weightGroup: jest.fn(),
3086
- virtualType: jest.fn(),
3087
- configureType: jest.fn(),
3088
- weightType: jest.fn(),
3089
- };
3090
-
3091
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
3092
-
3093
- const productMetadata: ProductMetadata = {
3094
- name: 'empty-children',
3095
- label: 'Empty Children',
3096
- };
3097
- const config: ProductChildGroup[] = [
3098
- {
3099
- name: 'standalone-group',
3100
- label: 'Standalone Group',
3101
- component: { name: 'StandaloneComponent' },
3102
- children: [],
3103
- },
3104
- ];
3105
-
3106
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
3107
-
3108
- pluginProduct.apply(mockPlugin, mockStore);
3109
-
3110
- // Group with component but empty children should still point to itself
3111
- const groupCall = setGroupDefaultTypeCalls.find((call) => call[0] === 'emptychildren-standalone-group');
3112
-
3113
- expect(groupCall).toBeDefined();
3114
- expect(groupCall[1]).toBe('emptychildren-standalone-group');
3115
- });
3116
- });
3117
- });
3118
-
3119
- describe('fromName convenience method', () => {
3120
- it('should create a new top-level product from a string name', () => {
3121
- const mockPlugin = createMockPlugin();
3122
- const pluginProduct = PluginProduct.fromName(mockPlugin, 'my-first-product');
3123
-
3124
- expect(pluginProduct.newProduct).toBe(true);
3125
- expect(mockPlugin._registerTopLevelProduct).toHaveBeenCalledTimes(1);
3126
- });
3127
-
3128
- it('should register a route with EmptyProductPage when created from a string name', () => {
3129
- const mockPlugin = createMockPlugin();
3130
-
3131
- PluginProduct.fromName(mockPlugin, 'my-first-product');
3132
-
3133
- expect(mockPlugin.addRoute).toHaveBeenCalledTimes(1);
3134
- });
3135
-
3136
- it('should use the string as both name and label for the product', () => {
3137
- const mockPlugin = createMockPlugin();
3138
- const mockStore = createMockStore();
3139
- const productCalls: any[] = [];
3140
- const mockDSL = {
3141
- product: jest.fn((...args: any[]) => productCalls.push(args)),
3142
- basicType: jest.fn(),
3143
- labelGroup: jest.fn(),
3144
- setGroupDefaultType: jest.fn(),
3145
- weightGroup: jest.fn(),
3146
- virtualType: jest.fn(),
3147
- configureType: jest.fn(),
3148
- weightType: jest.fn(),
3149
- headers: jest.fn(),
3150
- hideBulkActions: jest.fn(),
3151
-
3152
- mapGroup: jest.fn(),
3153
- ignoreGroup: jest.fn(),
3154
- mapType: jest.fn(),
3155
- ignoreType: jest.fn(),
3156
- };
3157
-
3158
- jest.spyOn(mockPlugin, 'DSL').mockReturnValue(mockDSL);
3159
-
3160
- const pluginProduct = PluginProduct.fromName(mockPlugin, 'my-first-product');
3161
-
3162
- pluginProduct.apply(mockPlugin, mockStore);
3163
-
3164
- expect(productCalls).toHaveLength(1);
3165
- expect(productCalls[0][0]).toStrictEqual(expect.objectContaining({
3166
- name: 'myfirstproduct',
3167
- label: 'my-first-product',
3168
- }));
3169
- });
3170
-
3171
- it('should handle product names with dashes by removing them for the internal name', () => {
3172
- const mockPlugin = createMockPlugin();
3173
- const mockStore = createMockStore();
3174
- const productCalls: any[] = [];
3175
- const mockDSL = {
3176
- product: jest.fn((...args: any[]) => productCalls.push(args)),
3177
- basicType: jest.fn(),
3178
- labelGroup: jest.fn(),
3179
- setGroupDefaultType: jest.fn(),
3180
- weightGroup: jest.fn(),
3181
- virtualType: jest.fn(),
3182
- configureType: jest.fn(),
3183
- weightType: jest.fn(),
3184
- headers: jest.fn(),
3185
- hideBulkActions: jest.fn(),
3186
-
3187
- mapGroup: jest.fn(),
3188
- ignoreGroup: jest.fn(),
3189
- mapType: jest.fn(),
3190
- ignoreType: jest.fn(),
3191
- };
3192
-
3193
- jest.spyOn(mockPlugin, 'DSL').mockReturnValue(mockDSL);
3194
-
3195
- const pluginProduct = PluginProduct.fromName(mockPlugin, 'test-product-name');
3196
-
3197
- pluginProduct.apply(mockPlugin, mockStore);
3198
-
3199
- expect(productCalls[0][0]).toStrictEqual(expect.objectContaining({ name: 'testproductname' }));
3200
- });
3201
-
3202
- it('should handle product names without dashes', () => {
3203
- const mockPlugin = createMockPlugin();
3204
- const mockStore = createMockStore();
3205
- const productCalls: any[] = [];
3206
- const mockDSL = {
3207
- product: jest.fn((...args: any[]) => productCalls.push(args)),
3208
- basicType: jest.fn(),
3209
- labelGroup: jest.fn(),
3210
- setGroupDefaultType: jest.fn(),
3211
- weightGroup: jest.fn(),
3212
- virtualType: jest.fn(),
3213
- configureType: jest.fn(),
3214
- weightType: jest.fn(),
3215
- headers: jest.fn(),
3216
- hideBulkActions: jest.fn(),
3217
-
3218
- mapGroup: jest.fn(),
3219
- ignoreGroup: jest.fn(),
3220
- mapType: jest.fn(),
3221
- ignoreType: jest.fn(),
3222
- };
3223
-
3224
- jest.spyOn(mockPlugin, 'DSL').mockReturnValue(mockDSL);
3225
-
3226
- const pluginProduct = PluginProduct.fromName(mockPlugin, 'myproduct');
3227
-
3228
- pluginProduct.apply(mockPlugin, mockStore);
3229
-
3230
- expect(productCalls[0][0]).toStrictEqual(expect.objectContaining({ name: 'myproduct' }));
3231
- });
3232
- });
3233
-
3234
- describe('documentation examples', () => {
3235
- describe('quick start: string convenience method', () => {
3236
- it('should create a new product from just a string name', () => {
3237
- const mockPlugin = createMockPlugin();
3238
- const pluginProduct = PluginProduct.fromName(mockPlugin, 'my-first-product');
3239
-
3240
- expect(pluginProduct.newProduct).toBe(true);
3241
- expect(mockPlugin._registerTopLevelProduct).toHaveBeenCalledTimes(1);
3242
- expect(mockPlugin.addRoute).toHaveBeenCalledTimes(1);
3243
- });
3244
- });
3245
-
3246
- describe('single page product', () => {
3247
- it('should create a product with a single page component and no config', () => {
3248
- const mockPlugin = createMockPlugin();
3249
- const product: ProductSinglePage = {
3250
- name: 'my-dashboard',
3251
- label: 'My Dashboard',
3252
- icon: 'globe',
3253
- component: { name: 'DashboardPage' },
3254
- };
3255
-
3256
- const pluginProduct = new PluginProduct(mockPlugin, product, []);
3257
-
3258
- expect(pluginProduct.newProduct).toBe(true);
3259
- expect(mockPlugin._registerTopLevelProduct).toHaveBeenCalledTimes(1);
3260
- expect(mockPlugin.addRoute).toHaveBeenCalledTimes(1);
3261
- });
3262
- });
3263
-
3264
- describe('product with custom pages', () => {
3265
- it('should register routes for each custom page', () => {
3266
- const mockPlugin = createMockPlugin();
3267
-
3268
- const overviewPage: ProductChildCustomPage = {
3269
- name: 'overview',
3270
- label: 'Overview',
3271
- component: { name: 'OverviewPage' },
3272
- weight: 2,
3273
- };
3274
-
3275
- const settingsPage: ProductChildCustomPage = {
3276
- name: 'settings',
3277
- label: 'Settings',
3278
- component: { name: 'SettingsPage' },
3279
- weight: 1,
3280
- };
3281
-
3282
- const product: ProductMetadata = {
3283
- name: 'my-app',
3284
- label: 'My App',
3285
- icon: 'gear',
3286
- };
3287
-
3288
- const pluginProduct = new PluginProduct(mockPlugin, product, [overviewPage, settingsPage]);
3289
-
3290
- expect(pluginProduct.newProduct).toBe(true);
3291
- expect(mockPlugin._registerTopLevelProduct).toHaveBeenCalledTimes(1);
3292
- expect(mockPlugin.addRoute).toHaveBeenCalledTimes(2);
3293
- });
3294
- });
3295
-
3296
- describe('product with resource pages', () => {
3297
- it('should register resource CRUD routes for resource page items', () => {
3298
- const mockPlugin = createMockPlugin();
3299
-
3300
- const clusterPage: ProductChildResourcePage = {
3301
- type: 'provisioning.cattle.io.cluster',
3302
- weight: 2,
3303
- config: {
3304
- displayName: 'Clusters',
3305
- isCreatable: true,
3306
- isEditable: true,
3307
- isRemovable: true,
3308
- canYaml: true,
3309
- },
3310
- };
3311
-
3312
- const nodePage: ProductChildResourcePage = {
3313
- type: 'management.cattle.io.node',
3314
- weight: 1,
3315
- };
3316
-
3317
- const product: ProductMetadata = {
3318
- name: 'my-resources',
3319
- label: 'My Resources',
3320
- };
3321
-
3322
- const pluginProduct = new PluginProduct(mockPlugin, product, [clusterPage, nodePage]);
3323
-
3324
- expect(pluginProduct.newProduct).toBe(true);
3325
- // Resource routes are only added once (shared CRUD routes) - mock generates list + detail = 2
3326
- expect(mockPlugin.addRoute).toHaveBeenCalledTimes(2);
3327
- });
3328
- });
3329
-
3330
- describe('product with groups', () => {
3331
- it('should register routes for a product with groups and standalone pages', () => {
3332
- const mockPlugin = createMockPlugin();
3333
-
3334
- const alertsPage: ProductChildCustomPage = {
3335
- name: 'alerts',
3336
- label: 'Alerts',
3337
- component: { name: 'AlertsPage' },
3338
- };
3339
-
3340
- const metricsPage: ProductChildCustomPage = {
3341
- name: 'metrics',
3342
- label: 'Metrics',
3343
- component: { name: 'MetricsPage' },
3344
- };
3345
-
3346
- const monitoringGroup: ProductChildGroup = {
3347
- name: 'monitoring',
3348
- label: 'Monitoring',
3349
- weight: 2,
3350
- children: [alertsPage, metricsPage],
3351
- };
3352
-
3353
- const overviewPage: ProductChildCustomPage = {
3354
- name: 'overview',
3355
- label: 'Overview',
3356
- component: { name: 'OverviewPage' },
3357
- weight: 3,
3358
- };
3359
-
3360
- const product: ProductMetadata = {
3361
- name: 'my-platform',
3362
- label: 'My Platform',
3363
- };
3364
-
3365
- const config: ProductChild[] = [overviewPage, monitoringGroup];
3366
- const pluginProduct = new PluginProduct(mockPlugin, product, config);
3367
-
3368
- expect(pluginProduct.newProduct).toBe(true);
3369
- expect(mockPlugin._registerTopLevelProduct).toHaveBeenCalledTimes(1);
3370
- // 1 standalone page + 1 group parent route + 2 group children routes = 4
3371
- expect(mockPlugin.addRoute).toHaveBeenCalledTimes(4);
3372
- });
3373
- });
3374
-
3375
- describe('extending an existing product', () => {
3376
- it('should extend explorer with a custom page', () => {
3377
- const mockPlugin = createMockPlugin();
3378
-
3379
- const customPage: ProductChildCustomPage = {
3380
- name: 'my-custom-view',
3381
- label: 'My Custom View',
3382
- component: { name: 'MyCustomView' },
3383
- };
3384
-
3385
- const pluginProduct = new PluginProduct(mockPlugin, StandardProductNames.EXPLORER, [customPage]);
3386
-
3387
- expect(pluginProduct.newProduct).toBe(false);
3388
- expect(mockPlugin._registerTopLevelProduct).not.toHaveBeenCalled();
3389
- expect(mockPlugin.addRoute).toHaveBeenCalledTimes(1);
3390
- });
3391
- });
3392
-
3393
- describe('mixed pages: custom + resource', () => {
3394
- it('should register routes for both custom pages and resource pages together', () => {
3395
- const mockPlugin = createMockPlugin();
3396
-
3397
- const dashboardPage: ProductChildCustomPage = {
3398
- name: 'dashboard',
3399
- label: 'Dashboard',
3400
- component: { name: 'Dashboard' },
3401
- weight: 3,
3402
- };
3403
-
3404
- const clusterPage: ProductChildResourcePage = {
3405
- type: 'provisioning.cattle.io.cluster',
3406
- weight: 2,
3407
- };
3408
-
3409
- const settingsPage: ProductChildCustomPage = {
3410
- name: 'settings',
3411
- label: 'Settings',
3412
- component: { name: 'Settings' },
3413
- weight: 1,
3414
- };
3415
-
3416
- const product: ProductMetadata = {
3417
- name: 'my-platform',
3418
- label: 'My Platform',
3419
- };
3420
-
3421
- const config: ProductChild[] = [dashboardPage, clusterPage, settingsPage];
3422
- const pluginProduct = new PluginProduct(mockPlugin, product, config);
3423
-
3424
- expect(pluginProduct.newProduct).toBe(true);
3425
- // 2 custom page routes + resource CRUD routes (list + detail = 2 from mock) = 4
3426
- expect(mockPlugin.addRoute).toHaveBeenCalledTimes(4);
3427
- // Verify addRoute was called for both custom pages and resource routes
3428
- const routeCalls = (mockPlugin.addRoute as jest.Mock).mock.calls;
3429
- const hasCustomRoutes = routeCalls.some((call) => call[0]?.name?.includes('dashboard'));
3430
- const hasResourceRoutes = routeCalls.some((call) => call[0]?.name?.includes('provisioning.cattle.io.cluster'));
3431
-
3432
- expect(hasCustomRoutes).toBe(true);
3433
- expect(hasResourceRoutes).toBe(true);
3434
- });
3435
- });
3436
-
3437
- describe('group with its own page', () => {
3438
- it('should register a route for the group page itself and its children', () => {
3439
- const mockPlugin = createMockPlugin();
3440
-
3441
- const alertsPage: ProductChildCustomPage = {
3442
- name: 'alerts',
3443
- label: 'Alerts',
3444
- component: { name: 'AlertsPage' },
3445
- };
3446
-
3447
- const metricsPage: ProductChildCustomPage = {
3448
- name: 'metrics',
3449
- label: 'Metrics',
3450
- component: { name: 'MetricsPage' },
3451
- };
3452
-
3453
- const monitoringGroup: ProductChildGroup = {
3454
- name: 'monitoring',
3455
- label: 'Monitoring',
3456
- component: { name: 'MonitoringOverview' },
3457
- children: [alertsPage, metricsPage],
3458
- };
3459
-
3460
- const product: ProductMetadata = {
3461
- name: 'my-platform',
3462
- label: 'My Platform',
3463
- };
3464
-
3465
- const pluginProduct = new PluginProduct(mockPlugin, product, [monitoringGroup]);
3466
-
3467
- expect(pluginProduct.newProduct).toBe(true);
3468
- // 1 group parent route (with component) + 2 children routes = 3
3469
- expect(mockPlugin.addRoute).toHaveBeenCalledTimes(3);
3470
- });
3471
-
3472
- it('should generate a route with the group name for proper side-menu highlighting', () => {
3473
- const mockPlugin = createMockPlugin();
3474
-
3475
- const childPage: ProductChildCustomPage = {
3476
- name: 'child',
3477
- label: 'Child',
3478
- component: { name: 'ChildComponent' },
3479
- };
3480
-
3481
- const monitoringGroup: ProductChildGroup = {
3482
- name: 'monitoring',
3483
- label: 'Monitoring',
3484
- component: { name: 'MonitoringOverview' },
3485
- children: [childPage],
3486
- };
3487
-
3488
- const product: ProductMetadata = {
3489
- name: 'my-platform',
3490
- label: 'My Platform',
3491
- };
3492
-
3493
- new PluginProduct(mockPlugin, product, [monitoringGroup]);
3494
-
3495
- // The group's own route should include the group name in the route name
3496
- // This ensures the side-menu can highlight the correct item
3497
- const routeCalls = (mockPlugin.addRoute as jest.Mock).mock.calls;
3498
- const groupRoute = routeCalls.find((call) => call[0]?.name?.includes('monitoring'));
3499
-
3500
- expect(groupRoute).toBeDefined();
3501
- expect(groupRoute[0].name).toStrictEqual(expect.stringContaining('monitoring'));
3502
- });
3503
- });
3504
-
3505
- describe('extending Cluster Explorer', () => {
3506
- it('should extend explorer with a standalone page', () => {
3507
- const mockPlugin = createMockPlugin();
3508
-
3509
- const customPage: ProductChildCustomPage = {
3510
- name: 'cost-analysis',
3511
- label: 'Cost Analysis',
3512
- component: { name: 'CostAnalysis' },
3513
- };
3514
-
3515
- const pluginProduct = new PluginProduct(mockPlugin, StandardProductNames.EXPLORER, [customPage]);
3516
-
3517
- expect(pluginProduct.newProduct).toBe(false);
3518
- expect(mockPlugin.addRoute).toHaveBeenCalledTimes(1);
3519
- });
3520
-
3521
- it('should extend explorer with a group containing multiple pages', () => {
3522
- const mockPlugin = createMockPlugin();
3523
-
3524
- const costPage: ProductChildCustomPage = {
3525
- name: 'cost-analysis',
3526
- label: 'Cost Analysis',
3527
- component: { name: 'CostAnalysis' },
3528
- };
3529
-
3530
- const usagePage: ProductChildCustomPage = {
3531
- name: 'usage-report',
3532
- label: 'Usage Report',
3533
- component: { name: 'UsageReport' },
3534
- };
3535
-
3536
- const insightsGroup: ProductChildGroup = {
3537
- name: 'insights',
3538
- label: 'Insights',
3539
- children: [costPage, usagePage],
3540
- };
3541
-
3542
- const pluginProduct = new PluginProduct(mockPlugin, StandardProductNames.EXPLORER, [insightsGroup]);
3543
-
3544
- expect(pluginProduct.newProduct).toBe(false);
3545
- // 1 group parent route + 2 child routes = 3
3546
- expect(mockPlugin.addRoute).toHaveBeenCalledTimes(3);
3547
- });
3548
- });
3549
-
3550
- describe('translation keys instead of labels', () => {
3551
- it('should accept labelKey instead of label for product and pages', () => {
3552
- const mockPlugin = createMockPlugin();
3553
-
3554
- const product: ProductMetadata = {
3555
- name: 'my-app',
3556
- labelKey: 'product.myApp.label',
3557
- icon: 'gear',
3558
- };
3559
-
3560
- const overviewPage: ProductChildCustomPage = {
3561
- name: 'overview',
3562
- labelKey: 'product.myApp.overview',
3563
- component: { name: 'OverviewPage' },
3564
- };
3565
-
3566
- const pluginProduct = new PluginProduct(mockPlugin, product, [overviewPage]);
3567
-
3568
- expect(pluginProduct.newProduct).toBe(true);
3569
- expect(mockPlugin._registerTopLevelProduct).toHaveBeenCalledTimes(1);
3570
- expect(mockPlugin.addRoute).toHaveBeenCalledTimes(1);
3571
- });
3572
-
3573
- it('should register labelKey on virtualType during apply', () => {
3574
- const mockPlugin = createMockPlugin();
3575
- const mockStore = createMockStore();
3576
- const virtualTypeCalls: any[] = [];
3577
- const mockDSL = {
3578
- product: jest.fn(),
3579
- basicType: jest.fn(),
3580
- labelGroup: jest.fn(),
3581
- setGroupDefaultType: jest.fn(),
3582
- weightGroup: jest.fn(),
3583
- virtualType: jest.fn((...args: any[]) => virtualTypeCalls.push(args)),
3584
- configureType: jest.fn(),
3585
- weightType: jest.fn(),
3586
- };
3587
-
3588
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
3589
-
3590
- const product: ProductMetadata = {
3591
- name: 'my-app',
3592
- labelKey: 'product.myApp.label',
3593
- };
3594
-
3595
- const overviewPage: ProductChildCustomPage = {
3596
- name: 'overview',
3597
- labelKey: 'product.myApp.overview',
3598
- component: { name: 'OverviewPage' },
3599
- };
3600
-
3601
- const pluginProduct = new PluginProduct(mockPlugin, product, [overviewPage]);
3602
-
3603
- pluginProduct.apply(mockPlugin, mockStore);
3604
-
3605
- expect(virtualTypeCalls).toHaveLength(1);
3606
- expect(virtualTypeCalls[0][0]).toStrictEqual(expect.objectContaining({ labelKey: 'product.myApp.overview' }));
3607
- });
3608
- });
3609
-
3610
- describe('duplicate page name detection', () => {
3611
- it('should throw when two custom pages have the same name in a new product', () => {
3612
- const mockPlugin = createMockPlugin();
3613
- const mockStore = createMockStore();
3614
- const mockDSL = {
3615
- product: jest.fn(),
3616
- basicType: jest.fn(),
3617
- labelGroup: jest.fn(),
3618
- setGroupDefaultType: jest.fn(),
3619
- weightGroup: jest.fn(),
3620
- virtualType: jest.fn(),
3621
- configureType: jest.fn(),
3622
- weightType: jest.fn(),
3623
- };
3624
-
3625
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
3626
-
3627
- const page1: ProductChildCustomPage = {
3628
- name: 'overview',
3629
- label: 'Overview',
3630
- component: { name: 'Page1' },
3631
- };
3632
-
3633
- const page2: ProductChildCustomPage = {
3634
- name: 'overview',
3635
- label: 'Overview Duplicate',
3636
- component: { name: 'Page2' },
3637
- };
3638
-
3639
- const product: ProductMetadata = {
3640
- name: 'my-app',
3641
- label: 'My App',
3642
- };
3643
-
3644
- const pluginProduct = new PluginProduct(mockPlugin, product, [page1, page2]);
3645
-
3646
- expect(() => {
3647
- pluginProduct.apply(mockPlugin, mockStore);
3648
- }).toThrow('Duplicate page name "overview"');
3649
- });
3650
-
3651
- it('should not throw when pages with the same name are in different groups (different resolved names)', () => {
3652
- const mockPlugin = createMockPlugin();
3653
- const mockStore = createMockStore();
3654
- const mockDSL = {
3655
- product: jest.fn(),
3656
- basicType: jest.fn(),
3657
- labelGroup: jest.fn(),
3658
- setGroupDefaultType: jest.fn(),
3659
- weightGroup: jest.fn(),
3660
- virtualType: jest.fn(),
3661
- configureType: jest.fn(),
3662
- weightType: jest.fn(),
3663
- };
3664
-
3665
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
3666
-
3667
- // Same page name 'overview' but in different groups produces different resolved names
3668
- const standalonePage: ProductChildCustomPage = {
3669
- name: 'cost-analysis',
3670
- label: 'Cost Analysis',
3671
- component: { name: 'CostAnalysis1' },
3672
- };
3673
-
3674
- const groupChildPage: ProductChildCustomPage = {
3675
- name: 'cost-analysis',
3676
- label: 'Cost Analysis',
3677
- component: { name: 'CostAnalysis2' },
3678
- };
3679
-
3680
- const group: ProductChildGroup = {
3681
- name: 'insights',
3682
- label: 'Insights',
3683
- children: [groupChildPage],
3684
- };
3685
-
3686
- const product: ProductMetadata = {
3687
- name: 'my-app',
3688
- label: 'My App',
3689
- };
3690
-
3691
- const config: ProductChild[] = [standalonePage, group];
3692
- const pluginProduct = new PluginProduct(mockPlugin, product, config);
3693
-
3694
- // Different groups produce different resolved names (myapp-cost-analysis vs myapp-insights-cost-analysis)
3695
- expect(() => {
3696
- pluginProduct.apply(mockPlugin, mockStore);
3697
- }).not.toThrow();
3698
- });
3699
-
3700
- it('should throw when two resource pages have the same type', () => {
3701
- const mockPlugin = createMockPlugin();
3702
- const mockStore = createMockStore();
3703
- const mockDSL = {
3704
- product: jest.fn(),
3705
- basicType: jest.fn(),
3706
- labelGroup: jest.fn(),
3707
- setGroupDefaultType: jest.fn(),
3708
- weightGroup: jest.fn(),
3709
- virtualType: jest.fn(),
3710
- configureType: jest.fn(),
3711
- weightType: jest.fn(),
3712
- };
3713
-
3714
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
3715
-
3716
- const resource1: ProductChildResourcePage = { type: 'provisioning.cattle.io.cluster' };
3717
-
3718
- const resource2: ProductChildResourcePage = { type: 'provisioning.cattle.io.cluster' };
3719
-
3720
- const product: ProductMetadata = {
3721
- name: 'my-app',
3722
- label: 'My App',
3723
- };
3724
-
3725
- const pluginProduct = new PluginProduct(mockPlugin, product, [resource1, resource2]);
3726
-
3727
- expect(() => {
3728
- pluginProduct.apply(mockPlugin, mockStore);
3729
- }).toThrow('Duplicate resource type "provisioning.cattle.io.cluster"');
3730
- });
3731
-
3732
- it('should not throw when pages have different names', () => {
3733
- const mockPlugin = createMockPlugin();
3734
- const mockStore = createMockStore();
3735
- const mockDSL = {
3736
- product: jest.fn(),
3737
- basicType: jest.fn(),
3738
- labelGroup: jest.fn(),
3739
- setGroupDefaultType: jest.fn(),
3740
- weightGroup: jest.fn(),
3741
- virtualType: jest.fn(),
3742
- configureType: jest.fn(),
3743
- weightType: jest.fn(),
3744
- };
3745
-
3746
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
3747
-
3748
- const page1: ProductChildCustomPage = {
3749
- name: 'overview',
3750
- label: 'Overview',
3751
- component: { name: 'Page1' },
3752
- };
3753
-
3754
- const page2: ProductChildCustomPage = {
3755
- name: 'settings',
3756
- label: 'Settings',
3757
- component: { name: 'Page2' },
3758
- };
3759
-
3760
- const product: ProductMetadata = {
3761
- name: 'my-app',
3762
- label: 'My App',
3763
- };
3764
-
3765
- const pluginProduct = new PluginProduct(mockPlugin, product, [page1, page2]);
3766
-
3767
- expect(() => {
3768
- pluginProduct.apply(mockPlugin, mockStore);
3769
- }).not.toThrow();
3770
- });
3771
- });
3772
-
3773
- describe('group with component route naming', () => {
3774
- it('should include the group name in the virtualType route for side-menu highlighting', () => {
3775
- const mockPlugin = createMockPlugin();
3776
- const mockStore = createMockStore();
3777
- const virtualTypeCalls: any[] = [];
3778
- const mockDSL = {
3779
- product: jest.fn(),
3780
- basicType: jest.fn(),
3781
- labelGroup: jest.fn(),
3782
- setGroupDefaultType: jest.fn(),
3783
- weightGroup: jest.fn(),
3784
- virtualType: jest.fn((...args: any[]) => virtualTypeCalls.push(args)),
3785
- configureType: jest.fn(),
3786
- weightType: jest.fn(),
3787
- };
3788
-
3789
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
3790
-
3791
- const childPage: ProductChildCustomPage = {
3792
- name: 'alerts',
3793
- label: 'Alerts',
3794
- component: { name: 'AlertsPage' },
3795
- };
3796
-
3797
- const monitoringGroup: ProductChildGroup = {
3798
- name: 'monitoring',
3799
- label: 'Monitoring',
3800
- component: { name: 'MonitoringOverview' },
3801
- children: [childPage],
3802
- };
3803
-
3804
- const product: ProductMetadata = {
3805
- name: 'my-app',
3806
- label: 'My App',
3807
- };
3808
-
3809
- const pluginProduct = new PluginProduct(mockPlugin, product, [monitoringGroup]);
3810
-
3811
- pluginProduct.apply(mockPlugin, mockStore);
3812
-
3813
- // Find the virtualType call for the group (has exact + overview flags)
3814
- const groupVirtualType = virtualTypeCalls.find((call) => call[0].exact === true && call[0].overview === true);
3815
-
3816
- expect(groupVirtualType).toBeDefined();
3817
- // The route name should contain the group name 'monitoring', not a generic 'group'
3818
- expect(groupVirtualType[0].route.name).toStrictEqual(expect.stringContaining('monitoring'));
3819
- // It should NOT be a generic route without the group name
3820
- expect(groupVirtualType[0].route.name).not.toBe('myapp-c-cluster');
3821
- });
3822
- describe('resource page: headers support', () => {
3823
- it('should call DSL headers method when resource page has headers configured', () => {
3824
- const mockPlugin = createMockPlugin();
3825
- const mockStore = createMockStore();
3826
- const mockDSL = (mockPlugin.DSL as jest.Mock)();
3827
-
3828
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
3829
-
3830
- const productMetadata: ProductMetadata = {
3831
- name: 'headers-test',
3832
- label: 'Headers Test',
3833
- };
3834
-
3835
- const testHeaders = [
3836
- {
3837
- name: 'name', label: 'Name', value: 'metadata.name'
3838
- },
3839
- {
3840
- name: 'status', labelKey: 'tableHeaders.status', value: 'status'
3841
- },
3842
- ];
3843
-
3844
- const config: ProductChildPage[] = [
3845
- { type: 'some.resource.type', headers: testHeaders },
3846
- ];
3847
-
3848
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
3849
-
3850
- pluginProduct.apply(mockPlugin, mockStore);
3851
-
3852
- expect(mockDSL.headers).toHaveBeenCalledWith('some.resource.type', testHeaders, undefined);
3853
- });
3854
-
3855
- it('should call DSL headers method with sspHeaders when configured', () => {
3856
- const mockPlugin = createMockPlugin();
3857
- const mockStore = createMockStore();
3858
- const mockDSL = (mockPlugin.DSL as jest.Mock)();
3859
-
3860
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
3861
-
3862
- const productMetadata: ProductMetadata = {
3863
- name: 'ssp-headers-test',
3864
- label: 'SSP Headers Test',
3865
- };
3866
-
3867
- const testSspHeaders = [
3868
- {
3869
- name: 'name', label: 'Name', value: 'metadata.name'
3870
- },
3871
- {
3872
- name: 'namespace', labelKey: 'tableHeaders.namespace', value: 'metadata.namespace'
3873
- },
3874
- ];
3875
-
3876
- const config: ProductChildPage[] = [
3877
- { type: 'some.resource.type', sspHeaders: testSspHeaders },
3878
- ];
3879
-
3880
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
3881
-
3882
- pluginProduct.apply(mockPlugin, mockStore);
3883
-
3884
- expect(mockDSL.headers).toHaveBeenCalledWith('some.resource.type', undefined, testSspHeaders);
3885
- });
3886
-
3887
- it('should call DSL headers method with both headers and sspHeaders when both are configured', () => {
3888
- const mockPlugin = createMockPlugin();
3889
- const mockStore = createMockStore();
3890
- const mockDSL = (mockPlugin.DSL as jest.Mock)();
3891
-
3892
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
3893
-
3894
- const productMetadata: ProductMetadata = {
3895
- name: 'both-headers-test',
3896
- label: 'Both Headers Test',
3897
- };
3898
-
3899
- const testHeaders = [
3900
- {
3901
- name: 'name', label: 'Name', value: 'metadata.name'
3902
- },
3903
- ];
3904
-
3905
- const testSspHeaders = [
3906
- {
3907
- name: 'name', label: 'Name', value: 'metadata.name'
3908
- },
3909
- ];
3910
-
3911
- const config: ProductChildPage[] = [
3912
- {
3913
- type: 'some.resource.type',
3914
- headers: testHeaders,
3915
- sspHeaders: testSspHeaders,
3916
- },
3917
- ];
3918
-
3919
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
3920
-
3921
- pluginProduct.apply(mockPlugin, mockStore);
3922
-
3923
- expect(mockDSL.headers).toHaveBeenCalledWith('some.resource.type', testHeaders, testSspHeaders);
3924
- });
3925
-
3926
- it('should not call DSL headers method when resource page has no headers or sspHeaders', () => {
3927
- const mockPlugin = createMockPlugin();
3928
- const mockStore = createMockStore();
3929
- const mockDSL = (mockPlugin.DSL as jest.Mock)();
3930
-
3931
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
3932
-
3933
- const productMetadata: ProductMetadata = {
3934
- name: 'no-headers-test',
3935
- label: 'No Headers Test',
3936
- };
3937
-
3938
- const config: ProductChildPage[] = [{ type: 'some.resource.type' }];
3939
-
3940
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
3941
-
3942
- pluginProduct.apply(mockPlugin, mockStore);
3943
-
3944
- expect(mockDSL.headers).not.toHaveBeenCalled();
3945
- });
3946
- });
3947
-
3948
- describe('resource page: overrideListResourceName (mapType) support', () => {
3949
- it('should call DSL mapType when resource page has overrideListResourceName', () => {
3950
- const mockPlugin = createMockPlugin();
3951
- const mockStore = createMockStore();
3952
- const mockDSL = (mockPlugin.DSL as jest.Mock)();
3953
-
3954
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
3955
-
3956
- const productMetadata: ProductMetadata = {
3957
- name: 'maptype-test',
3958
- label: 'MapType Test',
3959
- };
3960
-
3961
- const config: ProductChildPage[] = [
3962
- { type: 'apps.deployment', overrideListResourceName: 'Deployments' },
3963
- ];
3964
-
3965
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
3966
-
3967
- pluginProduct.apply(mockPlugin, mockStore);
3968
-
3969
- expect(mockDSL.mapType).toHaveBeenCalledWith('apps.deployment', 'Deployments');
3970
- });
3971
-
3972
- it('should not call mapType when overrideListResourceName is not set', () => {
3973
- const mockPlugin = createMockPlugin();
3974
- const mockStore = createMockStore();
3975
- const mockDSL = (mockPlugin.DSL as jest.Mock)();
3976
-
3977
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
3978
-
3979
- const productMetadata: ProductMetadata = {
3980
- name: 'no-maptype',
3981
- label: 'No MapType',
3982
- };
3983
-
3984
- const config: ProductChildPage[] = [{ type: 'apps.deployment' }];
3985
-
3986
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
3987
-
3988
- pluginProduct.apply(mockPlugin, mockStore);
3989
-
3990
- expect(mockDSL.mapType).not.toHaveBeenCalled();
3991
- });
3992
- });
3993
-
3994
- describe('resource page: hideFromNav (ignoreType) support', () => {
3995
- it('should call DSL ignoreType when resource page has hideFromNav set', () => {
3996
- const mockPlugin = createMockPlugin();
3997
- const mockStore = createMockStore();
3998
- const mockDSL = (mockPlugin.DSL as jest.Mock)();
3999
-
4000
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4001
-
4002
- const productMetadata: ProductMetadata = {
4003
- name: 'ignoretype-test',
4004
- label: 'IgnoreType Test',
4005
- };
4006
-
4007
- const config: ProductChildPage[] = [
4008
- { type: 'secret.type', hideFromNav: true },
4009
- ];
4010
-
4011
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4012
-
4013
- pluginProduct.apply(mockPlugin, mockStore);
4014
-
4015
- expect(mockDSL.ignoreType).toHaveBeenCalledWith('secret.type');
4016
- });
4017
-
4018
- it('should not call ignoreType when hideFromNav is not set', () => {
4019
- const mockPlugin = createMockPlugin();
4020
- const mockStore = createMockStore();
4021
- const mockDSL = (mockPlugin.DSL as jest.Mock)();
4022
-
4023
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4024
-
4025
- const productMetadata: ProductMetadata = {
4026
- name: 'no-ignoretype',
4027
- label: 'No IgnoreType',
4028
- };
4029
-
4030
- const config: ProductChildPage[] = [{ type: 'apps.deployment' }];
4031
-
4032
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4033
-
4034
- pluginProduct.apply(mockPlugin, mockStore);
4035
-
4036
- expect(mockDSL.ignoreType).not.toHaveBeenCalled();
4037
- });
4038
- });
4039
-
4040
- describe('resource page: hideBulkActions support', () => {
4041
- it('should call DSL hideBulkActions when resource page has hideBulkActions set', () => {
4042
- const mockPlugin = createMockPlugin();
4043
- const mockStore = createMockStore();
4044
- const mockDSL = (mockPlugin.DSL as jest.Mock)();
4045
-
4046
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4047
-
4048
- const productMetadata: ProductMetadata = {
4049
- name: 'bulk-actions-test',
4050
- label: 'Bulk Actions Test',
4051
- };
4052
-
4053
- const config: ProductChildPage[] = [
4054
- { type: 'management.cattle.io.feature', hideBulkActions: true },
4055
- ];
4056
-
4057
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4058
-
4059
- pluginProduct.apply(mockPlugin, mockStore);
4060
-
4061
- expect(mockDSL.hideBulkActions).toHaveBeenCalledWith('management.cattle.io.feature', true);
4062
- });
4063
- });
4064
-
4065
- describe('resource page: ordering - weight is set last', () => {
4066
- it('should call weightType after configureType for resource pages', () => {
4067
- const mockPlugin = createMockPlugin();
4068
- const mockStore = createMockStore();
4069
- const callOrder: string[] = [];
4070
- const mockDSL = {
4071
- product: jest.fn(),
4072
- basicType: jest.fn(),
4073
- labelGroup: jest.fn(),
4074
- setGroupDefaultType: jest.fn(),
4075
- weightGroup: jest.fn(),
4076
- virtualType: jest.fn(),
4077
- configureType: jest.fn(() => callOrder.push('configureType')),
4078
- weightType: jest.fn(() => callOrder.push('weightType')),
4079
- headers: jest.fn(() => callOrder.push('headers')),
4080
- hideBulkActions: jest.fn(() => callOrder.push('hideBulkActions')),
4081
-
4082
- mapGroup: jest.fn(),
4083
- ignoreGroup: jest.fn(),
4084
- mapType: jest.fn(() => callOrder.push('mapType')),
4085
- ignoreType: jest.fn(() => callOrder.push('ignoreType')),
4086
- };
4087
-
4088
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4089
-
4090
- const productMetadata: ProductMetadata = {
4091
- name: 'order-test',
4092
- label: 'Order Test',
4093
- };
4094
-
4095
- const config: ProductChildPage[] = [
4096
- {
4097
- type: 'apps.deployment',
4098
- weight: 10,
4099
- headers: [{ name: 'col1', label: 'Col1' }],
4100
- hideBulkActions: true,
4101
- overrideListResourceName: 'Deployments',
4102
- hideFromNav: true,
4103
- },
4104
- ];
4105
-
4106
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4107
-
4108
- pluginProduct.apply(mockPlugin, mockStore);
4109
-
4110
- // weight must be the last thing set
4111
- const weightIndex = callOrder.indexOf('weightType');
4112
- const configureIndex = callOrder.indexOf('configureType');
4113
-
4114
- expect(weightIndex).toBeGreaterThan(configureIndex);
4115
- expect(callOrder[callOrder.length - 1]).toBe('weightType');
4116
- });
4117
- });
4118
-
4119
- describe('product-level: renameGroups support', () => {
4120
- it('should call DSL mapGroup for each renameGroups entry in product metadata', () => {
4121
- const mockPlugin = createMockPlugin();
4122
- const mockStore = createMockStore();
4123
- const mockDSL = (mockPlugin.DSL as jest.Mock)();
4124
-
4125
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4126
-
4127
- const productMetadata: ProductMetadata = {
4128
- name: 'maptogroup-test',
4129
- label: 'MapToGroup Test',
4130
- renameGroups: [
4131
- { groupSelector: /some\.regex/, newName: 'my-group' },
4132
- { groupSelector: 'exact.match', newName: 'other-group' },
4133
- ],
4134
- };
4135
-
4136
- const config: ProductChildPage[] = [
4137
- {
4138
- name: 'overview', label: 'Overview', component: { name: 'OverviewPage' }
4139
- },
4140
- ];
4141
-
4142
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4143
-
4144
- pluginProduct.apply(mockPlugin, mockStore);
4145
-
4146
- expect(mockDSL.mapGroup).toHaveBeenCalledTimes(2);
4147
- expect(mockDSL.mapGroup).toHaveBeenCalledWith(/some\.regex/, 'my-group');
4148
- expect(mockDSL.mapGroup).toHaveBeenCalledWith('exact.match', 'other-group');
4149
- });
4150
-
4151
- it('should not call mapGroup when no renameGroups entries exist', () => {
4152
- const mockPlugin = createMockPlugin();
4153
- const mockStore = createMockStore();
4154
- const mockDSL = (mockPlugin.DSL as jest.Mock)();
4155
-
4156
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4157
-
4158
- const productMetadata: ProductMetadata = {
4159
- name: 'no-maptogroup',
4160
- label: 'No MapToGroup',
4161
- };
4162
-
4163
- const config: ProductChildPage[] = [
4164
- {
4165
- name: 'overview', label: 'Overview', component: { name: 'OverviewPage' }
4166
- },
4167
- ];
4168
-
4169
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4170
-
4171
- pluginProduct.apply(mockPlugin, mockStore);
4172
-
4173
- expect(mockDSL.mapGroup).not.toHaveBeenCalled();
4174
- });
4175
- });
4176
-
4177
- describe('product-level: ignoreGroups support', () => {
4178
- it('should call DSL ignoreGroup with callback when condition is provided', () => {
4179
- const mockPlugin = createMockPlugin();
4180
- const mockStore = createMockStore();
4181
- const mockDSL = (mockPlugin.DSL as jest.Mock)();
4182
-
4183
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4184
-
4185
- const cbFn = jest.fn(() => true);
4186
-
4187
- const productMetadata: ProductMetadata = {
4188
- name: 'ignoregroups-test',
4189
- label: 'IgnoreGroups Test',
4190
- ignoreGroups: [
4191
- { groupSelector: 'hidden-group', condition: cbFn },
4192
- ],
4193
- };
4194
-
4195
- const config: ProductChildPage[] = [
4196
- {
4197
- name: 'overview', label: 'Overview', component: { name: 'OverviewPage' }
4198
- },
4199
- ];
4200
-
4201
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4202
-
4203
- pluginProduct.apply(mockPlugin, mockStore);
4204
-
4205
- expect(mockDSL.ignoreGroup).toHaveBeenCalledTimes(1);
4206
- expect(mockDSL.ignoreGroup).toHaveBeenCalledWith('hidden-group', cbFn);
4207
- });
4208
-
4209
- it('should call DSL ignoreGroup without callback when condition is not provided (unconditional hide)', () => {
4210
- const mockPlugin = createMockPlugin();
4211
- const mockStore = createMockStore();
4212
- const mockDSL = (mockPlugin.DSL as jest.Mock)();
4213
-
4214
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4215
-
4216
- const productMetadata: ProductMetadata = {
4217
- name: 'ignoregroups-unconditional',
4218
- label: 'IgnoreGroups Unconditional',
4219
- ignoreGroups: [
4220
- { groupSelector: 'always-hidden' },
4221
- ],
4222
- };
4223
-
4224
- const config: ProductChildPage[] = [
4225
- {
4226
- name: 'overview', label: 'Overview', component: { name: 'OverviewPage' }
4227
- },
4228
- ];
4229
-
4230
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4231
-
4232
- pluginProduct.apply(mockPlugin, mockStore);
4233
-
4234
- expect(mockDSL.ignoreGroup).toHaveBeenCalledTimes(1);
4235
- expect(mockDSL.ignoreGroup).toHaveBeenCalledWith('always-hidden');
4236
- });
4237
-
4238
- it('should support regex patterns in ignoreGroups', () => {
4239
- const mockPlugin = createMockPlugin();
4240
- const mockStore = createMockStore();
4241
- const mockDSL = (mockPlugin.DSL as jest.Mock)();
4242
-
4243
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4244
-
4245
- const productMetadata: ProductMetadata = {
4246
- name: 'ignoregroups-regex',
4247
- label: 'IgnoreGroups Regex',
4248
- ignoreGroups: [
4249
- { groupSelector: /^internal-.*/ },
4250
- ],
4251
- };
4252
-
4253
- const config: ProductChildPage[] = [
4254
- {
4255
- name: 'overview', label: 'Overview', component: { name: 'OverviewPage' }
4256
- },
4257
- ];
4258
-
4259
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4260
-
4261
- pluginProduct.apply(mockPlugin, mockStore);
4262
-
4263
- expect(mockDSL.ignoreGroup).toHaveBeenCalledTimes(1);
4264
- expect(mockDSL.ignoreGroup).toHaveBeenCalledWith(/^internal-.*/);
4265
- });
4266
-
4267
- it('should not call ignoreGroup when no ignoreGroups entries exist', () => {
4268
- const mockPlugin = createMockPlugin();
4269
- const mockStore = createMockStore();
4270
- const mockDSL = (mockPlugin.DSL as jest.Mock)();
4271
-
4272
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4273
-
4274
- const productMetadata: ProductMetadata = {
4275
- name: 'no-ignoregroups',
4276
- label: 'No IgnoreGroups',
4277
- };
4278
-
4279
- const config: ProductChildPage[] = [
4280
- {
4281
- name: 'overview', label: 'Overview', component: { name: 'OverviewPage' }
4282
- },
4283
- ];
4284
-
4285
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4286
-
4287
- pluginProduct.apply(mockPlugin, mockStore);
4288
-
4289
- expect(mockDSL.ignoreGroup).not.toHaveBeenCalled();
4290
- });
4291
- });
4292
-
4293
- describe('product-level DSL options are not called when extending', () => {
4294
- it('should not call mapGroup, ignoreGroup, or moveType when extending an existing product', () => {
4295
- const mockPlugin = createMockPlugin();
4296
- const mockStore = createMockStore();
4297
- const mockDSL = (mockPlugin.DSL as jest.Mock)();
4298
-
4299
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4300
-
4301
- const config: ProductChildPage[] = [
4302
- {
4303
- name: 'overview', label: 'Overview', component: { name: 'OverviewPage' }
4304
- },
4305
- ];
4306
-
4307
- const pluginProduct = new PluginProduct(mockPlugin, StandardProductNames.EXPLORER, config);
4308
-
4309
- pluginProduct.apply(mockPlugin, mockStore);
4310
-
4311
- expect(mockDSL.mapGroup).not.toHaveBeenCalled();
4312
- expect(mockDSL.ignoreGroup).not.toHaveBeenCalled();
4313
- expect(mockDSL.moveType).not.toHaveBeenCalled();
4314
- });
4315
- });
4316
-
4317
- describe('product-level: moveToGroup support', () => {
4318
- it('should call basicType and moveType to move a resource type into a group', () => {
4319
- const mockPlugin = createMockPlugin();
4320
- const mockStore = createMockStore();
4321
- const mockDSL = (mockPlugin.DSL as jest.Mock)();
4322
-
4323
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4324
-
4325
- const monitoringGroup: ProductChildGroup = {
4326
- name: 'monitoring',
4327
- label: 'Monitoring',
4328
- children: [
4329
- {
4330
- name: 'alerts', label: 'Alerts', component: { name: 'AlertsPage' }
4331
- },
4332
- ],
4333
- };
4334
-
4335
- const productMetadata: ProductMetadata = {
4336
- name: 'my-app',
4337
- label: 'My App',
4338
- moveToGroup: [
4339
- { entryId: 'pod', groupName: 'monitoring' },
4340
- ],
4341
- };
4342
-
4343
- const config: ProductChild[] = [
4344
- monitoringGroup,
4345
- { type: 'pod' },
4346
- ];
4347
-
4348
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4349
-
4350
- pluginProduct.apply(mockPlugin, mockStore);
4351
-
4352
- // basicType re-registers the page under the resolved group for the nav tree
4353
- expect(mockDSL.basicType).toHaveBeenCalledWith(['pod'], 'myapp-monitoring');
4354
- // moveType also called for resource types (non-basic view modes)
4355
- expect(mockDSL.moveType).toHaveBeenCalledTimes(1);
4356
- expect(mockDSL.moveType).toHaveBeenCalledWith('pod', 'myapp-monitoring', undefined);
4357
- });
4358
-
4359
- it('should call basicType but NOT moveType when moving a custom page into a group', () => {
4360
- const mockPlugin = createMockPlugin();
4361
- const mockStore = createMockStore();
4362
- const mockDSL = (mockPlugin.DSL as jest.Mock)();
4363
-
4364
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4365
-
4366
- const monitoringGroup: ProductChildGroup = {
4367
- name: 'monitoring',
4368
- label: 'Monitoring',
4369
- children: [
4370
- {
4371
- name: 'alerts', label: 'Alerts', component: { name: 'AlertsPage' }
4372
- },
4373
- ],
4374
- };
4375
-
4376
- const customPage: ProductChildCustomPage = {
4377
- name: 'dashboard', label: 'Dashboard', component: { name: 'DashboardPage' }
4378
- };
4379
-
4380
- const productMetadata: ProductMetadata = {
4381
- name: 'my-app',
4382
- label: 'My App',
4383
- moveToGroup: [
4384
- { entryId: 'dashboard', groupName: 'monitoring' },
4385
- ],
4386
- };
4387
-
4388
- const config: ProductChild[] = [monitoringGroup, customPage];
4389
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4390
-
4391
- pluginProduct.apply(mockPlugin, mockStore);
4392
-
4393
- // basicType re-registers the custom page under the resolved group
4394
- expect(mockDSL.basicType).toHaveBeenCalledWith(['myapp-dashboard'], 'myapp-monitoring');
4395
- // moveType is NOT called for custom pages (no schema to match against)
4396
- expect(mockDSL.moveType).not.toHaveBeenCalled();
4397
- });
4398
-
4399
- it('should pass weight to DSL moveType when specified', () => {
4400
- const mockPlugin = createMockPlugin();
4401
- const mockStore = createMockStore();
4402
- const mockDSL = (mockPlugin.DSL as jest.Mock)();
4403
-
4404
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4405
-
4406
- const myGroup: ProductChildGroup = {
4407
- name: 'resources',
4408
- label: 'Resources',
4409
- children: [
4410
- {
4411
- name: 'overview', label: 'Overview', component: { name: 'OverviewPage' }
4412
- },
4413
- ],
4414
- };
4415
-
4416
- const productMetadata: ProductMetadata = {
4417
- name: 'my-app',
4418
- label: 'My App',
4419
- moveToGroup: [
4420
- {
4421
- entryId: 'apps.deployment', groupName: 'resources', weight: 10
4422
- },
4423
- ],
4424
- };
4425
-
4426
- const config: ProductChild[] = [myGroup, { type: 'apps.deployment' }];
4427
-
4428
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4429
-
4430
- pluginProduct.apply(mockPlugin, mockStore);
4431
-
4432
- expect(mockDSL.basicType).toHaveBeenCalledWith(['apps.deployment'], 'myapp-resources');
4433
- expect(mockDSL.moveType).toHaveBeenCalledWith('apps.deployment', 'myapp-resources', 10);
4434
- });
4435
-
4436
- it('should throw when moveToGroup references a groupName that does not exist in the config', () => {
4437
- const mockPlugin = createMockPlugin();
4438
- const mockStore = createMockStore();
4439
- const mockDSL = (mockPlugin.DSL as jest.Mock)();
4440
-
4441
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4442
-
4443
- const productMetadata: ProductMetadata = {
4444
- name: 'my-app',
4445
- label: 'My App',
4446
- moveToGroup: [
4447
- { entryId: 'pod', groupName: 'nonexistent-group' },
4448
- ],
4449
- };
4450
-
4451
- const config: ProductChildPage[] = [
4452
- { type: 'pod' },
4453
- ];
4454
-
4455
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4456
-
4457
- expect(() => {
4458
- pluginProduct.apply(mockPlugin, mockStore);
4459
- }).toThrow('moveToGroup target group "nonexistent-group" not found');
4460
- });
4461
-
4462
- it('should throw when moveToGroup entryId does not match any registered page', () => {
4463
- const mockPlugin = createMockPlugin();
4464
- const mockStore = createMockStore();
4465
- const mockDSL = (mockPlugin.DSL as jest.Mock)();
4466
-
4467
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4468
-
4469
- const myGroup: ProductChildGroup = {
4470
- name: 'monitoring',
4471
- label: 'Monitoring',
4472
- children: [
4473
- {
4474
- name: 'alerts', label: 'Alerts', component: { name: 'AlertsPage' }
4475
- },
4476
- ],
4477
- };
4478
-
4479
- const productMetadata: ProductMetadata = {
4480
- name: 'my-app',
4481
- label: 'My App',
4482
- moveToGroup: [
4483
- { entryId: 'nonexistent-page', groupName: 'monitoring' },
4484
- ],
4485
- };
4486
-
4487
- const config: ProductChild[] = [myGroup];
4488
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4489
-
4490
- expect(() => {
4491
- pluginProduct.apply(mockPlugin, mockStore);
4492
- }).toThrow('moveToGroup entryId "nonexistent-page" not found');
4493
- });
4494
-
4495
- it('should not call moveType when no moveToGroup entries exist', () => {
4496
- const mockPlugin = createMockPlugin();
4497
- const mockStore = createMockStore();
4498
- const mockDSL = (mockPlugin.DSL as jest.Mock)();
4499
-
4500
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4501
-
4502
- const productMetadata: ProductMetadata = {
4503
- name: 'no-move',
4504
- label: 'No Move',
4505
- };
4506
-
4507
- const config: ProductChildPage[] = [
4508
- {
4509
- name: 'overview', label: 'Overview', component: { name: 'OverviewPage' }
4510
- },
4511
- ];
4512
-
4513
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4514
-
4515
- pluginProduct.apply(mockPlugin, mockStore);
4516
-
4517
- expect(mockDSL.moveType).not.toHaveBeenCalled();
4518
- });
4519
-
4520
- it('should support multiple moveToGroup entries targeting different groups', () => {
4521
- const mockPlugin = createMockPlugin();
4522
- const mockStore = createMockStore();
4523
- const mockDSL = (mockPlugin.DSL as jest.Mock)();
4524
-
4525
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4526
-
4527
- const networkingGroup: ProductChildGroup = {
4528
- name: 'networking',
4529
- label: 'Networking',
4530
- children: [
4531
- {
4532
- name: 'net-overview', label: 'Overview', component: { name: 'NetOverview' }
4533
- },
4534
- ],
4535
- };
4536
-
4537
- const storageGroup: ProductChildGroup = {
4538
- name: 'storage',
4539
- label: 'Storage',
4540
- children: [
4541
- {
4542
- name: 'storage-overview', label: 'Overview', component: { name: 'StorageOverview' }
4543
- },
4544
- ],
4545
- };
4546
-
4547
- const productMetadata: ProductMetadata = {
4548
- name: 'my-app',
4549
- label: 'My App',
4550
- moveToGroup: [
4551
- { entryId: 'networking.ingress', groupName: 'networking' },
4552
- { entryId: 'storage.pvc', groupName: 'storage' },
4553
- ],
4554
- };
4555
-
4556
- const config: ProductChild[] = [
4557
- networkingGroup,
4558
- storageGroup,
4559
- { type: 'networking.ingress' },
4560
- { type: 'storage.pvc' },
4561
- ];
4562
-
4563
- const pluginProduct = new PluginProduct(mockPlugin, productMetadata, config);
4564
-
4565
- pluginProduct.apply(mockPlugin, mockStore);
4566
-
4567
- expect(mockDSL.basicType).toHaveBeenCalledWith(['networking.ingress'], 'myapp-networking');
4568
- expect(mockDSL.basicType).toHaveBeenCalledWith(['storage.pvc'], 'myapp-storage');
4569
- expect(mockDSL.moveType).toHaveBeenCalledTimes(2);
4570
- expect(mockDSL.moveType).toHaveBeenCalledWith('networking.ingress', 'myapp-networking', undefined);
4571
- expect(mockDSL.moveType).toHaveBeenCalledWith('storage.pvc', 'myapp-storage', undefined);
4572
- });
4573
- });
4574
-
4575
- describe('resource page DSL options work when extending a product', () => {
4576
- it('should support headers, hideBulkActions, overrideListResourceName, hideFromNav on resource pages when extending', () => {
4577
- const mockPlugin = createMockPlugin();
4578
- const mockStore = createMockStore();
4579
- const mockDSL = (mockPlugin.DSL as jest.Mock)();
4580
-
4581
- (mockPlugin.DSL as jest.Mock).mockReturnValue(mockDSL);
4582
-
4583
- const testHeaders = [{ name: 'col1', label: 'Column 1' }];
4584
-
4585
- const config: ProductChildPage[] = [
4586
- {
4587
- type: 'custom.resource.type',
4588
- headers: testHeaders,
4589
- hideBulkActions: true,
4590
- overrideListResourceName: 'Custom Name',
4591
- hideFromNav: true,
4592
- },
4593
- ];
4594
-
4595
- const pluginProduct = new PluginProduct(mockPlugin, StandardProductNames.EXPLORER, config);
4596
-
4597
- pluginProduct.apply(mockPlugin, mockStore);
4598
-
4599
- expect(mockDSL.headers).toHaveBeenCalledWith('custom.resource.type', testHeaders, undefined);
4600
- expect(mockDSL.hideBulkActions).toHaveBeenCalledWith('custom.resource.type', true);
4601
- expect(mockDSL.mapType).toHaveBeenCalledWith('custom.resource.type', 'Custom Name');
4602
- expect(mockDSL.ignoreType).toHaveBeenCalledWith('custom.resource.type');
4603
- });
4604
- });
4605
- });
4606
- });
4607
-
4608
- describe('addProduct duplicate guard', () => {
4609
- it('should throw when addProduct is called twice with the same product name (object form)', () => {
4610
- const plugin = new Plugin('test-extension');
4611
-
4612
- const product: ProductMetadata = {
4613
- name: 'my-product',
4614
- label: 'My Product',
4615
- };
4616
-
4617
- const config: ProductChildPage[] = [
4618
- {
4619
- name: 'page-a', label: 'Page A', component: { name: 'PageA' }
4620
- },
4621
- ];
4622
-
4623
- plugin.addProduct(product, config);
4624
-
4625
- expect(() => {
4626
- plugin.addProduct(product, [{
4627
- name: 'page-b', label: 'Page B', component: { name: 'PageB' }
4628
- }]);
4629
- }).toThrow('addProduct can only be called once per product');
4630
- });
4631
-
4632
- it('should throw when addProduct is called twice with the same product name (string form)', () => {
4633
- const plugin = new Plugin('test-extension');
4634
-
4635
- plugin.addProduct('my-product');
4636
-
4637
- expect(() => {
4638
- plugin.addProduct('my-product');
4639
- }).toThrow('addProduct can only be called once per product');
4640
- });
4641
-
4642
- it('should throw when addProduct is called twice mixing string and object form for the same name', () => {
4643
- const plugin = new Plugin('test-extension');
4644
-
4645
- plugin.addProduct('my-product');
4646
-
4647
- expect(() => {
4648
- plugin.addProduct({ name: 'my-product', label: 'My Product' }, []);
4649
- }).toThrow('addProduct can only be called once per product');
4650
- });
4651
-
4652
- it('should allow addProduct for different product names', () => {
4653
- const plugin = new Plugin('test-extension');
4654
-
4655
- plugin.addProduct('product-a');
4656
-
4657
- expect(() => {
4658
- plugin.addProduct('product-b');
4659
- }).not.toThrow();
4660
-
4661
- expect(plugin.productConfigs).toHaveLength(2);
4662
- });
4663
-
4664
- it('should allow addProduct and extendProduct for the same name (extending is separate)', () => {
4665
- const plugin = new Plugin('test-extension');
4666
-
4667
- plugin.addProduct('my-product');
4668
-
4669
- expect(() => {
4670
- plugin.extendProduct('explorer', [{
4671
- name: 'extra-page', label: 'Extra', component: { name: 'Extra' }
4672
- }]);
4673
- }).not.toThrow();
4674
-
4675
- expect(plugin.productConfigs).toHaveLength(2);
4676
- });
4677
-
4678
- it('should throw when addProduct is called twice with single page product form', () => {
4679
- const plugin = new Plugin('test-extension');
4680
-
4681
- const singlePage: ProductSinglePage = {
4682
- name: 'my-dashboard',
4683
- label: 'My Dashboard',
4684
- component: { name: 'DashboardPage' },
4685
- };
4686
-
4687
- plugin.addProduct(singlePage);
4688
-
4689
- expect(() => {
4690
- plugin.addProduct(singlePage);
4691
- }).toThrow('addProduct can only be called once per product');
4692
- });
4693
- });
4694
- });