@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
@@ -403,6 +403,99 @@ describe('class: Resource', () => {
403
403
 
404
404
  expect(cards).toHaveLength(0);
405
405
  });
406
+
407
+ it('should include the resources card when relationships exist', () => {
408
+ const resource = new Resource({
409
+ type: 'test',
410
+ metadata: {
411
+ relationships: [
412
+ {
413
+ rel: 'uses', toType: 'svc', toId: 'a'
414
+ },
415
+ {
416
+ rel: 'uses', fromType: 'pod', fromId: 'b'
417
+ },
418
+ ]
419
+ }
420
+ }, {
421
+ getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
422
+ dispatch: jest.fn(),
423
+ rootGetters: {
424
+ 'i18n/t': (key: string) => key,
425
+ 'cluster/all': () => []
426
+ },
427
+ });
428
+
429
+ const cards = resource.cards;
430
+
431
+ expect(cards).toHaveLength(1);
432
+ expect(cards[0].props.title).toBe('component.resource.detail.card.resourcesCard.title');
433
+ });
434
+ });
435
+
436
+ describe('getter: resourcesCard', () => {
437
+ it('should return null when there are no relationships', () => {
438
+ const resource = new Resource({ type: 'test' }, {
439
+ getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
440
+ dispatch: jest.fn(),
441
+ rootGetters: { 'i18n/t': (key: string) => key },
442
+ });
443
+
444
+ expect(resource.resourcesCard).toBeNull();
445
+ });
446
+
447
+ it('should return rows for both referredToBy and refersTo when relationships exist in both directions', () => {
448
+ const resource = new Resource({
449
+ type: 'test',
450
+ metadata: {
451
+ relationships: [
452
+ {
453
+ rel: 'owner', fromType: 'rs', fromId: 'r-1'
454
+ },
455
+ {
456
+ rel: 'uses', toType: 'svc', toId: 's-1'
457
+ },
458
+ {
459
+ rel: 'uses', toType: 'svc', toId: 's-2'
460
+ },
461
+ ]
462
+ }
463
+ }, {
464
+ getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
465
+ dispatch: jest.fn(),
466
+ rootGetters: { 'i18n/t': (key: string) => key },
467
+ });
468
+
469
+ const rows = resource.resourcesCardRows;
470
+
471
+ expect(rows).toHaveLength(2);
472
+ expect(rows[0].label).toBe('component.resource.detail.card.resourcesCard.rows.referredToBy');
473
+ expect(rows[0].counts[0].count).toBe(1);
474
+ expect(rows[1].label).toBe('component.resource.detail.card.resourcesCard.rows.refersTo');
475
+ expect(rows[1].counts[0].count).toBe(2);
476
+ });
477
+
478
+ it('should omit a direction with no relationships', () => {
479
+ const resource = new Resource({
480
+ type: 'test',
481
+ metadata: {
482
+ relationships: [
483
+ {
484
+ rel: 'uses', toType: 'svc', toId: 's-1'
485
+ },
486
+ ]
487
+ }
488
+ }, {
489
+ getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
490
+ dispatch: jest.fn(),
491
+ rootGetters: { 'i18n/t': (key: string) => key },
492
+ });
493
+
494
+ const rows = resource.resourcesCardRows;
495
+
496
+ expect(rows).toHaveLength(1);
497
+ expect(rows[0].label).toBe('component.resource.detail.card.resourcesCard.rows.refersTo');
498
+ });
406
499
  });
407
500
 
408
501
  describe('getter: insightCardProps', () => {
@@ -12,6 +12,7 @@ import {
12
12
  AS,
13
13
  MODE
14
14
  } from '@shell/config/query-params';
15
+ import { EVENT } from '@shell/config/types';
15
16
  import { VIEW_IN_API, DEV } from '@shell/store/prefs';
16
17
  import { addObject, addObjects, findBy, removeAt } from '@shell/utils/array';
17
18
  import CustomValidators from '@shell/utils/custom-validators';
@@ -39,8 +40,7 @@ import { handleConflict } from '@shell/plugins/dashboard-store/normalize';
39
40
  import { ExtensionPoint, ActionLocation } from '@shell/core/types';
40
41
  import { getApplicableExtensionEnhancements } from '@shell/core/plugin-helpers';
41
42
  import { parse } from '@shell/utils/selector';
42
- import { EVENT } from '@shell/config/types';
43
- import { useResourceCardRow } from '@shell/components/Resource/Detail/Card/StateCard/composables';
43
+ import { useResourceCardRow, useResourceCardRowFromRelationships } from '@shell/components/Resource/Detail/Card/StateCard/composables';
44
44
 
45
45
  export const DNS_LIKE_TYPES = ['dnsLabel', 'dnsLabelRestricted', 'hostname'];
46
46
 
@@ -507,7 +507,11 @@ export function colorForState(state, isError, isTransitioning) {
507
507
  return `text-${ color }`;
508
508
  }
509
509
 
510
- export function stateDisplay(state) {
510
+ export function simpleColorForState(state, isError = false, isTransitioning = false) {
511
+ return colorForState(state, isError, isTransitioning).replace('text-', '') || 'disabled';
512
+ }
513
+
514
+ export function stateDisplay(state, preserveOriginal = false) {
511
515
  // @TODO use translations
512
516
  const key = (state || 'active').toLowerCase();
513
517
 
@@ -515,6 +519,11 @@ export function stateDisplay(state) {
515
519
  return REMAP_STATE[key];
516
520
  }
517
521
 
522
+ // Preserves the original state name returned by the
523
+ if ( preserveOriginal ) {
524
+ return ucFirst(state);
525
+ }
526
+
518
527
  return key.split(/-/).map(ucFirst).join('-');
519
528
  }
520
529
 
@@ -754,7 +763,7 @@ export default class Resource {
754
763
  }
755
764
 
756
765
  get stateSimpleColor() {
757
- return this.stateColor.replace('text-', '');
766
+ return simpleColorForState(this.state, this.stateObj?.error, this.stateObj?.transitioning);
758
767
  }
759
768
 
760
769
  get stateBackground() {
@@ -2073,7 +2082,7 @@ export default class Resource {
2073
2082
 
2074
2083
  if ( r.selector ) {
2075
2084
  // A selector is a stringified version of a matchLabel (https://github.com/kubernetes/apimachinery/blob/master/pkg/labels/selector.go#L1010)
2076
- addObjects(out.selectors, {
2085
+ addObject(out.selectors, {
2077
2086
  type: r.toType,
2078
2087
  namespace: r.toNamespace,
2079
2088
  selector: r.selector
@@ -2083,7 +2092,7 @@ export default class Resource {
2083
2092
  let namespace = r[`${ direction }Namespace`];
2084
2093
  let name = r[`${ direction }Id`];
2085
2094
 
2086
- if ( !namespace && name.includes('/') ) {
2095
+ if ( !namespace && name?.includes('/') ) {
2087
2096
  const idx = name.indexOf('/');
2088
2097
 
2089
2098
  namespace = name.substr(0, idx);
@@ -2245,12 +2254,58 @@ export default class Resource {
2245
2254
  };
2246
2255
  }
2247
2256
 
2257
+ get _resourcesCardRows() {
2258
+ const rows = [];
2259
+ const relationships = this.metadata?.relationships || [];
2260
+
2261
+ const referredToByRels = relationships.filter((r) => r.fromType && r.fromId && !r.selector);
2262
+ const refersToRels = relationships.filter((r) => r.toType && r.toId && !r.selector && !r.fromType);
2263
+
2264
+ if (referredToByRels.length) {
2265
+ rows.push(useResourceCardRowFromRelationships(
2266
+ this.t('component.resource.detail.card.resourcesCard.rows.referredToBy'),
2267
+ referredToByRels,
2268
+ { hash: '#related' }
2269
+ ));
2270
+ }
2271
+
2272
+ if (refersToRels.length) {
2273
+ rows.push(useResourceCardRowFromRelationships(
2274
+ this.t('component.resource.detail.card.resourcesCard.rows.refersTo'),
2275
+ refersToRels,
2276
+ { hash: '#related' }
2277
+ ));
2278
+ }
2279
+
2280
+ return rows;
2281
+ }
2282
+
2283
+ get resourcesCardRows() {
2284
+ return this._resourcesCardRows;
2285
+ }
2286
+
2287
+ get resourcesCard() {
2288
+ const rows = this.resourcesCardRows;
2289
+
2290
+ if (!rows.length) {
2291
+ return null;
2292
+ }
2293
+
2294
+ return {
2295
+ component: markRaw(defineAsyncComponent(() => import('@shell/components/Resource/Detail/Card/StateCard/index.vue'))),
2296
+ props: {
2297
+ title: this.t('component.resource.detail.card.resourcesCard.title'),
2298
+ rows
2299
+ }
2300
+ };
2301
+ }
2302
+
2248
2303
  get _cards() {
2249
2304
  // All cards are opt in, we're leaving the insights card as part of the base resource since it should proliferate to most resources
2250
2305
  return [];
2251
2306
  }
2252
2307
 
2253
2308
  get cards() {
2254
- return this._cards;
2309
+ return [this.resourcesCard, ...this._cards].filter((c) => c);
2255
2310
  }
2256
2311
  }
@@ -0,0 +1,212 @@
1
+ import actions from '@shell/plugins/steve/actions';
2
+ import paginationUtils from '@shell/utils/pagination-utils';
3
+ import stevePaginationUtils from '@shell/plugins/steve/steve-pagination-utils';
4
+ import { PaginationParamFilter } from '@shell/types/store/pagination.types';
5
+
6
+ const { fetchResourceSummary } = actions;
7
+
8
+ describe('steve: actions:', () => {
9
+ describe('fetchResourceSummary', () => {
10
+ const schema = {
11
+ id: 'pod',
12
+ links: { collection: '/v1/pods' },
13
+ attributes: { namespaced: true },
14
+ };
15
+
16
+ const baseCtx = () => ({
17
+ getters: {
18
+ normalizeType: (type: string) => type,
19
+ schemaFor: (type: string) => (type === 'pod' ? schema : undefined),
20
+ },
21
+ dispatch: jest.fn(),
22
+ rootGetters: {},
23
+ });
24
+
25
+ let warnSpy: jest.SpyInstance;
26
+
27
+ beforeEach(() => {
28
+ warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
29
+ jest.spyOn(paginationUtils, 'isSteveCacheEnabled').mockReturnValue(true);
30
+ });
31
+
32
+ afterEach(() => {
33
+ jest.restoreAllMocks();
34
+ });
35
+
36
+ it('should return undefined and warn when schema is not found', async() => {
37
+ const ctx = baseCtx();
38
+ const result = await fetchResourceSummary.call({}, ctx, { type: 'nonexistent', opt: { summaryField: 'metadata.state.name' } });
39
+
40
+ expect(result).toBeUndefined();
41
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('no schema found'));
42
+ });
43
+
44
+ it('should return undefined and warn when VAI is not enabled', async() => {
45
+ jest.spyOn(paginationUtils, 'isSteveCacheEnabled').mockReturnValue(false);
46
+ const ctx = baseCtx();
47
+ const result = await fetchResourceSummary.call({}, ctx, { type: 'pod', opt: { summaryField: 'metadata.state.name' } });
48
+
49
+ expect(result).toBeUndefined();
50
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('VAI is not enabled'));
51
+ });
52
+
53
+ it('should return undefined and warn when summaryField is missing', async() => {
54
+ const ctx = baseCtx();
55
+ const result = await fetchResourceSummary.call({}, ctx, { type: 'pod', opt: {} });
56
+
57
+ expect(result).toBeUndefined();
58
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('summaryField is required'));
59
+ });
60
+
61
+ it('should construct the correct URL with summary and summaryonly params', async() => {
62
+ const ctx = baseCtx();
63
+
64
+ ctx.dispatch.mockResolvedValue({ count: 5, summary: [{ property: 'metadata.state.name', counts: { running: { total: 5 } } }] });
65
+
66
+ await fetchResourceSummary.call({}, ctx, { type: 'pod', opt: { summaryField: 'metadata.state.name' } });
67
+
68
+ const requestUrl = ctx.dispatch.mock.calls[0][1].opt.url;
69
+
70
+ expect(requestUrl).toContain('summary=metadata.state.name');
71
+ expect(requestUrl).toContain('summaryonly=');
72
+ expect(requestUrl).not.toContain('summarynamespaced');
73
+ });
74
+
75
+ it('should not include summaryonly when summaryOnly is false', async() => {
76
+ const ctx = baseCtx();
77
+
78
+ ctx.dispatch.mockResolvedValue({ count: 5, summary: [{ property: 'metadata.state.name', counts: { running: { total: 5 } } }] });
79
+
80
+ await fetchResourceSummary.call({}, ctx, { type: 'pod', opt: { summaryField: 'metadata.state.name', summaryOnly: false } });
81
+
82
+ const requestUrl = ctx.dispatch.mock.calls[0][1].opt.url;
83
+
84
+ expect(requestUrl).not.toContain('summaryonly');
85
+ });
86
+
87
+ it('should include summarynamespaced param when namespaceCounts is true', async() => {
88
+ const ctx = baseCtx();
89
+
90
+ ctx.dispatch.mockResolvedValue({ count: 5, summary: [{ property: 'metadata.state.name', counts: { running: { total: 5 } } }] });
91
+
92
+ await fetchResourceSummary.call({}, ctx, { type: 'pod', opt: { summaryField: 'metadata.state.name', namespaceCounts: true } });
93
+
94
+ const requestUrl = ctx.dispatch.mock.calls[0][1].opt.url;
95
+
96
+ expect(requestUrl).toContain('summarynamespaced=');
97
+ });
98
+
99
+ it('should append namespace to path for namespaced resources', async() => {
100
+ const ctx = baseCtx();
101
+
102
+ ctx.dispatch.mockResolvedValue({ count: 2, summary: null });
103
+
104
+ await fetchResourceSummary.call({}, ctx, { type: 'pod', opt: { summaryField: 'metadata.state.name', namespace: 'cattle-system' } });
105
+
106
+ const requestUrl = ctx.dispatch.mock.calls[0][1].opt.url;
107
+
108
+ expect(requestUrl).toMatch(/\/v1\/pods\/cattle-system\?/);
109
+ });
110
+
111
+ it('should not append namespace when schema is not namespaced', async() => {
112
+ const nonNsSchema = { ...schema, attributes: { namespaced: false } };
113
+ const ctx = baseCtx();
114
+
115
+ ctx.getters.schemaFor = () => nonNsSchema;
116
+ ctx.dispatch.mockResolvedValue({ count: 1, summary: null });
117
+
118
+ await fetchResourceSummary.call({}, ctx, { type: 'pod', opt: { summaryField: 'metadata.state.name', namespace: 'default' } });
119
+
120
+ const requestUrl = ctx.dispatch.mock.calls[0][1].opt.url;
121
+
122
+ expect(requestUrl).not.toContain('/default');
123
+ });
124
+
125
+ it('should append filter params when filters are provided', async() => {
126
+ const ctx = baseCtx();
127
+ const filters = [PaginationParamFilter.createSingleField({ field: 'metadata.namespace', value: 'default' })];
128
+
129
+ jest.spyOn(stevePaginationUtils, 'convertPaginationParams').mockReturnValue('filter=metadata.namespace%3Ddefault');
130
+ ctx.dispatch.mockResolvedValue({ count: 3, summary: null });
131
+
132
+ await fetchResourceSummary.call({}, ctx, { type: 'pod', opt: { summaryField: 'metadata.state.name', filters } });
133
+
134
+ const requestUrl = ctx.dispatch.mock.calls[0][1].opt.url;
135
+
136
+ expect(requestUrl).toContain('filter=');
137
+ expect(stevePaginationUtils.convertPaginationParams).toHaveBeenCalledWith(expect.objectContaining({ filters }));
138
+ });
139
+
140
+ it('should return count and summary from the response', async() => {
141
+ const ctx = baseCtx();
142
+ const apiResponse = {
143
+ count: 10,
144
+ summary: [{ property: 'metadata.state.name', counts: { running: { total: 7 }, error: { total: 3 } } }]
145
+ };
146
+
147
+ ctx.dispatch.mockResolvedValue(apiResponse);
148
+
149
+ const result = await fetchResourceSummary.call({}, ctx, { type: 'pod', opt: { summaryField: 'metadata.state.name' } });
150
+
151
+ expect(result).toStrictEqual(apiResponse);
152
+ });
153
+
154
+ it('should pass through object-style counts as-is', async() => {
155
+ const ctx = baseCtx();
156
+ const counts = { running: { total: 7 }, error: { total: 3 } };
157
+ const apiResponse = {
158
+ count: 10,
159
+ summary: [{ property: 'metadata.state.name', counts }]
160
+ };
161
+
162
+ ctx.dispatch.mockResolvedValue(apiResponse);
163
+
164
+ const result = await fetchResourceSummary.call({}, ctx, { type: 'pod', opt: { summaryField: 'metadata.state.name' } });
165
+
166
+ expect(result).toStrictEqual({
167
+ count: 10,
168
+ summary: [{ property: 'metadata.state.name', counts }]
169
+ });
170
+ });
171
+
172
+ it('should default count to 0 and summary to null when response is empty', async() => {
173
+ const ctx = baseCtx();
174
+
175
+ ctx.dispatch.mockResolvedValue({});
176
+
177
+ const result = await fetchResourceSummary.call({}, ctx, { type: 'pod', opt: { summaryField: 'metadata.state.name' } });
178
+
179
+ expect(result).toStrictEqual({ count: 0, summary: null });
180
+ });
181
+
182
+ it('should append label selector params when labelSelector is provided', async() => {
183
+ const ctx = baseCtx();
184
+ const labelSelector = {
185
+ matchExpressions: [{
186
+ key: 'app', operator: 'In', values: ['nginx']
187
+ }]
188
+ };
189
+
190
+ jest.spyOn(stevePaginationUtils, 'convertLabelSelectorPaginationParams').mockReturnValue('filter=metadata.labels[app] IN (nginx)');
191
+ ctx.dispatch.mockResolvedValue({ count: 2, summary: null });
192
+
193
+ await fetchResourceSummary.call({}, ctx, { type: 'pod', opt: { summaryField: 'metadata.state.name', labelSelector } });
194
+
195
+ const requestUrl = ctx.dispatch.mock.calls[0][1].opt.url;
196
+
197
+ expect(requestUrl).toContain('filter=');
198
+ expect(stevePaginationUtils.convertLabelSelectorPaginationParams).toHaveBeenCalledWith({ labelSelector });
199
+ });
200
+
201
+ it('should return undefined and warn when the request fails', async() => {
202
+ const ctx = baseCtx();
203
+
204
+ ctx.dispatch.mockRejectedValue(new Error('network error'));
205
+
206
+ const result = await fetchResourceSummary.call({}, ctx, { type: 'pod', opt: { summaryField: 'metadata.state.name' } });
207
+
208
+ expect(result).toBeUndefined();
209
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('summary API request failed'), expect.any(Error));
210
+ });
211
+ });
212
+ });
@@ -10,6 +10,7 @@ import { NAMESPACE } from '@shell/config/types';
10
10
  import { handleKubeApiHeaderWarnings } from '@shell/plugins/steve/header-warnings';
11
11
  import { steveCleanForDownload } from '@shell/plugins/steve/resource-utils';
12
12
  import paginationUtils from '@shell/utils/pagination-utils';
13
+ import stevePaginationUtils from '@shell/plugins/steve/steve-pagination-utils';
13
14
 
14
15
  export default {
15
16
 
@@ -221,6 +222,101 @@ export default {
221
222
  }
222
223
  },
223
224
 
225
+ /**
226
+ * Fetch aggregated state counts for a resource type via the Steve summary API.
227
+ * Requires VAI (ui-sql-cache) to be enabled; returns undefined otherwise.
228
+ *
229
+ * Uses `summaryonly` by default so no resource data is returned.
230
+ *
231
+ * @param {string} type - Resource type (e.g. 'pod', 'service')
232
+ * @param {object} [opt] - Options object
233
+ * @param {string} opt.summaryField - Field to aggregate counts by.
234
+ * Must be a field indexed by the VAI cache (see StevePaginationUtils.VALID_FIELDS in steve-pagination-utils.ts)
235
+ * @param {string} [opt.namespace] - Namespace to scope the request to (only applies to namespaced resource types)
236
+ * @param {boolean} [opt.summaryOnly=true] - Omit resource data from the response (set to false to include data)
237
+ * @param {boolean} [opt.namespaceCounts] - Include per-namespace breakdowns in counts
238
+ * @param {PaginationParamFilter[]} [opt.filters] - Pre-built filters from PaginationParamFilter.createSingleField()
239
+ * @param {KubeLabelSelector} [opt.labelSelector] - Kube label selector to filter by (converted via convertLabelSelectorPaginationParams)
240
+ * @returns {Promise<{ count: number, summary: { property: string, counts: Record<string, { total: number, namespace?: Record<string, number> }> }[] | null } | undefined>}
241
+ *
242
+ * @example
243
+ * const result = await dispatch('fetchResourceSummary', {
244
+ * type: 'pod',
245
+ * opt: { summaryField: 'metadata.state.name', labelSelector: { matchExpressions: podMatchExpression } }
246
+ * });
247
+ * // result.summary[0].counts => { running: { total: 3 }, error: { total: 1 } }
248
+ *
249
+ * // With namespace breakdowns:
250
+ * const result = await dispatch('fetchResourceSummary', {
251
+ * type: 'pod',
252
+ * opt: { summaryField: 'metadata.state.name', namespaceCounts: true }
253
+ * });
254
+ * // result.summary[0].counts => { running: { total: 3, namespace: { default: 2, 'kube-system': 1 } } }
255
+ */
256
+ async fetchResourceSummary({ getters, dispatch, rootGetters }, { type, opt = {} }) {
257
+ type = getters.normalizeType(type);
258
+ const schema = getters.schemaFor(type);
259
+
260
+ if (!schema) {
261
+ console.warn(`fetchResourceSummary: no schema found for type "${ type }"`); // eslint-disable-line no-console
262
+
263
+ return undefined;
264
+ }
265
+
266
+ if (!paginationUtils.isSteveCacheEnabled({ rootGetters })) {
267
+ console.warn(`fetchResourceSummary: VAI is not enabled, summary API unavailable for type "${ type }"`); // eslint-disable-line no-console
268
+
269
+ return undefined;
270
+ }
271
+
272
+ if (!opt.summaryField) {
273
+ console.warn(`fetchResourceSummary: summaryField is required and must be a string for type "${ type }"`); // eslint-disable-line no-console
274
+
275
+ return undefined;
276
+ }
277
+
278
+ try {
279
+ const url = new URL(schema.links.collection, window.location.origin);
280
+
281
+ if (schema.attributes?.namespaced && opt.namespace) {
282
+ url.pathname += `/${ opt.namespace }`;
283
+ }
284
+
285
+ url.searchParams.set('summary', opt.summaryField);
286
+
287
+ if (opt.summaryOnly !== false) {
288
+ url.searchParams.set('summaryonly', '');
289
+ }
290
+
291
+ if (opt.namespaceCounts) {
292
+ url.searchParams.set('summarynamespaced', '');
293
+ }
294
+
295
+ if (opt.filters?.length) {
296
+ const filterParams = new URLSearchParams(stevePaginationUtils.convertPaginationParams({ schema, filters: opt.filters }));
297
+
298
+ filterParams.forEach((v, k) => url.searchParams.append(k, v));
299
+ }
300
+
301
+ if (opt.labelSelector) {
302
+ const labelParams = new URLSearchParams(stevePaginationUtils.convertLabelSelectorPaginationParams({ labelSelector: opt.labelSelector }));
303
+
304
+ labelParams.forEach((v, k) => url.searchParams.append(k, v));
305
+ }
306
+
307
+ const res = await dispatch('request', { opt: { url: url.pathname + url.search } });
308
+
309
+ return {
310
+ count: res.count ?? 0,
311
+ summary: res.summary || null
312
+ };
313
+ } catch (e) {
314
+ console.warn(`fetchResourceSummary: summary API request failed for type "${ type }"`, e); // eslint-disable-line no-console
315
+
316
+ return undefined;
317
+ }
318
+ },
319
+
224
320
  promptRestore({ commit, state }, resources ) {
225
321
  commit('action-menu/togglePromptRestore', resources, { root: true });
226
322
  },
@@ -656,7 +656,7 @@ class StevePaginationUtils extends NamespaceProjectFilters {
656
656
  * A lot of the requirements and details are taken directly from
657
657
  * https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
658
658
  */
659
- private convertLabelSelectorPaginationParams({ labelSelector }: { labelSelector: KubeLabelSelector}): string {
659
+ convertLabelSelectorPaginationParams({ labelSelector }: { labelSelector: KubeLabelSelector}): string {
660
660
  // Get a list of matchExpressions
661
661
  const expressions: KubeLabelSelectorExpression[] = labelSelector.matchExpressions ? [...labelSelector.matchExpressions] : [];
662
662
 
@@ -1,8 +1,14 @@
1
1
  <script lang="ts">
2
- import { defineComponent } from 'vue';
3
- import { mapGetters } from 'vuex';
2
+ import {
3
+ computed, defineComponent, nextTick, ref, useTemplateRef
4
+ } from 'vue';
5
+ import { mapGetters, useStore } from 'vuex';
6
+ import { useInSummary } from '@shell/components/TableOfContents/composables';
7
+ import { useI18n } from '@shell/composables/useI18n';
4
8
 
5
9
  export default defineComponent({
10
+ name: 'Accordion',
11
+
6
12
  props: {
7
13
  title: {
8
14
  type: String,
@@ -20,22 +26,59 @@ export default defineComponent({
20
26
  }
21
27
  },
22
28
 
29
+ setup(props) {
30
+ const store = useStore();
31
+ const { t } = useI18n(store);
32
+ const label = computed(() => props.titleKey && typeof t === 'function' ? t(props.titleKey) : props.title);
33
+
34
+ const isOpen = ref(props.openInitially);
35
+ const accordionSummarizedContainer = useTemplateRef<HTMLElement>('accordion-summarized-container');
36
+
37
+ const scrollTo = () => {
38
+ isOpen.value = true;
39
+ nextTick(() => {
40
+ accordionSummarizedContainer.value?.scrollIntoView();
41
+ });
42
+ };
43
+
44
+ const { summary } = useInSummary({
45
+ scrollTo,
46
+ label,
47
+ elementRef: accordionSummarizedContainer,
48
+ });
49
+
50
+ return {
51
+ summary,
52
+ isOpen,
53
+ scrollTo,
54
+ };
55
+ },
56
+
23
57
  data() {
24
- return { isOpen: this.openInitially };
58
+ return {};
25
59
  },
26
60
 
27
- computed: { ...mapGetters({ t: 'i18n/t' }) },
61
+ computed: {
62
+ ...mapGetters({ t: 'i18n/t' }),
63
+
64
+ displayTitle() {
65
+ return this.titleKey ? this.t(this.titleKey) : this.title;
66
+ },
67
+ },
28
68
 
29
69
  methods: {
30
70
  toggle() {
31
71
  this.isOpen = !this.isOpen;
32
- }
33
- }
72
+ },
73
+ },
34
74
  });
35
75
  </script>
36
76
 
37
77
  <template>
38
- <div class="accordion-container">
78
+ <div
79
+ ref="accordion-summarized-container"
80
+ class="accordion-container"
81
+ >
39
82
  <div
40
83
  class="accordion-header"
41
84
  data-testid="accordion-header"
@@ -51,7 +94,7 @@ export default defineComponent({
51
94
  data-testid="accordion-title-slot-content"
52
95
  class="mb-0"
53
96
  >
54
- {{ titleKey ? t(titleKey) : title }}
97
+ {{ displayTitle }}
55
98
  </h2>
56
99
  </slot>
57
100
  </div>
@@ -67,7 +110,8 @@ export default defineComponent({
67
110
 
68
111
  <style lang="scss" scoped>
69
112
  .accordion-container {
70
- border: 1px solid var(--border)
113
+ border: 1px solid var(--border);
114
+ border-radius: var(--border-radius);
71
115
  }
72
116
  .accordion-header {
73
117
  padding: 16px 16px 16px 11px;