@rancher/shell 3.0.8-rc.9 → 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 (146) 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/styles/global/_tooltip.scss +6 -1
  18. package/assets/translations/en-us.yaml +5 -0
  19. package/components/ActionMenuShell.vue +3 -1
  20. package/components/CruResource.vue +8 -1
  21. package/components/Drawer/ResourceDetailDrawer/__tests__/composables.test.ts +50 -1
  22. package/components/Drawer/ResourceDetailDrawer/composables.ts +19 -0
  23. package/components/Drawer/ResourceDetailDrawer/index.vue +3 -1
  24. package/components/LocaleSelector.vue +2 -2
  25. package/components/ModalManager.vue +11 -1
  26. package/components/Questions/__tests__/Yaml.test.ts +1 -1
  27. package/components/RelatedResources.vue +5 -0
  28. package/components/Resource/Detail/ResourcePopover/index.vue +5 -1
  29. package/components/ResourceDetail/Masthead/latest.vue +23 -21
  30. package/components/ResourceDetail/index.vue +3 -0
  31. package/components/ResourceTable.vue +54 -21
  32. package/components/SlideInPanelManager.vue +16 -11
  33. package/components/SortableTable/THead.vue +2 -1
  34. package/components/SortableTable/index.vue +20 -2
  35. package/components/Tabbed/index.vue +37 -2
  36. package/components/__tests__/NamespaceFilter.test.ts +49 -0
  37. package/components/auth/SelectPrincipal.vue +4 -0
  38. package/components/auth/login/ldap.vue +3 -3
  39. package/components/fleet/FleetSecretSelector.vue +1 -1
  40. package/components/form/KeyValue.vue +1 -1
  41. package/components/form/NameNsDescription.vue +1 -1
  42. package/components/form/NodeScheduling.vue +2 -2
  43. package/components/form/ResourceTabs/composable.ts +2 -2
  44. package/components/form/ResourceTabs/index.vue +0 -2
  45. package/components/form/__tests__/NameNsDescription.test.ts +42 -0
  46. package/components/formatter/LinkName.vue +5 -0
  47. package/components/nav/Group.vue +25 -7
  48. package/components/nav/Header.vue +1 -1
  49. package/components/nav/NamespaceFilter.vue +1 -0
  50. package/components/nav/Type.vue +17 -6
  51. package/components/nav/WindowManager/panels/TabBodyContainer.vue +1 -1
  52. package/components/nav/__tests__/Type.test.ts +59 -0
  53. package/composables/cruResource.ts +27 -0
  54. package/composables/focusTrap.ts +3 -1
  55. package/composables/resourceDetail.ts +15 -0
  56. package/composables/useLabeledFormElement.ts +3 -4
  57. package/config/product/fleet.js +1 -1
  58. package/config/router/navigation-guards/clusters.js +3 -3
  59. package/config/router/navigation-guards/products.js +1 -1
  60. package/config/router/routes.js +1 -5
  61. package/core/__tests__/extension-manager-impl.test.js +437 -0
  62. package/core/extension-manager-impl.js +6 -27
  63. package/core/plugin-helpers.ts +2 -2
  64. package/core/plugin.ts +9 -1
  65. package/core/plugins-loader.js +2 -2
  66. package/core/types-provisioning.ts +4 -0
  67. package/core/types.ts +35 -0
  68. package/detail/provisioning.cattle.io.cluster.vue +8 -6
  69. package/dialog/DeveloperLoadExtensionDialog.vue +1 -1
  70. package/dialog/MoveNamespaceDialog.vue +20 -4
  71. package/dialog/SearchDialog.vue +1 -0
  72. package/dialog/__tests__/MoveNamespaceDialog.test.ts +249 -0
  73. package/directives/__tests__/clean-tooltip.test.ts +298 -0
  74. package/directives/clean-tooltip.ts +234 -0
  75. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +2 -2
  76. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +98 -1
  77. package/edit/fleet.cattle.io.helmop.vue +5 -0
  78. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +21 -21
  79. package/edit/provisioning.cattle.io.cluster/index.vue +5 -5
  80. package/edit/provisioning.cattle.io.cluster/rke2.vue +8 -8
  81. package/edit/resources.cattle.io.restore.vue +1 -1
  82. package/edit/workload/Job.vue +2 -2
  83. package/edit/workload/index.vue +1 -1
  84. package/initialize/install-plugins.js +4 -5
  85. package/machine-config/azure.vue +1 -1
  86. package/machine-config/components/GCEImage.vue +1 -1
  87. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +16 -0
  88. package/models/chart.js +70 -74
  89. package/models/management.cattle.io.cluster.js +1 -1
  90. package/models/provisioning.cattle.io.cluster.js +11 -3
  91. package/package.json +7 -7
  92. package/pages/auth/login.vue +3 -3
  93. package/pages/auth/setup.vue +1 -1
  94. package/pages/auth/verify.vue +3 -3
  95. package/pages/c/_cluster/apps/charts/index.vue +122 -24
  96. package/pages/c/_cluster/apps/charts/install.vue +33 -0
  97. package/pages/c/_cluster/explorer/__tests__/index.test.ts +1 -1
  98. package/pages/c/_cluster/fleet/index.vue +4 -7
  99. package/pages/c/_cluster/settings/index.vue +5 -0
  100. package/pkg/auto-import.js +3 -3
  101. package/pkg/dynamic-importer.lib.js +1 -1
  102. package/pkg/import.js +1 -1
  103. package/plugins/__tests__/mutations.tests.ts +179 -0
  104. package/plugins/dashboard-store/getters.js +1 -1
  105. package/plugins/dashboard-store/model-loader.js +1 -1
  106. package/plugins/dashboard-store/mutations.js +23 -2
  107. package/plugins/dashboard-store/resource-class.js +8 -3
  108. package/plugins/plugin.js +2 -2
  109. package/plugins/steve/__tests__/steve-pagination-utils.test.ts +301 -128
  110. package/plugins/steve/steve-class.js +1 -1
  111. package/plugins/steve/steve-pagination-utils.ts +108 -43
  112. package/rancher-components/Form/Checkbox/Checkbox.vue +1 -1
  113. package/rancher-components/Form/LabeledInput/LabeledInput.vue +1 -1
  114. package/rancher-components/RcDropdown/useDropdownContext.ts +2 -4
  115. package/rancher-components/RcItemCard/RcItemCard.vue +1 -1
  116. package/scripts/publish-shell.sh +25 -0
  117. package/store/__tests__/catalog.test.ts +1 -1
  118. package/store/__tests__/type-map.test.ts +164 -2
  119. package/store/auth.js +23 -11
  120. package/store/i18n.js +3 -3
  121. package/store/index.js +5 -3
  122. package/store/notifications.ts +2 -0
  123. package/store/prefs.js +2 -2
  124. package/store/type-map.js +17 -7
  125. package/types/internal-api/shell/modal.d.ts +6 -6
  126. package/types/notifications/index.ts +126 -15
  127. package/types/rancher/index.d.ts +9 -0
  128. package/types/shell/index.d.ts +16 -1
  129. package/types/vue-shim.d.ts +5 -4
  130. package/utils/__tests__/router.test.js +238 -0
  131. package/utils/cluster.js +4 -1
  132. package/utils/fleet.ts +8 -1
  133. package/utils/pagination-utils.ts +2 -2
  134. package/utils/pagination-wrapper.ts +1 -1
  135. package/utils/router.js +50 -0
  136. package/utils/unit-tests/pagination-utils.spec.ts +8 -8
  137. package/vue.config.js +3 -3
  138. package/composables/useExtensionManager.ts +0 -17
  139. package/core/__test__/extension-manager-impl.test.js +0 -236
  140. package/core/plugins.js +0 -38
  141. package/directives/clean-tooltip.js +0 -32
  142. package/plugins/internal-api/index.ts +0 -37
  143. package/plugins/internal-api/shared/base-api.ts +0 -13
  144. package/plugins/internal-api/shell/shell.api.ts +0 -108
  145. package/types/internal-api/shell/growl.d.ts +0 -25
  146. package/types/internal-api/shell/slideIn.d.ts +0 -15
@@ -69,6 +69,22 @@ describe('class ProvCluster', () => {
69
69
  },
70
70
  expected: true
71
71
  },
72
+ {
73
+ description: 'should return true for an imported k3s cluster in waiting state',
74
+ clusterData: {
75
+ isLocal: false,
76
+ mgmt: { status: { provider: undefined, driver: 'k3s' } }
77
+ },
78
+ expected: true
79
+ },
80
+ {
81
+ description: 'should return true for an imported rke2 cluster in waiting state',
82
+ clusterData: {
83
+ isLocal: false,
84
+ mgmt: { status: { provider: undefined, driver: 'rke2' } }
85
+ },
86
+ expected: true
87
+ },
72
88
  {
73
89
  description: 'should return false for a provisioned k3s cluster',
74
90
  clusterData: {
package/models/chart.js CHANGED
@@ -136,91 +136,87 @@ export default class Chart extends SteveModel {
136
136
  * @returns {Object} Card content object with `subHeaderItems`, `footerItems`, and `statuses` arrays.
137
137
  */
138
138
  get cardContent() {
139
- if (!this._cardContent) {
140
- const latestVersion = this.latestCompatibleVersion;
141
- const subHeaderItems = [];
142
-
143
- if (latestVersion) {
144
- const hasZeroTime = latestVersion.created === ZERO_TIME;
145
-
146
- subHeaderItems.push({
147
- icon: 'icon-version-alt',
148
- iconTooltip: { key: 'tableHeaders.version' },
149
- label: latestVersion.version
150
- });
151
-
152
- const lastUpdatedItem = {
153
- icon: 'icon-refresh-alt',
154
- iconTooltip: { key: 'tableHeaders.lastUpdated' },
155
- label: hasZeroTime ? this.t('generic.na') : day(latestVersion.created).format('MMM D, YYYY')
156
- };
157
-
158
- if (hasZeroTime) {
159
- lastUpdatedItem.labelTooltip = this.t('catalog.charts.appChartCard.subHeaderItem.missingVersionDate');
160
- }
161
-
162
- subHeaderItems.push(lastUpdatedItem);
163
- }
139
+ const latestVersion = this.latestCompatibleVersion;
140
+ const subHeaderItems = [];
141
+
142
+ if (latestVersion) {
143
+ const hasZeroTime = latestVersion.created === ZERO_TIME;
144
+
145
+ subHeaderItems.push({
146
+ icon: 'icon-version-alt',
147
+ iconTooltip: { key: 'tableHeaders.version' },
148
+ label: latestVersion.version
149
+ });
150
+
151
+ const lastUpdatedItem = {
152
+ icon: 'icon-refresh-alt',
153
+ iconTooltip: { key: 'tableHeaders.lastUpdated' },
154
+ label: hasZeroTime ? this.t('generic.na') : day(latestVersion.created).format('MMM D, YYYY')
155
+ };
164
156
 
165
- const footerItems = [
166
- {
167
- type: REPO,
168
- icon: 'icon-repository-alt',
169
- iconTooltip: { key: 'tableHeaders.repoName' },
170
- labels: [this.repoNameDisplay],
171
- labelTooltip: this.t('catalog.charts.findSimilar.message', { type: this.t('catalog.charts.findSimilar.types.repo') }, true)
172
- }
173
- ];
174
-
175
- if (this.categories.length) {
176
- footerItems.push( {
177
- type: CATEGORY,
178
- icon: 'icon-category-alt',
179
- iconTooltip: { key: 'generic.category' },
180
- labels: this.categories,
181
- labelTooltip: this.t('catalog.charts.findSimilar.message', { type: this.t('catalog.charts.findSimilar.types.category') }, true)
182
- });
157
+ if (hasZeroTime) {
158
+ lastUpdatedItem.labelTooltip = this.t('catalog.charts.appChartCard.subHeaderItem.missingVersionDate');
183
159
  }
184
160
 
185
- if (this.tags.length) {
186
- footerItems.push({
187
- type: TAG,
188
- icon: 'icon-tag-alt',
189
- iconTooltip: { key: 'generic.tags' },
190
- labels: this.tags,
191
- labelTooltip: this.t('catalog.charts.findSimilar.message', { type: this.t('catalog.charts.findSimilar.types.tag') }, true)
192
- });
161
+ subHeaderItems.push(lastUpdatedItem);
162
+ }
163
+
164
+ const footerItems = [
165
+ {
166
+ type: REPO,
167
+ icon: 'icon-repository-alt',
168
+ iconTooltip: { key: 'tableHeaders.repoName' },
169
+ labels: [this.repoNameDisplay],
170
+ labelTooltip: this.t('catalog.charts.findSimilar.message', { type: this.t('catalog.charts.findSimilar.types.repo') }, true)
193
171
  }
172
+ ];
173
+
174
+ if (this.categories.length) {
175
+ footerItems.push( {
176
+ type: CATEGORY,
177
+ icon: 'icon-category-alt',
178
+ iconTooltip: { key: 'generic.category' },
179
+ labels: this.categories,
180
+ labelTooltip: this.t('catalog.charts.findSimilar.message', { type: this.t('catalog.charts.findSimilar.types.category') }, true)
181
+ });
182
+ }
194
183
 
195
- const statuses = [];
184
+ if (this.tags.length) {
185
+ footerItems.push({
186
+ type: TAG,
187
+ icon: 'icon-tag-alt',
188
+ iconTooltip: { key: 'generic.tags' },
189
+ labels: this.tags,
190
+ labelTooltip: this.t('catalog.charts.findSimilar.message', { type: this.t('catalog.charts.findSimilar.types.tag') }, true)
191
+ });
192
+ }
196
193
 
197
- if (this.deprecated) {
198
- statuses.push({
199
- icon: 'icon-alert-alt', color: 'error', tooltip: { key: 'generic.deprecated' }
200
- });
201
- }
194
+ const statuses = [];
202
195
 
203
- if (this.upgradeable) {
204
- statuses.push({
205
- icon: 'icon-upgrade-alt', color: 'info', tooltip: { key: 'generic.upgradeable' }
206
- });
207
- }
196
+ if (this.deprecated) {
197
+ statuses.push({
198
+ icon: 'icon-alert-alt', color: 'error', tooltip: { key: 'generic.deprecated' }
199
+ });
200
+ }
208
201
 
209
- if (this.isInstalled) {
210
- const installedVersion = this.matchingInstalledApps[0]?.spec?.chart?.metadata?.version;
202
+ if (this.upgradeable) {
203
+ statuses.push({
204
+ icon: 'icon-upgrade-alt', color: 'info', tooltip: { key: 'generic.upgradeable' }
205
+ });
206
+ }
211
207
 
212
- statuses.push({
213
- icon: 'icon-confirmation-alt', color: 'success', tooltip: { text: `${ this.t('generic.installed') } (${ installedVersion })` }
214
- });
215
- }
208
+ if (this.isInstalled) {
209
+ const installedVersion = this.matchingInstalledApps[0]?.spec?.chart?.metadata?.version;
216
210
 
217
- this._cardContent = {
218
- subHeaderItems,
219
- footerItems,
220
- statuses
221
- };
211
+ statuses.push({
212
+ icon: 'icon-confirmation-alt', color: 'success', tooltip: { text: `${ this.t('generic.installed') } (${ installedVersion })` }
213
+ });
222
214
  }
223
215
 
224
- return this._cardContent;
216
+ return {
217
+ subHeaderItems,
218
+ footerItems,
219
+ statuses
220
+ };
225
221
  }
226
222
  }
@@ -255,7 +255,7 @@ export default class MgmtCluster extends SteveModel {
255
255
  dispatch: this.$dispatch,
256
256
  getters: this.$getters,
257
257
  axios: this.$axios,
258
- $extension: this.$plugin,
258
+ $extension: this.$extension,
259
259
  t: (...args) => this.t.apply(this, args),
260
260
  };
261
261
 
@@ -277,7 +277,7 @@ export default class ProvCluster extends SteveModel {
277
277
  dispatch: this.$dispatch,
278
278
  getters: this.$getters,
279
279
  axios: this.$axios,
280
- $extension: this.$plugin,
280
+ $extension: this.$extension,
281
281
  t: (...args) => this.t.apply(this, args),
282
282
  };
283
283
 
@@ -326,8 +326,16 @@ export default class ProvCluster extends SteveModel {
326
326
 
327
327
  // imported rke2 and k3s have status.driver === rke2 and k3s respectively
328
328
  // Provisioned rke2 and k3s have status.driver === imported
329
- if (this.mgmt?.status?.provider === 'k3s' || this.mgmt?.status?.provider === 'rke2') {
330
- return this.mgmt?.status?.driver === this.mgmt?.status?.provider;
329
+ const provider = this.mgmt?.status?.provider;
330
+ const driver = this.mgmt?.status?.driver;
331
+
332
+ // The main case
333
+ if (provider === 'k3s' || provider === 'rke2') {
334
+ return driver === provider;
335
+ }
336
+ // The 'waiting' case
337
+ if (!provider && (driver === 'k3s' || driver === 'rke2')) {
338
+ return true;
331
339
  }
332
340
 
333
341
  // imported KEv2
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@rancher/shell",
3
- "version": "3.0.8-rc.9",
3
+ "version": "3.0.8",
4
4
  "description": "Rancher Dashboard Shell",
5
- "repository": "https://github.com/rancherlabs/dashboard",
5
+ "repository": "https://github.com/rancher/dashboard",
6
6
  "license": "Apache-2.0",
7
7
  "author": "SUSE",
8
8
  "private": false,
@@ -39,7 +39,7 @@
39
39
  "@babel/preset-typescript": "7.16.7",
40
40
  "@novnc/novnc": "1.2.0",
41
41
  "@popperjs/core": "2.11.8",
42
- "@rancher/icons": "2.0.53",
42
+ "@rancher/icons": "2.0.54",
43
43
  "@types/is-url": "1.2.30",
44
44
  "@types/node": "20.10.8",
45
45
  "@types/semver": "^7.5.8",
@@ -53,12 +53,12 @@
53
53
  "add": "2.0.6",
54
54
  "ansi_up": "5.0.0",
55
55
  "axios-retry": "3.1.9",
56
- "axios": "1.12.2",
56
+ "axios": "1.13.2",
57
57
  "babel-eslint": "10.1.0",
58
- "babel-plugin-module-resolver": "4.0.0",
58
+ "babel-plugin-module-resolver": "5.0.2",
59
59
  "babel-preset-vue": "2.0.2",
60
60
  "cache-loader": "4.1.0",
61
- "chart.js": "4.4.8",
61
+ "chart.js": "4.5.1",
62
62
  "clipboard-polyfill": "4.0.1",
63
63
  "codemirror-editor-vue3": "2.8.0",
64
64
  "codemirror": ">=5.64.0 <6",
@@ -140,7 +140,7 @@
140
140
  "vuedraggable": "4.1.0",
141
141
  "vuex": "4.1.0",
142
142
  "webpack-bundle-analyzer": "4.10.2",
143
- "webpack-virtual-modules": "0.4.3",
143
+ "webpack-virtual-modules": "0.6.2",
144
144
  "worker-loader": "3.0.8",
145
145
  "xterm-addon-canvas": "0.5.0",
146
146
  "xterm-addon-fit": "0.8.0",
@@ -315,9 +315,9 @@ export default {
315
315
  // so we manually load them here - other SSO auth providers bounce out and back to the Dashboard, so on the bounce-back
316
316
  // the plugins will load via the boot-time plugin
317
317
  await loadPlugins({
318
- app: this.$store.app,
319
- store: this.$store,
320
- $plugin: this.$store.$plugin
318
+ app: this.$store.app,
319
+ store: this.$store,
320
+ $extension: this.$store.$extension,
321
321
  });
322
322
 
323
323
  if (this.firstLogin || user[0]?.mustChangePassword) {
@@ -209,7 +209,7 @@ export default {
209
209
  const promises = [];
210
210
 
211
211
  try {
212
- await applyProducts(this.$store, this.$plugin);
212
+ await applyProducts(this.$store, this.$extension);
213
213
  await this.$store.dispatch('loadManagement');
214
214
 
215
215
  if ( this.mustChangePassword ) {
@@ -119,9 +119,9 @@ export default {
119
119
 
120
120
  // Load plugins
121
121
  await loadPlugins({
122
- app: this.$store.app,
123
- store: this.$store,
124
- $plugin: this.$store.$plugin
122
+ app: this.$store.app,
123
+ store: this.$store,
124
+ $extension: this.$store.$extension,
125
125
  });
126
126
 
127
127
  this.$router.replace(backTo);
@@ -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');
@@ -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
  },
@@ -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