@rancher/shell 3.0.5-rc.7 → 3.0.5-rc.9

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 (301) hide show
  1. package/assets/brand/classic/metadata.json +3 -0
  2. package/assets/styles/app.scss +1 -0
  3. package/assets/styles/base/_color.scss +19 -0
  4. package/assets/styles/base/_helpers.scss +10 -0
  5. package/assets/styles/base/_variables.scss +1 -1
  6. package/assets/styles/fonts/_icons.scss +1 -32
  7. package/assets/styles/global/_layout.scss +1 -1
  8. package/assets/styles/global/_tooltip.scss +7 -4
  9. package/assets/styles/themes/_dark.scss +272 -259
  10. package/assets/styles/themes/_light.scss +551 -516
  11. package/assets/styles/themes/_modern.scss +936 -0
  12. package/assets/translations/en-us.yaml +219 -38
  13. package/assets/translations/zh-hans.yaml +0 -1
  14. package/chart/__tests__/S3.test.ts +2 -1
  15. package/chart/monitoring/grafana/index.vue +8 -2
  16. package/cloud-credential/generic.vue +18 -10
  17. package/cloud-credential/harvester.vue +1 -9
  18. package/components/ActionMenuShell.vue +3 -1
  19. package/components/AdvancedSection.vue +8 -0
  20. package/components/ChartReadme.vue +17 -7
  21. package/components/Cron/CronExpressionEditor.vue +299 -0
  22. package/components/Cron/CronExpressionEditorModal.vue +247 -0
  23. package/components/Cron/CronTooltip.vue +87 -0
  24. package/components/Cron/types.ts +13 -0
  25. package/components/Drawer/ResourceDetailDrawer/__tests__/composables.test.ts +1 -26
  26. package/components/Drawer/ResourceDetailDrawer/composables.ts +0 -23
  27. package/components/Drawer/ResourceDetailDrawer/index.vue +17 -4
  28. package/components/ForceDirectedTreeChart/composable.ts +11 -0
  29. package/components/InstallHelmCharts.vue +656 -0
  30. package/components/LazyImage.vue +60 -4
  31. package/components/LocaleSelector.vue +7 -2
  32. package/components/Markdown.vue +4 -0
  33. package/components/PromptModal.vue +1 -1
  34. package/components/Resource/Detail/Card/__tests__/StateCard.test.ts +1 -0
  35. package/components/Resource/Detail/CopyToClipboard.vue +78 -0
  36. package/components/Resource/Detail/FetchLoader/__tests__/composables.test.ts +69 -0
  37. package/components/Resource/Detail/FetchLoader/composables.ts +27 -0
  38. package/components/Resource/Detail/Masthead/composable.ts +16 -0
  39. package/components/Resource/Detail/Masthead/index.vue +37 -0
  40. package/components/Resource/Detail/Metadata/Annotations/__tests__/index.test.ts +1 -1
  41. package/components/Resource/Detail/Metadata/Annotations/index.vue +1 -1
  42. package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/identifying-fields.test.ts +13 -61
  43. package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/index.test.ts +33 -6
  44. package/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields.ts +29 -43
  45. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +25 -5
  46. package/components/Resource/Detail/Metadata/KeyValue.vue +12 -23
  47. package/components/Resource/Detail/Metadata/KeyValueRow.vue +144 -0
  48. package/components/Resource/Detail/Metadata/Labels/__tests__/index.test.ts +1 -0
  49. package/components/Resource/Detail/Metadata/Labels/index.vue +1 -0
  50. package/components/Resource/Detail/Metadata/__tests__/KeyValue.test.ts +30 -32
  51. package/components/Resource/Detail/Metadata/__tests__/KeyValueRow.test.ts +108 -0
  52. package/components/Resource/Detail/Metadata/__tests__/composables.test.ts +10 -20
  53. package/components/Resource/Detail/Metadata/__tests__/index.test.ts +12 -5
  54. package/components/Resource/Detail/Metadata/composables.ts +9 -10
  55. package/components/Resource/Detail/Metadata/index.vue +18 -2
  56. package/components/Resource/Detail/Page.vue +35 -21
  57. package/components/Resource/Detail/Preview/Content.vue +63 -0
  58. package/components/Resource/Detail/Preview/Preview.vue +128 -0
  59. package/components/Resource/Detail/Preview/__tests__/Content.spec.ts +71 -0
  60. package/components/Resource/Detail/Preview/__tests__/Preview.spec.ts +121 -0
  61. package/components/Resource/Detail/ResourcePopover/ResourcePopoverCard.vue +141 -0
  62. package/components/Resource/Detail/ResourcePopover/__tests__/ResourcePopoverCard.test.ts +136 -0
  63. package/components/Resource/Detail/ResourcePopover/__tests__/index.test.ts +245 -0
  64. package/components/Resource/Detail/ResourcePopover/index.vue +226 -0
  65. package/components/Resource/Detail/SpacedRow.vue +1 -0
  66. package/components/Resource/Detail/TitleBar/__tests__/composables.test.ts +8 -14
  67. package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +1 -1
  68. package/components/Resource/Detail/TitleBar/composables.ts +3 -6
  69. package/components/Resource/Detail/TitleBar/index.vue +11 -29
  70. package/components/Resource/Detail/ViewOptions/composable.ts +9 -0
  71. package/components/Resource/Detail/ViewOptions/index.vue +41 -0
  72. package/components/Resource/Detail/__tests__/CopyToClipboard.spec.ts +82 -0
  73. package/components/ResourceDetail/Masthead/legacy.vue +0 -19
  74. package/components/ResourceDetail/index.vue +544 -74
  75. package/components/ResourceTable.vue +24 -0
  76. package/components/SlideInPanelManager.vue +10 -3
  77. package/components/SortableTable/index.vue +11 -5
  78. package/components/SortableTable/paging.js +3 -0
  79. package/components/Tabbed/Tab.vue +43 -1
  80. package/components/Tabbed/index.vue +32 -4
  81. package/components/__tests__/Cron/CronExpressionEditor.test.ts +151 -0
  82. package/components/__tests__/Cron/CronExpressionEditorModal.test.ts +81 -0
  83. package/components/__tests__/LazyImage.spec.ts +121 -0
  84. package/components/auth/login/saml.vue +86 -0
  85. package/components/fleet/FleetStatus.vue +4 -0
  86. package/components/form/ClusterAppearance.vue +5 -0
  87. package/components/form/LabeledSelect.vue +8 -8
  88. package/components/form/Members/ClusterPermissionsEditor.vue +1 -1
  89. package/components/form/ProjectMemberEditor.vue +1 -1
  90. package/components/form/ResourceLabeledSelect.vue +19 -6
  91. package/components/form/ResourceTabs/composable.ts +54 -0
  92. package/components/form/ResourceTabs/index.vue +30 -7
  93. package/components/form/SecretSelector.vue +9 -0
  94. package/components/form/Select.vue +13 -10
  95. package/components/form/__tests__/LabeledSelect.test.ts +133 -0
  96. package/components/form/__tests__/Select.test.ts +134 -0
  97. package/components/form/labeled-select-utils/labeled-select-pagination.ts +3 -38
  98. package/components/formatter/FleetApplicationSource.vue +25 -17
  99. package/components/nav/Favorite.vue +4 -0
  100. package/components/nav/NotificationCenter/Notification.vue +1 -27
  101. package/components/nav/WindowManager/index.vue +3 -3
  102. package/composables/useExtensionManager.ts +17 -0
  103. package/config/home-links.js +12 -0
  104. package/config/labels-annotations.js +1 -3
  105. package/config/page-actions.js +0 -1
  106. package/config/product/explorer.js +3 -1
  107. package/config/product/fleet.js +2 -7
  108. package/config/product/manager.js +0 -5
  109. package/config/query-params.js +1 -0
  110. package/config/router/navigation-guards/clusters.js +2 -1
  111. package/config/router/navigation-guards/products.js +1 -1
  112. package/core/extension-manager-impl.js +518 -0
  113. package/core/plugins.js +35 -468
  114. package/core/types.ts +8 -2
  115. package/detail/__tests__/autoscaling.horizontalpodautoscaler.test.ts +1 -0
  116. package/detail/__tests__/provisioning.cattle.io.cluster.test.ts +11 -0
  117. package/detail/__tests__/workload.test.ts +164 -0
  118. package/detail/catalog.cattle.io.app.vue +7 -4
  119. package/detail/configmap.vue +33 -75
  120. package/detail/fleet.cattle.io.bundle.vue +1 -5
  121. package/detail/fleet.cattle.io.cluster.vue +3 -2
  122. package/detail/fleet.cattle.io.gitrepo.vue +76 -49
  123. package/detail/fleet.cattle.io.helmop.vue +78 -49
  124. package/detail/projectsecret.vue +11 -0
  125. package/detail/provisioning.cattle.io.cluster.vue +350 -324
  126. package/detail/secret.vue +49 -308
  127. package/detail/workload/index.vue +38 -21
  128. package/dialog/AddonConfigConfirmationDialog.vue +1 -1
  129. package/dialog/GenericPrompt.vue +1 -1
  130. package/dialog/ImportDialog.vue +9 -2
  131. package/dialog/InstallExtensionDialog.vue +26 -15
  132. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +2 -1
  133. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +224 -0
  134. package/edit/__tests__/resources.cattle.io.restore.test.ts +106 -0
  135. package/edit/cloudcredential.vue +31 -17
  136. package/edit/constraints.gatekeeper.sh.constraint/index.vue +10 -2
  137. package/edit/fleet.cattle.io.cluster.vue +19 -0
  138. package/edit/fleet.cattle.io.gitrepo.vue +28 -22
  139. package/edit/fleet.cattle.io.helmop.vue +78 -56
  140. package/edit/logging.banzaicloud.io.output/index.vue +1 -1
  141. package/edit/logging.banzaicloud.io.output/providers/awsElasticsearch.vue +5 -6
  142. package/edit/monitoring.coreos.com.alertmanagerconfig/index.vue +12 -11
  143. package/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig.vue +11 -1
  144. package/edit/networking.k8s.io.ingress/Certificate.vue +9 -11
  145. package/edit/networking.k8s.io.ingress/DefaultBackend.vue +8 -3
  146. package/edit/networking.k8s.io.ingress/Rule.vue +2 -5
  147. package/edit/networking.k8s.io.ingress/RulePath.vue +17 -11
  148. package/edit/networking.k8s.io.networkpolicy/PolicyRuleTarget.vue +11 -10
  149. package/edit/networking.k8s.io.networkpolicy/PolicyRules.vue +1 -3
  150. package/edit/networking.k8s.io.networkpolicy/index.vue +17 -17
  151. package/edit/provisioning.cattle.io.cluster/index.vue +14 -19
  152. package/edit/provisioning.cattle.io.cluster/rke2.vue +31 -15
  153. package/edit/provisioning.cattle.io.cluster/tabs/AgentConfiguration.vue +9 -7
  154. package/edit/provisioning.cattle.io.cluster/tabs/DirectoryConfig.vue +10 -12
  155. package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +39 -38
  156. package/edit/provisioning.cattle.io.cluster/tabs/etcd/S3Config.vue +41 -19
  157. package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +16 -3
  158. package/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryConfigs.vue +30 -31
  159. package/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryMirrors.vue +9 -10
  160. package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +1 -3
  161. package/edit/provisioning.cattle.io.cluster/tabs/upgrade/DrainOptions.vue +16 -9
  162. package/edit/resources.cattle.io.restore.vue +5 -8
  163. package/edit/workload/index.vue +5 -14
  164. package/list/__tests__/workload.test.ts +1 -0
  165. package/list/provisioning.cattle.io.cluster.vue +1 -69
  166. package/list/workload.vue +8 -1
  167. package/machine-config/__tests__/vmwarevsphere.test.ts +5 -7
  168. package/machine-config/components/GCEImage.vue +6 -5
  169. package/machine-config/google.vue +20 -7
  170. package/machine-config/vmwarevsphere.vue +7 -17
  171. package/mixins/__tests__/chart.test.ts +139 -1
  172. package/mixins/chart.js +58 -20
  173. package/mixins/resource-fetch-api-pagination.js +3 -4
  174. package/models/__tests__/chart.test.ts +111 -80
  175. package/models/__tests__/fleet.cattle.io.helmop.test.ts +224 -0
  176. package/models/__tests__/namespace.test.ts +69 -0
  177. package/models/__tests__/node.test.ts +7 -63
  178. package/models/apps.statefulset.js +8 -10
  179. package/models/catalog.cattle.io.app.js +1 -1
  180. package/models/catalog.cattle.io.operation.js +1 -1
  181. package/models/chart.js +41 -21
  182. package/models/cloudcredential.js +2 -163
  183. package/models/cluster/node.js +7 -7
  184. package/models/cluster.x-k8s.io.machine.js +3 -3
  185. package/models/compliance.cattle.io.clusterscan.js +2 -2
  186. package/models/configmap.js +4 -0
  187. package/models/constraints.gatekeeper.sh.constraint.js +1 -1
  188. package/models/fleet-application.js +16 -63
  189. package/models/fleet.cattle.io.bundle.js +1 -38
  190. package/models/fleet.cattle.io.gitrepo.js +19 -1
  191. package/models/fleet.cattle.io.helmop.js +30 -22
  192. package/models/management.cattle.io.project.js +12 -0
  193. package/models/management.cattle.io.setting.js +4 -0
  194. package/models/namespace.js +30 -0
  195. package/models/persistentvolumeclaim.js +1 -1
  196. package/models/pod.js +2 -2
  197. package/models/provisioning.cattle.io.cluster.js +16 -40
  198. package/models/rke.cattle.io.etcdsnapshot.js +1 -1
  199. package/models/secret.js +4 -0
  200. package/models/storage.k8s.io.storageclass.js +2 -2
  201. package/models/workload.js +6 -3
  202. package/package.json +19 -18
  203. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +26 -10
  204. package/pages/c/_cluster/apps/charts/AppChartCardSubHeader.vue +4 -1
  205. package/pages/c/_cluster/apps/charts/__tests__/AppChartCardFooter.spec.js +41 -0
  206. package/pages/c/_cluster/apps/charts/chart.vue +440 -183
  207. package/pages/c/_cluster/apps/charts/index.vue +1 -0
  208. package/pages/c/_cluster/apps/charts/install.vue +7 -6
  209. package/pages/c/_cluster/explorer/projectsecret.vue +3 -13
  210. package/pages/c/_cluster/explorer/tools/__tests__/index.test.ts +102 -12
  211. package/pages/c/_cluster/explorer/tools/index.vue +145 -254
  212. package/pages/c/_cluster/fleet/__tests__/index.test.ts +608 -314
  213. package/pages/c/_cluster/fleet/index.vue +103 -44
  214. package/pages/c/_cluster/manager/cloudCredential/index.vue +20 -60
  215. package/pages/c/_cluster/manager/drivers/kontainerDriver/index.vue +12 -2
  216. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +11 -4
  217. package/pages/c/_cluster/uiplugins/__tests__/index.spec.ts +318 -0
  218. package/pages/c/_cluster/uiplugins/index.vue +256 -387
  219. package/pages/home.vue +1 -9
  220. package/plugins/dashboard-store/actions.js +42 -22
  221. package/plugins/dashboard-store/resource-class.js +80 -0
  222. package/plugins/steve/__tests__/getters.test.ts +1 -1
  223. package/plugins/steve/__tests__/subscribe.spec.ts +259 -1
  224. package/plugins/steve/getters.js +8 -2
  225. package/plugins/steve/resourceWatcher.js +10 -3
  226. package/plugins/steve/subscribe.js +192 -19
  227. package/plugins/steve/worker/web-worker.advanced.js +2 -0
  228. package/public/index.html +2 -1
  229. package/rancher-components/Card/Card.vue +1 -19
  230. package/rancher-components/Form/Checkbox/Checkbox.vue +1 -1
  231. package/rancher-components/Form/Radio/RadioButton.vue +1 -1
  232. package/rancher-components/Form/Radio/RadioGroup.vue +1 -1
  233. package/rancher-components/LabeledTooltip/LabeledTooltip.vue +1 -11
  234. package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.test.ts +53 -0
  235. package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.vue +65 -0
  236. package/rancher-components/Pill/RcCounterBadge/index.ts +1 -0
  237. package/rancher-components/Pill/RcCounterBadge/types.ts +7 -0
  238. package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.test.ts +15 -0
  239. package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +65 -0
  240. package/rancher-components/Pill/RcStatusBadge/index.ts +2 -0
  241. package/rancher-components/Pill/RcStatusBadge/types.ts +5 -0
  242. package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.test.ts +33 -0
  243. package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.vue +75 -0
  244. package/rancher-components/Pill/RcStatusIndicator/index.ts +2 -0
  245. package/rancher-components/Pill/RcStatusIndicator/types.ts +7 -0
  246. package/rancher-components/Pill/RcTag/RcTag.test.ts +64 -0
  247. package/rancher-components/Pill/RcTag/RcTag.vue +94 -0
  248. package/rancher-components/Pill/RcTag/index.ts +1 -0
  249. package/rancher-components/Pill/RcTag/types.ts +9 -0
  250. package/rancher-components/Pill/types.ts +3 -0
  251. package/rancher-components/RcButton/RcButton.vue +1 -1
  252. package/rancher-components/RcDropdown/RcDropdown.test.ts +98 -0
  253. package/rancher-components/RcDropdown/RcDropdown.vue +5 -0
  254. package/rancher-components/RcDropdown/RcDropdownItem.vue +7 -1
  255. package/rancher-components/RcDropdown/RcDropdownItemCheckbox.vue +2 -1
  256. package/rancher-components/RcDropdown/RcDropdownItemSelect.vue +2 -1
  257. package/rancher-components/RcDropdown/useDropdownContext.ts +21 -0
  258. package/rancher-components/RcDropdown/useDropdownItem.ts +30 -1
  259. package/rancher-components/RcItemCard/RcItemCard.test.ts +20 -0
  260. package/rancher-components/RcItemCard/RcItemCard.vue +41 -6
  261. package/rancher-components/RcItemCard/RcItemCardAction.vue +12 -0
  262. package/store/__tests__/catalog.test.ts +156 -1
  263. package/store/aws.js +19 -8
  264. package/store/catalog.js +10 -5
  265. package/store/type-map.js +3 -15
  266. package/types/extension-manager.ts +26 -0
  267. package/types/resources/settings.d.ts +1 -1
  268. package/types/shell/index.d.ts +149 -44
  269. package/types/uiplugins.ts +73 -0
  270. package/utils/__tests__/back-off.test.ts +354 -0
  271. package/utils/__tests__/kontainer.test.ts +19 -0
  272. package/utils/__tests__/product.test.ts +129 -0
  273. package/utils/__tests__/resource.test.ts +87 -0
  274. package/utils/__tests__/uiplugins.test.ts +84 -0
  275. package/utils/alertmanagerconfig.js +2 -2
  276. package/utils/auth.js +3 -76
  277. package/utils/back-off.ts +176 -0
  278. package/utils/dynamic-importer.js +8 -0
  279. package/utils/kontainer.ts +3 -5
  280. package/utils/product.ts +39 -0
  281. package/utils/resource.ts +35 -0
  282. package/utils/select.js +0 -24
  283. package/utils/style.ts +3 -0
  284. package/utils/uiplugins.ts +29 -2
  285. package/utils/validators/__tests__/setting.test.js +92 -0
  286. package/utils/validators/formRules/__tests__/index.test.ts +91 -7
  287. package/utils/validators/formRules/index.ts +84 -8
  288. package/utils/validators/setting.js +17 -0
  289. package/vue.config.js +1 -1
  290. package/cloud-credential/__tests__/harvester.test.ts +0 -18
  291. package/components/Resource/Detail/Metadata/Rectangle.vue +0 -34
  292. package/components/Resource/Detail/Metadata/__tests__/Rectangle.test.ts +0 -24
  293. package/components/ResourceDetail/Masthead/__tests__/legacy.test.ts +0 -65
  294. package/components/ResourceDetail/__tests__/index.test.ts +0 -135
  295. package/components/ResourceDetail/legacy.vue +0 -562
  296. package/components/formatter/CloudCredExpired.vue +0 -69
  297. package/pages/explorer/resource/detail/configmap.vue +0 -42
  298. package/pages/explorer/resource/detail/projectsecret.vue +0 -9
  299. package/pages/explorer/resource/detail/secret.vue +0 -63
  300. package/utils/aws.js +0 -0
  301. /package/components/{ForceDirectedTreeChart.vue → ForceDirectedTreeChart/index.vue} +0 -0
@@ -1,5 +1,5 @@
1
1
  <script lang="ts" setup>
2
- import { computed, onBeforeUnmount, watch } from 'vue';
2
+ import { computed, onBeforeUnmount, watch, useTemplateRef } from 'vue';
3
3
  import { useStore } from 'vuex';
4
4
  import {
5
5
  DEFAULT_FOCUS_TRAP_OPTS,
@@ -10,6 +10,9 @@ import { useRouter } from 'vue-router';
10
10
 
11
11
  const HEADER_HEIGHT = 55;
12
12
 
13
+ const slideInPanelManager = useTemplateRef('SlideInPanelManager');
14
+ const slideInPanelManagerClose = useTemplateRef('SlideInPanelManagerClose');
15
+
13
16
  const store = useStore();
14
17
  const isOpen = computed(() => store.getters['slideInPanel/isOpen']);
15
18
  const isClosing = computed(() => store.getters['slideInPanel/isClosing']);
@@ -72,7 +75,9 @@ watch(
72
75
  }
73
76
 
74
77
  return returnFocusSelector || '.dashboard-root';
75
- }
78
+ },
79
+ // putting the initial focus on the first element that is not conditionally displayed
80
+ initialFocus: slideInPanelManagerClose.value
76
81
  };
77
82
 
78
83
  useWatcherBasedSetupFocusTrapWithDestroyIncluded(
@@ -83,7 +88,7 @@ watch(
83
88
 
84
89
  return isOpen?.value && !isClosing?.value;
85
90
  },
86
- '#slide-in-panel-manager',
91
+ slideInPanelManager.value as HTMLElement,
87
92
  opts,
88
93
  false
89
94
  );
@@ -128,6 +133,7 @@ function closePanel() {
128
133
  <Teleport to="#slides">
129
134
  <div
130
135
  id="slide-in-panel-manager"
136
+ ref="SlideInPanelManager"
131
137
  @keydown.escape="closePanel"
132
138
  >
133
139
  <div
@@ -155,6 +161,7 @@ function closePanel() {
155
161
  {{ panelTitle }}
156
162
  </div>
157
163
  <i
164
+ ref="SlideInPanelManagerClose"
158
165
  class="icon icon-close"
159
166
  data-testid="slide-in-close"
160
167
  :tabindex="isOpen ? 0 : -1"
@@ -3,7 +3,7 @@ import { mapGetters, useStore } from 'vuex';
3
3
  import { defineAsyncComponent, ref, onMounted, onBeforeUnmount } from 'vue';
4
4
  import day from 'dayjs';
5
5
  import isEmpty from 'lodash/isEmpty';
6
- import { dasherize, ucFirst } from '@shell/utils/string';
6
+ import { dasherize, ucFirst, randomStr } from '@shell/utils/string';
7
7
  import { get, clone } from '@shell/utils/object';
8
8
  import { removeObject } from '@shell/utils/array';
9
9
  import { Checkbox } from '@components/Form/Checkbox';
@@ -26,6 +26,7 @@ import ButtonMultiAction from '@shell/components/ButtonMultiAction.vue';
26
26
  import ActionMenu from '@shell/components/ActionMenuShell.vue';
27
27
  import { useRuntimeFlag } from '@shell/composables/useRuntimeFlag';
28
28
  import ActionDropdownShell from '@shell/components/ActionDropdownShell.vue';
29
+ import { useTabCountUpdater } from '@shell/components/form/ResourceTabs/composable';
29
30
 
30
31
  // Uncomment for table performance debugging
31
32
  // import tableDebug from './debug';
@@ -51,7 +52,7 @@ export default {
51
52
  'group-value-change',
52
53
  'selection',
53
54
  'rowClick',
54
- 'enter',
55
+ 'enter'
55
56
  ],
56
57
 
57
58
  components: {
@@ -432,6 +433,7 @@ export default {
432
433
  $main?.addEventListener('scroll', this._onScroll);
433
434
 
434
435
  this.debouncedPaginationChanged();
436
+ this.updateTabCount(this.totalRows);
435
437
  },
436
438
 
437
439
  beforeUnmount() {
@@ -445,6 +447,7 @@ export default {
445
447
  const $main = document.querySelector('main');
446
448
 
447
449
  $main?.removeEventListener('scroll', this._onScroll);
450
+ this.clearTabCount();
448
451
  },
449
452
 
450
453
  watch: {
@@ -559,10 +562,13 @@ export default {
559
562
 
560
563
  const store = useStore();
561
564
  const { featureDropdownMenu } = useRuntimeFlag(store);
565
+ const { updateTabCount, clearTabCount } = useTabCountUpdater();
562
566
 
563
567
  return {
564
568
  table,
565
569
  featureDropdownMenu,
570
+ updateTabCount,
571
+ clearTabCount
566
572
  };
567
573
  },
568
574
 
@@ -735,7 +741,7 @@ export default {
735
741
  grp.rows.forEach((row) => {
736
742
  const rowData = {
737
743
  row,
738
- key: this.get(row, this.keyField),
744
+ key: this.get(row, this.keyField) ?? randomStr(),
739
745
  showSubRow: this.showSubRow(row, this.keyField),
740
746
  canRunBulkActionOfInterest: this.canRunBulkActionOfInterest(row),
741
747
  columns: []
@@ -1038,7 +1044,7 @@ export default {
1038
1044
  handleActionButtonClick(i, event) {
1039
1045
  // Each row in the table gets its own ref with
1040
1046
  // a number based on its index. If you are using
1041
- // an ActionMenu that doen't have a dependency on Vuex,
1047
+ // an ActionMenu that doesn't have a dependency on Vuex,
1042
1048
  // these refs are useful because you can reuse the
1043
1049
  // same ActionMenu component on a page with many different
1044
1050
  // target elements in a list,
@@ -1398,7 +1404,7 @@ export default {
1398
1404
  </slot>
1399
1405
  <template
1400
1406
  v-for="(row, i) in groupedRows.rows"
1401
- :key="i"
1407
+ :key="row.key"
1402
1408
  >
1403
1409
  <slot
1404
1410
  name="main-row"
@@ -99,6 +99,9 @@ export default {
99
99
  this.debouncedPaginationChanged();
100
100
  },
101
101
 
102
+ totalRows() {
103
+ this.updateTabCount(this.totalRows);
104
+ }
102
105
  },
103
106
 
104
107
  methods: {
@@ -1,4 +1,6 @@
1
1
  <script>
2
+ import { useTabCountWatcher } from '@shell/components/form/ResourceTabs/composable';
3
+
2
4
  export default {
3
5
  inject: ['addTab', 'removeTab', 'sideTabs'],
4
6
 
@@ -43,6 +45,20 @@ export default {
43
45
  required: false,
44
46
  type: Number
45
47
  },
48
+ /**
49
+ * False to hide the count from being displayed in a tab.
50
+ * Number override/display the number as the count on the tab.
51
+ */
52
+ count: {
53
+ default: undefined,
54
+ type: [Number, Boolean]
55
+ }
56
+ },
57
+
58
+ setup(props) {
59
+ const { count, isCountVisible } = useTabCountWatcher();
60
+
61
+ return { inferredCount: count, isInferredCountVisible: isCountVisible };
46
62
  },
47
63
 
48
64
  data() {
@@ -50,7 +66,7 @@ export default {
50
66
  },
51
67
 
52
68
  computed: {
53
- labelDisplay() {
69
+ baseLabelDisplay() {
54
70
  if ( this.labelKey ) {
55
71
  return this.$store.getters['i18n/t'](this.labelKey);
56
72
  }
@@ -62,12 +78,38 @@ export default {
62
78
  return this.name;
63
79
  },
64
80
 
81
+ labelDisplay() {
82
+ const baseLabel = this.baseLabelDisplay;
83
+
84
+ if ( this.displayCount === false ) {
85
+ return baseLabel;
86
+ }
87
+
88
+ return `${ baseLabel } (${ this.displayCount })`;
89
+ },
90
+
65
91
  shouldShowHeader() {
66
92
  if ( this.showHeader !== null ) {
67
93
  return this.showHeader;
68
94
  }
69
95
 
70
96
  return this.sideTabs || false;
97
+ },
98
+
99
+ displayCount() {
100
+ if (this.count === false) {
101
+ return false;
102
+ }
103
+
104
+ if (typeof this.count === 'number') {
105
+ return this.count;
106
+ }
107
+
108
+ if (this.isInferredCountVisible) {
109
+ return this.inferredCount;
110
+ }
111
+
112
+ return false;
71
113
  }
72
114
  },
73
115
 
@@ -71,6 +71,14 @@ export default {
71
71
  showExtensionTabs: {
72
72
  type: Boolean,
73
73
  default: true,
74
+ },
75
+ /**
76
+ * Inherited global identifier prefix for tests
77
+ * Define a term based on the parent component to avoid conflicts on multiple components
78
+ */
79
+ componentTestid: {
80
+ type: String,
81
+ default: 'tabbed'
74
82
  }
75
83
  },
76
84
 
@@ -253,8 +261,12 @@ export default {
253
261
 
254
262
  <template>
255
263
  <div
256
- :class="{'side-tabs': !!sideTabs, 'tabs-only': tabsOnly }"
257
- data-testid="tabbed"
264
+ class="tabbed-container"
265
+ :class="{
266
+ 'side-tabs': !!sideTabs,
267
+ 'tabs-only': tabsOnly
268
+ }"
269
+ :data-testid="componentTestid"
258
270
  >
259
271
  <ul
260
272
  v-if="!hideTabs"
@@ -262,7 +274,7 @@ export default {
262
274
  role="tablist"
263
275
  class="tabs"
264
276
  :class="{'clearfix':!sideTabs, 'vertical': sideTabs, 'horizontal': !sideTabs}"
265
- data-testid="tabbed-block"
277
+ :data-testid="`${componentTestid}-block`"
266
278
  tabindex="0"
267
279
  @keydown.right.prevent="selectNext(1)"
268
280
  @keydown.left.prevent="selectNext(-1)"
@@ -287,7 +299,9 @@ export default {
287
299
  @click.prevent="select(tab.name, $event)"
288
300
  @keyup.enter.space="select(tab.name, $event)"
289
301
  >
290
- <span>{{ tab.labelDisplay }}</span>
302
+ <span>
303
+ {{ tab.labelDisplay }}
304
+ </span>
291
305
  <span
292
306
  v-if="tab.badge"
293
307
  class="tab-badge"
@@ -369,6 +383,10 @@ export default {
369
383
  </template>
370
384
 
371
385
  <style lang="scss" scoped>
386
+ .tabbed-container {
387
+ min-width: fit-content;
388
+ }
389
+
372
390
  .tabs {
373
391
  list-style-type: none;
374
392
  margin: 0;
@@ -541,6 +559,7 @@ export default {
541
559
  list-style: none;
542
560
  padding: 0;
543
561
  margin-top: auto;
562
+ z-index: z-index('default');
544
563
 
545
564
  li {
546
565
  display: flex;
@@ -550,16 +569,25 @@ export default {
550
569
  flex: 1 1;
551
570
  display: flex;
552
571
  justify-content: center;
572
+
573
+ &:focus-visible {
574
+ @include focus-outline;
575
+ }
553
576
  }
554
577
 
555
578
  button:first-of-type {
556
579
  border-top: solid 1px var(--border);
557
580
  border-right: solid 1px var(--border);
581
+ border-top-left-radius: 0;
558
582
  border-top-right-radius: 0;
583
+ border-bottom-right-radius: 0;
559
584
  }
560
585
  button:last-of-type {
561
586
  border-top: solid 1px var(--border);
587
+ border-top-right-radius: 0;
562
588
  border-top-left-radius: 0;
589
+ border-bottom-left-radius: 0;
590
+ border-bottom-right-radius: 0;
563
591
  }
564
592
  }
565
593
  }
@@ -0,0 +1,151 @@
1
+ /* eslint-disable jest/no-hooks */
2
+ import { mount, VueWrapper } from '@vue/test-utils';
3
+ import { nextTick } from 'vue';
4
+ import { createStore } from 'vuex';
5
+ import CronExpressionEditor from '@shell/components/Cron/CronExpressionEditor.vue';
6
+ import type { CronField } from '@shell/components/Cron/types';
7
+
8
+ const translations: Record<string, string> = {
9
+ 'component.cron.expressionEditor.label.minute': 'Minute',
10
+ 'component.cron.expressionEditor.label.hour': 'Hour',
11
+ 'component.cron.expressionEditor.label.dayOfMonth': 'Day of Month',
12
+ 'component.cron.expressionEditor.label.month': 'Month',
13
+ 'component.cron.expressionEditor.label.dayOfWeek': 'Day of Week',
14
+ 'component.cron.expressionEditor.invalidValue': 'Invalid value',
15
+ };
16
+
17
+ const store = createStore({});
18
+
19
+ interface CronExpressionEditorVm extends InstanceType<typeof CronExpressionEditor> {
20
+ cronValues: Record<CronField, string>;
21
+ handleInput: (field: CronField, value: string) => void;
22
+ isValid: boolean;
23
+ readableCron: string;
24
+ errors: Record<CronField, boolean>;
25
+ focusedField: Record<CronField, boolean>;
26
+ tooltipRefs: Record<CronField, unknown>;
27
+ popperInstances: Record<CronField, unknown>;
28
+ handleFocus: (field: CronField) => void;
29
+ handleBlur: (field: CronField) => void;
30
+ }
31
+
32
+ describe('cronExpressionEditor', () => {
33
+ let wrapper: VueWrapper<CronExpressionEditorVm>;
34
+
35
+ const factory = (props: Partial<CronExpressionEditorVm> = {}) => mount(CronExpressionEditor, {
36
+ global: {
37
+ plugins: [store],
38
+ stubs: {
39
+ CronTooltip: true,
40
+ LabeledInput: {
41
+ name: 'LabeledInput',
42
+ props: ['label', 'tooltip', 'type', 'value'],
43
+ template: `
44
+ <div>
45
+ <label>{{ label }}</label>
46
+ <input ref="value" :value="value" />
47
+ </div>
48
+ `
49
+ }
50
+ },
51
+ mocks: { t: (key: string) => translations[key] || key },
52
+ },
53
+ props: { cronExpression: '0 0 * * *', ...props },
54
+ }) as VueWrapper<CronExpressionEditorVm>;
55
+
56
+ afterEach(() => wrapper?.unmount());
57
+
58
+ const getEmitted = (event: string) => wrapper.emitted(event) as unknown[][] || [];
59
+
60
+ it('renders 5 input fields with correct labels', () => {
61
+ wrapper = factory();
62
+ const labels = wrapper.findAll('label').map((l) => l.text());
63
+
64
+ expect(labels).toStrictEqual(['Minute', 'Hour', 'Day of Month', 'Month', 'Day of Week']);
65
+ });
66
+
67
+ it('initializes cron values and emits initial events', () => {
68
+ wrapper = factory();
69
+ const vm = wrapper.vm;
70
+
71
+ expect(vm.cronValues).toStrictEqual({
72
+ minute: '0', hour: '0', dayOfMonth: '*', month: '*', dayOfWeek: '*'
73
+ });
74
+ expect(vm.isValid).toBe(true);
75
+ expect(vm.readableCron).toContain('12:00');
76
+
77
+ expect(getEmitted('update:cronExpression')[0][0]).toBe('0 0 * * *');
78
+ expect(typeof getEmitted('update:readableCron')[0][0]).toBe('string');
79
+ expect(getEmitted('update:isValid')[0][0]).toBe(true);
80
+ });
81
+
82
+ it('emits correct events when cron value changes', async() => {
83
+ wrapper = factory();
84
+ const vm = wrapper.vm;
85
+
86
+ vm.handleInput('minute', '5');
87
+ await nextTick();
88
+
89
+ expect(getEmitted('update:cronExpression')[1][0]).toBe('5 0 * * *');
90
+ expect(getEmitted('update:readableCron')[1][0]).toBe(vm.readableCron);
91
+ expect(getEmitted('update:isValid')[1][0]).toBe(true);
92
+ });
93
+
94
+ it('validates individual fields correctly', () => {
95
+ wrapper = factory();
96
+ const vm = wrapper.vm;
97
+
98
+ vm.handleInput('minute', '0');
99
+ expect(vm.errors.minute).toBe(false);
100
+
101
+ vm.handleInput('minute', '61');
102
+ expect(vm.errors.minute).toBe(true);
103
+ });
104
+
105
+ it('handles invalid cron expressions gracefully', async() => {
106
+ wrapper = factory({ cronExpression: '61 * * * *' });
107
+ const vm = wrapper.vm;
108
+
109
+ await nextTick();
110
+ expect(vm.isValid).toBe(false);
111
+ });
112
+
113
+ it('updates readableCron correctly for valid and invalid inputs', async() => {
114
+ wrapper = factory({ cronExpression: '0 12 * * *' });
115
+ const vm = wrapper.vm;
116
+
117
+ await nextTick();
118
+ expect(vm.readableCron).toContain('12:00');
119
+
120
+ vm.handleInput('hour', '25');
121
+ await nextTick();
122
+ expect(vm.isValid).toBe(false);
123
+ });
124
+
125
+ it('shows tooltip and manages popper on focus/blur', async() => {
126
+ wrapper = factory();
127
+ const vm = wrapper.vm;
128
+
129
+ await vm.handleFocus('minute');
130
+ expect(vm.focusedField.minute).toBe(true);
131
+ expect(vm.tooltipRefs.minute).not.toBeNull();
132
+ expect(vm.popperInstances.minute).not.toBeNull();
133
+
134
+ vm.handleBlur('minute');
135
+ await nextTick();
136
+ expect(vm.focusedField.minute).toBe(false);
137
+ expect(vm.popperInstances.minute).toBeNull();
138
+ });
139
+
140
+ it('displays error tooltip for invalid input', async() => {
141
+ wrapper = factory();
142
+ const vm = wrapper.vm;
143
+
144
+ vm.handleInput('minute', '61');
145
+ await nextTick();
146
+
147
+ const inputWrapper = wrapper.findComponent({ name: 'LabeledInput' });
148
+
149
+ expect(inputWrapper.props('tooltip')).toBe('Invalid value');
150
+ });
151
+ });
@@ -0,0 +1,81 @@
1
+ /* eslint-disable jest/no-hooks */
2
+ import { mount, VueWrapper } from '@vue/test-utils';
3
+ import { createStore } from 'vuex';
4
+ import CronExpressionEditorModal from '@shell/components/Cron/CronExpressionEditorModal.vue';
5
+
6
+ interface CronExpressionEditorModalVm extends InstanceType<typeof CronExpressionEditorModal> {
7
+ localCron: string;
8
+ confirmCron?: () => void;
9
+ closeModal?: () => void;
10
+ }
11
+
12
+ const store = createStore({});
13
+
14
+ describe('cronExpressionEditorModal', () => {
15
+ let modalsDiv: HTMLElement;
16
+ let wrapper: VueWrapper<CronExpressionEditorModalVm>;
17
+
18
+ const factory = (props: Partial<CronExpressionEditorModalVm> = {}) => mount(CronExpressionEditorModal, {
19
+ global: {
20
+ plugins: [store],
21
+ stubs: {
22
+ AppModal: true,
23
+ CronExpressionEditor: true,
24
+ },
25
+ },
26
+ props: {
27
+ cronExpression: '0 0 * * *',
28
+ show: true,
29
+ ...props,
30
+ },
31
+ }) as VueWrapper<CronExpressionEditorModalVm>;
32
+
33
+ const getEmitted = (event: string) => wrapper.emitted(event) as unknown[][] || [];
34
+
35
+ beforeEach(() => {
36
+ modalsDiv = document.createElement('div');
37
+ modalsDiv.id = 'modals';
38
+ document.body.appendChild(modalsDiv);
39
+ });
40
+
41
+ afterEach(() => {
42
+ wrapper?.unmount();
43
+ modalsDiv.remove();
44
+ });
45
+
46
+ it('renders modal with correct initial props', () => {
47
+ wrapper = factory();
48
+ expect(wrapper.props('cronExpression')).toBe('0 0 * * *');
49
+ expect(wrapper.props('show')).toBe(true);
50
+ });
51
+
52
+ it('updates localCron when cronExpression prop changes', async() => {
53
+ wrapper = factory();
54
+ await wrapper.setProps({ cronExpression: '*/5 * * * *' });
55
+ expect(wrapper.vm.localCron).toBe('*/5 * * * *');
56
+ });
57
+
58
+ it('emits update:cronExpression and update:show on confirm', async() => {
59
+ wrapper = factory();
60
+ await wrapper.vm.confirmCron?.();
61
+
62
+ const cronEmits = getEmitted('update:cronExpression');
63
+ const showEmits = getEmitted('update:show');
64
+
65
+ expect(cronEmits).toHaveLength(1);
66
+ expect(cronEmits[0][0]).toBe('0 0 * * *');
67
+
68
+ expect(showEmits).toHaveLength(1);
69
+ expect(showEmits[0][0]).toBe(false);
70
+ });
71
+
72
+ it('emits update:show on cancel', async() => {
73
+ wrapper = factory();
74
+ await wrapper.vm.closeModal?.();
75
+
76
+ const showEmits = getEmitted('update:show');
77
+
78
+ expect(showEmits).toHaveLength(1);
79
+ expect(showEmits[0][0]).toBe(false);
80
+ });
81
+ });
@@ -0,0 +1,121 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import LazyImage from '@shell/components/LazyImage.vue';
3
+
4
+ describe('component: LazyImage.vue', () => {
5
+ const initialSrc = 'initial.jpg';
6
+ const src = 'test.jpg';
7
+ const errorSrc = 'error.jpg';
8
+
9
+ beforeEach(() => {
10
+ // Clear all mocks before each test to ensure test isolation
11
+ jest.clearAllMocks();
12
+ });
13
+
14
+ it('renders the initial source image', () => {
15
+ const wrapper = mount(LazyImage, {
16
+ propsData: {
17
+ initialSrc,
18
+ src,
19
+ errorSrc
20
+ },
21
+ });
22
+
23
+ const img = wrapper.find('img');
24
+
25
+ expect(img.attributes('src')).toBe(initialSrc);
26
+ });
27
+
28
+ it('does not load the main src image if not in viewport', async() => {
29
+ const wrapper = mount(LazyImage, {
30
+ propsData: {
31
+ initialSrc,
32
+ src,
33
+ errorSrc
34
+ },
35
+ });
36
+
37
+ const callback = window.IntersectionObserver.mock.calls[0][0];
38
+
39
+ // eslint-disable-next-line node/no-callback-literal
40
+ callback([{ isIntersecting: false }]);
41
+ await wrapper.vm.$nextTick();
42
+
43
+ const img = wrapper.find('img');
44
+
45
+ expect(img.attributes('src')).toBe(initialSrc);
46
+ });
47
+
48
+ it('loads the main src image when it enters the viewport', async() => {
49
+ const wrapper = mount(LazyImage, {
50
+ propsData: {
51
+ initialSrc,
52
+ src,
53
+ errorSrc
54
+ },
55
+ });
56
+
57
+ // Manually trigger the intersection observer
58
+ const callback = window.IntersectionObserver.mock.calls[0][0];
59
+
60
+ // eslint-disable-next-line node/no-callback-literal
61
+ callback([{ isIntersecting: true }]);
62
+
63
+ await wrapper.vm.$nextTick();
64
+
65
+ const img = wrapper.find('img');
66
+
67
+ expect(img.attributes('src')).toBe(src);
68
+ });
69
+
70
+ it('loads the error image if the main src image fails to load', async() => {
71
+ const wrapper = mount(LazyImage, {
72
+ propsData: {
73
+ initialSrc,
74
+ src,
75
+ errorSrc
76
+ },
77
+ });
78
+
79
+ // Manually trigger the intersection observer
80
+ const callback = window.IntersectionObserver.mock.calls[0][0];
81
+
82
+ // eslint-disable-next-line node/no-callback-literal
83
+ callback([{ isIntersecting: true }]);
84
+
85
+ await wrapper.vm.$nextTick();
86
+
87
+ const img = wrapper.find('img');
88
+
89
+ img.trigger('error');
90
+
91
+ await wrapper.vm.$nextTick();
92
+
93
+ expect(img.attributes('src')).toBe(errorSrc);
94
+ });
95
+
96
+ it('loads a new src image if the src prop changes', async() => {
97
+ const wrapper = mount(LazyImage, {
98
+ propsData: {
99
+ initialSrc,
100
+ src,
101
+ errorSrc
102
+ },
103
+ });
104
+
105
+ // Manually trigger the intersection observer
106
+ const callback = window.IntersectionObserver.mock.calls[0][0];
107
+
108
+ // eslint-disable-next-line node/no-callback-literal
109
+ callback([{ isIntersecting: true }]);
110
+
111
+ await wrapper.vm.$nextTick();
112
+
113
+ const newSrc = 'new.jpg';
114
+
115
+ await wrapper.setProps({ src: newSrc });
116
+
117
+ const img = wrapper.find('img');
118
+
119
+ expect(img.attributes('src')).toBe(newSrc);
120
+ });
121
+ });