@rancher/shell 3.0.5-rc.1 → 3.0.5-rc.3

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 (280) hide show
  1. package/assets/data/aws-regions.json +2 -0
  2. package/assets/images/providers/sks.svg +1 -0
  3. package/assets/styles/base/_helpers.scss +4 -0
  4. package/assets/styles/base/_variables.scss +1 -0
  5. package/assets/styles/global/_layout.scss +0 -1
  6. package/assets/translations/en-us.yaml +92 -34
  7. package/assets/translations/zh-hans.yaml +4 -13
  8. package/chart/monitoring/index.vue +4 -2
  9. package/components/ActionDropdownShell.vue +71 -0
  10. package/components/AppModal.vue +18 -4
  11. package/components/AsyncButton.vue +2 -0
  12. package/components/CodeMirror.vue +3 -3
  13. package/components/CommunityLinks.vue +3 -58
  14. package/components/CruResource.vue +109 -16
  15. package/components/ExplorerProjectsNamespaces.vue +19 -6
  16. package/components/FixedBanner.vue +19 -5
  17. package/components/GlobalRoleBindings.vue +5 -1
  18. package/components/GrowlManager.vue +1 -0
  19. package/components/LandingPagePreference.vue +2 -0
  20. package/components/LocaleSelector.vue +1 -1
  21. package/components/ModalManager.vue +55 -0
  22. package/components/PaginatedResourceTable.vue +7 -0
  23. package/components/PromptModal.vue +47 -8
  24. package/components/ResourceDetail/Masthead.vue +38 -13
  25. package/components/ResourceDetail/__tests__/Masthead.test.ts +5 -1
  26. package/components/ResourceDetail/index.vue +47 -12
  27. package/components/ResourceList/index.vue +2 -1
  28. package/components/ResourceTable.vue +54 -19
  29. package/components/SideNav.vue +5 -1
  30. package/components/SlideInPanelManager.vue +125 -0
  31. package/components/SortableTable/THead.vue +5 -2
  32. package/components/SortableTable/actions.js +1 -1
  33. package/components/SortableTable/index.vue +54 -40
  34. package/components/SortableTable/paging.js +16 -19
  35. package/components/SortableTable/selection.js +1 -12
  36. package/components/Tabbed/index.vue +6 -0
  37. package/components/Wizard.vue +2 -2
  38. package/components/__tests__/AsyncButton.test.ts +39 -0
  39. package/components/__tests__/CruResource.test.ts +63 -0
  40. package/components/__tests__/ModalManager.spec.ts +176 -0
  41. package/components/__tests__/PromptModal.test.ts +146 -0
  42. package/components/__tests__/SlideInPanelManager.spec.ts +166 -0
  43. package/components/auth/AuthBanner.vue +13 -11
  44. package/components/auth/Principal.vue +1 -0
  45. package/components/auth/login/ldap.vue +1 -1
  46. package/components/fleet/FleetResources.vue +21 -6
  47. package/components/form/ArrayList.vue +138 -118
  48. package/components/form/BannerSettings.vue +149 -85
  49. package/components/form/ColorInput.vue +35 -6
  50. package/components/form/EnvVars.vue +1 -0
  51. package/components/form/KeyValue.vue +10 -7
  52. package/components/form/LabeledSelect.vue +25 -23
  53. package/components/form/MatchExpressions.vue +9 -2
  54. package/components/form/NameNsDescription.vue +6 -2
  55. package/components/form/NotificationSettings.vue +15 -1
  56. package/components/form/Password.vue +1 -0
  57. package/components/form/Probe.vue +1 -0
  58. package/components/form/ResourceSelector.vue +26 -23
  59. package/components/form/ResourceTabs/index.vue +2 -1
  60. package/components/form/SSHKnownHosts/__tests__/KnownHostsEditDialog.test.ts +15 -34
  61. package/components/form/SSHKnownHosts/index.vue +14 -11
  62. package/components/form/Select.vue +8 -15
  63. package/components/form/UnitInput.vue +13 -0
  64. package/components/form/ValueFromResource.vue +12 -12
  65. package/components/form/__tests__/ArrayList.test.ts +34 -2
  66. package/components/form/__tests__/ColorInput.test.ts +35 -0
  67. package/components/form/__tests__/KeyValue.test.ts +36 -0
  68. package/components/form/__tests__/LabeledSelect.test.ts +73 -0
  69. package/components/form/__tests__/SSHKnownHosts.test.ts +11 -2
  70. package/components/form/__tests__/Select.test.ts +34 -1
  71. package/components/form/__tests__/UnitInput.test.ts +23 -1
  72. package/components/formatter/ClusterLink.vue +5 -8
  73. package/components/formatter/Description.vue +30 -0
  74. package/components/formatter/__tests__/ClusterLink.test.ts +2 -32
  75. package/components/nav/Group.vue +12 -4
  76. package/components/nav/Header.vue +16 -43
  77. package/components/nav/NamespaceFilter.vue +134 -86
  78. package/components/nav/TopLevelMenu.vue +4 -5
  79. package/components/nav/WindowManager/ContainerLogs.vue +87 -61
  80. package/components/nav/WindowManager/ContainerLogsActions.vue +76 -0
  81. package/components/nav/WindowManager/index.vue +1 -0
  82. package/components/templates/default.vue +6 -3
  83. package/components/templates/home.vue +6 -0
  84. package/components/templates/plain.vue +6 -3
  85. package/composables/focusTrap.ts +12 -4
  86. package/config/product/explorer.js +16 -13
  87. package/config/product/manager.js +1 -28
  88. package/config/settings.ts +11 -13
  89. package/config/store.js +4 -0
  90. package/config/table-headers.js +7 -5
  91. package/config/uiplugins.js +5 -1
  92. package/core/types.ts +7 -6
  93. package/detail/catalog.cattle.io.app.vue +5 -1
  94. package/detail/fleet.cattle.io.bundle.vue +70 -6
  95. package/detail/fleet.cattle.io.gitrepo.vue +1 -1
  96. package/detail/namespace.vue +0 -3
  97. package/detail/node.vue +17 -13
  98. package/detail/provisioning.cattle.io.cluster.vue +85 -9
  99. package/detail/service.vue +0 -1
  100. package/detail/workload/index.vue +21 -34
  101. package/dialog/AddCustomBadgeDialog.vue +0 -1
  102. package/{pages/c/_cluster/uiplugins/AddExtensionRepos.vue → dialog/AddExtensionReposDialog.vue} +72 -42
  103. package/dialog/AssignToDialog.vue +176 -0
  104. package/dialog/ChangePasswordDialog.vue +106 -0
  105. package/{pages/c/_cluster/uiplugins/DeveloperInstallDialog.vue → dialog/DeveloperLoadExtensionDialog.vue} +74 -71
  106. package/dialog/DisableAuthProviderDialog.vue +101 -0
  107. package/dialog/DrainNode.vue +1 -1
  108. package/{pages/c/_cluster/uiplugins/CatalogList/CatalogLoadDialog.vue → dialog/ExtensionCatalogInstallDialog.vue} +100 -88
  109. package/{pages/c/_cluster/uiplugins/CatalogList/CatalogUninstallDialog.vue → dialog/ExtensionCatalogUninstallDialog.vue} +83 -65
  110. package/dialog/FeatureFlagListDialog.vue +288 -0
  111. package/dialog/ForceMachineRemoveDialog.vue +1 -1
  112. package/{components/Import.vue → dialog/ImportDialog.vue} +0 -5
  113. package/{pages/c/_cluster/uiplugins/InstallDialog.vue → dialog/InstallExtensionDialog.vue} +124 -106
  114. package/{components/form/SSHKnownHosts → dialog}/KnownHostsEditDialog.vue +52 -62
  115. package/dialog/MoveNamespaceDialog.vue +157 -0
  116. package/dialog/ScalePoolDownDialog.vue +1 -1
  117. package/{components/nav/Jump.vue → dialog/SearchDialog.vue} +34 -14
  118. package/{pages/c/_cluster/uiplugins/UninstallDialog.vue → dialog/UninstallExtensionDialog.vue} +67 -58
  119. package/dialog/WechatDialog.vue +57 -0
  120. package/edit/__tests__/service.test.ts +2 -1
  121. package/edit/auth/azuread.vue +1 -1
  122. package/edit/auth/github.vue +1 -1
  123. package/edit/auth/googleoauth.vue +1 -1
  124. package/edit/auth/ldap/index.vue +1 -1
  125. package/edit/auth/oidc.vue +1 -1
  126. package/edit/auth/saml.vue +1 -1
  127. package/edit/cloudcredential.vue +24 -10
  128. package/edit/management.cattle.io.user.vue +28 -3
  129. package/edit/namespace.vue +1 -4
  130. package/edit/networking.k8s.io.networkpolicy/PolicyRule.vue +3 -14
  131. package/edit/networking.k8s.io.networkpolicy/PolicyRuleTarget.vue +57 -62
  132. package/edit/networking.k8s.io.networkpolicy/PolicyRules.vue +3 -14
  133. package/edit/networking.k8s.io.networkpolicy/__tests__/PolicyRuleTarget.test.ts +72 -41
  134. package/edit/networking.k8s.io.networkpolicy/__tests__/utils/mock.json +17 -1
  135. package/edit/networking.k8s.io.networkpolicy/index.vue +18 -30
  136. package/edit/provisioning.cattle.io.cluster/CustomCommand.vue +4 -1
  137. package/edit/provisioning.cattle.io.cluster/SelectCredential.vue +26 -10
  138. package/edit/provisioning.cattle.io.cluster/__tests__/Advanced.test.ts +8 -8
  139. package/edit/provisioning.cattle.io.cluster/__tests__/DirectoryConfig.test.ts +26 -12
  140. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +66 -0
  141. package/edit/provisioning.cattle.io.cluster/__tests__/utils/rke2-test-data.ts +58 -0
  142. package/edit/provisioning.cattle.io.cluster/index.vue +21 -73
  143. package/edit/provisioning.cattle.io.cluster/rke2.vue +24 -7
  144. package/edit/provisioning.cattle.io.cluster/tabs/DirectoryConfig.vue +5 -3
  145. package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +4 -1
  146. package/edit/service.vue +13 -28
  147. package/initialize/install-plugins.js +2 -1
  148. package/list/harvesterhci.io.management.cluster.vue +4 -1
  149. package/list/management.cattle.io.feature.vue +4 -288
  150. package/list/workload.vue +6 -1
  151. package/machine-config/azure.vue +16 -4
  152. package/mixins/resource-fetch-api-pagination.js +55 -43
  153. package/mixins/resource-fetch.js +14 -5
  154. package/mixins/vue-select-overrides.js +0 -4
  155. package/models/__tests__/workload.test.ts +1 -0
  156. package/models/cluster/node.js +1 -0
  157. package/models/cluster.js +32 -2
  158. package/models/fleet.cattle.io.cluster.js +8 -2
  159. package/models/fleet.cattle.io.gitrepo.js +8 -34
  160. package/models/management.cattle.io.cluster.js +0 -20
  161. package/models/management.cattle.io.feature.js +7 -1
  162. package/models/management.cattle.io.node.js +7 -22
  163. package/models/management.cattle.io.nodepool.js +12 -0
  164. package/models/namespace.js +12 -1
  165. package/models/provisioning.cattle.io.cluster.js +18 -64
  166. package/models/service.js +24 -9
  167. package/models/workload.js +70 -31
  168. package/package.json +1 -1
  169. package/pages/about.vue +13 -3
  170. package/pages/account/index.vue +12 -5
  171. package/pages/auth/login.vue +7 -4
  172. package/pages/auth/setup.vue +1 -0
  173. package/pages/auth/verify.vue +9 -7
  174. package/pages/c/_cluster/apps/charts/install.vue +25 -26
  175. package/pages/c/_cluster/auth/config/index.vue +10 -12
  176. package/pages/c/_cluster/explorer/EventsTable.vue +38 -33
  177. package/pages/c/_cluster/explorer/index.vue +28 -15
  178. package/pages/c/_cluster/istio/index.vue +2 -2
  179. package/pages/c/_cluster/longhorn/index.vue +3 -3
  180. package/pages/c/_cluster/monitoring/index.vue +1 -1
  181. package/pages/c/_cluster/monitoring/monitor/_namespace/_id.vue +4 -2
  182. package/pages/c/_cluster/monitoring/monitor/create.vue +4 -2
  183. package/pages/c/_cluster/monitoring/route-receiver/_id.vue +4 -2
  184. package/pages/c/_cluster/monitoring/route-receiver/create.vue +5 -2
  185. package/pages/c/_cluster/neuvector/index.vue +1 -1
  186. package/pages/c/_cluster/settings/banners.vue +60 -5
  187. package/pages/c/_cluster/settings/performance.vue +7 -26
  188. package/pages/c/_cluster/uiplugins/CatalogList/index.vue +8 -10
  189. package/pages/c/_cluster/uiplugins/__tests__/AddExtensionRepos.test.ts +4 -7
  190. package/pages/c/_cluster/uiplugins/index.vue +98 -55
  191. package/pages/diagnostic.vue +12 -9
  192. package/pages/fail-whale.vue +8 -5
  193. package/pages/home.vue +11 -52
  194. package/pages/prefs.vue +7 -6
  195. package/plugins/clean-html.js +2 -0
  196. package/plugins/dashboard-store/__tests__/actions.test.ts +4 -1
  197. package/plugins/dashboard-store/actions.js +122 -21
  198. package/plugins/dashboard-store/getters.js +74 -3
  199. package/plugins/dashboard-store/mutations.js +10 -5
  200. package/plugins/dashboard-store/resource-class.js +23 -3
  201. package/plugins/internal-api/index.ts +37 -0
  202. package/plugins/internal-api/shared/base-api.ts +13 -0
  203. package/plugins/internal-api/shell/shell.api.ts +108 -0
  204. package/plugins/steve/__tests__/getters.test.ts +18 -11
  205. package/plugins/steve/__tests__/steve-class.test.ts +1 -0
  206. package/plugins/steve/actions.js +34 -24
  207. package/plugins/steve/getters.js +39 -10
  208. package/plugins/steve/steve-class.js +5 -0
  209. package/plugins/steve/steve-pagination-utils.ts +199 -37
  210. package/plugins/steve/worker/web-worker.advanced.js +3 -1
  211. package/public/index.html +1 -0
  212. package/rancher-components/Banner/Banner.test.ts +51 -3
  213. package/rancher-components/Banner/Banner.vue +28 -6
  214. package/rancher-components/Card/Card.vue +1 -1
  215. package/rancher-components/Form/Checkbox/Checkbox.test.ts +59 -1
  216. package/rancher-components/Form/Checkbox/Checkbox.vue +27 -3
  217. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +51 -0
  218. package/rancher-components/Form/LabeledInput/LabeledInput.vue +20 -2
  219. package/rancher-components/Form/Radio/RadioButton.test.ts +36 -1
  220. package/rancher-components/Form/Radio/RadioButton.vue +20 -4
  221. package/rancher-components/Form/Radio/RadioGroup.test.ts +60 -0
  222. package/rancher-components/Form/Radio/RadioGroup.vue +75 -35
  223. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.test.ts +17 -0
  224. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +26 -1
  225. package/rancher-components/LabeledTooltip/LabeledTooltip.vue +10 -1
  226. package/rancher-components/RcButton/RcButton.vue +2 -1
  227. package/rancher-components/RcButton/types.ts +1 -0
  228. package/rancher-components/RcDropdown/RcDropdown.vue +17 -6
  229. package/rancher-components/RcDropdown/RcDropdownItem.vue +3 -56
  230. package/rancher-components/RcDropdown/RcDropdownItemCheckbox.vue +68 -0
  231. package/rancher-components/RcDropdown/RcDropdownItemSelect.vue +92 -0
  232. package/rancher-components/RcDropdown/index.ts +2 -0
  233. package/rancher-components/RcDropdown/useDropdownItem.ts +63 -0
  234. package/scripts/extension/bundle +20 -0
  235. package/scripts/extension/helm/charts/ui-plugin-server/templates/cr.yaml +2 -1
  236. package/scripts/extension/helm/charts/ui-plugin-server/values.yaml +2 -0
  237. package/scripts/extension/helmpatch +44 -31
  238. package/scripts/extension/publish +12 -13
  239. package/scripts/typegen.sh +2 -4
  240. package/store/action-menu.js +26 -56
  241. package/store/features.js +0 -1
  242. package/store/index.js +5 -0
  243. package/store/modal.ts +71 -0
  244. package/store/slideInPanel.ts +47 -0
  245. package/store/type-map.js +8 -1
  246. package/store/type-map.utils.ts +49 -6
  247. package/types/fleet.d.ts +1 -1
  248. package/types/global-vue.d.ts +5 -0
  249. package/types/internal-api/shell/growl.d.ts +25 -0
  250. package/types/internal-api/shell/modal.d.ts +77 -0
  251. package/types/internal-api/shell/slideIn.d.ts +15 -0
  252. package/types/kube/kube-api.ts +22 -0
  253. package/types/resources/fleet.d.ts +0 -14
  254. package/types/resources/settings.d.ts +0 -4
  255. package/types/shell/index.d.ts +375 -306
  256. package/types/store/dashboard-store.types.ts +24 -1
  257. package/types/store/pagination.types.ts +19 -2
  258. package/types/vue-shim.d.ts +4 -1
  259. package/utils/__mocks__/tabbable.js +13 -0
  260. package/utils/__tests__/object.test.ts +38 -4
  261. package/utils/cluster.js +24 -20
  262. package/utils/fleet.ts +15 -73
  263. package/utils/grafana.js +1 -0
  264. package/utils/object.js +36 -5
  265. package/utils/pagination-utils.ts +6 -2
  266. package/utils/perf-setting.utils.ts +28 -0
  267. package/utils/selector-typed.ts +205 -0
  268. package/utils/selector.js +29 -6
  269. package/utils/uiplugins.ts +10 -6
  270. package/utils/v-sphere.ts +5 -1
  271. package/utils/validators/formRules/__tests__/index.test.ts +10 -1
  272. package/utils/validators/formRules/index.ts +27 -3
  273. package/components/AssignTo.vue +0 -199
  274. package/components/DisableAuthProviderModal.vue +0 -115
  275. package/components/MoveModal.vue +0 -167
  276. package/components/PromptChangePassword.vue +0 -123
  277. package/components/fleet/FleetBundleResources.vue +0 -86
  278. package/components/formatter/RKETemplateName.vue +0 -37
  279. package/dialog/SaveAsRKETemplateDialog.vue +0 -139
  280. package/types/vue-shim.d +0 -20
@@ -49,6 +49,21 @@ export default {
49
49
  return this.$store.getters['i18n/t'](this.pagingLabel, opt);
50
50
  },
51
51
 
52
+ perPage() {
53
+ let out = this.rowsPerPage || 0;
54
+
55
+ if ( out <= 0 ) {
56
+ out = parseInt(this.$store.getters['prefs/get'](ROWS_PER_PAGE), 10) || 0;
57
+ }
58
+
59
+ // This should ideally never happen, but the preference value could be invalid, so return something...
60
+ if ( out <= 0 ) {
61
+ out = 10;
62
+ }
63
+
64
+ return out;
65
+ },
66
+
52
67
  pagedRows() {
53
68
  if (this.externalPaginationEnabled) {
54
69
  return this.rows;
@@ -61,9 +76,7 @@ export default {
61
76
  },
62
77
 
63
78
  data() {
64
- const perPage = this.getPerPage();
65
-
66
- return { page: 1, perPage };
79
+ return { page: 1 };
67
80
  },
68
81
 
69
82
  watch: {
@@ -89,22 +102,6 @@ export default {
89
102
  },
90
103
 
91
104
  methods: {
92
- getPerPage() {
93
- // perPage can not change while the list is displayed
94
- let out = this.rowsPerPage || 0;
95
-
96
- if ( out <= 0 ) {
97
- out = parseInt(this.$store.getters['prefs/get'](ROWS_PER_PAGE), 10) || 0;
98
- }
99
-
100
- // This should ideally never happen, but the preference value could be invalid, so return something...
101
- if ( out <= 0 ) {
102
- out = 10;
103
- }
104
-
105
- return out;
106
- },
107
-
108
105
  setPage(num) {
109
106
  if (this.page === num) {
110
107
  return;
@@ -340,17 +340,6 @@ export default {
340
340
  if ( !isSelected ) {
341
341
  this.update([node], this.selectedRows.slice());
342
342
  }
343
-
344
- let resources = this.selectedRows;
345
-
346
- if ( this.mangleActionResources ) {
347
- resources = await this.mangleActionResources(resources);
348
- }
349
-
350
- this.$store.commit(`action-menu/show`, {
351
- resources,
352
- event: e,
353
- });
354
343
  },
355
344
 
356
345
  keySelectRow(row, more = false) {
@@ -542,7 +531,7 @@ export default {
542
531
  },
543
532
 
544
533
  clearSelection() {
545
- this.update([], this.selectedRows);
534
+ this.update([], [...this.selectedRows]);
546
535
  },
547
536
 
548
537
  }
@@ -61,6 +61,11 @@ export default {
61
61
  tabsOnly: {
62
62
  type: Boolean,
63
63
  default: false,
64
+ },
65
+
66
+ resource: {
67
+ type: Object,
68
+ default: () => {}
64
69
  }
65
70
  },
66
71
 
@@ -355,6 +360,7 @@ export default {
355
360
  >
356
361
  <component
357
362
  :is="tab.component"
363
+ :resource="resource"
358
364
  />
359
365
  </Tab>
360
366
  </div>
@@ -426,7 +426,7 @@ export default {
426
426
  </div>
427
427
  <div
428
428
  id="wizard-footer-controls"
429
- class="controls-row pt-20"
429
+ class="controls-row"
430
430
  >
431
431
  <slot
432
432
  name="cancel"
@@ -674,7 +674,7 @@ $spacer: 10px;
674
674
  // We have to account for the absolute position of the .controls-row
675
675
  .footer-error {
676
676
  margin-top: -40px;
677
- margin-bottom: 70px;
677
+ margin-bottom: calc($footer-height + 10px);
678
678
  }
679
679
 
680
680
  .controls-row {
@@ -145,4 +145,43 @@ describe('component: AsyncButton', () => {
145
145
  expect(spyDone).toHaveBeenCalledWith('cancelled');
146
146
  expect(wrapper.vm.phase).toBe(ASYNC_BUTTON_STATES.ACTION);
147
147
  });
148
+
149
+ it('a11y: adding ARIA props should correctly fill out the appropriate fields on the component', () => {
150
+ const mockExists = jest.fn().mockReturnValue(true);
151
+ const mockT = jest.fn().mockReturnValue('some-string');
152
+ const ariaLabel = 'some-aria-label';
153
+ const ariaLabelledBy = 'some-aria-labelledby';
154
+
155
+ const wrapper: VueWrapper<InstanceType<typeof AsyncButton>> = mount(AsyncButton, {
156
+ props: { icon: 'some-icon', disabled: true },
157
+ attrs: { 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy },
158
+ global: {
159
+ mocks: {
160
+ $store: {
161
+ getters: {
162
+ 'i18n/exists': mockExists,
163
+ 'i18n/t': mockT
164
+ }
165
+ },
166
+ }
167
+ },
168
+ });
169
+
170
+ const item = wrapper.find('button');
171
+
172
+ const itemRole = item.attributes('role');
173
+ const itemAriaLabel = item.attributes('aria-label');
174
+ const itemAriaLabelledBy = item.attributes('aria-labelledby');
175
+ const itemAriaDisabled = item.attributes('aria-disabled');
176
+
177
+ // let's check some attributes passing...
178
+ expect(itemAriaLabel).toBe(ariaLabel);
179
+ expect(itemAriaLabelledBy).toBe(ariaLabelledBy);
180
+
181
+ // rest of the checks
182
+ expect(itemRole).toBe('button');
183
+ expect(itemAriaDisabled).toBe('true');
184
+ expect(item.find('span[data-testid="async-btn-display-label"]').attributes('id')).toBe(wrapper.vm.describedbyId);
185
+ expect(item.find('i').attributes('alt')).toBeDefined();
186
+ });
148
187
  });
@@ -68,6 +68,69 @@ describe('component: CruResource', () => {
68
68
  expect(node.text()).toContain(errors[1]);
69
69
  });
70
70
 
71
+ it.each([
72
+ ['error', 'error'],
73
+ [{
74
+ code: 'ActionNotAvailable', status: 500, message: 'error'
75
+ }, 'errors.actionNotAvailable'],
76
+ [{
77
+ status: 422, fieldName: 'Name', code: 'NotUnique', message: 'error'
78
+ }, 'errors.failedInApi.withName.withCodeExplanation.withMessageDetail'],
79
+ [{
80
+ status: 422, fieldName: 'Name', code: 'NotUnique'
81
+ }, 'errors.failedInApi.withName.withCodeExplanation.withoutMessageDetail'],
82
+ [{
83
+ status: 422, fieldName: 'Name', code: 'Brr', message: 'error'
84
+ }, 'errors.failedInApi.withName.withMessageDetail'],
85
+ [{
86
+ status: 422, fieldName: 'Name', code: 'Brr'
87
+ }, 'errors.failedInApi.withName.withoutAnythingElse'],
88
+ [{
89
+ status: 422, code: 'NotUnique', message: 'error'
90
+ }, 'errors.failedInApi.withoutName.withMessageDetail.withCodeExplanation'],
91
+ [{ status: 422, message: 'error' }, 'errors.failedInApi.withoutName.withMessageDetail.withoutCodeExplanation'],
92
+ [{ status: 422, code: 'NotUnique' }, 'errors.failedInApi.withoutName.withCode.withCodeExplanation'],
93
+ [{ status: 422, code: 'Brr' }, 'errors.failedInApi.withoutName.withCode.withoutCodeExplanation'],
94
+ [{ status: 422 }, 'errors.failedInApi.withoutAnything'],
95
+ [{ status: 404, message: 'message' }, 'errors.notFound.withoutUrl'],
96
+ [{
97
+ status: 404, message: 'message', opt: { url: 'test' }
98
+ }, 'errors.notFound.withUrl'],
99
+ [{ status: 500, message: 'message' }, 'errors.messageOrDetail'],
100
+ [{
101
+ status: 500, message: 'message', detail: 'detail'
102
+ }, 'errors.messageAndDetail'],
103
+ [{ status: 500, detail: 'detail' }, 'errors.messageOrDetail'],
104
+ ])('should display correct error', (err, res) => {
105
+ const wrapper = mount(CruResource, {
106
+ props: {
107
+ canYaml: false,
108
+ mode: _EDIT,
109
+ resource: {},
110
+ errors: [err]
111
+ },
112
+ global: {
113
+ mocks: {
114
+ $store: {
115
+ getters: {
116
+ currentStore: () => 'current_store',
117
+ 'current_store/schemaFor': jest.fn(),
118
+ 'current_store/all': jest.fn(),
119
+ 'i18n/t': (text: string) => text,
120
+ 'i18n/exists': jest.fn(),
121
+ },
122
+ dispatch: jest.fn(),
123
+ },
124
+ $route: { query: { AS: _YAML } },
125
+ $router: { applyQuery: jest.fn() },
126
+ },
127
+ }
128
+ });
129
+ const node = wrapper.find('#cru-errors');
130
+
131
+ expect(node.text()).toContain(res);
132
+ });
133
+
71
134
  it('should prevent default events on keypress Enter', async() => {
72
135
  const event = { preventDefault: jest.fn() };
73
136
  const wrapper = mount(CruResource, {
@@ -0,0 +1,176 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import { createStore, Store } from 'vuex';
3
+ import { nextTick } from 'vue';
4
+
5
+ import ModalManager from '@shell/components/ModalManager.vue';
6
+
7
+ interface ModalManagerMethods {
8
+ registerBackgroundClosing(fn: Function): void;
9
+ close(): void;
10
+ }
11
+
12
+ const MockComponent = {
13
+ template: '<div data-testid="modal-manager-component">Mock Content</div>',
14
+ props: ['someProp', 'resources', 'registerBackgroundClosing']
15
+ };
16
+
17
+ describe('modalManager.vue with Teleport', () => {
18
+ let store: Store<any>;
19
+ let getters: Record<string, () => any>;
20
+ let modalsDiv: HTMLDivElement;
21
+
22
+ beforeEach(() => {
23
+ // Create the teleport target container
24
+ modalsDiv = document.createElement('div');
25
+ modalsDiv.setAttribute('id', 'modals');
26
+ document.body.appendChild(modalsDiv);
27
+
28
+ getters = {
29
+ 'modal/isOpen': () => true,
30
+ 'modal/component': () => MockComponent,
31
+ 'modal/componentProps': () => ({ someProp: 'testValue' }),
32
+ 'modal/resources': () => ({ data: 'mockData' }),
33
+ 'modal/closeOnClickOutside': () => true,
34
+ 'modal/modalWidth': () => '500px'
35
+ };
36
+
37
+ store = createStore({
38
+ getters,
39
+ mutations: { 'modal/closeModal': jest.fn() }
40
+ });
41
+ });
42
+
43
+ afterEach(() => {
44
+ // Clean up the teleport container after each test
45
+ document.body.removeChild(modalsDiv);
46
+ });
47
+
48
+ const factory = () => {
49
+ return mount(ModalManager, {
50
+ attachTo: document.body, // attach so Teleport can work properly
51
+ global: {
52
+ plugins: [store],
53
+ stubs: {
54
+ AppModal: {
55
+ name: 'AppModal',
56
+ template: `<div data-testid="app-modal" @close="$emit('close')" :style="{ '--prompt-modal-width': width }"><slot /></div>`,
57
+ props: ['clickToClose', 'width']
58
+ }
59
+ }
60
+ }
61
+ });
62
+ };
63
+
64
+ it('renders the AppModal and dynamic component when modal is open', async() => {
65
+ factory();
66
+
67
+ await nextTick();
68
+
69
+ // Because Teleport moves content out of the normal wrapper,
70
+ // we query the document for the teleported elements.
71
+ const appModal = document.querySelector('[data-testid="app-modal"]');
72
+ const dynamicComponent = document.querySelector('[data-testid="modal-manager-component"]');
73
+
74
+ expect(appModal).toBeTruthy();
75
+ expect(dynamicComponent).toBeTruthy();
76
+ expect(appModal?.getAttribute('style')).toContain('--prompt-modal-width: 500px');
77
+
78
+ // We assume the mock component is rendered correctly if its markup is found.
79
+ });
80
+
81
+ it('does not render the AppModal when modal is closed', async() => {
82
+ getters['modal/isOpen'] = () => false;
83
+ store = createStore({
84
+ getters,
85
+ mutations: { 'modal/closeModal': jest.fn() }
86
+ });
87
+ factory();
88
+ await nextTick();
89
+
90
+ const appModal = document.querySelector('[data-testid="app-modal"]');
91
+
92
+ expect(appModal).toBeNull();
93
+ });
94
+
95
+ it('does not render the AppModal when dynamic component is null', async() => {
96
+ getters['modal/component'] = () => null;
97
+ store = createStore({
98
+ getters,
99
+ mutations: { 'modal/closeModal': jest.fn() }
100
+ });
101
+ factory();
102
+ await nextTick();
103
+
104
+ const appModal = document.querySelector('[data-testid="app-modal"]');
105
+
106
+ expect(appModal).toBeNull();
107
+ });
108
+
109
+ it('calls store commit when close is triggered', async() => {
110
+ const closeModalMutation = jest.fn();
111
+
112
+ getters['modal/isOpen'] = () => true;
113
+ store = createStore({
114
+ getters,
115
+ mutations: { 'modal/closeModal': closeModalMutation }
116
+ });
117
+ const wrapper = factory();
118
+
119
+ await nextTick();
120
+
121
+ const appModalWrapper = wrapper.findComponent({ name: 'AppModal' });
122
+
123
+ appModalWrapper.vm.$emit('close');
124
+ await nextTick();
125
+
126
+ expect(closeModalMutation).toHaveBeenCalledWith({}, undefined);
127
+ });
128
+
129
+ it('calls registered background closing function on close', async() => {
130
+ const closeModalMutation = jest.fn();
131
+
132
+ getters['modal/isOpen'] = () => true;
133
+ store = createStore({
134
+ getters,
135
+ mutations: { 'modal/closeModal': closeModalMutation }
136
+ });
137
+ const wrapper = factory();
138
+
139
+ await nextTick();
140
+
141
+ const backgroundFn = jest.fn();
142
+
143
+ (wrapper.vm as unknown as ModalManagerMethods).registerBackgroundClosing(backgroundFn);
144
+ await nextTick();
145
+
146
+ const appModalWrapper = wrapper.findComponent({ name: 'AppModal' });
147
+
148
+ appModalWrapper.vm.$emit('close');
149
+ await nextTick();
150
+
151
+ expect(backgroundFn).toHaveBeenCalledWith();
152
+ expect(closeModalMutation).toHaveBeenCalledWith({}, undefined);
153
+ });
154
+
155
+ it('does nothing if modal is already closed when close is triggered', async() => {
156
+ const closeModalMutation = jest.fn();
157
+
158
+ getters['modal/isOpen'] = () => false;
159
+ store = createStore({
160
+ getters,
161
+ mutations: { 'modal/closeModal': closeModalMutation }
162
+ });
163
+ const wrapper = factory();
164
+
165
+ await nextTick();
166
+
167
+ const modalManager = wrapper.vm as unknown as ModalManagerMethods;
168
+ const spy = jest.spyOn(modalManager, 'close');
169
+
170
+ modalManager.close();
171
+ await nextTick();
172
+
173
+ expect(spy).toHaveBeenCalledWith();
174
+ expect(closeModalMutation).not.toHaveBeenCalled();
175
+ });
176
+ });
@@ -0,0 +1,146 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import PromptModal from '@shell/components/PromptModal.vue';
3
+
4
+ import GenericPrompt from '@shell/dialog/GenericPrompt.vue';
5
+ import AddClusterMemberDialog from '@shell/dialog/AddClusterMemberDialog.vue';
6
+ import AddCustomBadgeDialog from '@shell/dialog/AddCustomBadgeDialog.vue';
7
+ import AddonConfigConfirmationDialog from '@shell/dialog/AddonConfigConfirmationDialog.vue';
8
+ import AddProjectMemberDialog from '@shell/dialog/AddProjectMemberDialog.vue';
9
+ import DeactivateDriverDialog from '@shell/dialog/DeactivateDriverDialog.vue';
10
+ import DiagnosticTimingsDialog from '@shell/dialog/DiagnosticTimingsDialog.vue';
11
+ import DrainNode from '@shell/dialog/DrainNode.vue';
12
+ import ForceMachineRemoveDialog from '@shell/dialog/ForceMachineRemoveDialog.vue';
13
+ import GitRepoForceUpdateDialog from '@shell/dialog/GitRepoForceUpdateDialog.vue';
14
+ import RollbackWorkloadDialog from '@shell/dialog/RollbackWorkloadDialog.vue';
15
+ import RotateCertificatesDialog from '@shell/dialog/RotateCertificatesDialog.vue';
16
+ import RotateEncryptionKeyDialog from '@shell/dialog/RotateEncryptionKeyDialog.vue';
17
+ import ScaleMachineDownDialog from '@shell/dialog/ScaleMachineDownDialog.vue';
18
+ import ScalePoolDownDialog from '@shell/dialog/ScalePoolDownDialog.vue';
19
+ import SloDialog from '@shell/dialog/SloDialog.vue';
20
+
21
+ import DisableAuthProviderDialog from '@shell/dialog/DisableAuthProviderDialog.vue';
22
+ import WechatDialog from '@shell/dialog/WechatDialog.vue';
23
+ import DeveloperLoadExtensionDialog from '@shell/dialog/DeveloperLoadExtensionDialog.vue';
24
+ import AddExtensionReposDialog from '@shell/dialog/AddExtensionReposDialog.vue';
25
+ import InstallExtensionDialog from '@shell/dialog/InstallExtensionDialog.vue';
26
+ import UninstallExtensionDialog from '@shell/dialog/UninstallExtensionDialog.vue';
27
+ import KnownHostsEditDialog from '@shell/dialog/KnownHostsEditDialog.vue';
28
+ import ImportDialog from '@shell/dialog/ImportDialog.vue';
29
+ import SearchDialog from '@shell/dialog/SearchDialog.vue';
30
+ import ChangePasswordDialog from '@shell/dialog/ChangePasswordDialog.vue';
31
+ import AssignToDialog from '@shell/dialog/AssignToDialog.vue';
32
+ import FeatureFlagListDialog from '@shell/dialog/FeatureFlagListDialog.vue';
33
+ import MoveNamespaceDialog from '@shell/dialog/MoveNamespaceDialog.vue';
34
+ import ExtensionCatalogInstallDialog from '@shell/dialog/ExtensionCatalogInstallDialog.vue';
35
+ import ExtensionCatalogUninstallDialog from '@shell/dialog/ExtensionCatalogUninstallDialog.vue';
36
+
37
+ import { createStore } from 'vuex';
38
+
39
+ jest.mock('@shell/utils/clipboard', () => {
40
+ return { copyTextToClipboard: jest.fn(() => Promise.resolve({})) };
41
+ });
42
+
43
+ function generateStore(component: any):any {
44
+ return createStore({
45
+ modules: { // promptModal
46
+ 'action-menu': {
47
+ namespaced: true,
48
+ state: {
49
+ modalData: {
50
+ closeOnClickOutside: true,
51
+ resources: [{ cluster: { isRke2: true, machines: [] } }], // ScaleMachineDownDialog
52
+ componentProps: {
53
+ drivers: [], // DeactivateDriverDialog
54
+ driverType: 'kontainerDrivers', // DeactivateDriverDialog
55
+ downloadData: () => jest.fn(), // DiagnosticTimingsDialog
56
+ gatherResponseTimes: () => jest.fn(), // DiagnosticTimingsDialog
57
+ kubeNodes: [{}], // DrainNode
58
+ repositories: [], // GitRepoForceUpdateDialog
59
+ workload: { metadata: {}, kind: '' }, // RollbackWorkloadDialog
60
+ catalog: {}
61
+ },
62
+ },
63
+
64
+ },
65
+ },
66
+ },
67
+ getters: {
68
+ 'type-map/importDialog': () => () => component, // promptModal
69
+ 'i18n/exists': () => jest.fn(), // promptModal
70
+ 'i18n/t': () => jest.fn(), // general usage
71
+ 'rancher/schemaFor': () => jest.fn(), // general usage
72
+ 'prefs/get': () => jest.fn(), // ScalePoolDownDialog
73
+ 'type-map/allTypes': () => jest.fn(), // SearchDialog
74
+ 'type-map/labelFor': () => jest.fn(), // ScaleMachineDownDialog
75
+ 'type-map/getTree': () => jest.fn().mockReturnValue([]), // SearchDialog
76
+ 'cluster/all': () => jest.fn(), // SearchDialog
77
+ currentProduct: () => { // SearchDialog
78
+ return { inStore: 'cluster' };
79
+ },
80
+ currentCluster: () => { // general usage
81
+ 'local';
82
+ },
83
+ }
84
+ });
85
+ }
86
+
87
+ describe('component: PromptModal', () => {
88
+ it.each([
89
+ // current prompt modals at time of coding
90
+ ['GenericPrompt', GenericPrompt],
91
+ ['AddClusterMemberDialog', AddClusterMemberDialog],
92
+ ['AddonConfigConfirmationDialog', AddonConfigConfirmationDialog],
93
+ ['AddProjectMemberDialog', AddProjectMemberDialog],
94
+ ['DeactivateDriverDialog', DeactivateDriverDialog],
95
+ ['DiagnosticTimingsDialog', DiagnosticTimingsDialog],
96
+ ['DrainNode', DrainNode],
97
+ ['ForceMachineRemoveDialog', ForceMachineRemoveDialog],
98
+ ['GitRepoForceUpdateDialog', GitRepoForceUpdateDialog],
99
+ ['RollbackWorkloadDialog', RollbackWorkloadDialog],
100
+ ['RotateCertificatesDialog', RotateCertificatesDialog],
101
+ ['RotateEncryptionKeyDialog', RotateEncryptionKeyDialog],
102
+ ['SloDialog', SloDialog],
103
+ ['AddCustomBadgeDialog', AddCustomBadgeDialog],
104
+ ['ScaleMachineDownDialog', ScaleMachineDownDialog],
105
+ ['ScalePoolDownDialog', ScalePoolDownDialog],
106
+ // new modals created/moved
107
+ ['DisableAuthProviderDialog', DisableAuthProviderDialog],
108
+ ['WechatDialog', WechatDialog],
109
+ ['DeveloperLoadExtensionDialog', DeveloperLoadExtensionDialog],
110
+ ['AddExtensionReposDialog', AddExtensionReposDialog],
111
+ ['InstallExtensionDialog', InstallExtensionDialog],
112
+ ['UninstallExtensionDialog', UninstallExtensionDialog],
113
+ ['KnownHostsEditDialog', KnownHostsEditDialog],
114
+ ['ImportDialog', ImportDialog],
115
+ ['SearchDialog', SearchDialog],
116
+ ['ChangePasswordDialog', ChangePasswordDialog],
117
+ ['AssignToDialog', AssignToDialog],
118
+ ['FeatureFlagListDialog', FeatureFlagListDialog],
119
+ ['MoveNamespaceDialog', MoveNamespaceDialog],
120
+ ['ExtensionCatalogInstallDialog', ExtensionCatalogInstallDialog],
121
+ ['ExtensionCatalogUninstallDialog', ExtensionCatalogUninstallDialog],
122
+ ])('prompt Modal should render modal %p', (modalName, component) => {
123
+ // mock structuredClone
124
+ window.structuredClone = (arg) => JSON.parse(JSON.stringify(arg));
125
+
126
+ document.body.innerHTML = '<div id="modals"></div>';
127
+ const wrapper = mount(PromptModal,
128
+ {
129
+ attachTo: document.body,
130
+ data() {
131
+ return { opened: true }; // this controls modal content visibility
132
+ },
133
+ global: {
134
+ mocks: {
135
+ $store: generateStore(component),
136
+ $fetchState: {}
137
+ },
138
+ stubs: { transition: false }
139
+ }
140
+ }
141
+ );
142
+
143
+ expect(wrapper.vm.opened).toBe(true);
144
+ expect(wrapper.findComponent(component as any).exists()).toBe(true);
145
+ });
146
+ });