@rancher/shell 3.0.12-rc.3 → 3.0.12-rc.4

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 (258) hide show
  1. package/assets/styles/global/_layout.scss +4 -0
  2. package/assets/translations/en-us.yaml +144 -41
  3. package/assets/translations/zh-hans.yaml +1 -7
  4. package/chart/monitoring/ClusterSelector.vue +0 -21
  5. package/chart/monitoring/prometheus/index.vue +6 -3
  6. package/components/CruResource.vue +161 -14
  7. package/components/ExplorerMembers.vue +8 -4
  8. package/components/ExplorerProjectsNamespaces.vue +10 -6
  9. package/components/GrowlManager.vue +4 -0
  10. package/components/MgmtNodeList.vue +184 -0
  11. package/components/Resource/Detail/Card/StateCard/__tests__/composables.test.ts +90 -1
  12. package/components/Resource/Detail/Card/StateCard/composables.ts +57 -87
  13. package/components/Resource/Detail/Card/StatusCard/__tests__/StatusCard.test.ts +61 -0
  14. package/components/Resource/Detail/Card/StatusCard/index.vue +61 -15
  15. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +2 -0
  16. package/components/Resource/Detail/Metadata/KeyValue.vue +5 -2
  17. package/components/Resource/Detail/Metadata/KeyValueRow.vue +2 -6
  18. package/components/ResourceDetail/index.vue +1 -1
  19. package/components/ResourceList/Masthead.vue +7 -1
  20. package/components/ResourceList/index.vue +82 -1
  21. package/components/RichTranslation.vue +5 -2
  22. package/components/Setting.vue +1 -0
  23. package/components/SubtleLink.vue +31 -6
  24. package/components/Tabbed/Tab.vue +29 -3
  25. package/components/Tabbed/index.vue +25 -3
  26. package/components/TableOfContents/TableOfContents.vue +109 -0
  27. package/components/TableOfContents/composables.ts +258 -0
  28. package/components/Window/ContainerShell.vue +21 -11
  29. package/components/Window/__tests__/ContainerShell.test.ts +107 -37
  30. package/components/Wizard.vue +9 -4
  31. package/components/fleet/AppCoChartGrid.vue +401 -0
  32. package/components/fleet/AppCoEmptyState.vue +127 -0
  33. package/components/fleet/AppCoPageHeader.vue +119 -0
  34. package/components/fleet/AppCoVersionSelect.vue +70 -0
  35. package/components/fleet/FleetClusterTargets/ClusterSelectionFields.vue +217 -0
  36. package/components/fleet/FleetClusterTargets/TargetsList.vue +123 -35
  37. package/components/fleet/FleetClusterTargets/index.vue +189 -146
  38. package/components/fleet/FleetIntro.vue +7 -3
  39. package/components/fleet/FleetNoWorkspaces.vue +7 -3
  40. package/components/fleet/FleetSecretSelector.vue +5 -3
  41. package/components/fleet/FleetValuesFrom.vue +8 -2
  42. package/components/fleet/GitRepoTargetTab.vue +0 -2
  43. package/components/fleet/HelmOpAdvancedTab.vue +19 -53
  44. package/components/fleet/HelmOpAppCoConfigTab.vue +593 -0
  45. package/components/fleet/HelmOpAppCoResourcesSection.vue +162 -0
  46. package/components/fleet/HelmOpResourcesSection.vue +82 -0
  47. package/components/fleet/HelmOpTargetOptionsSection.vue +89 -0
  48. package/components/fleet/HelmOpTargetTab.vue +64 -60
  49. package/components/fleet/HelmOpValuesTab.vue +129 -105
  50. package/components/fleet/__tests__/AppCoEmptyState.test.ts +71 -0
  51. package/components/fleet/__tests__/AppCoVersionSelect.test.ts +36 -0
  52. package/components/fleet/__tests__/ClusterSelectionFields.test.ts +62 -0
  53. package/components/fleet/__tests__/FleetClusterTargets.test.ts +253 -0
  54. package/components/fleet/__tests__/FleetSecretSelector.test.ts +16 -0
  55. package/components/fleet/__tests__/FleetValuesFrom.test.ts +44 -0
  56. package/components/fleet/__tests__/HelmOpAppCoConfigTab.test.ts +59 -0
  57. package/components/fleet/__tests__/HelmOpAppCoResourcesSection.test.ts +62 -0
  58. package/components/fleet/__tests__/HelmOpResourcesSection.test.ts +43 -0
  59. package/components/fleet/__tests__/HelmOpTargetOptionsSection.test.ts +34 -0
  60. package/components/fleet/__tests__/HelmOpValuesTab.test.ts +39 -0
  61. package/components/fleet/__tests__/__snapshots__/AppCoEmptyState.test.ts.snap +97 -0
  62. package/components/fleet/__tests__/__snapshots__/AppCoVersionSelect.test.ts.snap +30 -0
  63. package/components/fleet/__tests__/__snapshots__/ClusterSelectionFields.test.ts.snap +209 -0
  64. package/components/fleet/__tests__/__snapshots__/HelmOpTargetOptionsSection.test.ts.snap +140 -0
  65. package/components/fleet/dashboard/Empty.vue +8 -4
  66. package/components/fleet/dashboard/ResourceCard.vue +28 -0
  67. package/components/fleet/dashboard/ResourceDetails.vue +28 -0
  68. package/components/fleet/dashboard/__tests__/ResourceCard.test.ts +87 -0
  69. package/components/form/ArrayList.vue +61 -4
  70. package/components/form/KeyValue.vue +23 -2
  71. package/components/form/LabeledSelect.vue +39 -1
  72. package/components/form/Labels.vue +22 -3
  73. package/components/form/NameNsDescription.vue +13 -5
  74. package/components/form/ResourceTabs/index.vue +1 -0
  75. package/components/form/__tests__/NameNsDescription.test.ts +75 -0
  76. package/components/formatter/InternalExternalIP.vue +10 -4
  77. package/components/formatter/ServiceTargets.vue +26 -7
  78. package/components/formatter/__tests__/InternalExternalIP.test.ts +132 -0
  79. package/components/formatter/__tests__/ServiceTargets.test.ts +412 -0
  80. package/components/nav/Header.vue +4 -0
  81. package/components/nav/TopLevelMenu.vue +7 -2
  82. package/components/nav/__tests__/Header.test.ts +15 -0
  83. package/components/nav/__tests__/TopLevelMenu.test.ts +120 -2
  84. package/components/templates/default.vue +9 -4
  85. package/components/templates/home.vue +9 -4
  86. package/components/templates/plain.vue +9 -4
  87. package/composables/useHelmOpResources.test.ts +56 -0
  88. package/composables/useHelmOpResources.ts +32 -0
  89. package/composables/useStateColor.test.ts +325 -0
  90. package/composables/useStateColor.ts +128 -0
  91. package/config/home-links.js +1 -1
  92. package/config/labels-annotations.js +1 -0
  93. package/config/product/explorer.js +17 -4
  94. package/config/product/manager.js +2 -0
  95. package/config/router/index.js +16 -0
  96. package/config/router/navigation-guards/__tests__/authentication.test.ts +130 -0
  97. package/config/router/navigation-guards/authentication.js +10 -4
  98. package/config/router/routes.js +20 -6
  99. package/config/settings.ts +0 -2
  100. package/config/table-headers.js +3 -4
  101. package/config/types.js +9 -0
  102. package/core/plugin-products-base.ts +3 -3
  103. package/core/plugin-types.ts +83 -30
  104. package/core/plugin.ts +3 -0
  105. package/core/types-provisioning.ts +34 -1
  106. package/core/types.ts +15 -2
  107. package/detail/__tests__/provisioning.cattle.io.cluster.test.ts +114 -0
  108. package/detail/__tests__/workload.test.ts +3 -152
  109. package/detail/catalog.cattle.io.clusterrepo.vue +1 -1
  110. package/detail/provisioning.cattle.io.cluster.vue +30 -4
  111. package/detail/workload/index.vue +12 -55
  112. package/edit/__tests__/catalog.cattle.io.clusterrepo.test.ts +248 -0
  113. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +105 -0
  114. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +6 -0
  115. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/index.test.ts.snap +1 -0
  116. package/edit/auth/__tests__/azuread.test.ts +34 -9
  117. package/edit/auth/__tests__/github.test.ts +234 -0
  118. package/edit/auth/__tests__/oidc.test.ts +26 -6
  119. package/edit/auth/__tests__/saml.test.ts +196 -0
  120. package/edit/auth/azuread.vue +128 -95
  121. package/edit/auth/github.vue +72 -13
  122. package/edit/auth/ldap/__tests__/index.test.ts +206 -0
  123. package/edit/auth/ldap/config.vue +8 -0
  124. package/edit/auth/ldap/index.vue +75 -1
  125. package/edit/auth/oidc.vue +119 -73
  126. package/edit/auth/saml.vue +76 -12
  127. package/edit/catalog.cattle.io.clusterrepo.vue +140 -32
  128. package/edit/fleet.cattle.io.helmop.vue +491 -136
  129. package/edit/management.cattle.io.user.vue +5 -2
  130. package/edit/provisioning.cattle.io.cluster/rke2.vue +84 -10
  131. package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +11 -0
  132. package/list/group.principal.vue +5 -4
  133. package/list/harvesterhci.io.management.cluster.vue +8 -9
  134. package/list/management.cattle.io.user.vue +12 -9
  135. package/list/provisioning.cattle.io.cluster.vue +16 -10
  136. package/mixins/__tests__/auth-config.test.ts +90 -0
  137. package/mixins/__tests__/chart.test.ts +94 -0
  138. package/mixins/__tests__/resource-fetch-api-pagination.test.ts +48 -0
  139. package/mixins/auth-config.js +7 -0
  140. package/mixins/chart.js +11 -2
  141. package/mixins/child-hook.js +12 -6
  142. package/mixins/create-edit-view/impl.js +5 -3
  143. package/mixins/resource-fetch-api-pagination.js +21 -1
  144. package/models/__tests__/catalog.cattle.io.clusterrepo.test.ts +57 -0
  145. package/models/__tests__/compliance.cattle.io.clusterscan.test.ts +144 -0
  146. package/models/__tests__/fleet-application.test.ts +175 -0
  147. package/models/__tests__/fleet.cattle.io.bundle.test.ts +169 -0
  148. package/models/__tests__/fleet.cattle.io.helmop.test.ts +84 -0
  149. package/models/__tests__/management.cattle.io.node.ts +22 -0
  150. package/models/__tests__/namespace.test.ts +36 -0
  151. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +49 -0
  152. package/models/__tests__/workload.test.ts +401 -26
  153. package/models/catalog.cattle.io.clusterrepo.js +28 -4
  154. package/models/compliance.cattle.io.clusterscan.js +39 -4
  155. package/models/fleet-application.js +4 -0
  156. package/models/fleet.cattle.io.helmop.js +20 -1
  157. package/models/management.cattle.io.cluster.js +18 -2
  158. package/models/management.cattle.io.node.js +44 -3
  159. package/models/namespace.js +1 -1
  160. package/models/pod.js +33 -1
  161. package/models/provisioning.cattle.io.cluster.js +5 -5
  162. package/models/workload.js +108 -13
  163. package/models/workload.service.js +5 -0
  164. package/package.json +14 -13
  165. package/pages/about.vue +5 -6
  166. package/pages/auth/login.vue +0 -35
  167. package/pages/auth/setup.vue +11 -0
  168. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +2 -2
  169. package/pages/c/_cluster/apps/charts/AppChartCardSubHeader.vue +10 -1
  170. package/pages/c/_cluster/apps/charts/__tests__/index.test.ts +93 -0
  171. package/pages/c/_cluster/apps/charts/chart.vue +2 -1
  172. package/pages/c/_cluster/apps/charts/index.vue +48 -10
  173. package/pages/c/_cluster/apps/charts/install.vue +122 -116
  174. package/pages/c/_cluster/auth/roles/index.vue +5 -4
  175. package/pages/c/_cluster/explorer/workload-dashboard/ByNamespaceSection.vue +31 -0
  176. package/pages/c/_cluster/explorer/workload-dashboard/ByStateSection.vue +138 -0
  177. package/pages/c/_cluster/explorer/workload-dashboard/ByTypeSection.vue +30 -0
  178. package/pages/c/_cluster/explorer/workload-dashboard/WorkloadCard.vue +155 -0
  179. package/pages/c/_cluster/explorer/workload-dashboard/WorkloadNamespaceCard.vue +142 -0
  180. package/pages/c/_cluster/explorer/workload-dashboard/WorkloadTypeCard.vue +159 -0
  181. package/pages/c/_cluster/explorer/workload-dashboard/__tests__/composable.test.ts +561 -0
  182. package/pages/c/_cluster/explorer/workload-dashboard/composable.ts +440 -0
  183. package/pages/c/_cluster/explorer/workload-dashboard/index.vue +187 -0
  184. package/pages/c/_cluster/explorer/workload-dashboard/types.ts +80 -0
  185. package/pages/c/_cluster/fleet/application/create.vue +187 -136
  186. package/pages/c/_cluster/fleet/application/index.vue +5 -3
  187. package/pages/c/_cluster/fleet/application/suse-app-collection/ChartDetailBody.vue +338 -0
  188. package/pages/c/_cluster/fleet/application/suse-app-collection/ChartDetailHeader.vue +121 -0
  189. package/pages/c/_cluster/fleet/application/suse-app-collection/chart.vue +369 -0
  190. package/pages/c/_cluster/fleet/application/suse-app-collection/charts.vue +248 -0
  191. package/pages/c/_cluster/fleet/application/suse-app-collection/credentials.vue +310 -0
  192. package/pages/c/_cluster/fleet/index.vue +2 -2
  193. package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +96 -0
  194. package/pages/c/_cluster/uiplugins/index.vue +15 -0
  195. package/pages/fail-whale.vue +16 -11
  196. package/pages/home.vue +16 -46
  197. package/plugins/clean-html.d.ts +9 -0
  198. package/plugins/dashboard-store/__tests__/resource-class.test.ts +93 -0
  199. package/plugins/dashboard-store/resource-class.js +62 -7
  200. package/plugins/steve/__tests__/actions.test.ts +212 -0
  201. package/plugins/steve/actions.js +96 -0
  202. package/plugins/steve/steve-pagination-utils.ts +1 -1
  203. package/rancher-components/Accordion/Accordion.vue +53 -9
  204. package/rancher-components/Form/Checkbox/Checkbox.vue +14 -0
  205. package/rancher-components/Form/Radio/RadioButton.vue +17 -1
  206. package/rancher-components/Form/Radio/RadioGroup.vue +10 -0
  207. package/rancher-components/Pill/RcTag/RcTag.vue +3 -2
  208. package/rancher-components/RcButton/RcButton.test.ts +103 -0
  209. package/rancher-components/RcButton/RcButton.vue +94 -15
  210. package/rancher-components/RcButton/types.ts +3 -0
  211. package/rancher-components/RcItemCard/RcItemCard.test.ts +18 -0
  212. package/rancher-components/RcItemCard/RcItemCard.vue +2 -2
  213. package/rancher-components/RcSection/RcSection.vue +28 -3
  214. package/scripts/extension/helm/package/Dockerfile +1 -1
  215. package/scripts/test-plugins-build.sh +2 -1
  216. package/store/__tests__/notifications.test.ts +434 -0
  217. package/store/catalog.js +57 -0
  218. package/store/plugins.js +7 -4
  219. package/types/components/buttonGroup.ts +5 -0
  220. package/types/shell/index.d.ts +104 -70
  221. package/utils/__tests__/auth.test.ts +273 -0
  222. package/utils/__tests__/computed.test.ts +193 -0
  223. package/utils/__tests__/cspAdaptor.test.ts +163 -0
  224. package/utils/__tests__/dom.test.ts +81 -0
  225. package/utils/__tests__/duration.test.ts +37 -1
  226. package/utils/__tests__/dynamic-importer.test.ts +102 -0
  227. package/utils/__tests__/fleet-appco.test.ts +312 -0
  228. package/utils/__tests__/monitoring.test.ts +130 -0
  229. package/utils/__tests__/object.test.ts +22 -0
  230. package/utils/__tests__/platform.test.ts +91 -0
  231. package/utils/__tests__/position.test.ts +237 -0
  232. package/utils/__tests__/provider.test.ts +51 -1
  233. package/utils/__tests__/queue.test.ts +232 -0
  234. package/utils/__tests__/release-notes.test.ts +221 -0
  235. package/utils/__tests__/router.test.js +254 -1
  236. package/utils/__tests__/select.test.ts +208 -0
  237. package/utils/__tests__/time.test.ts +265 -1
  238. package/utils/__tests__/title.test.ts +47 -0
  239. package/utils/__tests__/width.test.ts +53 -0
  240. package/utils/__tests__/window.test.ts +158 -0
  241. package/utils/__tests__/xccdf.test.ts +126 -6
  242. package/utils/crypto/__tests__/browserHashUtils.test.ts +98 -0
  243. package/utils/crypto/__tests__/index.test.ts +144 -0
  244. package/utils/duration.ts +104 -0
  245. package/utils/dynamic-content/__tests__/notification-handler.test.ts +196 -0
  246. package/utils/dynamic-content/info.ts +2 -1
  247. package/utils/error.js +13 -0
  248. package/utils/fleet-appco.ts +323 -0
  249. package/utils/object.js +22 -2
  250. package/utils/provider.ts +12 -0
  251. package/utils/validators/__tests__/container-images.test.ts +104 -0
  252. package/utils/validators/__tests__/flow-output.test.ts +91 -0
  253. package/utils/validators/__tests__/logging-outputs.test.ts +58 -0
  254. package/utils/validators/__tests__/monitoring-route.test.ts +119 -0
  255. package/utils/xccdf.ts +39 -42
  256. package/vue.config.js +1 -1
  257. package/pages/support/index.vue +0 -264
  258. package/utils/duration.js +0 -43
@@ -1,5 +1,6 @@
1
1
  <script>
2
2
  import isEmpty from 'lodash/isEmpty';
3
+ import throttle from 'lodash/throttle';
3
4
  import { createYamlWithOptions } from '@shell/utils/create-yaml';
4
5
  import { clone, get } from '@shell/utils/object';
5
6
  import { SCHEMA, NAMESPACE } from '@shell/config/types';
@@ -11,6 +12,10 @@ import { stringify, exceptionToErrorsArray } from '@shell/utils/error';
11
12
  import CruResourceFooter from '@shell/components/CruResourceFooter';
12
13
  import { useResourceCreatePageProvider, useResourceEditPageProvider } from '@shell/composables/cruResource';
13
14
 
15
+ import { useFormSummary } from '@shell/components/TableOfContents/composables';
16
+ import { useTemplateRef } from 'vue';
17
+ import TableOfContents from '@shell/components/TableOfContents/TableOfContents.vue';
18
+
14
19
  import {
15
20
  _EDIT, _VIEW, AS, _YAML, _UNFLAG, SUB_TYPE, _CREATE
16
21
  } from '@shell/config/query-params';
@@ -31,7 +36,8 @@ export default {
31
36
  Banner,
32
37
  CruResourceFooter,
33
38
  ResourceYaml,
34
- Wizard
39
+ Wizard,
40
+ TableOfContents
35
41
  },
36
42
 
37
43
  props: {
@@ -162,9 +168,22 @@ export default {
162
168
  yamlModifiers: {
163
169
  type: Object,
164
170
  default: undefined
171
+ },
172
+
173
+ showToc: {
174
+ type: Boolean,
175
+ default: false
165
176
  }
166
177
  },
167
178
 
179
+ setup() {
180
+ const cruFormRef = useTemplateRef('cru-form');
181
+ const { locatedComponents } = useFormSummary(cruFormRef);
182
+ const accordions = locatedComponents;
183
+
184
+ return { accordions };
185
+ },
186
+
168
187
  data(props) {
169
188
  const inStore = this.$store.getters['currentStore'](this.resource);
170
189
  const schema = this.$store.getters[`${ inStore }/schemaFor`](this.resource.type);
@@ -176,27 +195,30 @@ export default {
176
195
  }
177
196
 
178
197
  return {
179
- isCancelModal: false,
180
- showAsForm: this.$route.query[AS] !== _YAML,
198
+ isCancelModal: false,
199
+ showAsForm: this.$route.query[AS] !== _YAML,
200
+ tocContainerHeight: 0,
201
+ mainLayoutEl: null,
202
+ throttledComputeTocContainerHeight: null,
181
203
  /**
182
204
  * Initialised on demand (given that it needs to make a request to fetch schema definition)
183
205
  */
184
- resourceYaml: null,
206
+ resourceYaml: null,
185
207
  /**
186
208
  * Initialised on demand (given that it needs to make a request to fetch schema definition)
187
209
  */
188
- initialYaml: null,
210
+ initialYaml: null,
189
211
  /**
190
212
  * Save a copy of the initial resource. This is used to calc the initial yaml later on
191
213
  */
192
- initialResource: clone(this.resource),
193
- abbrSizes: {
214
+ initialResource: clone(this.resource),
215
+ abbrSizes: {
194
216
  3: '24px',
195
217
  4: '18px',
196
218
  5: '16px',
197
219
  6: '14px'
198
220
  },
199
- schema
221
+ schema,
200
222
  };
201
223
  },
202
224
 
@@ -276,10 +298,11 @@ export default {
276
298
  icon: null
277
299
  }
278
300
  }), {});
279
- },
301
+ }
280
302
  },
281
303
 
282
304
  created() {
305
+ this.throttledComputeTocContainerHeight = throttle(this.computeTocContainerHeight, 20);
283
306
  if ( this._selectedSubtype ) {
284
307
  this.$emit('select-type', this._selectedSubtype);
285
308
  }
@@ -290,12 +313,42 @@ export default {
290
313
  },
291
314
 
292
315
  beforeUnmount() {
316
+ this.mainLayoutEl?.removeEventListener('scroll', this.throttledComputeTocContainerHeight);
317
+ window.removeEventListener('resize', this.throttledComputeTocContainerHeight);
318
+ this.throttledComputeTocContainerHeight?.cancel?.();
293
319
  this.$store.dispatch('cru-resource/setCreateNamespace', false);
294
320
  },
295
321
 
296
322
  methods: {
297
323
  stringify,
298
324
 
325
+ // as the user scrolls past the CruResource Masthead, the amount of vertical space available to the table of contents changes
326
+ computeTocContainerHeight() {
327
+ const root = this.$el;
328
+
329
+ if (!root) {
330
+ this.tocContainerHeight = 0;
331
+
332
+ return 0;
333
+ }
334
+
335
+ const tocEl = root.querySelector('.cru__toc');
336
+ const footerEl = root.querySelector('.cru__footer');
337
+
338
+ if (!tocEl || !footerEl) {
339
+ this.tocContainerHeight = 0;
340
+
341
+ return 0;
342
+ }
343
+
344
+ const tocTop = tocEl.getBoundingClientRect().top;
345
+ const footerTop = footerEl.getBoundingClientRect().top;
346
+ const gapLgValue = getComputedStyle(root).getPropertyValue('--gap-lg').trim();
347
+ const gapLg = Number.parseFloat(gapLgValue) || 0;
348
+
349
+ this.tocContainerHeight = Math.max(0, Math.round((footerTop - tocTop) - gapLg));
350
+ },
351
+
299
352
  confirmCancel(isCancelNotBack = true) {
300
353
  if (isCancelNotBack) {
301
354
  this.emitOrRoute();
@@ -532,13 +585,38 @@ export default {
532
585
  this.initialYaml = await this.createResourceYaml(undefined, this.initialResource);
533
586
  }
534
587
  }
588
+ },
589
+
590
+ showToc: {
591
+ handler(neu, old) {
592
+ if (neu) {
593
+ // Compute height on first render
594
+ this.$nextTick(() => {
595
+ this.throttledComputeTocContainerHeight?.();
596
+ });
597
+ // Add event listeners for computeTocContainerHeight on scroll
598
+ this.mainLayoutEl = document.querySelector('.main-layout');
599
+ this.mainLayoutEl?.addEventListener('scroll', this.throttledComputeTocContainerHeight, { passive: true });
600
+ // Add event listener for computeTocContainerHeight on window resize
601
+ window.addEventListener('resize', this.throttledComputeTocContainerHeight, { passive: true });
602
+ } else if (old) {
603
+ // Remove event listeners for computeTocContainerHeight when TOC is hidden
604
+ this.mainLayoutEl?.removeEventListener('scroll', this.throttledComputeTocContainerHeight);
605
+ window.removeEventListener('resize', this.throttledComputeTocContainerHeight);
606
+ }
607
+ },
608
+ immediate: true
535
609
  }
536
610
  }
537
611
  };
538
612
  </script>
539
613
 
540
614
  <template>
541
- <section class="cru">
615
+ <section
616
+ ref="cru-form"
617
+ :class="{'show-toc':showToc}"
618
+ class="cru"
619
+ >
542
620
  <slot name="noticeBanner" />
543
621
  <p
544
622
  v-if="description"
@@ -548,6 +626,7 @@ export default {
548
626
  </p>
549
627
  <component
550
628
  :is="(isView? 'div' : 'form')"
629
+
551
630
  :value="resource"
552
631
  data-testid="cru-form"
553
632
  class="create-resource-container cru__form"
@@ -771,15 +850,23 @@ export default {
771
850
  </template>
772
851
  <!------ SINGLE PROCESS ------>
773
852
  <template v-else-if="showAsForm">
853
+ <TableOfContents
854
+ v-if="showToc"
855
+ class="cru__toc"
856
+ :style="tocContainerHeight ? { '--toc-container-height': `${tocContainerHeight}px` } : {}"
857
+ :accordions="accordions"
858
+ />
774
859
  <div
775
860
  v-if="_selectedSubtype || !subtypes.length"
776
- class="resource-container cru__content"
861
+ class="cru__content resource-container"
862
+
777
863
  :style="[minHeight ? { 'min-height': minHeight } : {}]"
778
864
  >
779
865
  <slot name="single">
780
866
  <slot />
781
867
  </slot>
782
868
  </div>
869
+
783
870
  <slot name="form-footer">
784
871
  <CruResourceFooter
785
872
  v-if="!isView"
@@ -912,6 +999,11 @@ export default {
912
999
  </template>
913
1000
 
914
1001
  <style lang='scss' scoped>
1002
+ $logo: 60px;
1003
+ $logo-space: 100px;
1004
+
1005
+ $table-contents-width: 250px;
1006
+
915
1007
  .cru-resource-yaml-container {
916
1008
  .resource-yaml {
917
1009
  .yaml-editor {
@@ -937,9 +1029,6 @@ export default {
937
1029
  }
938
1030
  }
939
1031
 
940
- $logo: 60px;
941
- $logo-space: 100px;
942
-
943
1032
  .title {
944
1033
  margin-top: 20px;
945
1034
 
@@ -992,10 +1081,26 @@ form.create-resource-container .cru {
992
1081
  display: flex;
993
1082
  flex-direction: column;
994
1083
  flex-grow: 1;
1084
+
1085
+ }
1086
+
1087
+ &__toc {
1088
+ width: $table-contents-width;
1089
+ margin: 20px var(--gap-lg) 20px var(--gap-lg);
1090
+ min-width: $table-contents-width;
1091
+ max-width: $table-contents-width;
1092
+ position: sticky;
1093
+ top: 24px;
1094
+ align-self: flex-start;
1095
+ max-height: var(--toc-container-height, calc(100vh - 24px - $footer-height - calc( 2 * var(--gap-lg)) - 125px));
1096
+ transition: max-height 50ms ease-in-out;
1097
+ overflow-y: auto;
1098
+ overflow-x: hidden;
995
1099
  }
996
1100
 
997
1101
  &__content {
998
1102
  flex-grow: 1;
1103
+
999
1104
  &-wizard {
1000
1105
  display: flex;
1001
1106
  }
@@ -1025,6 +1130,48 @@ form.create-resource-container .cru {
1025
1130
  }
1026
1131
  }
1027
1132
 
1133
+ .show-toc.cru{
1134
+ &>.cru__form{
1135
+ display: grid;
1136
+ grid-template-columns: [content] 1fr [toc] calc(#{$table-contents-width} + var(--gap-lg));
1137
+ grid-template-rows: [errors] auto [content] 1fr [footer] min-content;
1138
+
1139
+ &>.cru__errors {
1140
+ grid-column: content;
1141
+ grid-row: errors;
1142
+ }
1143
+
1144
+ &>.cru__toc {
1145
+ grid-column: toc;
1146
+ grid-row: errors / footer;
1147
+ }
1148
+
1149
+ &>.cru__content {
1150
+ grid-column: content;
1151
+ grid-row: content;
1152
+ }
1153
+
1154
+ &>.cru__footer {
1155
+ grid-column: content / 3;
1156
+ grid-row: footer;
1157
+ }
1158
+ }
1159
+ }
1160
+
1161
+ @media (max-width: map-get($breakpoints, '--viewport-9')) {
1162
+ .show-toc.cru {
1163
+ & > .cru__form {
1164
+ display: flex;
1165
+ grid-template-columns: none;
1166
+ grid-template-rows: none;
1167
+
1168
+ & > .cru__toc {
1169
+ display: none;
1170
+ }
1171
+ }
1172
+ }
1173
+ }
1174
+
1028
1175
  .description {
1029
1176
  margin-bottom: 15px;
1030
1177
  margin-top: 5px;
@@ -12,6 +12,7 @@ import { mapGetters } from 'vuex';
12
12
  import { canViewProjectMembershipEditor } from '@shell/components/form/Members/ProjectMembershipEditor.vue';
13
13
  import { allHash } from '@shell/utils/promise';
14
14
  import { HARVESTER_NAME as HARVESTER } from '@shell/config/features';
15
+ import { RcButton } from '@components/RcButton';
15
16
 
16
17
  /**
17
18
  * Explorer members page.
@@ -26,7 +27,8 @@ export default {
26
27
  ResourceTable,
27
28
  Tabbed,
28
29
  Tab,
29
- SortableTable
30
+ SortableTable,
31
+ RcButton,
30
32
  },
31
33
 
32
34
  props: {
@@ -308,12 +310,14 @@ export default {
308
310
  v-if="canEditClusterMembers"
309
311
  class="row mb-10 cluster-add"
310
312
  >
311
- <router-link
313
+ <rc-button
314
+ size="large"
315
+ class="pull-right"
316
+ data-testid="button-cluster-member-add"
312
317
  :to="createLocation"
313
- class="btn role-primary pull-right"
314
318
  >
315
319
  {{ t('members.createActionLabel') }}
316
- </router-link>
320
+ </rc-button>
317
321
  </div>
318
322
  <ResourceTable
319
323
  :schema="schema"
@@ -20,6 +20,7 @@ import { HARVESTER_NAME as HARVESTER } from '@shell/config/features';
20
20
  import perfSettingsUtils from '@shell/utils/perf-setting.utils';
21
21
  import ActionMenu from '@shell/components/ActionMenuShell.vue';
22
22
  import { useRuntimeFlag } from '@shell/composables/useRuntimeFlag';
23
+ import { RcButton } from '@components/RcButton';
23
24
 
24
25
  export default {
25
26
  name: 'ListProjectNamespace',
@@ -29,6 +30,7 @@ export default {
29
30
  ResourceTable,
30
31
  ButtonMultiAction,
31
32
  ActionMenu,
33
+ RcButton
32
34
  },
33
35
  mixins: [ResourceFetch],
34
36
 
@@ -445,13 +447,14 @@ export default {
445
447
  v-if="showCreateNsButton"
446
448
  #extraActions
447
449
  >
448
- <router-link
450
+ <rc-button
451
+ size="large"
452
+ class="mr-10"
449
453
  :to="createNamespaceLocationFlatList()"
450
- class="btn role-primary mr-10"
451
454
  data-testid="create_project_namespaces"
452
455
  >
453
456
  {{ t('projectNamespaces.createNamespace') }}
454
- </router-link>
457
+ </rc-button>
455
458
  </template>
456
459
  </Masthead>
457
460
  <!-- Extensions area -->
@@ -495,13 +498,14 @@ export default {
495
498
  </div>
496
499
  </div>
497
500
  <div class="right mr-10">
498
- <router-link
501
+ <rc-button
499
502
  v-if="isNamespaceCreatable && (canSeeProjectlessNamespaces || group.group.key !== notInProjectKey)"
500
- class="create-namespace btn btn-sm role-secondary mr-5"
503
+ variant="secondary"
504
+ class="mr-5"
501
505
  :to="createNamespaceLocation(group.group)"
502
506
  >
503
507
  {{ t('projectNamespaces.createNamespace') }}
504
- </router-link>
508
+ </rc-button>
505
509
  <template v-if="featureDropdownMenu">
506
510
  <ActionMenu
507
511
  v-if="showProjectActionButton(group.group)"
@@ -204,6 +204,10 @@ export default {
204
204
  > P {
205
205
  padding-top: 2px;
206
206
 
207
+ // Limit size of message and scroll just the message, not the entire growl, so that the title and icon are always visible
208
+ max-height: 200px;
209
+ overflow-y: scroll;
210
+
207
211
  &.has-title {
208
212
  margin-top: 5px;
209
213
  }
@@ -0,0 +1,184 @@
1
+ <script>
2
+ import { MANAGEMENT } from '@shell/config/types';
3
+ import ResourceTable from '@shell/components/ResourceTable';
4
+ import {
5
+ INTERNAL_EXTERNAL_IP,
6
+ STATE, NAME as NAME_COL, AGE, MANAGEMENT_NODE_OS
7
+ } from '@shell/config/table-headers';
8
+
9
+ // exclude roles column - it is not necessarily used (or used the same way) outside of Rancher provisioning
10
+ export const DEFAULT_HEADERS = [STATE, {
11
+ ...NAME_COL,
12
+ value: 'status.nodeName',
13
+ formatterOpts: { reference: 'kubeNodeDetailLocation' }
14
+ }, INTERNAL_EXTERNAL_IP, MANAGEMENT_NODE_OS, AGE];
15
+
16
+ export default {
17
+ name: 'ClusterScopedManagementNodeList',
18
+
19
+ components: { ResourceTable },
20
+
21
+ props: {
22
+ resource: {
23
+ type: Object,
24
+ default: () => {
25
+ return {};
26
+ }
27
+ },
28
+
29
+ // override default management node schema headers
30
+ headers: {
31
+ type: Array,
32
+ default: () => DEFAULT_HEADERS
33
+ },
34
+
35
+ // function to get node group for a given node, used for grouping nodes by pool in the table
36
+ // result should be an object with name and (optionally) description properties
37
+ getNodeGroup: {
38
+ type: Function,
39
+ default: null
40
+ }
41
+
42
+ },
43
+
44
+ /**
45
+ * avoid fetching ALL nodes to find this cluster's nodes
46
+ * mgmt nodes do not have labels that can be used with a labelSelector action
47
+ * neither the prov cluster nor mgmt cluster list mgmt nodes in their metadata.relationships nor are the node names listed in either cluster's status
48
+ * BUT mgmt nodes are scoped to a cluster's namespace, so we can fetch only mgmt nodes in the cluster's namespace
49
+ */
50
+ async fetch() {
51
+ const canList = this.$store.getters['management/canList'](MANAGEMENT.NODE);
52
+
53
+ if ( canList ) {
54
+ const hasAllMgmtNodes = this.$store.getters['management/haveAll'](MANAGEMENT.NODE);
55
+
56
+ if (hasAllMgmtNodes) {
57
+ this.mgmtNodes = this.$store.getters['management/all'](MANAGEMENT.NODE);
58
+ } else {
59
+ const res = await this.$store.dispatch('management/findLabelSelector', {
60
+ type: MANAGEMENT.NODE,
61
+ matching: {
62
+ namespace: this.resource.mgmtClusterId,
63
+ labelSelector: {
64
+ matchExpressions: [
65
+ {
66
+ key: 'management.cattle.io/nodename',
67
+ operator: 'Exists',
68
+ }
69
+ ]
70
+ }
71
+ }
72
+ } );
73
+
74
+ this.mgmtNodes = res;
75
+ }
76
+ }
77
+ },
78
+
79
+ data() {
80
+ return {
81
+ mgmtNodes: [],
82
+
83
+ mgmtNodeSchema: this.$store.getters[`management/schemaFor`](MANAGEMENT.NODE),
84
+
85
+ noneGroupOption: {
86
+ tooltipKey: 'resourceTable.groupBy.none',
87
+ icon: 'icon-list-flat',
88
+ value: 'none',
89
+ },
90
+
91
+ poolGroupOption: {
92
+ tooltipKey: 'resourceTable.groupBy.pool',
93
+ icon: 'icon-cluster',
94
+ value: 'poolRef',
95
+ field: 'poolRef.name'
96
+ }
97
+
98
+ };
99
+ },
100
+
101
+ computed: {
102
+ nodes() {
103
+ return this.mgmtNodes.filter((x) => x.mgmtClusterId === this.resource.mgmtClusterId).map((node) => {
104
+ const poolRef = typeof this.getNodeGroup === 'function' ? this.getNodeGroup(node) : null;
105
+
106
+ node.poolRef = poolRef;
107
+
108
+ return node;
109
+ });
110
+ },
111
+ },
112
+ };
113
+ </script>
114
+
115
+ <template>
116
+ <ResourceTable
117
+ v-bind="$attrs"
118
+ :schema="mgmtNodeSchema"
119
+ :headers="headers"
120
+ :rows="nodes"
121
+ :ignore-filter="true"
122
+ group-ref="poolRef"
123
+ data-testid="mgmt-node-table"
124
+ :group-options="[noneGroupOption, poolGroupOption]"
125
+ >
126
+ <template #main-row:isFake="{fullColspan}">
127
+ <tr class="main-row">
128
+ <td
129
+ :colspan="fullColspan"
130
+ class="no-entries"
131
+ >
132
+ {{ t('node.list.noNodes') }}
133
+ </td>
134
+ </tr>
135
+ </template>
136
+
137
+ <template #group-by="{group}">
138
+ <div
139
+ class="pool-row"
140
+ :class="{'has-description':group.ref}"
141
+ >
142
+ <div
143
+ v-trim-whitespace
144
+ class="group-tab"
145
+ >
146
+ {{ group.ref?.name || t('resourceTable.groupLabel.notInANodePool') }}
147
+ <div
148
+ v-if="group.ref && group.ref.description"
149
+ v-clean-html="group.ref.description"
150
+ class="description text-muted text-small"
151
+ />
152
+ </div>
153
+ </div>
154
+ </template>
155
+ </ResourceTable>
156
+ </template>
157
+
158
+ <style scoped lang="scss">
159
+ .pool-row {
160
+ display: flex;
161
+ align-items: center;
162
+ justify-content: space-between;
163
+
164
+ &.has-description {
165
+ .group-tab {
166
+ &, &::after {
167
+ height: 50px;
168
+ }
169
+
170
+ &::after {
171
+ right: -20px;
172
+ }
173
+
174
+ .description {
175
+ margin-top: -20px;
176
+ }
177
+ }
178
+ }
179
+ .group-header-buttons {
180
+ align-items: center;
181
+ display: flex;
182
+ }
183
+ }
184
+ </style>
@@ -1,4 +1,4 @@
1
- import { useResourceCardRow } from '@shell/components/Resource/Detail/Card/StateCard/composables';
1
+ import { useResourceCardRow, useResourceCardRowFromRelationships } from '@shell/components/Resource/Detail/Card/StateCard/composables';
2
2
 
3
3
  describe('useResourceCardRow', () => {
4
4
  describe('with default keys', () => {
@@ -140,3 +140,92 @@ describe('useResourceCardRow', () => {
140
140
  });
141
141
  });
142
142
  });
143
+
144
+ describe('useResourceCardRowFromRelationships', () => {
145
+ it('should return empty props for empty relationships', () => {
146
+ const result = useResourceCardRowFromRelationships('Refers to', []);
147
+
148
+ expect(result.label).toBe('Refers to');
149
+ expect(result.color).toBeUndefined();
150
+ expect(result.counts).toBeUndefined();
151
+ });
152
+
153
+ it('should aggregate relationship states', () => {
154
+ const rels = [
155
+ { toType: 'configmap', state: 'active' },
156
+ { toType: 'secret', state: 'active' },
157
+ { toType: 'serviceaccount', state: 'error' }
158
+ ];
159
+
160
+ const result = useResourceCardRowFromRelationships('Refers to', rels);
161
+
162
+ expect(result.counts).toHaveLength(2);
163
+ expect(result.counts).toContainEqual(expect.objectContaining({ label: 'active', count: 2 }));
164
+ expect(result.counts).toContainEqual(expect.objectContaining({ label: 'error', count: 1 }));
165
+ });
166
+
167
+ it('should default missing state to "missing"', () => {
168
+ const rels = [
169
+ { toType: 'configmap' },
170
+ { toType: 'secret', state: 'active' }
171
+ ];
172
+
173
+ const result = useResourceCardRowFromRelationships('Refers to', rels);
174
+
175
+ expect(result.counts).toContainEqual(expect.objectContaining({
176
+ label: 'missing', count: 1, color: 'warning'
177
+ }));
178
+ expect(result.counts).toContainEqual(expect.objectContaining({
179
+ label: 'active', count: 1, color: 'success'
180
+ }));
181
+ });
182
+
183
+ it('should set the highest alert color as main color', () => {
184
+ const rels = [
185
+ { toType: 'configmap', state: 'active' },
186
+ { toType: 'secret', state: 'error' }
187
+ ];
188
+
189
+ const result = useResourceCardRowFromRelationships('Refers to', rels);
190
+
191
+ expect(result.color).toBe('error');
192
+ });
193
+
194
+ it('should sort by alert level then by count', () => {
195
+ const rels = [
196
+ { toType: 'a', state: 'active' },
197
+ { toType: 'b', state: 'active' },
198
+ { toType: 'c', state: 'active' },
199
+ { toType: 'd', state: 'error' },
200
+ { toType: 'e', state: 'warning' },
201
+ { toType: 'f', state: 'warning' }
202
+ ];
203
+
204
+ const result = useResourceCardRowFromRelationships('Refers to', rels);
205
+
206
+ expect(result.counts![0].color).toBe('error');
207
+ expect(result.counts![1].color).toBe('warning');
208
+ expect(result.counts![2].color).toBe('success');
209
+ });
210
+
211
+ it('should pass the to parameter through', () => {
212
+ const to = { hash: '#related' };
213
+ const result = useResourceCardRowFromRelationships('Refers to', [], to);
214
+
215
+ expect(result.to).toStrictEqual(to);
216
+ });
217
+
218
+ it('should handle all relationships having no state', () => {
219
+ const rels = [
220
+ { toType: 'configmap' },
221
+ { toType: 'secret' }
222
+ ];
223
+
224
+ const result = useResourceCardRowFromRelationships('Refers to', rels);
225
+
226
+ expect(result.counts).toHaveLength(1);
227
+ expect(result.counts![0]).toStrictEqual(expect.objectContaining({
228
+ label: 'missing', count: 2, color: 'warning'
229
+ }));
230
+ });
231
+ });