@rancher/shell 0.4.0 → 0.5.0

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 (274) hide show
  1. package/assets/images/providers/ovhcloudmks.svg +122 -0
  2. package/assets/images/providers/ovhcloudpubliccloud.svg +122 -0
  3. package/assets/styles/global/_layout.scss +99 -0
  4. package/assets/translations/en-us.yaml +30 -5
  5. package/assets/translations/zh-hans.yaml +1 -1
  6. package/babel.config.js +7 -1
  7. package/chart/monitoring/alerting/index.vue +7 -21
  8. package/chart/monitoring/grafana/index.vue +55 -0
  9. package/chart/monitoring/index.vue +51 -17
  10. package/chart/monitoring/prometheus/index.vue +37 -43
  11. package/chart/rancher-backup/index.vue +2 -1
  12. package/cloud-credential/azure.vue +4 -17
  13. package/components/AsyncButton.vue +17 -5
  14. package/components/Certificates.vue +164 -0
  15. package/components/CodeMirror.vue +19 -21
  16. package/components/CruResource.vue +1 -0
  17. package/components/DraggableZone.vue +2 -2
  18. package/components/EtcdInfoBanner.vue +1 -1
  19. package/components/ExplorerProjectsNamespaces.vue +25 -1
  20. package/components/IconOrSvg.vue +1 -1
  21. package/components/LandingPagePreference.vue +1 -4
  22. package/components/PodSecurityAdmission.vue +2 -2
  23. package/components/Questions/index.vue +1 -1
  24. package/components/ResourceDetail/Masthead.vue +16 -3
  25. package/components/ResourceTable.vue +14 -2
  26. package/components/ResourceYaml.vue +5 -0
  27. package/components/SideNav.vue +1 -1
  28. package/components/SingleClusterInfo.vue +1 -4
  29. package/components/Tabbed/index.vue +12 -0
  30. package/components/fleet/FleetRepos.vue +62 -27
  31. package/components/fleet/FleetResources.vue +6 -1
  32. package/components/form/ArrayListSelect.vue +10 -0
  33. package/components/form/Error.vue +3 -3
  34. package/components/form/Footer.vue +2 -2
  35. package/components/form/GitPicker.vue +83 -38
  36. package/components/form/KeyValue.vue +4 -0
  37. package/components/form/LabeledSelect.vue +4 -0
  38. package/components/formatter/Checked.vue +11 -3
  39. package/components/formatter/FleetClusterSummaryGraph.vue +27 -0
  40. package/components/formatter/FleetSummaryGraph.vue +23 -11
  41. package/components/formatter/LiveDuration.vue +1 -1
  42. package/components/formatter/PercentageBar.vue +1 -1
  43. package/components/formatter/__tests__/Checked.test.ts +19 -0
  44. package/components/nav/Group.vue +2 -2
  45. package/components/nav/Header.vue +0 -1
  46. package/components/nav/TopLevelMenu.vue +36 -6
  47. package/components/nav/Type.vue +1 -3
  48. package/components/nav/WindowManager/ContainerLogs.vue +101 -3
  49. package/components/nav/WindowManager/ContainerShell.vue +6 -1
  50. package/components/nav/WindowManager/__tests__/ContainerLogs.test.ts +186 -0
  51. package/components/nav/WindowManager/index.vue +11 -10
  52. package/components/nav/__tests__/TopLevelMenu.test.ts +33 -0
  53. package/components/nav/__tests__/Type.test.ts +1 -1
  54. package/components/nuxt/nuxt-child.js +14 -78
  55. package/components/nuxt/nuxt.js +1 -1
  56. package/{layouts → components/templates}/blank.vue +1 -1
  57. package/{layouts → components/templates}/default.vue +8 -98
  58. package/{layouts → components/templates}/error.vue +10 -19
  59. package/{layouts → components/templates}/home.vue +4 -1
  60. package/{layouts → components/templates}/plain.vue +4 -1
  61. package/{layouts → components/templates}/standalone.vue +1 -1
  62. package/{layouts → components/templates}/unauthenticated.vue +1 -1
  63. package/composables/useCompactInput.test.ts +36 -0
  64. package/composables/useCompactInput.ts +20 -0
  65. package/composables/useLabeledFormElement.test.ts +135 -0
  66. package/composables/useLabeledFormElement.ts +138 -0
  67. package/config/harvester-manager-types.js +2 -0
  68. package/config/private-label.js +22 -0
  69. package/config/product/explorer.js +3 -0
  70. package/config/product/fleet.js +6 -1
  71. package/config/product/manager.js +8 -2
  72. package/config/query-params.js +1 -0
  73. package/config/router.js +385 -364
  74. package/config/settings.ts +1 -0
  75. package/config/store.js +1 -1
  76. package/config/system-namespaces.js +3 -0
  77. package/config/table-headers.js +47 -0
  78. package/core/plugin-routes.ts +56 -114
  79. package/core/plugin.ts +16 -10
  80. package/core/plugins-loader.js +7 -9
  81. package/core/plugins.js +0 -3
  82. package/creators/app/files/.gitlab-ci.yml +1 -1
  83. package/detail/fleet.cattle.io.cluster.vue +11 -1
  84. package/detail/provisioning.cattle.io.cluster.vue +4 -3
  85. package/dialog/ScaleMachineDownDialog.vue +34 -17
  86. package/edit/__tests__/service.test.ts +89 -0
  87. package/edit/auth/googleoauth.vue +1 -5
  88. package/edit/cloudcredential.vue +2 -0
  89. package/edit/configmap.vue +2 -1
  90. package/edit/management.cattle.io.podsecurityadmissionconfigurationtemplate.vue +2 -2
  91. package/edit/networking.k8s.io.networkpolicy/__tests__/PolicyRuleTarget.spec.ts +1 -1
  92. package/edit/provisioning.cattle.io.cluster/SelectCredential.vue +15 -7
  93. package/edit/provisioning.cattle.io.cluster/__tests__/Advanced.test.ts +112 -0
  94. package/edit/provisioning.cattle.io.cluster/__tests__/Basics.test.ts +473 -0
  95. package/edit/provisioning.cattle.io.cluster/__tests__/{CustomCommand.tests.ts → CustomCommand.test.ts} +4 -0
  96. package/edit/provisioning.cattle.io.cluster/__tests__/DrainOptions.test.ts +1 -1
  97. package/edit/provisioning.cattle.io.cluster/__tests__/index.test.ts +73 -0
  98. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +7 -1
  99. package/edit/provisioning.cattle.io.cluster/__tests__/utils/cluster.ts +386 -0
  100. package/edit/provisioning.cattle.io.cluster/import.vue +2 -2
  101. package/edit/provisioning.cattle.io.cluster/index.vue +92 -36
  102. package/edit/provisioning.cattle.io.cluster/rke2.vue +171 -583
  103. package/edit/provisioning.cattle.io.cluster/tabs/AddOnConfig.vue +137 -0
  104. package/edit/provisioning.cattle.io.cluster/tabs/Advanced.vue +157 -0
  105. package/edit/provisioning.cattle.io.cluster/{Basics.vue → tabs/Basics.vue} +94 -19
  106. package/edit/provisioning.cattle.io.cluster/{MachinePool.vue → tabs/MachinePool.vue} +1 -0
  107. package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +135 -0
  108. package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +189 -0
  109. package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +144 -0
  110. package/edit/provisioning.cattle.io.cluster/tabs/upgrade/index.vue +76 -0
  111. package/edit/service.vue +12 -0
  112. package/edit/workload/mixins/workload.js +1 -1
  113. package/initialize/App.js +25 -71
  114. package/initialize/client.js +21 -162
  115. package/initialize/index.js +27 -123
  116. package/list/management.cattle.io.feature.vue +1 -7
  117. package/list/node.vue +1 -0
  118. package/machine-config/__tests__/vmwarevsphere.test.ts +100 -21
  119. package/machine-config/vmwarevsphere.vue +73 -51
  120. package/middleware/authenticated.js +10 -17
  121. package/mixins/auth-config.js +2 -7
  122. package/mixins/brand.js +29 -41
  123. package/mixins/create-edit-view/index.js +2 -2
  124. package/mixins/labeled-form-element.ts +6 -1
  125. package/models/__tests__/management.cattle.io.node.ts +85 -0
  126. package/models/__tests__/management.cattle.io.nodepool.ts +83 -0
  127. package/models/__tests__/namespace.test.ts +49 -9
  128. package/models/__tests__/workload.test.ts +91 -0
  129. package/models/cluster/node.js +4 -4
  130. package/models/cluster.x-k8s.io.machinedeployment.js +14 -0
  131. package/models/fleet.cattle.io.cluster.js +4 -0
  132. package/models/fleet.cattle.io.gitrepo.js +56 -13
  133. package/models/management.cattle.io.kontainerdriver.js +1 -1
  134. package/models/management.cattle.io.node.js +18 -14
  135. package/models/management.cattle.io.nodepool.js +17 -0
  136. package/models/namespace.js +1 -1
  137. package/models/pod.js +20 -0
  138. package/models/provisioning.cattle.io.cluster.js +20 -3
  139. package/models/secret.js +117 -18
  140. package/models/workload.js +16 -0
  141. package/models/workload.service.js +18 -0
  142. package/package.json +10 -9
  143. package/pages/about.vue +0 -1
  144. package/pages/account/create-key.vue +0 -1
  145. package/pages/account/index.vue +0 -1
  146. package/pages/auth/login.vue +0 -1
  147. package/pages/auth/logout.vue +0 -2
  148. package/pages/auth/setup.vue +0 -4
  149. package/pages/auth/verify.vue +14 -8
  150. package/pages/c/_cluster/apps/charts/install.vue +4 -4
  151. package/pages/c/_cluster/apps/index.vue +0 -2
  152. package/pages/c/_cluster/auth/index.vue +0 -2
  153. package/pages/c/_cluster/ecm/index.vue +0 -2
  154. package/pages/c/_cluster/explorer/index.vue +28 -2
  155. package/pages/c/_cluster/fleet/index.vue +1 -1
  156. package/pages/c/_cluster/index.vue +0 -2
  157. package/pages/c/_cluster/settings/banners.vue +0 -2
  158. package/pages/c/_cluster/settings/brand.vue +0 -2
  159. package/pages/c/_cluster/settings/index.vue +0 -2
  160. package/pages/c/_cluster/settings/links.vue +0 -1
  161. package/pages/c/_cluster/settings/performance.vue +0 -1
  162. package/pages/c/_cluster/uiplugins/CatalogList/CatalogLoadDialog.vue +2 -1
  163. package/pages/c/_cluster/uiplugins/CatalogList/index.vue +10 -46
  164. package/pages/c/_cluster/uiplugins/index.vue +0 -2
  165. package/pages/diagnostic.vue +1 -2
  166. package/pages/fail-whale.vue +0 -1
  167. package/pages/prefs.vue +0 -1
  168. package/pages/support/index.vue +2 -8
  169. package/pkg/auto-import.js +1 -1
  170. package/plugins/axios.js +0 -36
  171. package/plugins/back-button.js +3 -5
  172. package/plugins/codemirror-loader.js +1 -1
  173. package/plugins/codemirror.js +41 -0
  174. package/plugins/dashboard-store/__tests__/{mutations.spec.ts → mutations.test.ts} +1 -1
  175. package/plugins/dashboard-store/__tests__/resource-class.test.ts +49 -0
  176. package/plugins/dashboard-store/__tests__/utils/store-mocks.ts +7 -0
  177. package/plugins/dashboard-store/actions.js +30 -4
  178. package/plugins/dashboard-store/classify.js +1 -18
  179. package/plugins/dashboard-store/getters.js +10 -5
  180. package/plugins/dashboard-store/index.js +0 -12
  181. package/plugins/dashboard-store/mutations.js +0 -4
  182. package/plugins/dashboard-store/resource-class.js +59 -18
  183. package/plugins/steve/__tests__/steve-class.spec.ts +59 -0
  184. package/plugins/steve/__tests__/utils/steve-mocks.ts +31 -0
  185. package/plugins/steve/getters.js +4 -1
  186. package/plugins/steve/norman-class.js +19 -0
  187. package/plugins/steve/steve-class.js +22 -0
  188. package/plugins/steve/subscribe.js +4 -10
  189. package/rancher-components/Accordion/Accordion.test.ts +45 -0
  190. package/rancher-components/Accordion/Accordion.vue +86 -0
  191. package/rancher-components/Accordion/index.ts +1 -0
  192. package/rancher-components/BadgeState/BadgeState.vue +3 -3
  193. package/rancher-components/Banner/Banner.vue +2 -2
  194. package/rancher-components/Card/Card.vue +3 -3
  195. package/rancher-components/Form/Checkbox/Checkbox.vue +3 -3
  196. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +18 -1
  197. package/rancher-components/Form/LabeledInput/LabeledInput.vue +65 -24
  198. package/rancher-components/Form/Radio/RadioButton.test.ts +7 -3
  199. package/rancher-components/Form/Radio/RadioButton.vue +13 -7
  200. package/rancher-components/Form/Radio/RadioGroup.test.ts +30 -0
  201. package/rancher-components/Form/Radio/RadioGroup.vue +8 -3
  202. package/rancher-components/Form/TextArea/TextAreaAutoGrow.vue +6 -4
  203. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +7 -4
  204. package/rancher-components/LabeledTooltip/LabeledTooltip.vue +9 -4
  205. package/rancher-components/StringList/StringList.test.ts +270 -0
  206. package/rancher-components/StringList/StringList.vue +65 -26
  207. package/rancher-components/components/Accordion/Accordion.test.ts +45 -0
  208. package/rancher-components/components/Accordion/Accordion.vue +86 -0
  209. package/rancher-components/components/Accordion/index.ts +1 -0
  210. package/rancher-components/components/BadgeState/BadgeState.vue +3 -3
  211. package/rancher-components/components/Banner/Banner.vue +2 -2
  212. package/rancher-components/components/Card/Card.vue +3 -3
  213. package/rancher-components/components/Form/Checkbox/Checkbox.vue +3 -3
  214. package/rancher-components/components/Form/LabeledInput/LabeledInput.test.ts +18 -1
  215. package/rancher-components/components/Form/LabeledInput/LabeledInput.vue +57 -24
  216. package/rancher-components/components/Form/Radio/RadioButton.vue +13 -7
  217. package/rancher-components/components/Form/Radio/RadioGroup.vue +4 -3
  218. package/rancher-components/components/Form/TextArea/TextAreaAutoGrow.vue +6 -4
  219. package/rancher-components/components/Form/ToggleSwitch/ToggleSwitch.vue +7 -4
  220. package/rancher-components/components/LabeledTooltip/LabeledTooltip.vue +9 -4
  221. package/rancher-components/components/StringList/StringList.vue +8 -8
  222. package/scripts/.gitlab/workflows/build-extension-catalog.gitlab-ci.yml +50 -0
  223. package/scripts/extension/parse-tag-name +2 -2
  224. package/scripts/publish-shell.sh +10 -0
  225. package/scripts/test-plugins-build.sh +85 -9
  226. package/server/har-file.js +183 -0
  227. package/store/catalog.js +1 -1
  228. package/store/features.js +1 -0
  229. package/store/i18n.js +11 -0
  230. package/store/index.js +10 -11
  231. package/store/prefs.js +33 -35
  232. package/store/type-map.js +8 -7
  233. package/tsconfig.json +35 -9
  234. package/tsconfig.paths.json +21 -0
  235. package/types/shell/index.d.ts +427 -234
  236. package/types/vue-shim.d.ts +42 -0
  237. package/utils/__tests__/create-yaml.test.ts +60 -0
  238. package/utils/axios.js +0 -19
  239. package/utils/azure.js +24 -0
  240. package/utils/create-yaml.js +17 -10
  241. package/utils/git.ts +1 -1
  242. package/utils/monitoring.js +1 -1
  243. package/utils/nuxt.js +18 -39
  244. package/utils/object.js +14 -0
  245. package/utils/router.scrollBehavior.js +12 -14
  246. package/utils/time.js +1 -1
  247. package/utils/url.ts +1 -1
  248. package/vue.config.js +23 -2
  249. package/.DS_Store +0 -0
  250. package/assets/images/providers/aks-black.svg +0 -28
  251. package/assets/images/providers/aks.svg +0 -31
  252. package/edit/provisioning.cattle.io.cluster/__tests__/Basics.tests.ts +0 -234
  253. package/initialize/layouts.ts +0 -26
  254. package/mixins/fetch.server.js +0 -73
  255. package/pages/c/index.vue +0 -9
  256. package/pages/rio/mesh.vue +0 -508
  257. package/plugins/transitions.js +0 -4
  258. package/scripts/.DS_Store +0 -0
  259. package/scripts/verdaccio.log +0 -205
  260. package/tsconfig.default.json +0 -46
  261. package/yarn-error.log +0 -200
  262. /package/components/form/__tests__/{NameNsDescription.ts → NameNsDescription.test.ts} +0 -0
  263. /package/edit/networking.k8s.io.networkpolicy/__tests__/utils/{selectors.ts → selectors.test.ts} +0 -0
  264. /package/edit/provisioning.cattle.io.cluster/{AgentConfiguration.vue → tabs/AgentConfiguration.vue} +0 -0
  265. /package/edit/provisioning.cattle.io.cluster/{MemberRoles.vue → tabs/MemberRoles.vue} +0 -0
  266. /package/edit/provisioning.cattle.io.cluster/{S3Config.vue → tabs/etcd/S3Config.vue} +0 -0
  267. /package/edit/provisioning.cattle.io.cluster/{ACE.vue → tabs/networking/ACE.vue} +0 -0
  268. /package/edit/provisioning.cattle.io.cluster/{RegistryConfigs.vue → tabs/registries/RegistryConfigs.vue} +0 -0
  269. /package/edit/provisioning.cattle.io.cluster/{RegistryMirrors.vue → tabs/registries/RegistryMirrors.vue} +0 -0
  270. /package/edit/provisioning.cattle.io.cluster/{DrainOptions.vue → tabs/upgrade/DrainOptions.vue} +0 -0
  271. /package/plugins/dashboard-store/__tests__/{actions.spec.ts → actions.test.ts} +0 -0
  272. /package/plugins/dashboard-store/__tests__/{getters.spec.ts → getters.test.ts} +0 -0
  273. /package/rancher-components/BadgeState/{BadgeState.spec.ts → BadgeState.test.ts} +0 -0
  274. /package/rancher-components/components/BadgeState/{BadgeState.spec.ts → BadgeState.test.ts} +0 -0
@@ -398,6 +398,276 @@ describe('stringList.vue', () => {
398
398
  });
399
399
  });
400
400
 
401
+ describe('bulk delimiter', () => {
402
+ const delimiter = /;/;
403
+
404
+ describe('add', () => {
405
+ const items: string[] = [];
406
+
407
+ beforeEach(() => {
408
+ wrapper = mount(StringList, {
409
+ propsData: {
410
+ items,
411
+ bulkAdditionDelimiter: delimiter,
412
+ errorMessages: { duplicate: 'error, item is duplicate.' },
413
+ }
414
+ });
415
+ });
416
+
417
+ it('should split values if delimiter set', async() => {
418
+ const value = 'test;test1;test2';
419
+ const result = ['test', 'test1', 'test2'];
420
+
421
+ // activate create mode
422
+ await wrapper.setData({ isCreateItem: true });
423
+ const inputField = wrapper.find('[data-testid="item-create"]');
424
+
425
+ await inputField.setValue(value);
426
+
427
+ // press enter
428
+ await inputField.trigger('keydown.enter');
429
+ await wrapper.vm.$nextTick();
430
+
431
+ const itemsResult = (wrapper.emitted('change') || [])[0][0];
432
+
433
+ expect(JSON.stringify(itemsResult)).toBe(JSON.stringify(result));
434
+ });
435
+
436
+ it('should show warning if one of the values is a duplicate', async() => {
437
+ const value = 'test;test1;test2';
438
+
439
+ await wrapper.setProps({ items: ['test1'] });
440
+
441
+ // activate create mode
442
+ await wrapper.setData({ isCreateItem: true });
443
+ const inputField = wrapper.find('[data-testid="item-create"]');
444
+
445
+ await inputField.setValue(value);
446
+
447
+ // press enter
448
+ await inputField.trigger('keydown.enter');
449
+ await wrapper.vm.$nextTick();
450
+
451
+ const isDuplicate = (wrapper.emitted('errors') || [])[0][0].duplicate;
452
+
453
+ expect(isDuplicate).toBe(true);
454
+ });
455
+
456
+ it('should show a warning if the new values are all duplicates', async() => {
457
+ const value = 'test;test';
458
+
459
+ // activate create mode
460
+ await wrapper.setData({ isCreateItem: true });
461
+ const inputField = wrapper.find('[data-testid="item-create"]');
462
+
463
+ await inputField.setValue(value);
464
+
465
+ // press enter
466
+ await inputField.trigger('keydown.enter');
467
+ await wrapper.vm.$nextTick();
468
+
469
+ const isDuplicate = (wrapper.emitted('errors') || [])[0][0].duplicate;
470
+
471
+ expect(isDuplicate).toBe(true);
472
+ });
473
+
474
+ it('should not consider empty strings at the beginning', async() => {
475
+ const value = ';test;test1;test2';
476
+ const result = ['test', 'test1', 'test2'];
477
+
478
+ // activate create mode
479
+ await wrapper.setData({ isCreateItem: true });
480
+ const inputField = wrapper.find('[data-testid="item-create"]');
481
+
482
+ await inputField.setValue(value);
483
+
484
+ // press enter
485
+ await inputField.trigger('keydown.enter');
486
+ await wrapper.vm.$nextTick();
487
+
488
+ const itemsResult = (wrapper.emitted('change') || [])[0][0];
489
+
490
+ expect(JSON.stringify(itemsResult)).toBe(JSON.stringify(result));
491
+ });
492
+
493
+ it('should not consider empty strings in the middle', async() => {
494
+ const value = 'test;test1;;test2';
495
+ const result = ['test', 'test1', 'test2'];
496
+
497
+ // activate create mode
498
+ await wrapper.setData({ isCreateItem: true });
499
+ const inputField = wrapper.find('[data-testid="item-create"]');
500
+
501
+ await inputField.setValue(value);
502
+
503
+ // press enter
504
+ await inputField.trigger('keydown.enter');
505
+ await wrapper.vm.$nextTick();
506
+
507
+ const itemsResult = (wrapper.emitted('change') || [])[0][0];
508
+
509
+ expect(JSON.stringify(itemsResult)).toBe(JSON.stringify(result));
510
+ });
511
+
512
+ it('should not consider empty strings at the end', async() => {
513
+ const value = 'test;test1;test2;';
514
+ const result = ['test', 'test1', 'test2'];
515
+
516
+ // activate create mode
517
+ await wrapper.setData({ isCreateItem: true });
518
+ const inputField = wrapper.find('[data-testid="item-create"]');
519
+
520
+ await inputField.setValue(value);
521
+
522
+ // press enter
523
+ await inputField.trigger('keydown.enter');
524
+ await wrapper.vm.$nextTick();
525
+
526
+ const itemsResult = (wrapper.emitted('change') || [])[0][0];
527
+
528
+ expect(JSON.stringify(itemsResult)).toBe(JSON.stringify(result));
529
+ });
530
+ });
531
+
532
+ describe('edit', () => {
533
+ const items = ['test1', 'test2'];
534
+
535
+ beforeEach(() => {
536
+ wrapper = mount(StringList, {
537
+ propsData: {
538
+ items,
539
+ bulkAdditionDelimiter: delimiter,
540
+ errorMessages: { duplicate: 'error, item is duplicate.' },
541
+ }
542
+ });
543
+ });
544
+
545
+ it('should split values if delimiter set', async() => {
546
+ const newValue = 'test1;new;values';
547
+ const result = ['test1', 'new', 'values', 'test2'];
548
+
549
+ await wrapper.setData({ editedItem: items[0] });
550
+ const inputField = wrapper.find('[data-testid^="item-edit"]');
551
+
552
+ await inputField.setValue(newValue);
553
+
554
+ // press enter
555
+ await inputField.trigger('keydown.enter');
556
+ await wrapper.vm.$nextTick();
557
+
558
+ const itemsResult = (wrapper.emitted('change') || [])[0][0];
559
+
560
+ expect(JSON.stringify(itemsResult)).toBe(JSON.stringify(result));
561
+ });
562
+
563
+ it('should show warning if one of the values is a duplicate', async() => {
564
+ const newValue = 'test1;test2';
565
+
566
+ await wrapper.setData({ editedItem: items[0] });
567
+ const inputField = wrapper.find('[data-testid^="item-edit"]');
568
+
569
+ await inputField.setValue(newValue);
570
+
571
+ // press enter
572
+ await inputField.trigger('keydown.enter');
573
+ await wrapper.vm.$nextTick();
574
+
575
+ const isDuplicate = (wrapper.emitted('errors') || [])[0][0].duplicate;
576
+
577
+ expect(isDuplicate).toBe(true);
578
+ });
579
+
580
+ it('should show a warning if the new values are all duplicates', async() => {
581
+ const newValue = 'test;test';
582
+
583
+ await wrapper.setData({ editedItem: items[0] });
584
+ const inputField = wrapper.find('[data-testid^="item-edit"]');
585
+
586
+ await inputField.setValue(newValue);
587
+
588
+ // press enter
589
+ await inputField.trigger('keydown.enter');
590
+ await wrapper.vm.$nextTick();
591
+
592
+ const isDuplicate = (wrapper.emitted('errors') || [])[0][0].duplicate;
593
+
594
+ expect(isDuplicate).toBe(true);
595
+ });
596
+
597
+ it('should not consider empty strings at the beginning', async() => {
598
+ const newValue = ';test1;new;value';
599
+ const result = ['test1', 'new', 'value', 'test2'];
600
+
601
+ await wrapper.setData({ editedItem: items[0] });
602
+ const inputField = wrapper.find('[data-testid^="item-edit"]');
603
+
604
+ await inputField.setValue(newValue);
605
+
606
+ // press enter
607
+ await inputField.trigger('keydown.enter');
608
+ await wrapper.vm.$nextTick();
609
+
610
+ const itemsResult = (wrapper.emitted('change') || [])[0][0];
611
+
612
+ expect(JSON.stringify(itemsResult)).toBe(JSON.stringify(result));
613
+ });
614
+
615
+ it('should not consider empty strings in the middle 1', async() => {
616
+ const newValue = 'test1; ;new;value';
617
+ const result = ['test1', 'new', 'value', 'test2'];
618
+
619
+ await wrapper.setData({ editedItem: items[0] });
620
+ const inputField = wrapper.find('[data-testid^="item-edit"]');
621
+
622
+ await inputField.setValue(newValue);
623
+
624
+ // press enter
625
+ await inputField.trigger('keydown.enter');
626
+ await wrapper.vm.$nextTick();
627
+
628
+ const itemsResult = (wrapper.emitted('change') || [])[0][0];
629
+
630
+ expect(JSON.stringify(itemsResult)).toBe(JSON.stringify(result));
631
+ });
632
+
633
+ it('should not consider empty strings in the middle 2', async() => {
634
+ const newValue = 'test1;;new;value';
635
+ const result = ['test1', 'new', 'value', 'test2'];
636
+
637
+ await wrapper.setData({ editedItem: items[0] });
638
+ const inputField = wrapper.find('[data-testid^="item-edit"]');
639
+
640
+ await inputField.setValue(newValue);
641
+
642
+ // press enter
643
+ await inputField.trigger('keydown.enter');
644
+ await wrapper.vm.$nextTick();
645
+
646
+ const itemsResult = (wrapper.emitted('change') || [])[0][0];
647
+
648
+ expect(JSON.stringify(itemsResult)).toBe(JSON.stringify(result));
649
+ });
650
+
651
+ it('should not consider empty strings at the end', async() => {
652
+ const newValue = 'test1;new;value;';
653
+ const result = ['test1', 'new', 'value', 'test2'];
654
+
655
+ await wrapper.setData({ editedItem: items[0] });
656
+ const inputField = wrapper.find('[data-testid^="item-edit"]');
657
+
658
+ await inputField.setValue(newValue);
659
+
660
+ // press enter
661
+ await inputField.trigger('keydown.enter');
662
+ await wrapper.vm.$nextTick();
663
+
664
+ const itemsResult = (wrapper.emitted('change') || [])[0][0];
665
+
666
+ expect(JSON.stringify(itemsResult)).toBe(JSON.stringify(result));
667
+ });
668
+ });
669
+ });
670
+
401
671
  describe('errors handling', () => {
402
672
  it('show duplicate warning icon when errorMessages is defined', async() => {
403
673
  const items = ['test'];
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import Vue, { PropType } from 'vue';
2
+ import Vue, { PropType, defineComponent } from 'vue';
3
3
 
4
4
  import LabeledInput from '@components/Form/LabeledInput/LabeledInput.vue';
5
5
  import { findStringIndex, hasDuplicatedStrings } from '@shell/utils/array';
@@ -29,7 +29,7 @@ const CLASS = {
29
29
  /**
30
30
  * Manage a list of strings
31
31
  */
32
- export default Vue.extend({
32
+ export default defineComponent({
33
33
 
34
34
  name: 'StringList',
35
35
  components: { LabeledInput },
@@ -82,12 +82,19 @@ export default Vue.extend({
82
82
  return {} as ErrorMessages;
83
83
  },
84
84
  },
85
+ /**
86
+ * Enables bulk addition and defines the delimiter to split the input string.
87
+ */
88
+ bulkAdditionDelimiter: {
89
+ type: RegExp,
90
+ default: null,
91
+ }
85
92
  },
86
93
  data() {
87
94
  return {
88
- value: null as string | null,
95
+ value: undefined as string | undefined,
89
96
  selected: null as string | null,
90
- editedItem: null as string | null,
97
+ editedItem: undefined as string | undefined,
91
98
  isCreateItem: false,
92
99
  errors: { duplicate: false } as Record<Error, boolean>
93
100
  };
@@ -125,13 +132,9 @@ export default Vue.extend({
125
132
  },
126
133
 
127
134
  methods: {
128
- onChange(value: string) {
135
+ onChange(value: string, index?: number) {
129
136
  this.value = value;
130
-
131
- const items = [
132
- ...this.items,
133
- this.value
134
- ];
137
+ const items = this.addValueToItems(this.items, value, index);
135
138
 
136
139
  this.toggleError(
137
140
  'duplicate',
@@ -278,7 +281,7 @@ export default Vue.extend({
278
281
  this.isCreateItem = true;
279
282
  this.setFocus(INPUT.create);
280
283
  } else {
281
- this.value = null;
284
+ this.value = undefined;
282
285
  this.toggleError('duplicate', false);
283
286
  this.onSelectLeave();
284
287
 
@@ -300,11 +303,11 @@ export default Vue.extend({
300
303
  this.editedItem = item || '';
301
304
  this.setFocus(INPUT.edit);
302
305
  } else {
303
- this.value = null;
306
+ this.value = undefined;
304
307
  this.toggleError('duplicate', false);
305
308
  this.onSelectLeave();
306
309
 
307
- this.editedItem = null;
310
+ this.editedItem = undefined;
308
311
  }
309
312
  },
310
313
 
@@ -321,10 +324,7 @@ export default Vue.extend({
321
324
  const value = this.value?.trim();
322
325
 
323
326
  if (value) {
324
- const items = [
325
- ...this.items,
326
- value,
327
- ];
327
+ const items = this.addValueToItems(this.items, value);
328
328
 
329
329
  if (!hasDuplicatedStrings(items, this.caseSensitive)) {
330
330
  this.updateItems(items);
@@ -343,12 +343,8 @@ export default Vue.extend({
343
343
  const value = this.value?.trim();
344
344
 
345
345
  if (value) {
346
- const items = [...this.items];
347
- const index = findStringIndex(items, item, false);
348
-
349
- if (index !== -1) {
350
- items[index] = value;
351
- }
346
+ const index = findStringIndex(this.items, item, false);
347
+ const items = index !== -1 ? this.addValueToItems(this.items, value, index) : this.items;
352
348
 
353
349
  if (!hasDuplicatedStrings(items, this.caseSensitive)) {
354
350
  this.updateItems(items);
@@ -360,6 +356,49 @@ export default Vue.extend({
360
356
  }
361
357
  },
362
358
 
359
+ /**
360
+ * Add a new or update an exiting item in the items list.
361
+ *
362
+ * @param items The current items list.
363
+ * @param value The new value to be added.
364
+ * @param index The list index of the item to be updated (optional).
365
+ * @returns Updated items list.
366
+ */
367
+ addValueToItems(items: string[], value: string, index?: number): string[] {
368
+ const updatedItems = [...items];
369
+
370
+ // Add new item
371
+ if (index === undefined) {
372
+ if (this.bulkAdditionDelimiter && value.search(this.bulkAdditionDelimiter) >= 0) {
373
+ updatedItems.push(...this.splitBulkValue(value));
374
+ } else {
375
+ updatedItems.push(value);
376
+ }
377
+ } else { // Update existing item
378
+ if (this.bulkAdditionDelimiter && value.search(this.bulkAdditionDelimiter) >= 0) {
379
+ updatedItems.splice(index, 1, ...this.splitBulkValue(value));
380
+ } else {
381
+ updatedItems[index] = value;
382
+ }
383
+ }
384
+
385
+ return updatedItems;
386
+ },
387
+
388
+ /**
389
+ * Split the value by the defined delimiter and remove empty strings.
390
+ *
391
+ * @param value The value to be split.
392
+ * @returns Array containing split values.
393
+ */
394
+ splitBulkValue(value: string): string[] {
395
+ return value
396
+ .split(this.bulkAdditionDelimiter)
397
+ .filter((item) => {
398
+ return item.trim().length > 0;
399
+ });
400
+ },
401
+
363
402
  /**
364
403
  * Remove an item from items list
365
404
  */
@@ -393,7 +432,7 @@ export default Vue.extend({
393
432
  @dblclick="onClickEmptyBody()"
394
433
  >
395
434
  <div
396
- v-for="item in items"
435
+ v-for="(item, index) in items"
397
436
  :key="item"
398
437
  :ref="item"
399
438
  :class="{
@@ -421,7 +460,7 @@ export default Vue.extend({
421
460
  :data-testid="`item-edit-${item}`"
422
461
  class="edit-input static"
423
462
  :value="value != null ? value : item"
424
- @input="onChange($event)"
463
+ @input="onChange($event, index)"
425
464
  @blur.prevent="updateItem(item)"
426
465
  @keydown.native.enter="updateItem(item, !errors.duplicate)"
427
466
  />
@@ -463,7 +502,7 @@ export default Vue.extend({
463
502
  <button
464
503
  data-testid="button-add"
465
504
  class="btn btn-sm role-tertiary add-button"
466
- :disabled="isCreateItem || editedItem"
505
+ :disabled="!!isCreateItem || !!editedItem"
467
506
  @click.prevent="onClickPlusButton"
468
507
  >
469
508
  <span class="icon icon-plus icon-sm" />
@@ -0,0 +1,45 @@
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import { Accordion } from './index';
3
+
4
+ describe('component: Accordion', () => {
5
+ it('is closed initially by default', () => {
6
+ const title = 'Test Title';
7
+
8
+ const wrapper = shallowMount(Accordion, { propsData: { title } });
9
+
10
+ expect(wrapper.find('[data-testid="accordion-body"]').isVisible()).toBe(false);
11
+ expect(wrapper.find('[data-testid="accordion-header"]').text()).toBe(title);
12
+ });
13
+
14
+ it('is opened initially when openInitially prop is true', () => {
15
+ const wrapper = shallowMount(Accordion, { propsData: { openInitially: true } });
16
+
17
+ expect(wrapper.find('[data-testid="accordion-body"]').isVisible()).toBe(true);
18
+ });
19
+
20
+ it('when closed, opens when the header is clicked', async() => {
21
+ const wrapper = shallowMount(Accordion, { });
22
+
23
+ await wrapper.find('[data-testid="accordion-header"]').trigger('click');
24
+ expect(wrapper.find('[data-testid="accordion-body"]').isVisible()).toBe(true);
25
+ });
26
+
27
+ it('when open, closes when the header is clicked', async() => {
28
+ const wrapper = shallowMount(Accordion, { propsData: { openInitially: true } });
29
+
30
+ await wrapper.find('[data-testid="accordion-header"]').trigger('click');
31
+ expect(wrapper.find('[data-testid="accordion-body"]').isVisible()).toBe(false);
32
+ });
33
+
34
+ it('displays a chevron when closed', async() => {
35
+ const wrapper = shallowMount(Accordion, { propsData: { } });
36
+
37
+ expect(wrapper.find('[data-testid="accordion-header"] .icon-chevron-up').exists()).toBe(true);
38
+ });
39
+
40
+ it('displays an inverted chevron when open', async() => {
41
+ const wrapper = shallowMount(Accordion, { propsData: { openInitially: true } });
42
+
43
+ expect(wrapper.find('[data-testid="accordion-header"] .icon-chevron-down').exists()).toBe(true);
44
+ });
45
+ });
@@ -0,0 +1,86 @@
1
+ <script lang="ts">
2
+ import { defineComponent } from 'vue';
3
+ import { mapGetters } from 'vuex';
4
+
5
+ export default defineComponent({
6
+ props: {
7
+ title: {
8
+ type: String,
9
+ default: ''
10
+ },
11
+
12
+ titleKey: {
13
+ type: String,
14
+ default: null
15
+ },
16
+
17
+ openInitially: {
18
+ type: Boolean,
19
+ default: false
20
+ }
21
+ },
22
+
23
+ data() {
24
+ return { isOpen: this.openInitially };
25
+ },
26
+
27
+ computed: { ...mapGetters({ t: 'i18n/t' }) },
28
+
29
+ methods: {
30
+ toggle() {
31
+ this.isOpen = !this.isOpen;
32
+ }
33
+ }
34
+ });
35
+ </script>
36
+
37
+ <template>
38
+ <div class="accordion-container">
39
+ <div
40
+ class="accordion-header"
41
+ data-testid="accordion-header"
42
+ @click="toggle"
43
+ >
44
+ <i
45
+ class="icon text-primary"
46
+ :class="{'icon-chevron-down':isOpen, 'icon-chevron-up':!isOpen}"
47
+ data-testid="accordion-chevron"
48
+ />
49
+ <slot name="header">
50
+ <h4
51
+ data-testid="accordion-title-slot-content"
52
+ class="mb-0"
53
+ >
54
+ {{ titleKey ? t(titleKey) : title }}
55
+ </h4>
56
+ </slot>
57
+ </div>
58
+ <div
59
+ v-show="isOpen"
60
+ class="accordion-body"
61
+ data-testid="accordion-body"
62
+ >
63
+ <slot />
64
+ </div>
65
+ </div>
66
+ </template>
67
+
68
+ <style lang="scss" scoped>
69
+ .accordion-container {
70
+ border: 1px solid var(--border)
71
+ }
72
+ .accordion-header {
73
+ padding: 5px;
74
+ display: flex;
75
+ align-items: center;
76
+ &>*{
77
+ padding: 5px 0px 5px 0px;
78
+ }
79
+ I {
80
+ margin: 0px 10px 0px 10px;
81
+ }
82
+ }
83
+ .accordion-body {
84
+ padding: 10px;
85
+ }
86
+ </style>
@@ -0,0 +1 @@
1
+ export { default as Accordion } from './Accordion.vue';
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import Vue, { PropType } from 'vue';
2
+ import { PropType, defineComponent } from 'vue';
3
3
 
4
4
  interface Badge {
5
5
  stateBackground: string;
@@ -11,7 +11,7 @@ interface Badge {
11
11
  * <p>Represents a badge whose label and color is either taken from the value property or
12
12
  * from the label and color properties. The state property takes precedence.</p>
13
13
  */
14
- export default Vue.extend({
14
+ export default defineComponent({
15
15
  props: {
16
16
  /**
17
17
  * A value having the properties `stateBackground` and `stateDisplay`
@@ -59,7 +59,7 @@ export default Vue.extend({
59
59
  </script>
60
60
 
61
61
  <template>
62
- <span :class="{'badge-state': true, [bg]: true}">
62
+ <span :class="['badge-state', bg]">
63
63
  <i
64
64
  v-if="icon"
65
65
  class="icon"
@@ -1,9 +1,9 @@
1
1
  <script lang="ts">
2
- import Vue from 'vue';
2
+ import { defineComponent } from 'vue';
3
3
  import { nlToBr } from '@shell/utils/string';
4
4
  import { stringify } from '@shell/utils/error';
5
5
 
6
- export default Vue.extend({
6
+ export default defineComponent({
7
7
  props: {
8
8
  /**
9
9
  * A color class that represents the color of the banner.
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
- import Vue from 'vue';
2
+ import { defineComponent, PropType } from 'vue';
3
3
 
4
- export default Vue.extend({
4
+ export default defineComponent({
5
5
  name: 'Card',
6
6
  props: {
7
7
  /**
@@ -22,7 +22,7 @@ export default Vue.extend({
22
22
  * The function to invoke when the default action button is clicked.
23
23
  */
24
24
  buttonAction: {
25
- type: Function,
25
+ type: Function as PropType<(event: MouseEvent) => void>,
26
26
  default: (): void => { }
27
27
  },
28
28
  /**
@@ -1,10 +1,10 @@
1
1
  <script lang="ts">
2
- import Vue, { PropType } from 'vue';
2
+ import { PropType, defineComponent } from 'vue';
3
3
  import { _EDIT, _VIEW } from '@shell/config/query-params';
4
4
  import { addObject, removeObject } from '@shell/utils/array';
5
5
  import cloneDeep from 'lodash/cloneDeep';
6
6
 
7
- export default Vue.extend({
7
+ export default defineComponent({
8
8
  name: 'Checkbox',
9
9
 
10
10
  props: {
@@ -140,7 +140,7 @@ export default Vue.extend({
140
140
  /**
141
141
  * Toggles the checked state for the checkbox and emits an 'input' event.
142
142
  */
143
- clicked(event: MouseEvent): boolean | void {
143
+ clicked(event: MouseEvent | KeyboardEvent): boolean | void {
144
144
  if ((event.target as HTMLLinkElement).tagName === 'A' && (event.target as HTMLLinkElement).href) {
145
145
  // Ignore links inside the checkbox label so you can click them
146
146
  return true;
@@ -1,4 +1,3 @@
1
-
2
1
  import { mount } from '@vue/test-utils';
3
2
  import { LabeledInput } from './index';
4
3
 
@@ -20,4 +19,22 @@ describe('component: LabeledInput', () => {
20
19
  expect(wrapper.emitted('input')).toHaveLength(1);
21
20
  expect(wrapper.emitted('input')![0][0]).toBe(value);
22
21
  });
22
+
23
+ it('using mode "multiline" should emit input value correctly', () => {
24
+ const value = 'any-string';
25
+ const delay = 1;
26
+ const wrapper = mount(LabeledInput as any, {
27
+ propsData: { delay, multiline: true },
28
+ mocks: { $store: { getters: { 'i18n/t': jest.fn() } } }
29
+ });
30
+
31
+ jest.useFakeTimers();
32
+ wrapper.find('input').setValue('1');
33
+ wrapper.find('input').setValue(value);
34
+ jest.advanceTimersByTime(delay);
35
+ jest.useRealTimers();
36
+
37
+ expect(wrapper.emitted('input')).toHaveLength(1);
38
+ expect(wrapper.emitted('input')![0][0]).toBe(value);
39
+ });
23
40
  });