@rancher/shell 3.0.4 → 3.0.5-rc.2

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 (270) hide show
  1. package/assets/images/providers/sks.svg +1 -0
  2. package/assets/styles/base/_basic.scss +6 -0
  3. package/assets/styles/base/_helpers.scss +4 -0
  4. package/assets/styles/base/_variables.scss +1 -0
  5. package/assets/styles/global/_button.scss +1 -0
  6. package/assets/translations/en-us.yaml +65 -15
  7. package/assets/translations/zh-hans.yaml +4 -3
  8. package/chart/monitoring/index.vue +3 -1
  9. package/cloud-credential/aws.vue +2 -0
  10. package/components/ActionDropdownShell.vue +71 -0
  11. package/components/AppModal.vue +18 -4
  12. package/components/AsyncButton.vue +24 -7
  13. package/components/BannerGraphic.vue +1 -0
  14. package/components/CommunityLinks.vue +4 -59
  15. package/components/CopyToClipboardText.vue +2 -1
  16. package/components/CruResource.vue +6 -1
  17. package/components/DetailText.vue +5 -0
  18. package/components/ExplorerMembers.vue +1 -1
  19. package/components/ExplorerProjectsNamespaces.vue +68 -18
  20. package/components/GlobalRoleBindings.vue +5 -1
  21. package/components/GrowlManager.vue +1 -0
  22. package/components/LandingPagePreference.vue +7 -3
  23. package/components/LocaleSelector.vue +39 -95
  24. package/components/ModalManager.vue +55 -0
  25. package/components/ModalWithCard.vue +1 -0
  26. package/components/PromptModal.vue +47 -8
  27. package/components/PromptRemove.vue +1 -0
  28. package/components/PromptRestore.vue +1 -0
  29. package/components/ResourceCancelModal.vue +1 -0
  30. package/components/ResourceDetail/Masthead.vue +38 -12
  31. package/components/ResourceDetail/__tests__/Masthead.test.ts +5 -1
  32. package/components/ResourceDetail/index.vue +47 -12
  33. package/components/ResourceTable.vue +54 -19
  34. package/components/SideNav.vue +5 -1
  35. package/components/SlideInPanelManager.vue +126 -0
  36. package/components/SortableTable/THead.vue +5 -2
  37. package/components/SortableTable/actions.js +1 -1
  38. package/components/SortableTable/index.vue +64 -51
  39. package/components/SortableTable/paging.js +16 -19
  40. package/components/SortableTable/selection.js +0 -11
  41. package/components/Wizard.vue +2 -2
  42. package/components/__tests__/AsyncButton.test.ts +2 -2
  43. package/components/__tests__/ModalManager.spec.ts +176 -0
  44. package/components/__tests__/PromptModal.test.ts +148 -0
  45. package/components/__tests__/SlideInPanelManager.spec.ts +166 -0
  46. package/components/auth/AuthBanner.vue +13 -11
  47. package/components/auth/Principal.vue +1 -0
  48. package/components/auth/__tests__/RoleDetailEdit.test.ts +3 -2
  49. package/components/auth/login/ldap.vue +1 -1
  50. package/components/fleet/FleetResources.vue +21 -6
  51. package/components/form/ArrayList.vue +76 -60
  52. package/components/form/BannerSettings.vue +17 -2
  53. package/components/form/ColorInput.vue +35 -6
  54. package/components/form/Command.vue +6 -15
  55. package/components/form/EnvVars.vue +16 -8
  56. package/components/form/HealthCheck.vue +3 -3
  57. package/components/form/HookOption.vue +11 -16
  58. package/components/form/LabeledSelect.vue +18 -22
  59. package/components/form/LifecycleHooks.vue +3 -3
  60. package/components/form/MatchExpressions.vue +14 -8
  61. package/components/form/NameNsDescription.vue +128 -104
  62. package/components/form/Networking.vue +20 -12
  63. package/components/form/NodeAffinity.vue +31 -23
  64. package/components/form/NodeScheduling.vue +13 -3
  65. package/components/form/NotificationSettings.vue +15 -1
  66. package/components/form/Password.vue +1 -0
  67. package/components/form/PodAffinity.vue +43 -43
  68. package/components/form/Probe.vue +68 -66
  69. package/components/form/ResourceQuota/Project.vue +5 -1
  70. package/components/form/ResourceSelector.vue +7 -9
  71. package/components/form/SSHKnownHosts/__tests__/KnownHostsEditDialog.test.ts +16 -24
  72. package/components/form/SSHKnownHosts/index.vue +30 -13
  73. package/components/form/Security.vue +54 -56
  74. package/components/form/Select.vue +32 -21
  75. package/components/form/ShellInput.vue +5 -1
  76. package/components/form/Tolerations.vue +5 -1
  77. package/components/form/ValueFromResource.vue +134 -121
  78. package/components/form/WorkloadPorts.vue +18 -18
  79. package/components/form/__tests__/ArrayList.test.ts +5 -2
  80. package/components/form/__tests__/ColorInput.test.ts +35 -0
  81. package/components/form/__tests__/LabeledSelect.test.ts +40 -0
  82. package/components/form/__tests__/MatchExpressions.test.ts +12 -12
  83. package/components/form/__tests__/NameNsDescription.test.ts +115 -14
  84. package/components/form/__tests__/Probe.test.ts +12 -8
  85. package/components/form/__tests__/SSHKnownHosts.test.ts +22 -2
  86. package/components/form/__tests__/Select.test.ts +37 -0
  87. package/components/formatter/InternalExternalIP.vue +2 -0
  88. package/components/formatter/SecretData.vue +20 -7
  89. package/components/nav/Group.vue +27 -5
  90. package/components/nav/Header.vue +17 -43
  91. package/components/nav/NamespaceFilter.vue +134 -86
  92. package/components/nav/TopLevelMenu.vue +4 -5
  93. package/components/nav/Type.vue +12 -1
  94. package/components/nav/WindowManager/ContainerLogs.vue +87 -61
  95. package/components/nav/WindowManager/ContainerLogsActions.vue +76 -0
  96. package/components/templates/blank.vue +4 -1
  97. package/components/templates/default.vue +8 -3
  98. package/components/templates/home.vue +10 -1
  99. package/components/templates/plain.vue +10 -4
  100. package/composables/focusTrap.ts +12 -4
  101. package/composables/useRuntimeFlag.ts +29 -0
  102. package/config/router/routes.js +20 -13
  103. package/config/store.js +4 -0
  104. package/config/uiplugins.js +5 -1
  105. package/core/types.ts +12 -6
  106. package/detail/catalog.cattle.io.app.vue +6 -1
  107. package/detail/fleet.cattle.io.bundle.vue +70 -6
  108. package/detail/fleet.cattle.io.gitrepo.vue +1 -1
  109. package/detail/namespace.vue +0 -3
  110. package/detail/node.vue +17 -13
  111. package/detail/provisioning.cattle.io.cluster.vue +72 -6
  112. package/dialog/AddCustomBadgeDialog.vue +1 -1
  113. package/{pages/c/_cluster/uiplugins/AddExtensionRepos.vue → dialog/AddExtensionReposDialog.vue} +72 -42
  114. package/{components/AssignTo.vue → dialog/AssignToDialog.vue} +71 -80
  115. package/dialog/ChangePasswordDialog.vue +106 -0
  116. package/dialog/DeactivateDriverDialog.vue +1 -0
  117. package/{pages/c/_cluster/uiplugins/DeveloperInstallDialog.vue → dialog/DeveloperLoadExtensionDialog.vue} +74 -71
  118. package/dialog/DisableAuthProviderDialog.vue +101 -0
  119. package/dialog/DrainNode.vue +1 -1
  120. package/{pages/c/_cluster/uiplugins/CatalogList/CatalogLoadDialog.vue → dialog/ExtensionCatalogInstallDialog.vue} +100 -88
  121. package/{pages/c/_cluster/uiplugins/CatalogList/CatalogUninstallDialog.vue → dialog/ExtensionCatalogUninstallDialog.vue} +69 -57
  122. package/dialog/FeatureFlagListDialog.vue +288 -0
  123. package/dialog/ForceMachineRemoveDialog.vue +5 -2
  124. package/{components/Import.vue → dialog/ImportDialog.vue} +0 -5
  125. package/{pages/c/_cluster/uiplugins/InstallDialog.vue → dialog/InstallExtensionDialog.vue} +124 -106
  126. package/{components/form/SSHKnownHosts → dialog}/KnownHostsEditDialog.vue +52 -59
  127. package/dialog/MoveNamespaceDialog.vue +157 -0
  128. package/dialog/ScalePoolDownDialog.vue +1 -1
  129. package/{components/nav/Jump.vue → dialog/SearchDialog.vue} +34 -14
  130. package/{pages/c/_cluster/uiplugins/UninstallDialog.vue → dialog/UninstallExtensionDialog.vue} +67 -58
  131. package/dialog/WechatDialog.vue +57 -0
  132. package/edit/__tests__/monitoring.coreos.com.prometheusrule.test.ts +16 -3
  133. package/edit/auth/__tests__/oidc.test.ts +152 -109
  134. package/edit/auth/azuread.vue +2 -1
  135. package/edit/auth/github.vue +1 -1
  136. package/edit/auth/googleoauth.vue +5 -1
  137. package/edit/auth/ldap/index.vue +1 -1
  138. package/edit/auth/oidc.vue +38 -5
  139. package/edit/auth/saml.vue +1 -1
  140. package/edit/cloudcredential.vue +24 -9
  141. package/edit/logging.banzaicloud.io.output/__tests__/logging.banzaicloud.io.output.test.ts +40 -9
  142. package/edit/management.cattle.io.user.vue +28 -3
  143. package/edit/namespace.vue +1 -4
  144. package/edit/networking.k8s.io.ingress/IngressClass.vue +7 -3
  145. package/edit/networking.k8s.io.ingress/__tests__/IngressClass.test.ts +58 -0
  146. package/edit/persistentvolume/__tests__/persistentvolume.test.ts +14 -2
  147. package/edit/provisioning.cattle.io.cluster/CustomCommand.vue +4 -1
  148. package/edit/provisioning.cattle.io.cluster/SelectCredential.vue +26 -9
  149. package/edit/provisioning.cattle.io.cluster/__tests__/Advanced.test.ts +8 -8
  150. package/edit/provisioning.cattle.io.cluster/__tests__/DirectoryConfig.test.ts +26 -12
  151. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +66 -0
  152. package/edit/provisioning.cattle.io.cluster/__tests__/utils/rke2-test-data.ts +58 -0
  153. package/edit/provisioning.cattle.io.cluster/rke2.vue +49 -41
  154. package/edit/provisioning.cattle.io.cluster/tabs/AgentConfiguration.vue +6 -1
  155. package/edit/provisioning.cattle.io.cluster/tabs/DirectoryConfig.vue +5 -3
  156. package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +33 -2
  157. package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +2 -2
  158. package/edit/token.vue +2 -0
  159. package/edit/workload/index.vue +1 -0
  160. package/edit/workload/mixins/workload.js +0 -2
  161. package/initialize/install-plugins.js +2 -1
  162. package/list/harvesterhci.io.management.cluster.vue +4 -1
  163. package/list/management.cattle.io.feature.vue +4 -287
  164. package/list/provisioning.cattle.io.cluster.vue +20 -12
  165. package/machine-config/azure.vue +16 -4
  166. package/mixins/vue-select-overrides.js +0 -4
  167. package/models/__tests__/namespace.test.ts +25 -1
  168. package/models/cloudcredential.js +5 -0
  169. package/models/fleet.cattle.io.cluster.js +8 -2
  170. package/models/fleet.cattle.io.gitrepo.js +8 -34
  171. package/models/kontainerdriver.js +6 -3
  172. package/models/management.cattle.io.feature.js +7 -1
  173. package/models/management.cattle.io.node.js +3 -3
  174. package/models/namespace.js +11 -6
  175. package/models/nodedriver.js +6 -3
  176. package/models/workload.js +4 -1
  177. package/package.json +3 -3
  178. package/pages/about.vue +13 -3
  179. package/pages/account/index.vue +16 -6
  180. package/pages/auth/login.vue +18 -7
  181. package/pages/auth/logout.vue +4 -1
  182. package/pages/auth/setup.vue +2 -0
  183. package/pages/auth/verify.vue +13 -8
  184. package/pages/c/_cluster/apps/charts/chart.vue +1 -1
  185. package/pages/c/_cluster/apps/charts/install.vue +26 -26
  186. package/pages/c/_cluster/auth/config/index.vue +10 -12
  187. package/pages/c/_cluster/explorer/EventsTable.vue +38 -33
  188. package/pages/c/_cluster/explorer/index.vue +17 -15
  189. package/pages/c/_cluster/istio/index.vue +2 -2
  190. package/pages/c/_cluster/longhorn/index.vue +1 -1
  191. package/pages/c/_cluster/monitoring/index.vue +1 -1
  192. package/pages/c/_cluster/monitoring/monitor/_namespace/_id.vue +4 -2
  193. package/pages/c/_cluster/monitoring/monitor/create.vue +4 -2
  194. package/pages/c/_cluster/monitoring/route-receiver/_id.vue +4 -2
  195. package/pages/c/_cluster/monitoring/route-receiver/create.vue +5 -2
  196. package/pages/c/_cluster/neuvector/index.vue +1 -1
  197. package/pages/c/_cluster/settings/banners.vue +4 -3
  198. package/pages/c/_cluster/uiplugins/CatalogList/index.vue +8 -10
  199. package/pages/c/_cluster/uiplugins/__tests__/AddExtensionRepos.test.ts +4 -7
  200. package/pages/c/_cluster/uiplugins/index.vue +98 -55
  201. package/pages/diagnostic.vue +59 -11
  202. package/pages/fail-whale.vue +14 -8
  203. package/pages/home.vue +24 -18
  204. package/pages/prefs.vue +7 -6
  205. package/pages/support/index.vue +4 -1
  206. package/plugins/internal-api/index.ts +37 -0
  207. package/plugins/internal-api/shared/base-api.ts +13 -0
  208. package/plugins/internal-api/shell/shell.api.ts +108 -0
  209. package/plugins/steve/actions.js +0 -12
  210. package/public/index.html +1 -0
  211. package/rancher-components/Card/Card.vue +1 -1
  212. package/rancher-components/Form/Checkbox/Checkbox.test.ts +59 -1
  213. package/rancher-components/Form/Checkbox/Checkbox.vue +27 -3
  214. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +47 -0
  215. package/rancher-components/Form/LabeledInput/LabeledInput.vue +20 -2
  216. package/rancher-components/Form/Radio/RadioButton.test.ts +36 -1
  217. package/rancher-components/Form/Radio/RadioButton.vue +20 -4
  218. package/rancher-components/Form/Radio/RadioGroup.test.ts +60 -0
  219. package/rancher-components/Form/Radio/RadioGroup.vue +52 -10
  220. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.test.ts +17 -0
  221. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +5 -0
  222. package/rancher-components/LabeledTooltip/LabeledTooltip.vue +10 -1
  223. package/rancher-components/RcButton/RcButton.vue +2 -1
  224. package/rancher-components/RcButton/types.ts +1 -0
  225. package/rancher-components/RcDropdown/RcDropdown.vue +18 -6
  226. package/rancher-components/RcDropdown/RcDropdownItem.vue +3 -56
  227. package/rancher-components/RcDropdown/RcDropdownItemCheckbox.vue +68 -0
  228. package/rancher-components/RcDropdown/RcDropdownItemSelect.vue +92 -0
  229. package/rancher-components/RcDropdown/RcDropdownTrigger.vue +10 -0
  230. package/rancher-components/RcDropdown/index.ts +2 -0
  231. package/rancher-components/RcDropdown/useDropdownCollection.ts +8 -0
  232. package/rancher-components/RcDropdown/useDropdownContext.ts +9 -3
  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 -12
  239. package/scripts/typegen.sh +2 -4
  240. package/server/har-file.js +25 -3
  241. package/store/action-menu.js +26 -56
  242. package/store/features.js +2 -1
  243. package/store/index.js +5 -0
  244. package/store/modal.ts +71 -0
  245. package/store/slideInPanel.ts +47 -0
  246. package/store/type-map.js +12 -1
  247. package/store/type-map.utils.ts +4 -4
  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/resources/fleet.d.ts +0 -14
  253. package/types/shell/index.d.ts +43 -24
  254. package/types/vue-shim.d.ts +4 -1
  255. package/utils/__mocks__/tabbable.js +13 -0
  256. package/utils/__tests__/object.test.ts +38 -4
  257. package/utils/cluster.js +35 -0
  258. package/utils/fleet.ts +15 -73
  259. package/utils/object.js +48 -5
  260. package/utils/validators/formRules/__tests__/index.test.ts +10 -1
  261. package/utils/validators/formRules/index.ts +27 -3
  262. package/utils/validators/machine-pool.ts +20 -0
  263. package/components/DisableAuthProviderModal.vue +0 -114
  264. package/components/MoveModal.vue +0 -166
  265. package/components/PromptChangePassword.vue +0 -123
  266. package/components/fleet/FleetBundleResources.vue +0 -86
  267. package/components/formatter/ExtensionCache.vue +0 -74
  268. package/components/formatter/Port.vue +0 -24
  269. package/components/formatter/SecretType.vue +0 -41
  270. package/types/vue-shim.d +0 -20
@@ -44,25 +44,9 @@ export default {
44
44
  },
45
45
 
46
46
  data() {
47
- const rows = clone(this.value || []).map((row) => {
48
- row._showHost = false;
49
- row._serviceType = row._serviceType || '';
50
- row._name = row.name ? `${ row.name }` : `${ row.containerPort }${ row.protocol.toLowerCase() }${ row.hostPort || row._listeningPort || '' }`;
51
- if (row.hostPort || row.hostIP) {
52
- row._showHost = true;
53
- }
54
-
55
- row._ipam = '';
56
-
57
- return row;
58
- });
59
-
60
- // show host port column if existing port data has any host ports defined
61
- const showHostPorts = !!rows.some((row) => !!row.hostPort);
62
-
63
47
  return {
64
- rows,
65
- showHostPorts,
48
+ rows: [],
49
+ showHostPorts: false,
66
50
  workloadPortOptions: ['TCP', 'UDP']
67
51
  };
68
52
  },
@@ -168,6 +152,22 @@ export default {
168
152
  },
169
153
 
170
154
  created() {
155
+ const rows = clone(this.value || []).map((row) => {
156
+ row._showHost = false;
157
+ row._serviceType = row._serviceType || '';
158
+ row._name = row.name ? `${ row.name }` : `${ row.containerPort }${ row.protocol.toLowerCase() }${ row.hostPort || row._listeningPort || '' }`;
159
+ if (row.hostPort || row.hostIP) {
160
+ row._showHost = true;
161
+ }
162
+
163
+ row._ipam = '';
164
+
165
+ return row;
166
+ });
167
+
168
+ this.rows = rows;
169
+ // show host port column if existing port data has any host ports defined
170
+ this.showHostPorts = !!rows.some((row) => !!row.hostPort);
171
171
  this.queueUpdate = debounce(this.update, 500);
172
172
  this.rows.map((row) => {
173
173
  this.setServiceType(row);
@@ -4,6 +4,8 @@ import { _EDIT, _VIEW } from '@shell/config/query-params';
4
4
  import { ExtendedVue, Vue } from 'vue/types/vue';
5
5
  import { DefaultProps } from 'vue/types/options';
6
6
 
7
+ jest.mock('lodash/debounce', () => jest.fn((fn) => fn));
8
+
7
9
  describe('the ArrayList', () => {
8
10
  it('is empty', () => {
9
11
  const wrapper = mount(ArrayList, {
@@ -56,12 +58,13 @@ describe('the ArrayList', () => {
56
58
  });
57
59
 
58
60
  jest.useFakeTimers();
59
- await (wrapper.get('[data-testid="remove-item-1"]').element as HTMLElement).click();
61
+ await (wrapper.get('[data-testid="array-list-remove-item-1"]').element as HTMLElement).click();
60
62
  jest.advanceTimersByTime(50);
61
63
  jest.useRealTimers();
62
64
 
63
- expect(wrapper.find('[data-testid="remove-item-2"]').exists()).toBe(false);
65
+ expect(wrapper.find('[data-testid="array-list-remove-item-2"]').exists()).toBe(false);
64
66
  expect((wrapper.emitted('remove')![0][0] as any).row.value).toStrictEqual('string 1');
67
+ expect(wrapper.vm.rows).toStrictEqual([{ value: 'string 0' }, { value: 'string 2' }]);
65
68
  expect(wrapper.emitted('update:value')![0][0]).toStrictEqual(['string 0', 'string 2']);
66
69
  });
67
70
 
@@ -24,4 +24,39 @@ describe('colorInput.vue', () => {
24
24
  expect(colorWrapper.classes()).not.toContain('disabled');
25
25
  expect(Object.keys(colorInput.attributes())).not.toContain('disabled');
26
26
  });
27
+
28
+ it('a11y: adding ARIA props should correctly fill out the appropriate fields on the component', () => {
29
+ const label = 'some-label';
30
+ const describeById = 'some-id';
31
+
32
+ const wrapper = shallowMount(ColorInput, {
33
+ props: { label },
34
+ attrs: { 'aria-describedby': describeById }
35
+ });
36
+
37
+ const colorInput = wrapper.find('input');
38
+ const ariaDisabled = colorInput.attributes('aria-disabled');
39
+ const ariaLabel = colorInput.attributes('aria-label');
40
+ const ariaDescribedBy = colorInput.attributes('aria-describedby');
41
+
42
+ expect(ariaDisabled).toBe('false');
43
+ expect(ariaLabel).toBe(label);
44
+ expect(ariaDescribedBy).toBe(describeById);
45
+ });
46
+
47
+ it('a11y: adding aria-label ($attrs) from parent should override label-based aria-label', () => {
48
+ const inputLabel = 'some-label';
49
+ const overrideLabel = 'some-override-label';
50
+
51
+ const wrapper = shallowMount(ColorInput, {
52
+ props: { label: inputLabel },
53
+ attrs: { 'aria-label': overrideLabel }
54
+ });
55
+
56
+ const colorInput = wrapper.find('input');
57
+ const ariaLabel = colorInput.attributes('aria-label');
58
+
59
+ expect(ariaLabel).toBe(overrideLabel);
60
+ expect(ariaLabel).not.toBe(inputLabel);
61
+ });
27
62
  });
@@ -214,4 +214,44 @@ describe('component: LabeledSelect', () => {
214
214
  expect(wrapper.vm.$data.myValue).toStrictEqual(expectation);
215
215
  });
216
216
  });
217
+
218
+ it('a11y: adding ARIA props should correctly fill out the appropriate fields on the component', async() => {
219
+ const label = 'Foo';
220
+ const value = 'foo';
221
+ const ariaDescribedById = 'some-described-by-id';
222
+ const itemLabel = 'some-label';
223
+
224
+ const wrapper = mount(LabeledSelect, {
225
+ props: {
226
+ value,
227
+ label: itemLabel,
228
+ options: [
229
+ { label, value },
230
+ ],
231
+ required: true
232
+ },
233
+ attrs: { 'aria-describedby': ariaDescribedById }
234
+ });
235
+
236
+ const labeledSelectContainer = wrapper.find('.labeled-select');
237
+ const ariaExpanded = labeledSelectContainer.attributes('aria-expanded');
238
+ const ariaDescribedBy = labeledSelectContainer.attributes('aria-describedby');
239
+ const ariaRequired = labeledSelectContainer.attributes('aria-required');
240
+ const containerId = labeledSelectContainer.attributes('id');
241
+ const labelFor = wrapper.find('label').attributes('for');
242
+
243
+ const vSelectInput = wrapper.find('.v-select');
244
+
245
+ expect(ariaExpanded).toBe('false');
246
+ expect(ariaDescribedBy).toBe(ariaDescribedById);
247
+ expect(ariaRequired).toBe('true');
248
+ expect(containerId).toBe(wrapper.vm.labeledSelectLabelId);
249
+ expect(labelFor).toBe(wrapper.vm.labeledSelectLabelId);
250
+
251
+ // make sure it's hardcoded to a "neutral" value so that
252
+ // in the current architecture of the component
253
+ // screen readers won't pick up the default "Select option" aria-label
254
+ // from the library
255
+ expect(vSelectInput.attributes('aria-label')).toBe('-');
256
+ });
217
257
  });
@@ -6,9 +6,9 @@ import { nextTick } from 'vue';
6
6
  describe('component: MatchExpressions', () => {
7
7
  it('should display all the inputs', () => {
8
8
  const wrapper = mount(MatchExpressions, {
9
- props: { mode: _CREATE },
10
- data: () => ({
11
- rules: [
9
+ props: {
10
+ mode: _CREATE,
11
+ value: [
12
12
  {
13
13
  id: '123',
14
14
  key: '123',
@@ -16,7 +16,7 @@ describe('component: MatchExpressions', () => {
16
16
  values: '123'
17
17
  }
18
18
  ]
19
- })
19
+ },
20
20
  });
21
21
 
22
22
  const inputWraps = wrapper.findAll('[data-testid^=input-match-expression-]');
@@ -29,9 +29,9 @@ describe('component: MatchExpressions', () => {
29
29
  'values',
30
30
  ])('should emit an update on %p input', async(field) => {
31
31
  const wrapper = mount(MatchExpressions, {
32
- props: { mode: _CREATE },
33
- data: () => ({
34
- rules: [
32
+ props: {
33
+ mode: _CREATE,
34
+ value: [
35
35
  {
36
36
  id: '123',
37
37
  key: '123',
@@ -39,7 +39,7 @@ describe('component: MatchExpressions', () => {
39
39
  values: '123'
40
40
  }
41
41
  ]
42
- })
42
+ },
43
43
  });
44
44
  const input = wrapper.find(`[data-testid="input-match-expression-${ field }-0"]`).find('input');
45
45
  const newValue = 123;
@@ -54,9 +54,9 @@ describe('component: MatchExpressions', () => {
54
54
  'operator',
55
55
  ])('should emit an update on %p selection change', async(field) => {
56
56
  const wrapper = mount(MatchExpressions, {
57
- props: { mode: _CREATE },
58
- data: () => ({
59
- rules: [
57
+ props: {
58
+ mode: _CREATE,
59
+ value: [
60
60
  {
61
61
  id: '123',
62
62
  key: '123',
@@ -64,7 +64,7 @@ describe('component: MatchExpressions', () => {
64
64
  values: '123'
65
65
  }
66
66
  ]
67
- })
67
+ },
68
68
  });
69
69
 
70
70
  const select = wrapper.find(`[data-testid="input-match-expression-${ field }-0"]`);
@@ -1,10 +1,18 @@
1
1
  import { mount } from '@vue/test-utils';
2
2
  import NameNsDescription from '@shell/components/form/NameNsDescription.vue';
3
+ import { createStore } from 'vuex';
3
4
 
4
5
  describe('component: NameNsDescription', () => {
5
6
  // Accessing to computed value due code complexity
6
7
  it('should map namespaces to options', () => {
7
8
  const namespaceName = 'test';
9
+ const store = createStore({
10
+ getters: {
11
+ allowedNamespaces: () => () => ({ [namespaceName]: true }),
12
+ currentStore: () => () => 'cluster',
13
+ 'cluster/schemaFor': () => jest.fn()
14
+ }
15
+ });
8
16
  const result = [
9
17
  {
10
18
  label: namespaceName,
@@ -13,20 +21,21 @@ describe('component: NameNsDescription', () => {
13
21
  ];
14
22
  const wrapper = mount(NameNsDescription, {
15
23
  props: {
16
- value: {},
24
+ value: {
25
+ setAnnotation: jest.fn(),
26
+ metadata: {}
27
+ },
17
28
  mode: 'create',
18
29
  cluster: {},
19
30
  },
20
31
  global: {
21
- mocks: {
32
+ provide: { store },
33
+ mocks: {
22
34
  $store: {
23
35
  dispatch: jest.fn(),
24
36
  getters: {
25
- namespaces: jest.fn(),
26
- allowedNamespaces: () => ({ [namespaceName]: true }),
27
- currentStore: () => 'cluster',
28
- 'cluster/schemaFor': jest.fn(),
29
- 'i18n/t': jest.fn(),
37
+ namespaces: jest.fn(),
38
+ 'i18n/t': jest.fn(),
30
39
  },
31
40
  },
32
41
  },
@@ -38,27 +47,35 @@ describe('component: NameNsDescription', () => {
38
47
 
39
48
  it('should emit in case of new namespace', () => {
40
49
  const namespaceName = 'test';
50
+ const store = createStore({
51
+ getters: {
52
+ allowedNamespaces: () => () => ({ [namespaceName]: true }),
53
+ currentStore: () => () => 'cluster',
54
+ 'cluster/schemaFor': () => jest.fn()
55
+ }
56
+ });
41
57
  const newNamespaceName = 'bananas';
42
58
  const wrapper = mount(NameNsDescription, {
43
59
  props: {
44
- value: { metadata: {} },
45
- mode: 'create',
60
+ value: {
61
+ setAnnotation: jest.fn(),
62
+ metadata: {}
63
+ },
64
+ mode: 'create',
46
65
  },
47
66
  global: {
48
- mocks: {
67
+ provide: { store },
68
+ mocks: {
49
69
  $store: {
50
70
  dispatch: jest.fn(),
51
71
  getters: {
52
72
  namespaces: jest.fn(),
53
- allowedNamespaces: () => ({ [namespaceName]: true }),
54
73
  'customizations/getPreviewCluster': {
55
74
  ready: true,
56
75
  isLocal: false,
57
76
  badge: {},
58
77
  },
59
- currentStore: () => 'cluster',
60
- 'cluster/schemaFor': jest.fn(),
61
- 'i18n/t': jest.fn(),
78
+ 'i18n/t': jest.fn(),
62
79
  },
63
80
  },
64
81
  },
@@ -69,4 +86,88 @@ describe('component: NameNsDescription', () => {
69
86
 
70
87
  expect(wrapper.emitted().isNamespaceNew?.[0][0]).toBe(true);
71
88
  });
89
+
90
+ it('renders the name input with the expected value', () => {
91
+ const namespaceName = 'test';
92
+ const store = createStore({
93
+ getters: {
94
+ allowedNamespaces: () => () => ({ [namespaceName]: true }),
95
+ currentStore: () => () => 'cluster',
96
+ 'cluster/schemaFor': () => jest.fn()
97
+ }
98
+ });
99
+ const wrapper = mount(NameNsDescription, {
100
+ props: {
101
+ value: {
102
+ setAnnotation: jest.fn(),
103
+ metadata: { name: 'Default' }
104
+ },
105
+ mode: 'create',
106
+ },
107
+ global: {
108
+ provide: { store },
109
+ mocks: {
110
+ $store: {
111
+ dispatch: jest.fn(),
112
+ getters: {
113
+ namespaces: jest.fn(),
114
+ 'customizations/getPreviewCluster': {
115
+ ready: true,
116
+ isLocal: false,
117
+ badge: {},
118
+ },
119
+ 'i18n/t': jest.fn(),
120
+ },
121
+ },
122
+ },
123
+ },
124
+ });
125
+
126
+ const nameInput = wrapper.find('[data-testid="NameNsDescriptionNameInput"]');
127
+
128
+ expect(nameInput.element.value).toBe('Default');
129
+ });
130
+
131
+ it('sets the name using the nameKey prop', () => {
132
+ const namespaceName = 'test';
133
+ const store = createStore({
134
+ getters: {
135
+ allowedNamespaces: () => () => ({ [namespaceName]: true }),
136
+ currentStore: () => () => 'cluster',
137
+ 'cluster/schemaFor': () => jest.fn()
138
+ }
139
+ });
140
+ const wrapper = mount(NameNsDescription, {
141
+ props: {
142
+ value: {
143
+ setAnnotation: jest.fn(),
144
+ metadata: {},
145
+ spec: { displayName: 'Default' }
146
+ },
147
+ mode: 'create',
148
+ nameKey: 'spec.displayName'
149
+ },
150
+ global: {
151
+ provide: { store },
152
+ mocks: {
153
+ $store: {
154
+ dispatch: jest.fn(),
155
+ getters: {
156
+ namespaces: jest.fn(),
157
+ 'customizations/getPreviewCluster': {
158
+ ready: true,
159
+ isLocal: false,
160
+ badge: {},
161
+ },
162
+ 'i18n/t': jest.fn(),
163
+ },
164
+ },
165
+ },
166
+ },
167
+ });
168
+
169
+ const nameInput = wrapper.find('[data-testid="NameNsDescriptionNameInput"]');
170
+
171
+ expect(nameInput.element.value).toBe('Default');
172
+ });
72
173
  });
@@ -6,18 +6,20 @@ import { DefaultProps } from 'vue/types/options';
6
6
 
7
7
  describe('component: Probe', () => {
8
8
  describe.each([
9
- ['HTTPS', ['port', 'path']],
10
- ['tcp', ['socket']],
11
- ['exec', ['command']],
12
- ])('given kind %p', (kind, extraFields) => {
9
+ [{ httpGet: { scheme: 'https' } }, ['port', 'path']],
10
+ [{ tcpSocket: {} }, ['socket']],
11
+ [{ exec: {} }, ['command']],
12
+ ])('given kind %p', (value, extraFields) => {
13
13
  it.each([
14
14
  ...extraFields,
15
15
  'successThreshold',
16
16
  'failureThreshold',
17
17
  ])('should emit an update on %p input', (field) => {
18
18
  const wrapper = mount(Probe as unknown as ExtendedVue<Vue, {}, {}, {}, DefaultProps>, {
19
- props: { mode: _EDIT },
20
- data: () => ({ kind })
19
+ props: {
20
+ mode: _EDIT,
21
+ value,
22
+ },
21
23
  });
22
24
  const input = wrapper.find(`[data-testid="input-probe-${ field }"]`).find('input');
23
25
  const newValue = 123;
@@ -33,8 +35,10 @@ describe('component: Probe', () => {
33
35
  'timeoutSeconds',
34
36
  ])('should emit an update on %p input and blur', (field) => {
35
37
  const wrapper = mount(Probe as unknown as ExtendedVue<Vue, {}, {}, {}, DefaultProps>, {
36
- props: { mode: _EDIT },
37
- data: () => ({ kind })
38
+ props: {
39
+ mode: _EDIT,
40
+ value
41
+ },
38
42
  });
39
43
  const input = wrapper.find(`[data-testid="input-probe-${ field }"]`).find('input');
40
44
  const newValue = 123;
@@ -1,6 +1,18 @@
1
1
  import { mount } from '@vue/test-utils';
2
2
  import { _EDIT, _VIEW } from '@shell/config/query-params';
3
3
  import SSHKnownHosts from '@shell/components/form/SSHKnownHosts/index.vue';
4
+ import { createStore } from 'vuex';
5
+
6
+ jest.mock('focus-trap', () => {
7
+ return {
8
+ createFocusTrap: jest.fn().mockImplementation(() => {
9
+ return {
10
+ activate: jest.fn(),
11
+ deactivate: jest.fn(),
12
+ };
13
+ }),
14
+ };
15
+ });
4
16
 
5
17
  describe('component: SSHKnownHosts', () => {
6
18
  it.each([
@@ -42,18 +54,26 @@ describe('component: SSHKnownHosts', () => {
42
54
  });
43
55
 
44
56
  it('mode edit: should open edit dialog', async() => {
57
+ const actions = { 'management/promptModal': jest.fn() };
58
+
45
59
  const wrapper = mount(SSHKnownHosts, {
46
60
  props: {
47
61
  mode: _EDIT,
48
62
  value: '',
63
+ },
64
+ global: {
65
+ mocks: {
66
+ $store: createStore({ actions }),
67
+ $fetchState: {}
68
+ },
69
+ stubs: { transition: false }
49
70
  }
50
71
  });
51
72
 
52
73
  const knownSshHostsOpenDialog = wrapper.find('[data-testid="input-known-ssh-hosts_open-dialog"]');
53
- const editDialog = wrapper.vm.$refs['editDialog'] as any;
54
74
 
55
75
  await knownSshHostsOpenDialog.trigger('click');
56
76
 
57
- expect(editDialog.showModal).toBe(true);
77
+ expect(actions['management/promptModal']).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ component: 'KnownHostsEditDialog' }));
58
78
  });
59
79
  });
@@ -27,4 +27,41 @@ describe('select.vue', () => {
27
27
  // eslint-disable-next-line no-console
28
28
  expect(console.warn).not.toHaveBeenCalled();
29
29
  });
30
+
31
+ it('a11y: adding ARIA props should correctly fill out the appropriate fields on the component', async() => {
32
+ const label = 'Foo';
33
+ const value = 'foo';
34
+ const ariaDescribedById = 'some-described-by-id';
35
+ const ariaLabelText = 'some-aria-label';
36
+
37
+ const wrapper = shallowMount(SelectComponent, {
38
+ props: {
39
+ value,
40
+ options: [
41
+ { label, value },
42
+ ],
43
+ },
44
+ attrs: {
45
+ 'aria-describedby': ariaDescribedById,
46
+ 'aria-label': ariaLabelText,
47
+ }
48
+ });
49
+
50
+ const labeledSelectContainer = wrapper.find('.unlabeled-select');
51
+ const ariaExpanded = labeledSelectContainer.attributes('aria-expanded');
52
+ const ariaDescribedBy = labeledSelectContainer.attributes('aria-describedby');
53
+ const ariaLabel = labeledSelectContainer.attributes('aria-label');
54
+
55
+ const vSelectInput = wrapper.find('.inline');
56
+
57
+ expect(ariaExpanded).toBe('false');
58
+ expect(ariaDescribedBy).toBe(ariaDescribedById);
59
+ expect(ariaLabel).toBe(ariaLabelText);
60
+
61
+ // make sure it's hardcoded to a "neutral" value so that
62
+ // in the current architecture of the component
63
+ // screen readers won't pick up the default "Select option" aria-label
64
+ // from the library
65
+ expect(vSelectInput.attributes('aria-label')).toBe('-');
66
+ });
30
67
  });
@@ -28,6 +28,7 @@ export default {
28
28
  <span>
29
29
  <template v-if="isIp(row.externalIp)">
30
30
  {{ row.externalIp }} <CopyToClipboard
31
+ :aria-label="t('internalExternalIP.copyExternalIp')"
31
32
  label-as="tooltip"
32
33
  :text="row.externalIp"
33
34
  class="icon-btn"
@@ -43,6 +44,7 @@ export default {
43
44
  </template>
44
45
  <template v-else-if="isIp(row.internalIp)">
45
46
  {{ row.internalIp }}<CopyToClipboard
47
+ :aria-label="t('internalExternalIP.copyInternalIp')"
46
48
  label-as="tooltip"
47
49
  :text="row.internalIp"
48
50
  class="icon-btn"
@@ -13,17 +13,25 @@ export default {
13
13
  },
14
14
  },
15
15
 
16
- data() {
16
+ beforeMount() {
17
17
  if (this.value.issuer) {
18
18
  const { cn, notAfter, sans = [] } = this.value;
19
19
 
20
- return {
21
- cn, expiration: notAfter, sans, isTLS: true
22
- };
23
- } else {
24
- return { isTLS: false };
20
+ this.expiration = notAfter;
21
+ this.sans = sans;
22
+ this.cn = cn;
23
+ this.isTLS = true;
25
24
  }
26
25
  },
26
+
27
+ data() {
28
+ return {
29
+ isTLS: false,
30
+ cn: null,
31
+ sans: [],
32
+ expiration: null,
33
+ };
34
+ },
27
35
  computed: {
28
36
  // use 'text-warning' or 'text-error' classes if the cert is <8 days from expiring or expired respectively
29
37
  dateClass() {
@@ -43,7 +51,12 @@ export default {
43
51
 
44
52
  <template>
45
53
  <div v-if="isTLS">
46
- <t k="secret.certificate.cn" /> {{ cn }} <span v-if="row.unrepeatedSans && row.unrepeatedSans.length">{{ t('secret.certificate.plusMore', {n:row.unrepeatedSans.length}) }}</span><br>
54
+ <t k="secret.certificate.cn" />
55
+ {{ cn }}
56
+ <span v-if="row.unrepeatedSans && row.unrepeatedSans.length">
57
+ {{ t('secret.certificate.plusMore', {n:row.unrepeatedSans.length}) }}
58
+ </span>
59
+ <br>
47
60
  <t k="secret.certificate.expires" />: <DateComponent
48
61
  :class="dateClass"
49
62
  :value="expiration"
@@ -99,6 +99,11 @@ export default {
99
99
  },
100
100
 
101
101
  groupSelected() {
102
+ // Can not click on groups that are fixed open
103
+ if (this.fixedOpen) {
104
+ return;
105
+ }
106
+
102
107
  // Don't auto-select first group entry if we're already expanded and contain the currently-selected nav item
103
108
  if (this.hasActiveRoute() && this.isExpanded) {
104
109
  return;
@@ -158,6 +163,17 @@ export default {
158
163
  items = this.group;
159
164
  }
160
165
 
166
+ let parentPath = '';
167
+ const cluster = this.$route.params?.cluster;
168
+
169
+ // Where we use nested route configuration, consider the parent route when trying to identify the nav location
170
+ if (this.$route.matched.length > 1) {
171
+ const parentRoute = this.$route.matched[this.$route.matched.length - 2];
172
+
173
+ parentPath = parentRoute.path.replace(':cluster', cluster);
174
+ parentPath = parentPath === '/' ? undefined : parentPath;
175
+ }
176
+
161
177
  for (const item of items.children) {
162
178
  if (item.children && this.hasActiveRoute(item)) {
163
179
  return true;
@@ -166,8 +182,11 @@ export default {
166
182
  const matchesNavLevel = navLevels.filter((param) => !this.$route.params[param] || this.$route.params[param] !== item.route.params[param]).length === 0;
167
183
  const withoutHash = this.$route.hash ? this.$route.fullPath.slice(0, this.$route.fullPath.indexOf(this.$route.hash)) : this.$route.fullPath;
168
184
  const withoutQuery = withoutHash.split('?')[0];
185
+ const itemFullPath = this.$router.resolve(item.route).fullPath;
169
186
 
170
- if (matchesNavLevel || this.$router.resolve(item.route).fullPath === withoutQuery) {
187
+ if (matchesNavLevel || itemFullPath === withoutQuery) {
188
+ return true;
189
+ } else if (parentPath && itemFullPath === parentPath) {
171
190
  return true;
172
191
  }
173
192
  }
@@ -205,14 +224,14 @@ export default {
205
224
  <template>
206
225
  <div
207
226
  class="accordion"
208
- :class="{[`depth-${depth}`]: true, 'expanded': isExpanded, 'has-children': hasChildren, 'group-highlight': isGroupActive}"
227
+ :class="{[`depth-${depth}`]: true, 'expanded': isExpanded, 'has-children': hasChildren, 'group-highlight': isGroupActive }"
209
228
  >
210
229
  <div
211
230
  v-if="showHeader"
212
231
  class="header"
213
- :class="{'active': isOverview, 'noHover': !canCollapse}"
232
+ :class="{'active': isOverview, 'noHover': !canCollapse || fixedOpen}"
214
233
  role="button"
215
- tabindex="0"
234
+ :tabindex="fixedOpen ? -1 : 0"
216
235
  :aria-label="group.labelDisplay || group.label || ''"
217
236
  @click="groupSelected()"
218
237
  @keyup.enter="groupSelected()"
@@ -260,7 +279,7 @@ export default {
260
279
  v-if="child.divider"
261
280
  :key="idx"
262
281
  >
263
- <hr>
282
+ <hr role="none">
264
283
  </li>
265
284
  <!-- <div v-else-if="child[childrenKey] && hideGroup(child[childrenKey])" :key="child.name">
266
285
  HIDDEN
@@ -358,6 +377,9 @@ export default {
358
377
  &:hover:not(.active) {
359
378
  background-color: var(--nav-hover);
360
379
  }
380
+ &:hover:not(.active).noHover {
381
+ background-color: inherit;
382
+ }
361
383
  }
362
384
  }
363
385