@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,11 +1,26 @@
1
1
  import { Count, Props as ResourceRowProps } from '@shell/components/Resource/Detail/ResourceRow.types';
2
- import { useI18n } from '@shell/composables/useI18n';
3
- import { INGRESS, SERVICE } from '@shell/config/types';
4
2
  import { isHigherAlert, StateColor } from '@shell/utils/style';
5
- import { computed, Ref, toValue } from 'vue';
6
- import { useStore } from 'vuex';
7
- import { Props as StateCardProps } from '@shell/components/Resource/Detail/Card/StateCard/types';
8
3
  import { RouteLocationRaw } from 'vue-router';
4
+ import { simpleColorForState, stateDisplay as stateDisplayFn } from '@shell/plugins/dashboard-store/resource-class';
5
+
6
+ interface Tuple extends Count {
7
+ color: StateColor;
8
+ }
9
+
10
+ export interface SummaryCountValue {
11
+ total: number;
12
+ namespace?: Record<string, number>;
13
+ }
14
+
15
+ export interface SummaryEntry {
16
+ property: string;
17
+ counts: Record<string, SummaryCountValue>;
18
+ }
19
+
20
+ export interface SummaryResult {
21
+ count: number;
22
+ summary: SummaryEntry[] | null;
23
+ }
9
24
 
10
25
  export function useResourceCardRow(label: string, resources: any[], stateColorKey = 'stateSimpleColor', stateDisplayKey = 'stateDisplay', to?: RouteLocationRaw): ResourceRowProps {
11
26
  const agg: any = {};
@@ -20,12 +35,9 @@ export function useResourceCardRow(label: string, resources: any[], stateColorKe
20
35
  agg[state].count++;
21
36
  });
22
37
 
23
- interface Tuple extends Count {
24
- color: StateColor;
25
- }
26
38
  const tuples: Tuple[] = Object.values(agg);
27
39
 
28
- tuples.sort((left: any, right: any) => {
40
+ tuples.sort((left: Tuple, right: Tuple) => {
29
41
  if (isHigherAlert(left.color, right.color)) {
30
42
  return -1;
31
43
  }
@@ -49,94 +61,52 @@ export function useResourceCardRow(label: string, resources: any[], stateColorKe
49
61
  };
50
62
  }
51
63
 
52
- export interface Pairs {
53
- label: string;
54
- to?: RouteLocationRaw;
55
- resources: any[];
56
- }
57
-
58
- export function useDefaultResources(pairs: Ref<Pairs[]>) {
59
- const pairsValue = toValue(pairs);
60
- const rows = computed(() => pairsValue.map(({ label, resources, to }) => useResourceCardRow(label, resources, undefined, undefined, to)));
61
-
62
- return rows;
63
- }
64
-
65
- export function useResourcePair(label: string, type: string, resources?: any[]) {
66
- const store = useStore();
64
+ /**
65
+ * Builds a ResourceRowProps from relationship state data.
66
+ * Each relationship entry includes a `state` field (e.g. "active", "error").
67
+ */
68
+ export function useResourceCardRowFromRelationships(label: string, relationships: any[], to?: RouteLocationRaw): ResourceRowProps {
69
+ if (!relationships.length) {
70
+ return {
71
+ label, color: undefined, counts: undefined, to
72
+ };
73
+ }
67
74
 
68
- return computed(() => {
69
- if (!resources) {
70
- return undefined;
71
- }
75
+ const agg: Record<string, { color: StateColor; label: string; count: number }> = {};
72
76
 
73
- const resourcesValue = toValue(resources);
77
+ relationships.forEach((r: any) => {
78
+ const state = (r.state || 'missing').toLowerCase();
79
+ const color = simpleColorForState(state) as StateColor;
80
+ const display = (stateDisplayFn(state) as string)?.toLowerCase() || state;
74
81
 
75
- return {
76
- label,
77
- resources: resourcesValue,
78
- to: (resourcesValue?.length > 0) ? {
79
- name: 'c-cluster-product-resource',
80
- params: {
81
- cluster: store.getters.currentCluster.id,
82
- product: store.getters.currentProduct.name,
83
- resource: type
84
- }
85
- } : undefined,
82
+ agg[state] = agg[state] || {
83
+ color, label: display, count: 0
86
84
  };
85
+ agg[state].count++;
87
86
  });
88
- }
89
87
 
90
- export function useDefaultWorkloadResources(services?: any[], ingresses?: any[], referredToBy?: any[], refersTo?: any[]): StateCardProps {
91
- const store = useStore();
92
- const i18n = useI18n(store);
93
-
94
- const pairs: Ref<(Pairs | undefined)[]> = computed(() => [
95
- useResourcePair(i18n.t('component.resource.detail.card.resourcesCard.rows.services'), SERVICE, services).value,
96
- useResourcePair(i18n.t('component.resource.detail.card.resourcesCard.rows.ingresses'), INGRESS, ingresses).value,
97
- referredToBy ? {
98
- label: i18n.t('component.resource.detail.card.resourcesCard.rows.referredToBy'),
99
- resources: toValue(referredToBy || []),
100
- to: { hash: '#related' }
101
- } : undefined,
102
- refersTo ? {
103
- label: i18n.t('component.resource.detail.card.resourcesCard.rows.refersTo'),
104
- resources: toValue(refersTo || []),
105
- to: { hash: '#related' }
106
- } : undefined
107
- ]);
108
-
109
- const remainingPairs = computed(() => pairs.value.filter((p: (Pairs | undefined)): p is Pairs => p !== undefined));
110
-
111
- const rows = useDefaultResources(remainingPairs);
88
+ const tuples: Tuple[] = Object.values(agg);
112
89
 
113
- return {
114
- title: 'Resources',
115
- rows: rows.value
116
- };
117
- }
90
+ tuples.sort((left: Tuple, right: Tuple) => {
91
+ if (isHigherAlert(left.color, right.color)) {
92
+ return -1;
93
+ }
118
94
 
119
- export function useDefaultWorkloadInsightsCardProps(): StateCardProps {
120
- const store = useStore();
121
- const i18n = useI18n(store);
122
-
123
- const rows: ResourceRowProps[] = [
124
- {
125
- label: i18n.t('component.resource.detail.card.insightsCard.rows.conditions'),
126
- to: '#conditions',
127
- color: 'disabled',
128
- counts: [{ label: 'Available', count: 1 }, { label: 'Progressing', count: 1 }]
129
- },
130
- {
131
- label: i18n.t('component.resource.detail.card.insightsCard.rows.events'),
132
- to: '#events',
133
- color: 'disabled',
134
- counts: [{ label: 'Normal', count: 2 }]
95
+ if (left.color !== right.color) {
96
+ return 1;
135
97
  }
136
- ];
98
+
99
+ if (left.count === right.count) {
100
+ return 0;
101
+ }
102
+
103
+ return left.count > right.count ? -1 : 1;
104
+ });
137
105
 
138
106
  return {
139
- title: i18n.t('component.resource.detail.card.insightsCard.title'),
140
- rows
107
+ label,
108
+ color: tuples.length ? tuples[0].color : undefined,
109
+ counts: tuples.length ? tuples : undefined,
110
+ to
141
111
  };
142
112
  }
@@ -3,6 +3,7 @@ import StatusCard from '@shell/components/Resource/Detail/Card/StatusCard/index.
3
3
  import StatusBar from '@shell/components/Resource/Detail/StatusBar.vue';
4
4
  import StatusRow from '@shell/components/Resource/Detail/StatusRow.vue';
5
5
  import Scaler from '@shell/components/Resource/Detail/Card/Scaler.vue';
6
+ import type { SummaryResult } from '@shell/components/Resource/Detail/Card/StateCard/composables';
6
7
 
7
8
  describe('component: StatusCard', () => {
8
9
  const mockResource = (stateDisplay: string, stateSimpleColor: string) => ({
@@ -106,4 +107,64 @@ describe('component: StatusCard', () => {
106
107
  expect(wrapper.findComponent(Scaler).exists()).toBe(false);
107
108
  });
108
109
  });
110
+
111
+ describe('with summaryData', () => {
112
+ it('should render StatusBar and StatusRows from summary counts', () => {
113
+ const summaryData: SummaryResult = {
114
+ count: 5,
115
+ summary: [{ property: 'metadata.state.name', counts: { running: { total: 3 }, error: { total: 2 } } }]
116
+ };
117
+
118
+ const wrapper = mountCard({ summaryData });
119
+
120
+ expect(wrapper.findComponent(StatusBar).exists()).toBe(true);
121
+ expect(wrapper.findAllComponents(StatusRow)).toHaveLength(2);
122
+ });
123
+
124
+ it('should use summaryData over resources when both are provided', () => {
125
+ const summaryData: SummaryResult = {
126
+ count: 4,
127
+ summary: [{ property: 'metadata.state.name', counts: { running: { total: 3 }, completed: { total: 1 } } }]
128
+ };
129
+ const resources = [
130
+ mockResource('Running', 'text-success'),
131
+ ];
132
+
133
+ const wrapper = mountCard({ summaryData, resources });
134
+
135
+ expect(wrapper.findAllComponents(StatusRow)).toHaveLength(2);
136
+ });
137
+
138
+ it('should fall back to resources when summaryData has no summary', () => {
139
+ const summaryData: SummaryResult = { count: 0, summary: null };
140
+ const resources = [
141
+ mockResource('Running', 'text-success'),
142
+ mockResource('Error', 'text-error'),
143
+ ];
144
+
145
+ const wrapper = mountCard({ summaryData, resources });
146
+
147
+ expect(wrapper.findAllComponents(StatusRow)).toHaveLength(2);
148
+ });
149
+
150
+ it('should show noResourcesMessage when summaryData has no counts', () => {
151
+ const summaryData: SummaryResult = { count: 0, summary: [] };
152
+
153
+ const wrapper = mountCard({ summaryData, noResourcesMessage: 'No pods' });
154
+
155
+ expect(wrapper.findComponent(StatusBar).exists()).toBe(false);
156
+ expect(wrapper.find('.text-deemphasized').text()).toBe('No pods');
157
+ });
158
+
159
+ it('should render a single state from summary', () => {
160
+ const summaryData: SummaryResult = {
161
+ count: 2,
162
+ summary: [{ property: 'metadata.state.name', counts: { active: { total: 2 } } }]
163
+ };
164
+
165
+ const wrapper = mountCard({ summaryData });
166
+
167
+ expect(wrapper.findAllComponents(StatusRow)).toHaveLength(1);
168
+ });
169
+ });
109
170
  });
@@ -8,10 +8,13 @@ import { useI18n } from '@shell/composables/useI18n';
8
8
  import { StateColor } from '@shell/utils/style';
9
9
  import { computed } from 'vue';
10
10
  import { useStore } from 'vuex';
11
+ import { colorForState as colorForStateFn, stateDisplay as stateDisplayFn } from '@shell/plugins/dashboard-store/resource-class';
12
+ import type { SummaryResult } from '@shell/components/Resource/Detail/Card/StateCard/composables';
11
13
 
12
14
  export interface Props {
13
15
  title: string;
14
16
  resources?: any[];
17
+ summaryData?: SummaryResult | null;
15
18
  showScaling?: boolean;
16
19
  noResourcesMessage?: string;
17
20
  }
@@ -23,23 +26,47 @@ const i18n = useI18n(store);
23
26
 
24
27
  const props = withDefaults(defineProps<Props>(), {
25
28
  resources: undefined,
29
+ summaryData: undefined,
26
30
  showScaling: false,
27
31
  noResourcesMessage: undefined
28
32
  });
29
33
  const emit = defineEmits(['decrease', 'increase']);
30
34
 
35
+ const summaryStateCounts = computed(() => {
36
+ const summary = props.summaryData?.summary;
37
+
38
+ if (!summary?.length) {
39
+ return null;
40
+ }
41
+
42
+ const entry = summary.find((s) => s.property === 'metadata.state.name');
43
+
44
+ return entry?.counts || null;
45
+ });
46
+
31
47
  const segmentAccumulator = computed(() => {
32
48
  interface Value {
33
49
  count: number;
34
50
  }
35
51
  const accumulator: {[key in StateColor]?: Value} = {};
36
-
37
- props.resources?.forEach((resource: any) => {
38
- const color: StateColor = resource.stateSimpleColor;
39
-
40
- accumulator[color] = accumulator[color] || { count: 0 };
41
- accumulator[color].count++;
42
- });
52
+ const stateCounts = summaryStateCounts.value;
53
+
54
+ if (stateCounts) {
55
+ for (const [state, stateCount] of Object.entries(stateCounts)) {
56
+ const colorRaw = colorForStateFn(state) as string;
57
+ const color = (colorRaw?.replace('text-', '') || 'disabled') as StateColor;
58
+
59
+ accumulator[color] = accumulator[color] || { count: 0 };
60
+ accumulator[color].count += stateCount.total;
61
+ }
62
+ } else {
63
+ props.resources?.forEach((resource: any) => {
64
+ const color: StateColor = resource.stateSimpleColor || 'disabled';
65
+
66
+ accumulator[color] = accumulator[color] || { count: 0 };
67
+ accumulator[color].count++;
68
+ });
69
+ }
43
70
 
44
71
  return accumulator;
45
72
  });
@@ -50,21 +77,40 @@ const rowAccumulator = computed(() => {
50
77
  color: StateColor;
51
78
  }
52
79
  const accumulator: {[key in string]: Value} = {};
53
-
54
- props.resources?.forEach((resource: any) => {
55
- accumulator[resource.stateDisplay] = accumulator[resource.stateDisplay] || { count: 0 };
56
- accumulator[resource.stateDisplay].count++;
57
- accumulator[resource.stateDisplay].color = resource.stateSimpleColor.replace('text-', '') as StateColor;
58
- });
80
+ const stateCounts = summaryStateCounts.value;
81
+
82
+ if (stateCounts) {
83
+ for (const [state, stateCount] of Object.entries(stateCounts)) {
84
+ const display = stateDisplayFn(state) as string;
85
+ const colorRaw = colorForStateFn(state) as string;
86
+ const color = (colorRaw?.replace('text-', '') || 'disabled') as StateColor;
87
+
88
+ accumulator[display] = accumulator[display] || { count: 0, color };
89
+ accumulator[display].count += stateCount.total;
90
+ }
91
+ } else {
92
+ props.resources?.forEach((resource: any) => {
93
+ const color = (resource.stateSimpleColor?.replace('text-', '') || 'disabled') as StateColor;
94
+
95
+ accumulator[resource.stateDisplay] = accumulator[resource.stateDisplay] || { count: 0, color };
96
+ accumulator[resource.stateDisplay].count++;
97
+ });
98
+ }
59
99
 
60
100
  return accumulator;
61
101
  });
62
102
 
63
103
  const percent = (count: number, total: number) => {
64
- return count / total * 100;
104
+ return total > 0 ? count / total * 100 : 0;
65
105
  };
66
106
 
67
- const count = computed(() => props.resources?.length || 0);
107
+ const count = computed(() => {
108
+ if (summaryStateCounts.value) {
109
+ return props.summaryData?.count ?? 0;
110
+ }
111
+
112
+ return props.resources?.length ?? 0;
113
+ });
68
114
 
69
115
  const segmentColors = computed(() => Object.keys(segmentAccumulator.value) as StateColor[]);
70
116
  const segments = computed(() => segmentColors.value.map((color: StateColor) => ({
@@ -103,6 +103,8 @@ const getRowValueId = (row:Row): string => `value-${ row.label }:${ row.value }`
103
103
  flex-direction: column;
104
104
 
105
105
  .row {
106
+ display: flex;
107
+ align-items: center;
106
108
  gap: 8px;
107
109
 
108
110
  // Hide clearfix pseudo-elements inherited from the global .row class
@@ -110,9 +110,12 @@ const showConfigurationMoreFocusSelector = computed(() => `[data-testid="${ show
110
110
  }
111
111
 
112
112
  .row {
113
- display: block;
114
- width: 100%;
115
113
  display: inline-block;
114
+ width: 100%;
115
+
116
+ &::before, &::after {
117
+ display: none;
118
+ }
116
119
 
117
120
  &:not(:nth-child(2)) {
118
121
  margin-top: 4px;
@@ -92,18 +92,14 @@ const previewId = randomStr();
92
92
  max-width: calc(100%);
93
93
  }
94
94
 
95
- .rc-tag {
96
- display: inline-block;
97
- line-height: normal;
98
- }
99
-
100
95
  .tag-data {
101
96
  display: inline-block;
102
97
  overflow: hidden;
103
98
  text-overflow: ellipsis;
104
99
  white-space: nowrap;
100
+ min-width: 0;
105
101
  max-width: calc(100%);
106
- line-height: normal;
102
+ line-height: 1;
107
103
  }
108
104
 
109
105
  & .btn.btn-medium.rc-button.variant-ghost {
@@ -169,7 +169,7 @@ export default {
169
169
  } else {
170
170
  let fqid = id;
171
171
 
172
- if ( schema.attributes?.namespaced && namespace ) {
172
+ if ( schema?.attributes?.namespaced && namespace ) {
173
173
  fqid = `${ namespace }/${ fqid }`;
174
174
  }
175
175
 
@@ -74,6 +74,11 @@ export default {
74
74
  default: false
75
75
  },
76
76
 
77
+ showFavorite: {
78
+ type: Boolean,
79
+ default: true
80
+ },
81
+
77
82
  /**
78
83
  * Inherited global identifier prefix for tests
79
84
  * Define a term based on the parent component to avoid conflicts on multiple components
@@ -202,7 +207,7 @@ export default {
202
207
  <div class="title">
203
208
  <h1 class="m-0">
204
209
  <TabTitle>{{ _typeDisplay }}</TabTitle> <Favorite
205
- v-if="isExplorer"
210
+ v-if="isExplorer && showFavorite"
206
211
  :resource="favoriteResource || resource"
207
212
  />
208
213
  </h1>
@@ -267,6 +272,7 @@ export default {
267
272
  'title actions'
268
273
  'sub-header sub-header'
269
274
  'state-banner state-banner';
275
+ margin-bottom: 24px;
270
276
  }
271
277
 
272
278
  .sub-header {
@@ -10,6 +10,9 @@ import { PanelLocation, ExtensionPoint } from '@shell/core/types';
10
10
  import ExtensionPanel from '@shell/components/ExtensionPanel';
11
11
  import { sameContents } from '@shell/utils/array';
12
12
  import perfSettingsUtils from '@shell/utils/perf-setting.utils';
13
+ import { BadgeState } from '@components/BadgeState';
14
+ import { stateDisplay } from '@shell/plugins/dashboard-store/resource-class';
15
+ import { useStateColor } from '@shell/composables/useStateColor';
13
16
 
14
17
  export default {
15
18
  name: ResourceListComponentName,
@@ -20,10 +23,17 @@ export default {
20
23
  Masthead,
21
24
  ResourceLoadingIndicator,
22
25
  IconMessage,
23
- ExtensionPanel
26
+ ExtensionPanel,
27
+ BadgeState,
24
28
  },
25
29
  mixins: [ResourceFetch],
26
30
 
31
+ setup() {
32
+ const { toStateColor } = useStateColor();
33
+
34
+ return { toStateColor };
35
+ },
36
+
27
37
  props: {
28
38
  hasAdvancedFiltering: {
29
39
  type: Boolean,
@@ -155,6 +165,41 @@ export default {
155
165
  return perfSettingsUtils.incrementalLoadingUtils.isEnabled(this.calcCanPaginate(), this.perfConfig);
156
166
  },
157
167
 
168
+ activeStateFilters() {
169
+ const raw = this.$route?.query?.stateFilter;
170
+
171
+ if (!raw) {
172
+ return [];
173
+ }
174
+
175
+ const states = raw.split(',').filter(Boolean);
176
+
177
+ return states.map((s) => {
178
+ const match = this.rows.find((row) => row.metadata?.state?.name === s);
179
+
180
+ if (match) {
181
+ return {
182
+ label: match.stateDisplay,
183
+ color: match.stateBackground,
184
+ };
185
+ }
186
+
187
+ return {
188
+ label: stateDisplay(s, true),
189
+ color: `bg-${ this.toStateColor(s, this.resource) }`,
190
+ };
191
+ });
192
+ },
193
+
194
+ },
195
+
196
+ methods: {
197
+ clearStateFilter() {
198
+ const query = { ...this.$route.query };
199
+
200
+ delete query.stateFilter;
201
+ this.$router.push({ ...this.$route, query });
202
+ },
158
203
  },
159
204
 
160
205
  watch: {
@@ -260,6 +305,28 @@ export default {
260
305
  :load-resources="loadResources"
261
306
  :load-indeterminate="loadIndeterminate"
262
307
  >
308
+ <template
309
+ v-if="activeStateFilters.length"
310
+ #subHeader
311
+ >
312
+ <div class="state-filter-bar text-muted">
313
+ {{ t('resourceList.stateFilterApplied') }}
314
+ <BadgeState
315
+ v-for="state in activeStateFilters"
316
+ :key="state.label"
317
+ :color="state.color"
318
+ :label="state.label"
319
+ class="badge-state-font-size"
320
+ />
321
+ <span>.</span>
322
+ <a
323
+ role="button"
324
+ @click="clearStateFilter"
325
+ >
326
+ {{ t('resourceList.clearStateFilter') }}
327
+ </a>
328
+ </div>
329
+ </template>
263
330
  <template #extraActions>
264
331
  <slot name="extraActions" />
265
332
  </template>
@@ -317,4 +384,18 @@ export default {
317
384
  top: 10px;
318
385
  right: 10px;
319
386
  }
387
+
388
+ .state-filter-bar {
389
+ display: flex;
390
+ align-items: center;
391
+ gap: 4px;
392
+
393
+ a {
394
+ cursor: pointer;
395
+ }
396
+ }
397
+
398
+ .badge-state-font-size {
399
+ font-size: .85em;
400
+ }
320
401
  </style>
@@ -74,8 +74,11 @@ export default defineComponent({
74
74
  const content = enclosingTagName ? match[2] : '';
75
75
 
76
76
  if (slots[tagName]) {
77
- // If a slot is provided for this tag, render the slot with the content.
78
- children.push(slots[tagName]({ content: purifyHTML(content) }));
77
+ const slotContent = slots[tagName]({ content: purifyHTML(content) });
78
+
79
+ if (slotContent) {
80
+ children.push(...slotContent);
81
+ }
79
82
  } else if (ALLOWED_TAGS.includes(tagName.toLowerCase())) {
80
83
  // If it's an allowed HTML tag, render it directly.
81
84
  if (content) {
@@ -29,6 +29,7 @@ export default {
29
29
  <span
30
30
  v-if="value.fromEnv"
31
31
  class="modified"
32
+ :data-testid="`advanced-setting-env-label-${value.id}`"
32
33
  >{{ t('advancedSettings.setEnv') }}</span>
33
34
  <span
34
35
  v-else-if="value.customized"
@@ -1,25 +1,50 @@
1
1
  <script setup lang="ts">
2
+ import { computed } from 'vue';
2
3
  import { RouterLink, RouteLocationRaw } from 'vue-router';
3
4
 
4
5
  export interface Props {
5
- to: RouteLocationRaw;
6
+ to?: RouteLocationRaw;
7
+ href?: string;
8
+ target?: string;
9
+ openInNewTabLabel?: string;
6
10
  }
7
11
 
8
- const { to } = defineProps<Props>();
12
+ const props = defineProps<Props>();
13
+
14
+ const isExternal = computed(() => !!props.href);
9
15
  </script>
10
16
 
11
17
  <template>
12
- <RouterLink
18
+ <component
19
+ :is="isExternal ? 'a' : RouterLink"
13
20
  class="subtle-link"
14
- :to="to"
21
+ v-bind="isExternal
22
+ ? { href, target, rel: target === '_blank' ? 'noopener noreferrer nofollow' : undefined }
23
+ : { to }"
15
24
  >
16
- <slot name="default" />
17
- </RouterLink>
25
+ <span
26
+ v-if="openInNewTabLabel"
27
+ class="sr-only"
28
+ >{{ openInNewTabLabel }}</span>
29
+ <slot name="default" /><span v-if="openInNewTabLabel">&nbsp;</span><i
30
+ v-if="openInNewTabLabel"
31
+ class="link-icon icon icon-external-link"
32
+ />
33
+ </component>
18
34
  </template>
19
35
 
20
36
  <style lang="scss" scoped>
21
37
  .subtle-link {
22
38
  text-decoration: underline;
23
39
  color: var(--body-text);
40
+
41
+ &:hover,
42
+ &:active {
43
+ text-decoration: none;
44
+ }
45
+ }
46
+
47
+ .link-icon {
48
+ display: inline;
24
49
  }
25
50
  </style>