@rancher/shell 3.0.8-rc.8 → 3.0.8

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 (260) hide show
  1. package/apis/impl/apis.ts +61 -0
  2. package/apis/index.ts +40 -0
  3. package/apis/intf/modal.ts +90 -0
  4. package/apis/intf/shell.ts +36 -0
  5. package/apis/intf/slide-in.ts +98 -0
  6. package/apis/intf/system.ts +41 -0
  7. package/apis/shell/__tests__/modal.test.ts +80 -0
  8. package/apis/shell/__tests__/notifications.test.ts +71 -0
  9. package/apis/shell/__tests__/slide-in.test.ts +54 -0
  10. package/apis/shell/__tests__/system.test.ts +129 -0
  11. package/apis/shell/index.ts +38 -0
  12. package/apis/shell/modal.ts +41 -0
  13. package/apis/shell/notifications.ts +65 -0
  14. package/apis/shell/slide-in.ts +33 -0
  15. package/apis/shell/system.ts +65 -0
  16. package/apis/vue-shim.d.ts +11 -0
  17. package/assets/brand/suse/dark/rancher-logo.svg +1 -64
  18. package/assets/styles/global/_tooltip.scss +6 -1
  19. package/assets/translations/en-us.yaml +14 -1
  20. package/components/ActionMenuShell.vue +3 -1
  21. package/components/BackLink.vue +8 -0
  22. package/components/BannerGraphic.vue +1 -5
  23. package/components/BrandImage.vue +17 -6
  24. package/components/Cron/CronExpressionEditor.vue +1 -1
  25. package/components/Cron/CronExpressionEditorModal.vue +1 -1
  26. package/components/CruResource.vue +8 -1
  27. package/components/Drawer/ResourceDetailDrawer/ConfigTab.vue +1 -0
  28. package/components/Drawer/ResourceDetailDrawer/__tests__/composables.test.ts +50 -1
  29. package/components/Drawer/ResourceDetailDrawer/composables.ts +19 -0
  30. package/components/Drawer/ResourceDetailDrawer/index.vue +4 -1
  31. package/components/Drawer/ResourceDetailDrawer/types.ts +2 -1
  32. package/components/LocaleSelector.vue +2 -2
  33. package/components/ModalManager.vue +11 -1
  34. package/components/Questions/__tests__/Yaml.test.ts +1 -1
  35. package/components/Questions/__tests__/index.test.ts +159 -0
  36. package/components/RelatedResources.vue +5 -0
  37. package/components/Resource/Detail/Metadata/Annotations/index.vue +2 -2
  38. package/components/Resource/Detail/Metadata/Labels/index.vue +2 -2
  39. package/components/Resource/Detail/Metadata/index.vue +3 -3
  40. package/components/Resource/Detail/ResourcePopover/index.vue +5 -1
  41. package/components/Resource/Detail/composables.ts +2 -2
  42. package/components/ResourceDetail/Masthead/latest.vue +23 -21
  43. package/components/ResourceDetail/index.vue +3 -0
  44. package/components/ResourceTable.vue +54 -21
  45. package/components/SlideInPanelManager.vue +16 -11
  46. package/components/SortableTable/THead.vue +2 -1
  47. package/components/SortableTable/index.vue +20 -2
  48. package/components/Tabbed/__tests__/index.test.ts +86 -0
  49. package/components/Tabbed/index.vue +37 -2
  50. package/components/__tests__/NamespaceFilter.test.ts +49 -0
  51. package/components/auth/SelectPrincipal.vue +28 -6
  52. package/components/auth/__tests__/SelectPrincipal.test.ts +119 -0
  53. package/components/auth/login/ldap.vue +3 -3
  54. package/components/fleet/FleetSecretSelector.vue +1 -1
  55. package/components/form/KeyValue.vue +1 -1
  56. package/components/form/NameNsDescription.vue +1 -1
  57. package/components/form/NodeScheduling.vue +2 -2
  58. package/components/form/ResourceTabs/composable.ts +2 -2
  59. package/components/form/ResourceTabs/index.vue +0 -2
  60. package/components/form/__tests__/NameNsDescription.test.ts +42 -0
  61. package/components/formatter/InternalExternalIP.vue +4 -1
  62. package/components/formatter/LinkName.vue +5 -0
  63. package/components/formatter/__tests__/InternalExternalIP.test.ts +1 -1
  64. package/components/nav/Group.vue +25 -7
  65. package/components/nav/Header.vue +1 -1
  66. package/components/nav/NamespaceFilter.vue +1 -0
  67. package/components/nav/Type.vue +17 -6
  68. package/components/nav/WindowManager/panels/TabBodyContainer.vue +1 -1
  69. package/components/nav/__tests__/Type.test.ts +59 -0
  70. package/components/templates/standalone.vue +1 -1
  71. package/composables/cruResource.ts +27 -0
  72. package/composables/focusTrap.ts +3 -1
  73. package/composables/resourceDetail.ts +15 -0
  74. package/composables/useI18n.ts +10 -1
  75. package/composables/useLabeledFormElement.ts +3 -4
  76. package/config/__test__/uiplugins.test.ts +309 -0
  77. package/config/labels-annotations.js +1 -0
  78. package/config/product/explorer.js +3 -1
  79. package/config/product/fleet.js +1 -1
  80. package/config/router/navigation-guards/clusters.js +3 -3
  81. package/config/router/navigation-guards/products.js +1 -1
  82. package/config/router/routes.js +7 -7
  83. package/config/types.js +7 -0
  84. package/config/uiplugins.js +46 -2
  85. package/core/__tests__/extension-manager-impl.test.js +437 -0
  86. package/core/extension-manager-impl.js +21 -25
  87. package/core/plugin-helpers.ts +2 -2
  88. package/core/plugin.ts +9 -1
  89. package/core/plugins-loader.js +2 -2
  90. package/core/types-provisioning.ts +5 -1
  91. package/core/types.ts +35 -0
  92. package/detail/provisioning.cattle.io.cluster.vue +9 -6
  93. package/dialog/DeveloperLoadExtensionDialog.vue +13 -4
  94. package/dialog/MoveNamespaceDialog.vue +20 -4
  95. package/dialog/RollbackWorkloadDialog.vue +2 -5
  96. package/dialog/SearchDialog.vue +1 -0
  97. package/dialog/__tests__/MoveNamespaceDialog.test.ts +249 -0
  98. package/directives/__tests__/clean-tooltip.test.ts +298 -0
  99. package/directives/clean-tooltip.ts +234 -0
  100. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +2 -2
  101. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +100 -3
  102. package/edit/autoscaling.horizontalpodautoscaler/index.vue +1 -0
  103. package/edit/configmap.vue +1 -0
  104. package/edit/constraints.gatekeeper.sh.constraint/index.vue +1 -0
  105. package/edit/fleet.cattle.io.helmop.vue +11 -6
  106. package/edit/helm.cattle.io.projecthelmchart.vue +1 -0
  107. package/edit/k8s.cni.cncf.io.networkattachmentdefinition.vue +1 -0
  108. package/edit/logging-flow/index.vue +1 -0
  109. package/edit/logging.banzaicloud.io.output/index.vue +1 -0
  110. package/edit/management.cattle.io.fleetworkspace.vue +1 -1
  111. package/edit/management.cattle.io.project.vue +1 -0
  112. package/edit/monitoring.coreos.com.alertmanagerconfig/index.vue +4 -1
  113. package/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig.vue +2 -1
  114. package/edit/monitoring.coreos.com.prometheusrule/index.vue +1 -0
  115. package/edit/monitoring.coreos.com.receiver/index.vue +2 -1
  116. package/edit/monitoring.coreos.com.route.vue +1 -1
  117. package/edit/namespace.vue +1 -0
  118. package/edit/networking.istio.io.destinationrule/index.vue +1 -0
  119. package/edit/networking.k8s.io.ingress/index.vue +1 -0
  120. package/edit/networking.k8s.io.networkpolicy/PolicyRules.vue +1 -0
  121. package/edit/networking.k8s.io.networkpolicy/index.vue +1 -0
  122. package/edit/node.vue +1 -0
  123. package/edit/persistentvolume/index.vue +27 -22
  124. package/edit/persistentvolume/plugins/awsElasticBlockStore.vue +13 -14
  125. package/edit/persistentvolume/plugins/azureDisk.vue +49 -48
  126. package/edit/persistentvolume/plugins/azureFile.vue +15 -14
  127. package/edit/persistentvolume/plugins/cephfs.vue +15 -14
  128. package/edit/persistentvolume/plugins/cinder.vue +15 -14
  129. package/edit/persistentvolume/plugins/csi.vue +18 -16
  130. package/edit/persistentvolume/plugins/fc.vue +13 -14
  131. package/edit/persistentvolume/plugins/flexVolume.vue +15 -14
  132. package/edit/persistentvolume/plugins/flocker.vue +1 -3
  133. package/edit/persistentvolume/plugins/gcePersistentDisk.vue +13 -14
  134. package/edit/persistentvolume/plugins/glusterfs.vue +15 -14
  135. package/edit/persistentvolume/plugins/hostPath.vue +40 -39
  136. package/edit/persistentvolume/plugins/iscsi.vue +13 -14
  137. package/edit/persistentvolume/plugins/local.vue +1 -3
  138. package/edit/persistentvolume/plugins/longhorn.vue +23 -22
  139. package/edit/persistentvolume/plugins/nfs.vue +15 -14
  140. package/edit/persistentvolume/plugins/photonPersistentDisk.vue +1 -14
  141. package/edit/persistentvolume/plugins/portworxVolume.vue +15 -14
  142. package/edit/persistentvolume/plugins/quobyte.vue +15 -14
  143. package/edit/persistentvolume/plugins/rbd.vue +15 -14
  144. package/edit/persistentvolume/plugins/scaleIO.vue +15 -14
  145. package/edit/persistentvolume/plugins/storageos.vue +15 -14
  146. package/edit/persistentvolume/plugins/vsphereVolume.vue +1 -3
  147. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +21 -21
  148. package/edit/provisioning.cattle.io.cluster/index.vue +5 -5
  149. package/edit/provisioning.cattle.io.cluster/rke2.vue +9 -8
  150. package/edit/resources.cattle.io.restore.vue +1 -1
  151. package/edit/secret/index.vue +1 -1
  152. package/edit/service.vue +1 -0
  153. package/edit/serviceaccount.vue +1 -0
  154. package/edit/storage.k8s.io.storageclass/index.vue +1 -0
  155. package/edit/workload/Job.vue +2 -2
  156. package/edit/workload/index.vue +2 -1
  157. package/edit/workload/mixins/workload.js +1 -1
  158. package/initialize/App.vue +4 -4
  159. package/initialize/install-plugins.js +19 -5
  160. package/machine-config/azure.vue +1 -1
  161. package/machine-config/components/GCEImage.vue +1 -1
  162. package/mixins/__tests__/brand.spec.ts +2 -2
  163. package/mixins/brand.js +1 -7
  164. package/mixins/create-edit-view/index.js +5 -0
  165. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +128 -5
  166. package/models/chart.js +70 -74
  167. package/models/management.cattle.io.cluster.js +21 -3
  168. package/models/provisioning.cattle.io.cluster.js +31 -11
  169. package/package.json +11 -10
  170. package/pages/auth/login.vue +4 -6
  171. package/pages/auth/setup.vue +1 -1
  172. package/pages/auth/verify.vue +3 -3
  173. package/pages/c/_cluster/apps/charts/__tests__/chart.test.ts +135 -0
  174. package/pages/c/_cluster/apps/charts/chart.vue +33 -15
  175. package/pages/c/_cluster/apps/charts/index.vue +122 -24
  176. package/pages/c/_cluster/apps/charts/install.vue +33 -0
  177. package/pages/c/_cluster/explorer/__tests__/index.test.ts +1 -1
  178. package/pages/c/_cluster/explorer/index.vue +8 -6
  179. package/pages/c/_cluster/fleet/index.vue +4 -7
  180. package/pages/c/_cluster/manager/hostedprovider/index.vue +12 -6
  181. package/pages/c/_cluster/settings/brand.vue +1 -1
  182. package/pages/c/_cluster/settings/index.vue +5 -0
  183. package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +7 -0
  184. package/pages/c/_cluster/uiplugins/catalogs.vue +147 -0
  185. package/pages/c/_cluster/uiplugins/index.vue +126 -184
  186. package/pkg/auto-import.js +3 -3
  187. package/pkg/dynamic-importer.lib.js +1 -1
  188. package/pkg/import.js +1 -1
  189. package/plugins/__tests__/mutations.tests.ts +179 -0
  190. package/plugins/dashboard-client-init.js +3 -0
  191. package/plugins/dashboard-store/getters.js +19 -2
  192. package/plugins/dashboard-store/model-loader.js +1 -1
  193. package/plugins/dashboard-store/mutations.js +23 -2
  194. package/plugins/dashboard-store/resource-class.js +11 -5
  195. package/plugins/i18n.js +8 -0
  196. package/plugins/plugin.js +2 -2
  197. package/plugins/steve/__tests__/steve-pagination-utils.test.ts +506 -0
  198. package/plugins/steve/steve-class.js +1 -1
  199. package/plugins/steve/steve-pagination-utils.ts +131 -47
  200. package/rancher-components/Form/Checkbox/Checkbox.vue +1 -1
  201. package/rancher-components/Form/LabeledInput/LabeledInput.vue +1 -1
  202. package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +6 -42
  203. package/rancher-components/Pill/RcStatusBadge/index.ts +0 -1
  204. package/rancher-components/Pill/RcStatusBadge/types.ts +1 -1
  205. package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.vue +5 -28
  206. package/rancher-components/Pill/RcStatusIndicator/types.ts +2 -1
  207. package/rancher-components/Pill/types.ts +0 -1
  208. package/rancher-components/RcDropdown/useDropdownContext.ts +2 -4
  209. package/rancher-components/RcIcon/RcIcon.test.ts +51 -0
  210. package/rancher-components/RcIcon/RcIcon.vue +46 -0
  211. package/rancher-components/RcIcon/index.ts +1 -0
  212. package/rancher-components/RcIcon/types.ts +160 -0
  213. package/rancher-components/RcItemCard/RcItemCard.vue +1 -1
  214. package/rancher-components/utils/status.test.ts +67 -0
  215. package/rancher-components/utils/status.ts +77 -0
  216. package/scripts/publish-shell.sh +25 -0
  217. package/scripts/typegen.sh +1 -0
  218. package/store/__tests__/catalog.test.ts +1 -1
  219. package/store/__tests__/type-map.test.ts +164 -2
  220. package/store/action-menu.js +8 -0
  221. package/store/auth.js +25 -13
  222. package/store/catalog.js +6 -0
  223. package/store/i18n.js +3 -3
  224. package/store/index.js +8 -6
  225. package/store/notifications.ts +2 -0
  226. package/store/prefs.js +6 -7
  227. package/store/type-map.js +17 -7
  228. package/store/wm.ts +4 -4
  229. package/types/internal-api/shell/modal.d.ts +6 -6
  230. package/types/notifications/index.ts +126 -15
  231. package/types/rancher/index.d.ts +9 -0
  232. package/types/shell/index.d.ts +54 -3
  233. package/types/store/__tests__/pagination.types.spec.ts +137 -0
  234. package/types/store/pagination.types.ts +157 -9
  235. package/types/vue-shim.d.ts +5 -4
  236. package/utils/__tests__/provider.test.ts +98 -0
  237. package/utils/__tests__/router.test.js +238 -0
  238. package/utils/__tests__/selector-typed.test.ts +263 -0
  239. package/utils/cluster.js +4 -1
  240. package/utils/color.js +1 -1
  241. package/utils/dynamic-content/__tests__/info.test.ts +6 -0
  242. package/utils/dynamic-content/info.ts +43 -0
  243. package/utils/favicon.js +4 -4
  244. package/utils/fleet.ts +8 -1
  245. package/utils/pagination-utils.ts +2 -2
  246. package/utils/pagination-wrapper.ts +1 -1
  247. package/utils/provider.ts +14 -0
  248. package/utils/router.js +50 -0
  249. package/utils/selector-typed.ts +6 -2
  250. package/utils/unit-tests/pagination-utils.spec.ts +8 -8
  251. package/vue.config.js +3 -3
  252. package/composables/useExtensionManager.ts +0 -17
  253. package/core/plugins.js +0 -38
  254. package/directives/clean-tooltip.js +0 -32
  255. package/plugins/internal-api/index.ts +0 -37
  256. package/plugins/internal-api/shared/base-api.ts +0 -13
  257. package/plugins/internal-api/shell/shell.api.ts +0 -108
  258. package/plugins/nuxt-client-init.js +0 -3
  259. package/types/internal-api/shell/growl.d.ts +0 -25
  260. package/types/internal-api/shell/slideIn.d.ts +0 -15
@@ -6,7 +6,7 @@ import ButtonGroup from '@shell/components/ButtonGroup';
6
6
  import SortableTable from '@shell/components/SortableTable';
7
7
  import { NAMESPACE, AGE } from '@shell/config/table-headers';
8
8
  import { findBy } from '@shell/utils/array';
9
- import { ExtensionPoint, TableColumnLocation } from '@shell/core/types';
9
+ import { ExtensionPoint, TableColumnLocation, TableLocation } from '@shell/core/types';
10
10
  import { getApplicableExtensionEnhancements } from '@shell/core/plugin-helpers';
11
11
  import { ToggleSwitch } from '@components/Form/ToggleSwitch';
12
12
  import ResourceTableWatch from '@shell/mixins/resource-table-watch';
@@ -316,29 +316,10 @@ export default {
316
316
 
317
317
  // add custom table columns provided by the extensions ExtensionPoint.TABLE_COL hook
318
318
  // gate it so that we prevent errors on older versions of dashboard
319
- if (this.$store.$plugin?.getUIConfig) {
319
+ if (this.$store.$extension?.getUIConfig) {
320
320
  // { column: TableColumn, paginationColumn: PaginationTableColumn }[]
321
321
  const extensionCols = getApplicableExtensionEnhancements(this, ExtensionPoint.TABLE_COL, TableColumnLocation.RESOURCE, this.$route);
322
322
 
323
- // Try and insert the columns before the Age column
324
- let insertPosition = headers.length;
325
-
326
- if (headers.length > 0) {
327
- const ageColIndex = headers.findIndex((h) => h.name === AGE.name);
328
-
329
- if (ageColIndex >= 0) {
330
- insertPosition = ageColIndex;
331
- } else {
332
- // we've found some labels with ' ', which isn't necessarily empty (explore action/button)
333
- // if we are to add cols, let's push them before these so that the UI doesn't look weird
334
- const lastViableColIndex = headers.findIndex((h) => (!h.label || !h.label?.trim()) && (!h.labelKey || !h.labelKey?.trim()));
335
-
336
- if (lastViableColIndex >= 0) {
337
- insertPosition = lastViableColIndex;
338
- }
339
- }
340
- }
341
-
342
323
  // adding extension defined cols to the correct header config
343
324
  extensionCols.forEach((config) => {
344
325
  let { column: col, paginationColumn } = config;
@@ -377,6 +358,37 @@ export default {
377
358
  if (!col.value && col.getValue) {
378
359
  col.value = col.getValue;
379
360
  }
361
+
362
+ // Establish a valid header position for the new table column
363
+ let insertPosition = headers.length;
364
+
365
+ if (headers.length > 0) {
366
+ const ageColIndex = headers.findIndex((h) => h.name === AGE.name);
367
+
368
+ if (ageColIndex >= 0) {
369
+ // we will allow for the table col to be added right after the AGE col
370
+ // but that will be the limit
371
+ insertPosition = ageColIndex + 1;
372
+ } else {
373
+ // we've found some labels with ' ', which isn't necessarily empty (explore action/button)
374
+ // if we are to add cols, let's push them before these so that the UI doesn't look weird
375
+ const lastViableColIndex = headers.findIndex((h) => (!h.label || !h.label?.trim()) && (!h.labelKey || !h.labelKey?.trim()));
376
+
377
+ if (lastViableColIndex >= 0) {
378
+ insertPosition = lastViableColIndex;
379
+ }
380
+ }
381
+ }
382
+
383
+ // apply table col ordering if it's present on the new table col config
384
+ if (col.weight) {
385
+ if (col.weight < 0) {
386
+ insertPosition = 0;
387
+ } else if (col.weight < insertPosition) {
388
+ insertPosition = col.weight;
389
+ }
390
+ }
391
+
380
392
  headers.splice(insertPosition, 0, col);
381
393
  });
382
394
  }
@@ -414,6 +426,16 @@ export default {
414
426
  return headers;
415
427
  },
416
428
 
429
+ _applicableExtensionTableHooks() {
430
+ if (this.$store.$plugin?.getUIConfig) {
431
+ const extensionTableHooks = getApplicableExtensionEnhancements(this, ExtensionPoint.TABLE, TableLocation.RESOURCE, this.$route);
432
+
433
+ return extensionTableHooks;
434
+ }
435
+
436
+ return [];
437
+ },
438
+
417
439
  /**
418
440
  * Take rows and filter out entries given the namespace filter
419
441
  */
@@ -640,6 +662,16 @@ export default {
640
662
  }
641
663
  },
642
664
 
665
+ // this is where we handle the callbacks to the TABLE extension hooks
666
+ handleSortableTableInteraction(arg) {
667
+ if (this._applicableExtensionTableHooks?.length) {
668
+ this._applicableExtensionTableHooks.forEach((item) => {
669
+ if (item.tableHook) {
670
+ item.tableHook(arg);
671
+ }
672
+ });
673
+ }
674
+ }
643
675
  }
644
676
  };
645
677
  </script>
@@ -679,6 +711,7 @@ export default {
679
711
  @clickedActionButton="handleActionButtonClick"
680
712
  @group-value-change="group = $event"
681
713
  @enter="handleEnterKeyPress"
714
+ @sortable-table-interaction="handleSortableTableInteraction"
682
715
  >
683
716
  <template
684
717
  v-if="showGrouping && _groupOptions.length > 1"
@@ -57,17 +57,22 @@ watch(
57
57
  /**
58
58
  * trigger focus trap
59
59
  */
60
- () => currentProps?.value?.triggerFocusTrap,
61
- (neu) => {
62
- if (neu) {
63
- const opts = {
60
+ () => isOpen?.value,
61
+ (neu, old) => {
62
+ if (neu && neu !== old) {
63
+ const opts:any = {
64
64
  ...DEFAULT_FOCUS_TRAP_OPTS,
65
+ // putting the initial focus on the first element that is not conditionally displayed
66
+ initialFocus: slideInPanelManagerClose.value
67
+ };
68
+
69
+ const returnFocusSelector = currentProps?.value?.returnFocusSelector;
70
+
71
+ if (returnFocusSelector) {
65
72
  /**
66
73
  * will return focus to the first iterable node of this container select
67
74
  */
68
- setReturnFocus: () => {
69
- const returnFocusSelector = currentProps?.value?.returnFocusSelector;
70
-
75
+ opts.setReturnFocus = () => {
71
76
  if (returnFocusSelector && !document.querySelector(returnFocusSelector)) {
72
77
  console.warn('SlideInPanelManager: cannot find elem with "returnFocusSelector", returning focus to main view'); // eslint-disable-line no-console
73
78
 
@@ -75,10 +80,8 @@ watch(
75
80
  }
76
81
 
77
82
  return returnFocusSelector || '.dashboard-root';
78
- },
79
- // putting the initial focus on the first element that is not conditionally displayed
80
- initialFocus: slideInPanelManagerClose.value
81
- };
83
+ };
84
+ }
82
85
 
83
86
  useWatcherBasedSetupFocusTrapWithDestroyIncluded(
84
87
  () => {
@@ -166,6 +169,8 @@ function closePanel() {
166
169
  data-testid="slide-in-close"
167
170
  :tabindex="isOpen ? 0 : -1"
168
171
  @click="closePanel"
172
+ @keypress.enter="closePanel"
173
+ @keyup.space="closePanel"
169
174
  />
170
175
  </div>
171
176
  <div class="main-panel">
@@ -31,7 +31,8 @@ export default {
31
31
  },
32
32
  descending: {
33
33
  type: Boolean,
34
- required: true
34
+ required: false,
35
+ default: false
35
36
  },
36
37
  hasAdvancedFiltering: {
37
38
  type: Boolean,
@@ -52,7 +52,8 @@ export default {
52
52
  'group-value-change',
53
53
  'selection',
54
54
  'rowClick',
55
- 'enter'
55
+ 'enter',
56
+ 'sortable-table-interaction',
56
57
  ],
57
58
 
58
59
  components: {
@@ -765,7 +766,7 @@ export default {
765
766
  needRef = true;
766
767
  } else {
767
768
  // Check if we have a formatter from a plugin
768
- const pluginFormatter = this.$plugin?.getDynamic('formatters', c.formatter);
769
+ const pluginFormatter = this.$extension?.getDynamic('formatters', c.formatter);
769
770
 
770
771
  if (pluginFormatter) {
771
772
  component = defineAsyncComponent(pluginFormatter);
@@ -1058,6 +1059,23 @@ export default {
1058
1059
  },
1059
1060
 
1060
1061
  paginationChanged() {
1062
+ // event used for extensions TABLE hooks
1063
+ this.$emit('sortable-table-interaction', {
1064
+ pagination: {
1065
+ page: this.page,
1066
+ perPage: this.perPage,
1067
+ },
1068
+ filtering: {
1069
+ searchFields: this.searchFields,
1070
+ searchQuery: this.searchQuery
1071
+ },
1072
+ sorting: {
1073
+ sort: this.sortFields,
1074
+ sortBy: this.sortBy,
1075
+ descending: this.descending
1076
+ }
1077
+ });
1078
+
1061
1079
  if (!this.externalPaginationEnabled) {
1062
1080
  return;
1063
1081
  }
@@ -0,0 +1,86 @@
1
+ import { mount, VueWrapper } from '@vue/test-utils';
2
+ import Tabbed from '@shell/components/Tabbed/index.vue';
3
+ import Tab from '@shell/components/Tabbed/Tab.vue';
4
+
5
+ jest.mock('@shell/components/form/ResourceTabs/composable', () => ({ useTabCountWatcher: () => ({}) }));
6
+
7
+ const mockT = (key: string) => key;
8
+
9
+ const defaultGlobalMountOptions = {
10
+ components: { Tab },
11
+ mocks: {
12
+ $router: {
13
+ replace: jest.fn(),
14
+ currentRoute: { _value: { hash: '' } }
15
+ },
16
+ $route: { hash: '' },
17
+ t: mockT,
18
+ store: { getters: { 'i18n/t': mockT } }
19
+ }
20
+ };
21
+
22
+ describe('component: Tabbed', () => {
23
+ const findTabNav = (wrapper: VueWrapper<any>) => wrapper.find('[data-testid="tabbed-block"]');
24
+
25
+ it('should display tab navigation for a single tab when hideSingleTab is false (default)', async() => {
26
+ const wrapper = mount(Tabbed, {
27
+ slots: { default: { components: { Tab }, template: '<Tab name="tab1" label="Tab 1" />' } },
28
+ global: { ...defaultGlobalMountOptions },
29
+ });
30
+
31
+ await wrapper.vm.$nextTick();
32
+
33
+ expect(findTabNav(wrapper).exists()).toBe(true);
34
+ });
35
+
36
+ it('should display tab navigation for multiple tabs when hideSingleTab is false (default)', async() => {
37
+ const wrapper = mount(Tabbed, {
38
+ slots: {
39
+ default: {
40
+ components: { Tab },
41
+ template: `
42
+ <Tab name="tab1" label="Tab 1" />
43
+ <Tab name="tab2" label="Tab 2" />
44
+ `,
45
+ },
46
+ },
47
+ global: { ...defaultGlobalMountOptions },
48
+ });
49
+
50
+ await wrapper.vm.$nextTick();
51
+
52
+ expect(findTabNav(wrapper).exists()).toBe(true);
53
+ });
54
+
55
+ it('should NOT display tab navigation for a single tab when hideSingleTab is true', async() => {
56
+ const wrapper = mount(Tabbed, {
57
+ props: { hideSingleTab: true },
58
+ slots: { default: { components: { Tab }, template: '<Tab name="tab1" label="Tab 1" />' } },
59
+ global: { ...defaultGlobalMountOptions },
60
+ });
61
+
62
+ await wrapper.vm.$nextTick();
63
+
64
+ expect(findTabNav(wrapper).exists()).toBe(false);
65
+ });
66
+
67
+ it('should display tab navigation for multiple tabs when hideSingleTab is true', async() => {
68
+ const wrapper = mount(Tabbed, {
69
+ props: { hideSingleTab: true },
70
+ slots: {
71
+ default: {
72
+ components: { Tab },
73
+ template: `
74
+ <Tab name="tab1" label="Tab 1" />
75
+ <Tab name="tab2" label="Tab 2" />
76
+ `,
77
+ },
78
+ },
79
+ global: { ...defaultGlobalMountOptions },
80
+ });
81
+
82
+ await wrapper.vm.$nextTick();
83
+
84
+ expect(findTabNav(wrapper).exists()).toBe(true);
85
+ });
86
+ });
@@ -7,6 +7,10 @@ import findIndex from 'lodash/findIndex';
7
7
  import { ExtensionPoint, TabLocation } from '@shell/core/types';
8
8
  import { getApplicableExtensionEnhancements } from '@shell/core/plugin-helpers';
9
9
  import Tab from '@shell/components/Tabbed/Tab';
10
+ import { ref } from 'vue';
11
+ import { useIsInResourceDetailDrawer } from '@shell/components/Drawer/ResourceDetailDrawer/composables';
12
+ import { useIsInResourceDetailPage } from '@shell/composables/resourceDetail';
13
+ import { useIsInResourceCreatePage, useIsInResourceEditPage } from '@shell/composables/cruResource';
10
14
 
11
15
  export default {
12
16
  name: 'Tabbed',
@@ -105,7 +109,14 @@ export default {
105
109
  },
106
110
 
107
111
  data() {
108
- const extensionTabs = this.showExtensionTabs ? getApplicableExtensionEnhancements(this, ExtensionPoint.TAB, TabLocation.RESOURCE_DETAIL, this.$route, this, this.extensionParams) || [] : [];
112
+ const location = this.getInitialTabLocation();
113
+ let extensionTabs = this.showExtensionTabs ? getApplicableExtensionEnhancements(this, ExtensionPoint.TAB, location, this.$route, this, this.extensionParams) || [] : [];
114
+ const legacyExtensionTabs = this.showExtensionTabs ? getApplicableExtensionEnhancements(this, ExtensionPoint.TAB, TabLocation.RESOURCE_DETAIL, this.$route, this, this.extensionParams) || [] : [];
115
+
116
+ if (!extensionTabs.length) {
117
+ // Support legacy tabs for RESOURCE_DETAIL location
118
+ extensionTabs = legacyExtensionTabs;
119
+ }
109
120
 
110
121
  const parsedExtTabs = extensionTabs.map((item) => {
111
122
  return {
@@ -130,7 +141,18 @@ export default {
130
141
  // hide tabs based on tab count IF flag is active
131
142
  hideTabs() {
132
143
  return this.hideSingleTab && this.sortedTabs.length === 1;
133
- }
144
+ },
145
+ },
146
+
147
+ setup() {
148
+ const isInResourceDetailDrawer = ref(useIsInResourceDetailDrawer());
149
+ const isInResourceDetailPage = ref(useIsInResourceDetailPage());
150
+ const isInResourceEditPage = ref(useIsInResourceEditPage());
151
+ const isInResourceCreatePage = ref(useIsInResourceCreatePage());
152
+
153
+ return {
154
+ isInResourceDetailDrawer, isInResourceDetailPage, isInResourceEditPage, isInResourceCreatePage
155
+ };
134
156
  },
135
157
 
136
158
  watch: {
@@ -166,6 +188,19 @@ export default {
166
188
  },
167
189
 
168
190
  methods: {
191
+ getInitialTabLocation() {
192
+ if (this.isInResourceEditPage) {
193
+ return TabLocation.RESOURCE_EDIT_PAGE;
194
+ } else if (this.isInResourceDetailDrawer) {
195
+ return TabLocation.RESOURCE_SHOW_CONFIGURATION;
196
+ } else if (this.isInResourceDetailPage) {
197
+ return TabLocation.RESOURCE_DETAIL_PAGE;
198
+ } else if (this.isInResourceCreatePage) {
199
+ return TabLocation.RESOURCE_CREATE_PAGE;
200
+ } else {
201
+ return TabLocation.OTHER;
202
+ }
203
+ },
169
204
  hasIcon(tab) {
170
205
  return tab.displayAlertIcon || (tab.error && !tab.active);
171
206
  },
@@ -244,4 +244,53 @@ describe('component: NamespaceFilter', () => {
244
244
 
245
245
  it.todo('should generate the options based on the Rancher resources');
246
246
  });
247
+
248
+ describe('given filter input text selection', () => {
249
+ it('should allow text selection by stopping mousedown propagation', async() => {
250
+ const wrapper = mount(NamespaceFilter, {
251
+ computed: {
252
+ filtered: () => [],
253
+ options: () => [],
254
+ value: () => [],
255
+ },
256
+ global: {
257
+ mocks: {
258
+ t: (key: string) => key,
259
+ $store: { getters: { 'i18n/t': () => '', namespaceFilterMode: () => undefined } },
260
+ $fetchState: { pending: false }
261
+ },
262
+ directives: {
263
+ 'clean-tooltip': () => {},
264
+ shortkey: () => {},
265
+ },
266
+ stubs: { RcButton: { template: '<button><slot /></button>' } },
267
+ }
268
+ });
269
+
270
+ // Open the dropdown to reveal the filter input
271
+ const dropdown = wrapper.find('[data-testid="namespaces-dropdown"]');
272
+
273
+ await dropdown.trigger('click');
274
+
275
+ // Find the filter input
276
+ const filterInput = wrapper.find('.ns-filter-input');
277
+
278
+ expect(filterInput.exists()).toBe(true);
279
+
280
+ // Trigger mousedown on the filter input and capture the event
281
+ const mousedownEvent = new MouseEvent('mousedown', {
282
+ bubbles: true,
283
+ cancelable: true
284
+ });
285
+ const stopPropagationSpy = jest.spyOn(mousedownEvent, 'stopPropagation');
286
+
287
+ filterInput.element.dispatchEvent(mousedownEvent);
288
+
289
+ // Verify stopPropagation was called (which allows text selection)
290
+ expect(stopPropagationSpy).toHaveBeenCalledWith();
291
+
292
+ // Verify the default was NOT prevented (text selection should work)
293
+ expect(mousedownEvent.defaultPrevented).toBe(false);
294
+ });
295
+ });
247
296
  });
@@ -56,11 +56,13 @@ export default {
56
56
 
57
57
  data() {
58
58
  return {
59
- principals: null,
60
- searchStr: '',
61
- options: [],
62
- newValue: '',
63
- tooltipContent: null,
59
+ principals: null,
60
+ searchStr: '',
61
+ options: [],
62
+ newValue: '',
63
+ tooltipContent: null,
64
+ hasSearchTooShort: false,
65
+ minSearchLength: 2,
64
66
  };
65
67
  },
66
68
 
@@ -133,9 +135,20 @@ export default {
133
135
  this.searchStr = str;
134
136
 
135
137
  if ( str ) {
138
+ // Backend requires minimum 2 characters for search
139
+ if (str.length < this.minSearchLength) {
140
+ this.hasSearchTooShort = true;
141
+ this.options = [];
142
+ loading(false);
143
+
144
+ return;
145
+ }
146
+
147
+ this.hasSearchTooShort = false;
136
148
  loading(true);
137
149
  this.debouncedSearch(str, loading);
138
150
  } else {
151
+ this.hasSearchTooShort = false;
139
152
  this.search(null, loading);
140
153
  }
141
154
  },
@@ -162,6 +175,10 @@ export default {
162
175
  if ( this.searchStr === str ) {
163
176
  // If not, they've already typed something else
164
177
  this.options = res.map((x) => x.id);
178
+ // display the search results if the dropdown has been closed
179
+ if (this.options.length) {
180
+ this.$refs['labeled-select'].isOpen = true;
181
+ }
165
182
  }
166
183
  } catch (e) {
167
184
  this.options = [];
@@ -196,7 +213,12 @@ export default {
196
213
  @on-close="setTooltipContent()"
197
214
  >
198
215
  <template v-slot:no-options="{ searching }">
199
- <template v-if="searching">
216
+ <template v-if="hasSearchTooShort">
217
+ <span class="search-slot">
218
+ {{ t('cluster.memberRoles.addClusterMember.minCharacters', { count: minSearchLength }) }}
219
+ </span>
220
+ </template>
221
+ <template v-else-if="searching">
200
222
  <span class="search-slot">
201
223
  {{ t('cluster.memberRoles.addClusterMember.noResults') }}
202
224
  </span>
@@ -0,0 +1,119 @@
1
+ import { shallowMount, type VueWrapper } from '@vue/test-utils';
2
+ import SelectPrincipal from '@shell/components/auth/SelectPrincipal.vue';
3
+
4
+ describe('component: SelectPrincipal', () => {
5
+ const mockStore = { dispatch: jest.fn().mockResolvedValue([]) };
6
+
7
+ const defaultMountOptions = {
8
+ global: {
9
+ mocks: {
10
+ $fetchState: { pending: false },
11
+ $store: mockStore,
12
+ t: (key: string, opts?: any) => opts?.count ? `${ key } ${ opts.count }` : key,
13
+ },
14
+ stubs: {
15
+ LabeledSelect: {
16
+ template: '<div class="labeled-select-stub"><slot name="no-options" :searching="searching" /></div>',
17
+ props: ['options', 'searchable', 'filterable'],
18
+ data() {
19
+ return { searching: false };
20
+ }
21
+ },
22
+ Principal: true,
23
+ },
24
+ },
25
+ };
26
+
27
+ beforeEach(() => {
28
+ jest.clearAllMocks();
29
+ mockStore.dispatch.mockResolvedValue([]);
30
+ });
31
+
32
+ describe('onSearch', () => {
33
+ it('should set hasSearchTooShort to true when search string is less than minSearchLength', async() => {
34
+ const wrapper: VueWrapper<any> = shallowMount(SelectPrincipal, defaultMountOptions);
35
+
36
+ // Set principals to an empty array to avoid null errors
37
+ wrapper.vm.principals = [];
38
+ await wrapper.vm.$nextTick();
39
+
40
+ const loadingFn = jest.fn();
41
+
42
+ wrapper.vm.onSearch('a', loadingFn);
43
+
44
+ expect(wrapper.vm.hasSearchTooShort).toBe(true);
45
+ expect(wrapper.vm.options).toStrictEqual([]);
46
+ expect(loadingFn).toHaveBeenCalledWith(false);
47
+ });
48
+
49
+ it('should set hasSearchTooShort to false when search string meets minSearchLength', async() => {
50
+ const wrapper: VueWrapper<any> = shallowMount(SelectPrincipal, defaultMountOptions);
51
+
52
+ wrapper.vm.principals = [];
53
+ await wrapper.vm.$nextTick();
54
+
55
+ const loadingFn = jest.fn();
56
+
57
+ wrapper.vm.onSearch('ab', loadingFn);
58
+
59
+ expect(wrapper.vm.hasSearchTooShort).toBe(false);
60
+ expect(loadingFn).toHaveBeenCalledWith(true);
61
+ });
62
+
63
+ it('should set hasSearchTooShort to false when search string is empty', async() => {
64
+ const wrapper: VueWrapper<any> = shallowMount(SelectPrincipal, defaultMountOptions);
65
+
66
+ wrapper.vm.principals = [];
67
+ await wrapper.vm.$nextTick();
68
+
69
+ // First set hasSearchTooShort to true
70
+ wrapper.vm.hasSearchTooShort = true;
71
+
72
+ const loadingFn = jest.fn();
73
+
74
+ wrapper.vm.onSearch('', loadingFn);
75
+
76
+ expect(wrapper.vm.hasSearchTooShort).toBe(false);
77
+ });
78
+
79
+ it('should not call debouncedSearch when search string is too short', async() => {
80
+ const wrapper: VueWrapper<any> = shallowMount(SelectPrincipal, defaultMountOptions);
81
+
82
+ wrapper.vm.principals = [];
83
+ await wrapper.vm.$nextTick();
84
+
85
+ // Spy on the debounced search
86
+ const debouncedSearchSpy = jest.spyOn(wrapper.vm, 'debouncedSearch');
87
+ const loadingFn = jest.fn();
88
+
89
+ wrapper.vm.onSearch('x', loadingFn);
90
+
91
+ expect(debouncedSearchSpy).not.toHaveBeenCalled();
92
+ });
93
+
94
+ it('should call debouncedSearch when search string meets minimum length', async() => {
95
+ const wrapper: VueWrapper<any> = shallowMount(SelectPrincipal, defaultMountOptions);
96
+
97
+ wrapper.vm.principals = [];
98
+ await wrapper.vm.$nextTick();
99
+
100
+ const debouncedSearchSpy = jest.spyOn(wrapper.vm, 'debouncedSearch');
101
+ const loadingFn = jest.fn();
102
+
103
+ wrapper.vm.onSearch('xy', loadingFn);
104
+
105
+ expect(debouncedSearchSpy).toHaveBeenCalledWith('xy', loadingFn);
106
+ });
107
+ });
108
+
109
+ describe('minSearchLength', () => {
110
+ it('should have a default minSearchLength of 2', async() => {
111
+ const wrapper: VueWrapper<any> = shallowMount(SelectPrincipal, defaultMountOptions);
112
+
113
+ wrapper.vm.principals = [];
114
+ await wrapper.vm.$nextTick();
115
+
116
+ expect(wrapper.vm.minSearchLength).toBe(2);
117
+ });
118
+ });
119
+ });
@@ -34,9 +34,9 @@ export default {
34
34
  });
35
35
 
36
36
  await loadPlugins({
37
- app: this.$store.app,
38
- store: this.$store,
39
- $plugin: this.$store.$plugin
37
+ app: this.$store.app,
38
+ store: this.$store,
39
+ $extension: this.$store.$extension,
40
40
  });
41
41
 
42
42
  buttonCb(true);
@@ -1,5 +1,5 @@
1
1
  <script lang="ts" setup>
2
- import { ref, computed, defineProps, defineEmits } from 'vue';
2
+ import { ref, computed } from 'vue';
3
3
  import { _EDIT } from '@shell/config/query-params';
4
4
  import { TYPES } from '@shell/models/secret';
5
5
  import { SECRET } from '@shell/config/types';
@@ -693,7 +693,7 @@ export default {
693
693
  :aria-colindex="1"
694
694
  :class="{
695
695
  'labeled-input-key': keyErrors[row.key],
696
- 'v-popper--has-tooltip': keyErrors[row.key],
696
+ 'has-clean-tooltip': keyErrors[row.key],
697
697
  }"
698
698
  >
699
699
  <slot
@@ -305,7 +305,7 @@ export default {
305
305
  namespace.value = toRef(props.forceNamespace);
306
306
  updateNamespace(namespace);
307
307
  } else if (props.namespaceKey) {
308
- namespace.value = get(v, props.namespaceKey);
308
+ namespace.value = get(v.value, props.namespaceKey);
309
309
  } else {
310
310
  namespace.value = metadata?.namespace;
311
311
  }
@@ -181,7 +181,7 @@ export default {
181
181
  :options="selectNodeOptions"
182
182
  :mode="mode"
183
183
  :data-testid="'node-scheduling-selectNode'"
184
- @input="update"
184
+ @update:value="update"
185
185
  />
186
186
  </div>
187
187
  <template v-if="selectNode === 'nodeSelector'">
@@ -205,7 +205,7 @@ export default {
205
205
  v-model:value="nodeAffinity"
206
206
  :mode="mode"
207
207
  :data-testid="'node-scheduling-nodeAffinity'"
208
- @input="update"
208
+ @update:value="update"
209
209
  />
210
210
  </template>
211
211
  </div>