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

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 (30) hide show
  1. package/apis/intf/shell-api/slide-in.ts +46 -17
  2. package/apis/shell/__tests__/slide-in.test.ts +83 -2
  3. package/apis/shell/slide-in.ts +20 -0
  4. package/components/ExplorerProjectsNamespaces.vue +2 -2
  5. package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/identifying-fields.test.ts +9 -0
  6. package/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields.ts +5 -1
  7. package/components/SlideInPanelManager.vue +103 -25
  8. package/components/__tests__/SlideInPanelManager.spec.ts +153 -10
  9. package/components/auth/AuthBanner.vue +1 -1
  10. package/core/plugins-loader.js +2 -0
  11. package/models/__tests__/namespace.test.ts +133 -8
  12. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +57 -1
  13. package/models/namespace.js +34 -2
  14. package/models/provisioning.cattle.io.cluster.js +20 -13
  15. package/package.json +1 -1
  16. package/pages/c/_cluster/explorer/workload-dashboard/__tests__/composable.test.ts +136 -1
  17. package/pages/c/_cluster/explorer/workload-dashboard/composable.ts +69 -1
  18. package/pages/c/_cluster/fleet/index.vue +5 -9
  19. package/pkg/vue.config.js +4 -3
  20. package/plugins/codemirror.js +2 -0
  21. package/plugins/dashboard-store/resource-class.js +3 -6
  22. package/store/__tests__/action-menu.test.ts +622 -0
  23. package/store/__tests__/slideInPanel.test.ts +143 -43
  24. package/store/__tests__/wm.test.ts +503 -0
  25. package/store/aws.js +19 -4
  26. package/store/slideInPanel.ts +15 -3
  27. package/types/shell/index.d.ts +26 -2
  28. package/utils/__tests__/fleet-appco.test.ts +23 -0
  29. package/utils/fleet-appco.ts +6 -1
  30. package/utils/sort.js +1 -1
@@ -0,0 +1,622 @@
1
+ import {
2
+ state,
3
+ getters,
4
+ mutations,
5
+ actions,
6
+ } from '@shell/store/action-menu';
7
+
8
+ describe('action-menu store', () => {
9
+ describe('state', () => {
10
+ it('returns initial default values', () => {
11
+ const s = state();
12
+
13
+ expect(s.show).toStrictEqual(false);
14
+ expect(s.resources).toStrictEqual([]);
15
+ expect(s.elem).toStrictEqual(null);
16
+ expect(s.event).toStrictEqual(null);
17
+ expect(s.showPromptRemove).toStrictEqual(false);
18
+ expect(s.showPromptRestore).toStrictEqual(false);
19
+ expect(s.showModal).toStrictEqual(false);
20
+ expect(s.performCallbackData).toStrictEqual(undefined);
21
+ expect(s.toRemove).toStrictEqual([]);
22
+ expect(s.toRestore).toStrictEqual([]);
23
+ expect(s.modalData).toStrictEqual({});
24
+ });
25
+
26
+ it('returns a fresh object on each call', () => {
27
+ const s1 = state();
28
+ const s2 = state();
29
+
30
+ expect(s1).not.toBe(s2);
31
+ expect(s1.toRemove).not.toBe(s2.toRemove);
32
+ });
33
+ });
34
+
35
+ describe('getters', () => {
36
+ it('showing returns state.show', () => {
37
+ expect(getters.showing({ show: true } as any)).toStrictEqual(true);
38
+ expect(getters.showing({ show: false } as any)).toStrictEqual(false);
39
+ });
40
+
41
+ it('elem returns state.elem', () => {
42
+ const elem = document.createElement('div');
43
+
44
+ expect(getters.elem({ elem } as any)).toBe(elem);
45
+ });
46
+
47
+ it('event returns state.event', () => {
48
+ const event = { type: 'click' };
49
+
50
+ expect(getters.event({ event } as any)).toBe(event);
51
+ });
52
+
53
+ it('resources returns state.resources', () => {
54
+ const resources = [{ id: 'r1' }];
55
+
56
+ expect(getters.resources({ resources } as any)).toBe(resources);
57
+ });
58
+
59
+ it('performCallbackData returns state.performCallbackData', () => {
60
+ const data = { foo: 'bar' };
61
+
62
+ expect(getters.performCallbackData({ performCallbackData: data } as any)).toBe(data);
63
+ });
64
+
65
+ describe('optionsArray', () => {
66
+ it('returns empty array when resources is null', () => {
67
+ const s = { resources: null } as any;
68
+
69
+ expect(getters.optionsArray(s)).toStrictEqual([]);
70
+ });
71
+
72
+ it('returns empty array when resources is not an array', () => {
73
+ const s = { resources: 'not-an-array' } as any;
74
+
75
+ expect(getters.optionsArray(s)).toStrictEqual([]);
76
+ });
77
+
78
+ it('returns empty array when resources is empty', () => {
79
+ const s = { resources: [] } as any;
80
+
81
+ expect(getters.optionsArray(s)).toStrictEqual([]);
82
+ });
83
+
84
+ it('returns empty array when no node has availableActions', () => {
85
+ const s = { resources: [{ name: 'r1' }, { name: 'r2' }] } as any;
86
+
87
+ expect(getters.optionsArray(s)).toStrictEqual([]);
88
+ });
89
+
90
+ it('returns empty array when node availableActions is empty', () => {
91
+ const s = { resources: [{ availableActions: [] }] } as any;
92
+
93
+ expect(getters.optionsArray(s)).toStrictEqual([]);
94
+ });
95
+
96
+ it('includes an enabled action from a single resource', () => {
97
+ const s = {
98
+ resources: [{
99
+ availableActions: [{
100
+ action: 'edit', label: 'Edit', enabled: true
101
+ }]
102
+ }],
103
+ } as any;
104
+
105
+ const result = getters.optionsArray(s);
106
+
107
+ expect(result).toHaveLength(1);
108
+ expect(result[0].action).toStrictEqual('edit');
109
+ expect(result[0].enabled).toStrictEqual(true);
110
+ expect(result[0].available).toStrictEqual(1);
111
+ expect(result[0].total).toStrictEqual(1);
112
+ });
113
+
114
+ it('excludes an action that is disabled for all resources', () => {
115
+ const s = {
116
+ resources: [{
117
+ availableActions: [{
118
+ action: 'delete', label: 'Delete', enabled: false
119
+ }]
120
+ }],
121
+ } as any;
122
+
123
+ expect(getters.optionsArray(s)).toStrictEqual([]);
124
+ });
125
+
126
+ it('includes an action with undefined enabled as enabled', () => {
127
+ const s = { resources: [{ availableActions: [{ action: 'view', label: 'View' }] }] } as any;
128
+
129
+ const result = getters.optionsArray(s);
130
+
131
+ expect(result).toHaveLength(1);
132
+ expect(result[0].enabled).toStrictEqual(true);
133
+ });
134
+
135
+ it('marks action enabled=false when available < total across multiple resources', () => {
136
+ const s = {
137
+ resources: [
138
+ {
139
+ availableActions: [{
140
+ action: 'edit', label: 'Edit', enabled: true
141
+ }]
142
+ },
143
+ {
144
+ availableActions: [{
145
+ action: 'edit', label: 'Edit', enabled: false
146
+ }]
147
+ },
148
+ ],
149
+ } as any;
150
+
151
+ const result = getters.optionsArray(s);
152
+
153
+ // available=1 (only first resource has it enabled), total=2 → enabled = 1>=2 = false
154
+ expect(result).toHaveLength(1);
155
+ expect(result[0].enabled).toStrictEqual(false);
156
+ expect(result[0].available).toStrictEqual(1);
157
+ expect(result[0].total).toStrictEqual(2);
158
+ });
159
+
160
+ it('marks action enabled=true when all resources have it enabled', () => {
161
+ const s = {
162
+ resources: [
163
+ { availableActions: [{ action: 'view', enabled: true }] },
164
+ { availableActions: [{ action: 'view', enabled: true }] },
165
+ ],
166
+ } as any;
167
+
168
+ const result = getters.optionsArray(s);
169
+
170
+ expect(result).toHaveLength(1);
171
+ expect(result[0].enabled).toStrictEqual(true);
172
+ expect(result[0].available).toStrictEqual(2);
173
+ expect(result[0].total).toStrictEqual(2);
174
+ });
175
+
176
+ it('merges actions from multiple resources into a single list', () => {
177
+ const s = {
178
+ resources: [
179
+ { availableActions: [{ action: 'edit', enabled: true }, { action: 'delete', enabled: true }] },
180
+ { availableActions: [{ action: 'edit', enabled: true }] },
181
+ ],
182
+ } as any;
183
+
184
+ const result = getters.optionsArray(s);
185
+
186
+ const actions = result.map((a: any) => a.action);
187
+
188
+ expect(actions).toContain('edit');
189
+ expect(actions).toContain('delete');
190
+ // delete only appears in first resource — available=1, total=1 (only added once from first resource)
191
+ const deleteAction = result.find((a: any) => a.action === 'delete');
192
+
193
+ expect(deleteAction.enabled).toStrictEqual(true);
194
+ });
195
+
196
+ it('skips nodes without availableActions property', () => {
197
+ const s = {
198
+ resources: [
199
+ { name: 'r1' },
200
+ { availableActions: [{ action: 'view', enabled: true }] },
201
+ ],
202
+ } as any;
203
+
204
+ const result = getters.optionsArray(s);
205
+
206
+ expect(result).toHaveLength(1);
207
+ expect(result[0].action).toStrictEqual('view');
208
+ });
209
+ });
210
+
211
+ describe('options', () => {
212
+ it('returns spread of optionsArray', () => {
213
+ const mockGetters = { optionsArray: [{ action: 'edit' }, { action: 'delete' }] } as any;
214
+ const result = getters.options(null as any, mockGetters);
215
+
216
+ expect(result).toStrictEqual({ 0: { action: 'edit' }, 1: { action: 'delete' } });
217
+ });
218
+
219
+ it('returns empty object for empty optionsArray', () => {
220
+ const mockGetters = { optionsArray: [] } as any;
221
+
222
+ expect(getters.options(null as any, mockGetters)).toStrictEqual({});
223
+ });
224
+ });
225
+ });
226
+
227
+ describe('mutations', () => {
228
+ describe('show', () => {
229
+ it('sets show=true, elem, event and wraps single resource in array', () => {
230
+ const s = state();
231
+ const resource = { id: 'r1' };
232
+ const elem = document.createElement('div');
233
+ const event = { type: 'click' };
234
+
235
+ mutations.show(s, {
236
+ resources: resource, elem, event,
237
+ });
238
+
239
+ expect(s.show).toStrictEqual(true);
240
+ expect(s.resources).toStrictEqual([resource]);
241
+ expect(s.elem).toBe(elem);
242
+ expect(s.event).toBe(event);
243
+ });
244
+
245
+ it('keeps array resources as-is', () => {
246
+ const s = state();
247
+ const resources = [{ id: 'r1' }, { id: 'r2' }];
248
+
249
+ mutations.show(s, {
250
+ resources, elem: null, event: null,
251
+ });
252
+
253
+ expect(s.resources).toBe(resources);
254
+ expect(s.show).toStrictEqual(true);
255
+ });
256
+ });
257
+
258
+ describe('hide', () => {
259
+ it('sets show=false, resources=null, elem=null', () => {
260
+ const s = state();
261
+
262
+ s.show = true;
263
+ s.resources = [{ id: 'r1' }] as any;
264
+ s.elem = document.createElement('div') as any;
265
+ mutations.hide(s);
266
+
267
+ expect(s.show).toStrictEqual(false);
268
+ expect(s.resources).toStrictEqual(null);
269
+ expect(s.elem).toStrictEqual(null);
270
+ });
271
+ });
272
+
273
+ describe('togglePromptRemove', () => {
274
+ it('sets showPromptRemove=false and toRemove=[] when resources is null', () => {
275
+ const s = state();
276
+
277
+ s.showPromptRemove = true;
278
+ mutations.togglePromptRemove(s, null as any);
279
+
280
+ expect(s.showPromptRemove).toStrictEqual(false);
281
+ expect(s.toRemove).toStrictEqual([]);
282
+ });
283
+
284
+ it('sets showPromptRemove=false and toRemove=[] when resources is undefined', () => {
285
+ const s = state();
286
+
287
+ s.showPromptRemove = true;
288
+ mutations.togglePromptRemove(s, undefined as any);
289
+
290
+ expect(s.showPromptRemove).toStrictEqual(false);
291
+ expect(s.toRemove).toStrictEqual([]);
292
+ });
293
+
294
+ it('toggles showPromptRemove to true and wraps single resource', () => {
295
+ const s = state();
296
+ const resource = { id: 'r1' };
297
+
298
+ mutations.togglePromptRemove(s, resource as any);
299
+
300
+ expect(s.showPromptRemove).toStrictEqual(true);
301
+ expect(s.toRemove).toStrictEqual([resource]);
302
+ });
303
+
304
+ it('toggles showPromptRemove back to false on second call with resources', () => {
305
+ const s = state();
306
+ const resource = { id: 'r1' };
307
+
308
+ mutations.togglePromptRemove(s, resource as any);
309
+ mutations.togglePromptRemove(s, resource as any);
310
+
311
+ expect(s.showPromptRemove).toStrictEqual(false);
312
+ });
313
+
314
+ it('keeps array resources without wrapping', () => {
315
+ const s = state();
316
+ const resources = [{ id: 'r1' }, { id: 'r2' }];
317
+
318
+ mutations.togglePromptRemove(s, resources as any);
319
+
320
+ expect(s.toRemove).toBe(resources);
321
+ });
322
+ });
323
+
324
+ describe('togglePromptRestore', () => {
325
+ it('sets showPromptRestore=false and toRestore=[] when resources is null', () => {
326
+ const s = state();
327
+
328
+ s.showPromptRestore = true;
329
+ mutations.togglePromptRestore(s, null as any);
330
+
331
+ expect(s.showPromptRestore).toStrictEqual(false);
332
+ expect(s.toRestore).toStrictEqual([]);
333
+ });
334
+
335
+ it('toggles showPromptRestore to true and wraps single resource', () => {
336
+ const s = state();
337
+ const resource = { id: 'r1' };
338
+
339
+ mutations.togglePromptRestore(s, resource as any);
340
+
341
+ expect(s.showPromptRestore).toStrictEqual(true);
342
+ expect(s.toRestore).toStrictEqual([resource]);
343
+ });
344
+
345
+ it('toggles showPromptRestore back to false on second call', () => {
346
+ const s = state();
347
+ const resource = { id: 'r1' };
348
+
349
+ mutations.togglePromptRestore(s, resource as any);
350
+ mutations.togglePromptRestore(s, resource as any);
351
+
352
+ expect(s.showPromptRestore).toStrictEqual(false);
353
+ });
354
+
355
+ it('keeps array resources without wrapping', () => {
356
+ const s = state();
357
+ const resources = [{ id: 'r1' }, { id: 'r2' }];
358
+
359
+ mutations.togglePromptRestore(s, resources as any);
360
+
361
+ expect(s.toRestore).toBe(resources);
362
+ });
363
+ });
364
+
365
+ describe('togglePromptModal', () => {
366
+ it('sets showModal=false and modalData=null when data is null', () => {
367
+ const s = state();
368
+
369
+ s.showModal = true;
370
+ mutations.togglePromptModal(s, null as any);
371
+
372
+ expect(s.showModal).toStrictEqual(false);
373
+ expect(s.modalData).toStrictEqual(null);
374
+ });
375
+
376
+ it('sets performCallbackData and showModal=false when data.performCallback is truthy', () => {
377
+ const s = state();
378
+ const data = { performCallback: true, action: 'confirm' };
379
+
380
+ mutations.togglePromptModal(s, data as any);
381
+
382
+ expect(s.performCallbackData).toBe(data);
383
+ expect(s.showModal).toStrictEqual(false);
384
+ expect(s.modalData).toBe(data);
385
+ });
386
+
387
+ it('sets showModal=true when data has no performCallback', () => {
388
+ const s = state();
389
+ const data = { title: 'Confirm', action: 'delete' };
390
+
391
+ mutations.togglePromptModal(s, data as any);
392
+
393
+ expect(s.showModal).toStrictEqual(true);
394
+ expect(s.modalData).toBe(data);
395
+ });
396
+
397
+ it('does not set performCallbackData when data has no performCallback', () => {
398
+ const s = state();
399
+ const prevCallback = { existing: true };
400
+
401
+ s.performCallbackData = prevCallback as any;
402
+ mutations.togglePromptModal(s, { title: 'Modal' } as any);
403
+
404
+ expect(s.performCallbackData).toBe(prevCallback);
405
+ });
406
+ });
407
+
408
+ describe('updateModalData', () => {
409
+ it('merges key/value pairs into existing modalData', () => {
410
+ const s = state();
411
+
412
+ s.modalData = { existing: 'value' };
413
+ mutations.updateModalData(s, [
414
+ { key: 'foo', value: 'bar' },
415
+ { key: 'num', value: 42 },
416
+ ]);
417
+
418
+ expect(s.modalData).toStrictEqual({
419
+ existing: 'value',
420
+ foo: 'bar',
421
+ num: 42,
422
+ });
423
+ });
424
+
425
+ it('creates modalData object when it is falsy before merging', () => {
426
+ const s = state();
427
+
428
+ s.modalData = null as any;
429
+ mutations.updateModalData(s, [{ key: 'x', value: 1 }]);
430
+
431
+ expect(s.modalData).toStrictEqual({ x: 1 });
432
+ });
433
+
434
+ it('overwrites existing key with new value', () => {
435
+ const s = state();
436
+
437
+ s.modalData = { key: 'old' };
438
+ mutations.updateModalData(s, [{ key: 'key', value: 'new' }]);
439
+
440
+ expect(s.modalData.key).toStrictEqual('new');
441
+ });
442
+ });
443
+
444
+ describe('clearCallbackData', () => {
445
+ it('sets performCallbackData to undefined', () => {
446
+ const s = state();
447
+
448
+ s.performCallbackData = { some: 'data' } as any;
449
+ mutations.clearCallbackData(s);
450
+
451
+ expect(s.performCallbackData).toStrictEqual(undefined);
452
+ });
453
+ });
454
+
455
+ describe('SET_RESOURCE', () => {
456
+ it('wraps a single non-array resource in an array', () => {
457
+ const s = state();
458
+ const resource = { id: 'r1' };
459
+
460
+ mutations.SET_RESOURCE(s, resource as any);
461
+
462
+ expect(s.resources).toStrictEqual([resource]);
463
+ });
464
+
465
+ it('keeps array resources as-is', () => {
466
+ const s = state();
467
+ const resources = [{ id: 'r1' }, { id: 'r2' }];
468
+
469
+ mutations.SET_RESOURCE(s, resources as any);
470
+
471
+ expect(s.resources).toBe(resources);
472
+ });
473
+ });
474
+ });
475
+
476
+ describe('actions', () => {
477
+ describe('execute', () => {
478
+ it('calls the action function on a single resource', async() => {
479
+ const editFn = jest.fn().mockResolvedValue('edit-result');
480
+ const resource = { edit: editFn };
481
+ const s = { resources: [resource] };
482
+ const action = { action: 'edit' };
483
+
484
+ await actions.execute({ state: s } as any, {
485
+ action,
486
+ args: ['arg1'],
487
+ opts: {},
488
+ });
489
+
490
+ expect(editFn).toHaveBeenCalledWith('arg1');
491
+ });
492
+
493
+ it('skips resources that do not have the action function', async() => {
494
+ const resource1 = { edit: jest.fn().mockResolvedValue('ok') };
495
+ const resource2 = {};
496
+ const s = { resources: [resource1, resource2] };
497
+
498
+ await actions.execute({ state: s } as any, {
499
+ action: { action: 'edit' },
500
+ args: [],
501
+ opts: {},
502
+ });
503
+
504
+ expect(resource1.edit).toHaveBeenCalledWith();
505
+ });
506
+
507
+ it('calls altAction when opts.alt is true and action.altAction is defined', async() => {
508
+ const normalFn = jest.fn();
509
+ const altFn = jest.fn().mockResolvedValue('alt-result');
510
+ const resource = {
511
+ edit: normalFn,
512
+ editAlt: altFn,
513
+ };
514
+ const s = { resources: [resource] };
515
+
516
+ await actions.execute({ state: s } as any, {
517
+ action: { action: 'edit', altAction: 'editAlt' },
518
+ args: [],
519
+ opts: { alt: true },
520
+ });
521
+
522
+ expect(altFn).toHaveBeenCalledWith();
523
+ expect(normalFn).not.toHaveBeenCalled();
524
+ });
525
+
526
+ it('uses altResource when action.altResource is defined', async() => {
527
+ const altEditFn = jest.fn().mockResolvedValue('alt-resource-result');
528
+ const altResource = { edit: altEditFn };
529
+ const resource = { edit: jest.fn() };
530
+ const s = { resources: [resource] };
531
+
532
+ await actions.execute({ state: s } as any, {
533
+ action: { action: 'edit', altResource },
534
+ args: [],
535
+ opts: {},
536
+ });
537
+
538
+ expect(altEditFn).toHaveBeenCalledWith();
539
+ expect(resource.edit).not.toHaveBeenCalled();
540
+ });
541
+
542
+ it('calls bulkAction on resources[0] when multiple resources and bulkAction defined', async() => {
543
+ const bulkFn = jest.fn().mockReturnValue('bulk-result');
544
+ const resource1 = { bulkDelete: bulkFn };
545
+ const resource2 = { bulkDelete: jest.fn() };
546
+ const s = { resources: [resource1, resource2] };
547
+
548
+ await actions.execute({ state: s } as any, {
549
+ action: { action: 'delete', bulkAction: 'bulkDelete' },
550
+ args: ['extra'],
551
+ opts: {},
552
+ });
553
+
554
+ expect(bulkFn).toHaveBeenCalledWith([resource1, resource2], 'extra');
555
+ });
556
+
557
+ it('falls back to per-resource loop when opts.alt is true even with bulkAction', async() => {
558
+ const normalFn = jest.fn().mockResolvedValue('ok');
559
+ const resource1 = { delete: normalFn };
560
+ const resource2 = { delete: normalFn };
561
+ const s = { resources: [resource1, resource2] };
562
+
563
+ await actions.execute({ state: s } as any, {
564
+ action: { action: 'delete', bulkAction: 'bulkDelete' },
565
+ args: [],
566
+ opts: { alt: true },
567
+ });
568
+
569
+ expect(normalFn).toHaveBeenCalledTimes(2);
570
+ });
571
+
572
+ it('defaults args to [] when not provided', async() => {
573
+ const editFn = jest.fn().mockResolvedValue('ok');
574
+ const resource = { edit: editFn };
575
+ const s = { resources: [resource] };
576
+
577
+ await actions.execute({ state: s } as any, {
578
+ action: { action: 'edit' },
579
+ args: null as any,
580
+ opts: {},
581
+ });
582
+
583
+ expect(editFn).toHaveBeenCalledWith();
584
+ });
585
+
586
+ it('calls actions on multiple resources and returns all results', async() => {
587
+ const res1 = { edit: jest.fn().mockResolvedValue('result-1') };
588
+ const res2 = { edit: jest.fn().mockResolvedValue('result-2') };
589
+ const s = { resources: [res1, res2] };
590
+
591
+ const result = await actions.execute({ state: s } as any, {
592
+ action: { action: 'edit' },
593
+ args: [],
594
+ opts: {},
595
+ });
596
+
597
+ expect(result).toStrictEqual(['result-1', 'result-2']);
598
+ });
599
+ });
600
+
601
+ describe('setResource', () => {
602
+ it('commits SET_RESOURCE with the resource', () => {
603
+ const commit = jest.fn();
604
+ const resource = { id: 'r1' };
605
+
606
+ actions.setResource({ commit } as any, resource as any);
607
+
608
+ expect(commit).toHaveBeenCalledWith('SET_RESOURCE', resource);
609
+ });
610
+ });
611
+
612
+ describe('clearCallbackData', () => {
613
+ it('commits clearCallbackData', () => {
614
+ const commit = jest.fn();
615
+
616
+ actions.clearCallbackData({ commit } as any);
617
+
618
+ expect(commit).toHaveBeenCalledWith('clearCallbackData');
619
+ });
620
+ });
621
+ });
622
+ });