@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
@@ -65,6 +65,19 @@ export default {
65
65
  this.installedApps = await this.$store.dispatch('cluster/findAll', { type: CATALOG_TYPES.APP });
66
66
  },
67
67
 
68
+ updated() {
69
+ if (!this.observerInitialized && this.filteredCharts.length > 0) {
70
+ this.initIntersectionObserver();
71
+ }
72
+ this.ensureOverflow();
73
+ },
74
+
75
+ beforeUnmount() {
76
+ if (this.observer) {
77
+ this.observer.disconnect();
78
+ }
79
+ },
80
+
68
81
  data() {
69
82
  return {
70
83
  DOCS_BASE,
@@ -107,7 +120,6 @@ export default {
107
120
  }
108
121
  }
109
122
  ],
110
- appCardsCache: {},
111
123
  selectedSortOption: CATALOG_SORT_OPTIONS.RECOMMENDED,
112
124
  sortOptions: [
113
125
  { kind: 'group', label: this.t('catalog.charts.sort.prefix') },
@@ -115,7 +127,10 @@ export default {
115
127
  { value: CATALOG_SORT_OPTIONS.LAST_UPDATED_DESC, label: this.t('catalog.charts.sort.lastUpdatedDesc') },
116
128
  { value: CATALOG_SORT_OPTIONS.ALPHABETICAL_ASC, label: this.t('catalog.charts.sort.alphaAscending') },
117
129
  { value: CATALOG_SORT_OPTIONS.ALPHABETICAL_DESC, label: this.t('catalog.charts.sort.alphaDescending') },
118
- ]
130
+ ],
131
+ initialVisibleChartsCount: 30,
132
+ visibleChartsCount: 20,
133
+ hasOverflow: false
119
134
  };
120
135
  },
121
136
 
@@ -262,26 +277,21 @@ export default {
262
277
  },
263
278
 
264
279
  appChartCards() {
265
- return this.filteredCharts.map((chart) => {
266
- if (!this.appCardsCache[chart.id]) {
267
- // Cache the converted value. We're caching chart.cardContent anyway, so no need to worry about showing updates to state
268
- this.appCardsCache[chart.id] = {
269
- id: chart.id,
270
- pill: chart.featured ? { label: { key: 'generic.shortFeatured' }, tooltip: { key: 'generic.featured' } } : undefined,
271
- header: {
272
- title: { text: chart.chartNameDisplay },
273
- statuses: chart.cardContent.statuses
274
- },
275
- subHeaderItems: chart.cardContent.subHeaderItems,
276
- image: { src: chart.latestCompatibleVersion.icon, alt: { text: this.t('catalog.charts.iconAlt', { app: get(chart, 'chartNameDisplay') }) } },
277
- content: { text: chart.chartDescription },
278
- footerItems: chart.cardContent.footerItems,
279
- rawChart: chart
280
- };
281
- }
282
-
283
- return this.appCardsCache[chart.id];
284
- });
280
+ const charts = this.filteredCharts.slice(0, this.visibleChartsCount);
281
+
282
+ return charts.map((chart) => ({
283
+ id: chart.id,
284
+ pill: chart.featured ? { label: { key: 'generic.shortFeatured' }, tooltip: { key: 'generic.featured' } } : undefined,
285
+ header: {
286
+ title: { text: chart.chartNameDisplay },
287
+ statuses: chart.cardContent.statuses
288
+ },
289
+ subHeaderItems: chart.cardContent.subHeaderItems,
290
+ image: { src: chart.latestCompatibleVersion.icon, alt: { text: this.t('catalog.charts.iconAlt', { app: get(chart, 'chartNameDisplay') }) } },
291
+ content: { text: chart.chartDescription },
292
+ footerItems: chart.cardContent.footerItems,
293
+ rawChart: chart
294
+ }));
285
295
  },
286
296
 
287
297
  clusterId() {
@@ -293,7 +303,7 @@ export default {
293
303
  },
294
304
 
295
305
  totalMessage() {
296
- const count = !this.isFilterUpdating ? this.appChartCards.length : '. . .';
306
+ const count = !this.isFilterUpdating ? this.filteredCharts.length : '. . .';
297
307
 
298
308
  if (this.noFiltersApplied) {
299
309
  return this.t('catalog.charts.totalChartsMessage', { count });
@@ -304,6 +314,10 @@ export default {
304
314
  },
305
315
 
306
316
  watch: {
317
+ debouncedSearchQuery() {
318
+ this.resetLazyLoadState();
319
+ },
320
+
307
321
  searchQuery: {
308
322
  handler: debounce(function(q) {
309
323
  this.debouncedSearchQuery = q;
@@ -315,6 +329,8 @@ export default {
315
329
  filters: {
316
330
  deep: true,
317
331
  handler(newFilters) {
332
+ this.resetLazyLoadState();
333
+
318
334
  const query = {
319
335
  [REPO]: normalizeFilterQuery(newFilters.repos),
320
336
  [CATEGORY]: normalizeFilterQuery(newFilters.categories),
@@ -425,11 +441,80 @@ export default {
425
441
  });
426
442
  },
427
443
 
444
+ resetLazyLoadState() {
445
+ this.visibleChartsCount = this.initialVisibleChartsCount;
446
+ this.observerInitialized = false;
447
+ this.hasOverflow = false;
448
+ },
449
+
450
+ // The lazy loading implementation has two parts
451
+ // 1. Initial Load (ensureOverflow): Having a simple calculation of how many items to load
452
+ // can fail in edge cases like browser zoom, where element sizing and viewport
453
+ // height can lead to miscalculations. If not enough content is loaded, the page
454
+ // won't be scrollable, breaking the IntersectionObserver. This method, called
455
+ // iteratively by the `updated` lifecycle hook, adds batches of charts and
456
+ // re-measures until the content height factually overflows the container,
457
+ // guaranteeing a scrollbar. It then sets `hasOverflow = true` to stop itself.
458
+ // 2. Scroll-based Load (IntersectionObserver): Once the page is scrollable, a standard
459
+ // IntersectionObserver (`initIntersectionObserver` and `loadMore`) takes care of
460
+ // loading new batches of charts as the user scrolls to the bottom.
461
+ ensureOverflow() {
462
+ this.$nextTick(() => {
463
+ if (this.hasOverflow || !this.$refs.chartsContainer) {
464
+ return;
465
+ }
466
+
467
+ const mainLayout = document.querySelector('.main-layout');
468
+
469
+ if (!mainLayout) {
470
+ return;
471
+ }
472
+
473
+ const contentHeight = this.$refs.chartsContainer.offsetHeight;
474
+ const containerHeight = mainLayout.offsetHeight;
475
+
476
+ if (contentHeight > containerHeight) {
477
+ this.hasOverflow = true;
478
+ } else if (this.visibleChartsCount < this.filteredCharts.length) {
479
+ // Load another batch
480
+ this.visibleChartsCount += this.initialVisibleChartsCount;
481
+ } else {
482
+ // All charts are visible
483
+ this.hasOverflow = true;
484
+ }
485
+ });
486
+ },
487
+
428
488
  resetAllFilters() {
429
489
  this.internalFilters = createInitialFilters();
430
490
  this.filters = createInitialFilters();
431
491
  this.searchQuery = '';
432
492
  },
493
+
494
+ loadMore() {
495
+ if (this.visibleChartsCount >= this.filteredCharts.length) {
496
+ return;
497
+ }
498
+ this.visibleChartsCount += this.initialVisibleChartsCount;
499
+ },
500
+
501
+ initIntersectionObserver() {
502
+ if (this.observer) {
503
+ this.observer.disconnect();
504
+ }
505
+ const mainLayout = document.querySelector('.main-layout');
506
+ const sentinel = this.$refs.sentinel;
507
+
508
+ if (sentinel && mainLayout) {
509
+ this.observer = new IntersectionObserver((entries) => {
510
+ if (entries[0].isIntersecting) {
511
+ this.loadMore();
512
+ }
513
+ }, { mainLayout });
514
+ this.observer.observe(sentinel);
515
+ this.observerInitialized = true;
516
+ }
517
+ }
433
518
  },
434
519
  };
435
520
  </script>
@@ -552,7 +637,10 @@ export default {
552
637
  >
553
638
  <div class="total-and-sort">
554
639
  <div class="total">
555
- <p class="total-message">
640
+ <p
641
+ class="total-message"
642
+ data-testid="charts-total-message"
643
+ >
556
644
  {{ totalMessage }}
557
645
  </p>
558
646
  <a
@@ -594,6 +682,7 @@ export default {
594
682
  </Select>
595
683
  </div>
596
684
  <div
685
+ ref="chartsContainer"
597
686
  class="app-chart-cards"
598
687
  data-testid="app-chart-cards-container"
599
688
  >
@@ -629,6 +718,11 @@ export default {
629
718
  </template>
630
719
  </rc-item-card>
631
720
  </div>
721
+ <div
722
+ ref="sentinel"
723
+ class="sentinel-charts"
724
+ data-testid="charts-lazy-load-sentinel"
725
+ />
632
726
  </div>
633
727
  </div>
634
728
  </div>
@@ -673,6 +767,10 @@ export default {
673
767
  flex-direction: column;
674
768
  gap: var(--gap-md);
675
769
  flex: 1;
770
+
771
+ .sentinel-charts {
772
+ height: 1px;
773
+ }
676
774
  }
677
775
 
678
776
  .total-and-sort {
@@ -310,6 +310,7 @@ export default {
310
310
  two different Helm chart versions is a "user value," or
311
311
  a user-selected customization.
312
312
  */
313
+ this.preserveCustomRegistryValue();
313
314
  userValues = diff(this.loadedVersionValues, this.chartValues);
314
315
  } else if ( this.existing ) {
315
316
  await this.existing.fetchValues(); // In theory this has already been called, but do again to be safe
@@ -824,6 +825,35 @@ export default {
824
825
  },
825
826
 
826
827
  methods: {
828
+ /**
829
+ * The custom registry UI fields (checkbox and input) are not directly bound to chartValues.
830
+ * Before calculating the diff to carry over user customizations, we must
831
+ * first synchronize the state of these UI fields with chartValues. This
832
+ * ensures any user changes to the custom registry settings are
833
+ * included in the diff and preserved when changing versions.
834
+ */
835
+ preserveCustomRegistryValue() {
836
+ if (!this.showCustomRegistry) {
837
+ return;
838
+ }
839
+
840
+ if (this.showCustomRegistryInput) {
841
+ set(this.chartValues, 'global.systemDefaultRegistry', this.customRegistrySetting);
842
+ set(this.chartValues, 'global.cattle.systemDefaultRegistry', this.customRegistrySetting);
843
+ } else {
844
+ // Note: Using `delete` here is safe because this is not a reactive property update
845
+ // that the UI needs to track. This is a one-time mutation before a diff.
846
+ if (get(this.chartValues, 'global.systemDefaultRegistry')) {
847
+ delete this.chartValues.global.systemDefaultRegistry;
848
+ }
849
+ if (get(this.chartValues, 'global.cattle.systemDefaultRegistry')) {
850
+ // It's possible `this.chartValues.global.cattle` doesn't exist,
851
+ // but `get` ensures we only proceed if the full path exists.
852
+ delete this.chartValues.global.cattle.systemDefaultRegistry;
853
+ }
854
+ }
855
+ },
856
+
827
857
  async getClusterRegistry() {
828
858
  const hasPermissionToSeeProvCluster = this.$store.getters[`management/schemaFor`](CAPI.RANCHER_CLUSTER);
829
859
 
@@ -1367,6 +1397,7 @@ export default {
1367
1397
  <!-- We have a chart for the app, let the user select a new version -->
1368
1398
  <LabeledSelect
1369
1399
  v-if="chart"
1400
+ data-testid="chart-version-selector"
1370
1401
  :label="t('catalog.install.version')"
1371
1402
  :value="query.versionName"
1372
1403
  :options="filteredVersions"
@@ -1435,6 +1466,7 @@ export default {
1435
1466
  v-if="showCustomRegistry"
1436
1467
  v-model:value="showCustomRegistryInput"
1437
1468
  class="mb-20"
1469
+ data-testid="custom-registry-checkbox"
1438
1470
  :label="t('catalog.chart.registry.custom.checkBoxLabel')"
1439
1471
  :tooltip="t('catalog.chart.registry.tooltip')"
1440
1472
  />
@@ -1443,6 +1475,7 @@ export default {
1443
1475
  <LabeledInput
1444
1476
  v-if="showCustomRegistryInput"
1445
1477
  v-model:value="customRegistrySetting"
1478
+ data-testid="custom-registry-input"
1446
1479
  label-key="catalog.chart.registry.custom.inputLabel"
1447
1480
  placeholder-key="catalog.chart.registry.custom.placeholder"
1448
1481
  :min-height="30"
@@ -211,7 +211,7 @@ describe('page: cluster dashboard', () => {
211
211
 
212
212
  expect(box.element).toBeDefined();
213
213
  expect(box.element.classList).toContain(status);
214
- expect(!!(box.element as any).$_popper).toBe(clickable);
214
+ expect(!!(box.element as any).__tooltipOptions__?.content).toBe(clickable);
215
215
  expect(icon.element.classList).toContain(iconClass);
216
216
 
217
217
  await box.trigger('click');
@@ -222,7 +222,7 @@ export default {
222
222
  displayProvider() {
223
223
  const other = 'other';
224
224
 
225
- let provider = this.currentCluster?.status?.provider || other;
225
+ let provider = this.currentCluster?.status?.provider || this.currentCluster?.status?.driver.toLowerCase() || other;
226
226
 
227
227
  if (provider === 'rke.windows') {
228
228
  provider = 'rkeWindows';
@@ -484,6 +484,12 @@ export default {
484
484
  hasNodes() {
485
485
  return this.nodes?.length > 0;
486
486
  },
487
+ kubernetesVersion() {
488
+ const base = this.currentCluster?.kubernetesVersionBase || '';
489
+ const extension = this.currentCluster?.kubernetesVersionExtension || '';
490
+
491
+ return `${ base }${ extension }`;
492
+ }
487
493
  },
488
494
 
489
495
  methods: {
@@ -661,11 +667,7 @@ export default {
661
667
  </div>
662
668
  <div data-testid="kubernetesVersion__label">
663
669
  <label>{{ t('glance.version') }}: </label>
664
- <span>{{ currentCluster.kubernetesVersionBase }}</span>
665
- <span
666
- v-if="currentCluster.kubernetesVersionExtension"
667
- style="font-size: 0.75em"
668
- >{{ currentCluster.kubernetesVersionExtension }}</span>
670
+ <span>{{ kubernetesVersion }}</span>
669
671
  </div>
670
672
  <div
671
673
  v-if="hasNodes"
@@ -321,16 +321,13 @@ export default {
321
321
 
322
322
  this.selectedCard = selected;
323
323
 
324
- this.$shell.slideInPanel({
325
- component: ResourceDetails,
324
+ this.$shell.slideIn.open(ResourceDetails, {
326
325
  componentProps: {
326
+ showHeader: false,
327
+ width: window.innerWidth / 3 > 530 ? `${ window.innerWidth / 3 }px` : '530px',
327
328
  value,
328
329
  statePanel,
329
- workspace,
330
- showHeader: false,
331
- width: window.innerWidth / 3 > 530 ? `${ window.innerWidth / 3 }px` : '530px',
332
- triggerFocusTrap: true,
333
- returnFocusSelector: `[data-testid="resource-card-${ value.id }"]`
330
+ workspace
334
331
  }
335
332
  });
336
333
  },
@@ -9,6 +9,7 @@ import RcStatusBadge from '@components/Pill/RcStatusBadge/RcStatusBadge.vue';
9
9
  import { exceptionToErrorsArray } from '@shell/utils/error';
10
10
  import { isRancherPrime } from '@shell/config/version';
11
11
  import { stateDisplay, STATES_ENUM } from '@shell/plugins/dashboard-store/resource-class';
12
+ import { getHostedProviders } from '@shell/utils/provider';
12
13
 
13
14
  export default {
14
15
  name: 'HostedProviders',
@@ -67,12 +68,9 @@ export default {
67
68
  axios: this.$store.$axios,
68
69
  $extension: this.$store.app.$extension,
69
70
  t: (...args) => this.t.apply(this, args),
70
- isCreate: this.isCreate,
71
- isEdit: this.isEdit,
72
- isView: this.isView,
73
71
  };
74
72
 
75
- return this.$extension.getProviders(context);
73
+ return getHostedProviders(context);
76
74
  },
77
75
  getSettings() {
78
76
  this.settingResource = this.$store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.KEV2_OPERATORS );
@@ -107,7 +105,7 @@ export default {
107
105
  }
108
106
  },
109
107
  async generateRows() {
110
- this.rows = this.allProviders.filter((p) => p.group === 'hosted').map((p) => {
108
+ this.rows = this.allProviders.map((p) => {
111
109
  const active = p.id in this.settings ? this.settings[p.id] : true;
112
110
  const canNotPrime = p.prime && !this.prime;
113
111
  const canNotChangeSettings = !this.settingResource?.canUpdate;
@@ -196,7 +194,8 @@ export default {
196
194
  <span class="mr-10">{{ row.name }}</span>
197
195
  <RcStatusBadge
198
196
  v-if="row.prime"
199
- status="prime"
197
+ class="prime-badge"
198
+ status="success"
200
199
  >
201
200
  {{ t('providers.hosted.prime') }}
202
201
  </RcStatusBadge>
@@ -212,3 +211,10 @@ export default {
212
211
  </ResourceTable>
213
212
  </div>
214
213
  </template>
214
+
215
+ <style lang="scss" scoped>
216
+ .prime-badge {
217
+ font-size: 10px;
218
+ line-height: 15px;
219
+ }
220
+ </style>
@@ -18,7 +18,7 @@ import { _EDIT, _VIEW } from '@shell/config/query-params';
18
18
  import { setFavIcon } from '@shell/utils/favicon';
19
19
  import TabTitle from '@shell/components/TabTitle';
20
20
 
21
- const Color = require('color');
21
+ import Color from 'color';
22
22
 
23
23
  export default {
24
24
  components: {
@@ -1,8 +1,13 @@
1
1
  <script>
2
+ import { h } from 'vue';
2
3
  import { NAME as SETTINGS } from '@shell/config/product/settings';
3
4
  import { MANAGEMENT } from '@shell/config/types';
4
5
 
5
6
  export default {
7
+ render() {
8
+ // Suppress warning: Component is missing template or render function
9
+ return h('div');
10
+ },
6
11
  beforeCreate() {
7
12
  const hasSettings = !!this.$store.getters[`management/schemaFor`](MANAGEMENT.SETTING);
8
13
 
@@ -164,6 +164,13 @@ describe('page: UI plugins/Extensions', () => {
164
164
  });
165
165
 
166
166
  describe('getFooterItems', () => {
167
+ it('should return "developer" label for isDeveloper plugins', () => {
168
+ const plugin = { isDeveloper: true };
169
+ const items = wrapper.vm.getFooterItems(plugin);
170
+
171
+ expect(items[0].labels).toContain('plugins.labels.isDeveloper');
172
+ });
173
+
167
174
  it('should return "builtin" label for builtin plugins', () => {
168
175
  const plugin = { builtin: true };
169
176
  const items = wrapper.vm.getFooterItems(plugin);
@@ -0,0 +1,147 @@
1
+
2
+ <script>
3
+ import CatalogList from './CatalogList/index.vue';
4
+
5
+ export default {
6
+ name: 'ExtensionsAirgappedView',
7
+ components: { CatalogList },
8
+ data() {
9
+ return {
10
+ extensionsPageLink: {
11
+ name: 'c-cluster-uiplugins',
12
+ params: { cluster: this.$route.params.cluster }
13
+ },
14
+ reloadRequired: false
15
+ };
16
+ },
17
+ methods: {
18
+ returnToExtensionsPage() {
19
+ this.$router.push(this.extensionsPageLink);
20
+ },
21
+ reload() {
22
+ this.$router.go();
23
+ },
24
+ showCatalogLoadDialog() {
25
+ this.$store.dispatch('management/promptModal', {
26
+ component: 'ExtensionCatalogInstallDialog',
27
+ returnFocusSelector: '[data-testid="extensions-catalog-load-dialog"]',
28
+ componentProps: {
29
+ refresh: () => {
30
+ this.reloadRequired = true;
31
+ }
32
+ }
33
+ });
34
+ },
35
+ showCatalogUninstallDialog(ev) {
36
+ this.$store.dispatch('management/promptModal', {
37
+ component: 'ExtensionCatalogUninstallDialog',
38
+ returnFocusSelector: '[data-testid="extensions-catalog-load-dialog"]',
39
+ componentProps: {
40
+ catalog: ev,
41
+ refresh: () => {
42
+ this.reloadRequired = true;
43
+ }
44
+ }
45
+ });
46
+ },
47
+ }
48
+ };
49
+ </script>
50
+
51
+ <template>
52
+ <div id="extensions-airgapped-main-page">
53
+ <div class="plugin-header">
54
+ <!-- catalog/airgapped header -->
55
+ <div class="catalog-title">
56
+ <h2
57
+ class="mb-0 mr-10"
58
+ data-testid="extensions-catalog-title"
59
+ >
60
+ <a
61
+ class="link"
62
+ role="link"
63
+ tabindex="0"
64
+ :aria-label="t('plugins.manageCatalog.title')"
65
+ @click="returnToExtensionsPage()"
66
+ >
67
+ {{ t('plugins.manageCatalog.title') }}:
68
+ </a>
69
+ <t k="plugins.manageCatalog.subtitle" />
70
+ </h2>
71
+ </div>
72
+ <div class="actions-container">
73
+ <!-- extensions reload toast/notification -->
74
+ <div
75
+ v-if="reloadRequired"
76
+ class="plugin-reload-banner mmr-6"
77
+ data-testid="extension-reload-banner"
78
+ >
79
+ <i class="icon icon-checkmark mr-10" />
80
+ <span>
81
+ {{ t('plugins.reload') }}
82
+ </span>
83
+ <button
84
+ class="ml-10 btn btn-sm role-primary"
85
+ data-testid="extension-reload-banner-reload-btn"
86
+ role="button"
87
+ :aria-label="t('plugins.labels.reloadRancher')"
88
+ @click="reload()"
89
+ >
90
+ {{ t('generic.reload') }}
91
+ </button>
92
+ </div>
93
+ </div>
94
+ </div>
95
+ <div>
96
+ <!-- Catalog list view -->
97
+ <CatalogList
98
+ @showCatalogLoadDialog="showCatalogLoadDialog"
99
+ @showCatalogUninstallDialog="showCatalogUninstallDialog($event)"
100
+ />
101
+ </div>
102
+ </div>
103
+ </template>
104
+
105
+ <style lang="scss" scoped>
106
+ .plugin-header {
107
+ display: flex;
108
+ align-items: center;
109
+ justify-content: space-between;
110
+ margin-bottom: 10px;
111
+
112
+ .catalog-title {
113
+ display: flex;
114
+ flex-direction: row;
115
+ align-items: center;
116
+ }
117
+
118
+ > h2 {
119
+ flex: 1;
120
+ margin-bottom: 0;
121
+ }
122
+
123
+ .link {
124
+ cursor: pointer;
125
+ }
126
+ }
127
+
128
+ .plugin-reload-banner {
129
+ align-items: center;
130
+ background-color: var(--success-banner-bg);
131
+ display: flex;
132
+ padding: 4px 4px 4px 12px;
133
+ border-radius: 5px;
134
+ min-height: 36px;
135
+
136
+ > i {
137
+ color: var(--success);
138
+ font-size: 14px;
139
+ font-weight: bold;
140
+ }
141
+
142
+ > button {
143
+ line-height: 30px;
144
+ min-height: 30px;
145
+ }
146
+ }
147
+ </style>