@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
@@ -0,0 +1,561 @@
1
+ import { useWorkloadDashboard } from '@shell/pages/c/_cluster/explorer/workload-dashboard/composable';
2
+ import { WORKLOAD_RESOURCE_TYPES } from '@shell/pages/c/_cluster/explorer/workload-dashboard/types';
3
+ import { defineComponent, h } from 'vue';
4
+ import { shallowMount, flushPromises } from '@vue/test-utils';
5
+
6
+ const mockGetters: Record<string, any> = {};
7
+ const mockDispatch = jest.fn();
8
+ const mockRouterPush = jest.fn();
9
+
10
+ jest.mock('vuex', () => ({
11
+ useStore: () => ({
12
+ getters: new Proxy(mockGetters, {
13
+ get(target, prop: string) {
14
+ return target[prop];
15
+ },
16
+ }),
17
+ dispatch: mockDispatch,
18
+ }),
19
+ }));
20
+
21
+ jest.mock('vue-router', () => ({ useRouter: () => ({ push: mockRouterPush }) }));
22
+
23
+ jest.mock('@shell/composables/useI18n', () => ({ useI18n: () => ({ t: (key: string, args?: Record<string, any>) => `%${ key }%${ args ? JSON.stringify(args) : '' }` }) }));
24
+
25
+ jest.mock('@shell/plugins/steve/steve-pagination-utils', () => ({
26
+ __esModule: true,
27
+ default: {
28
+ createParamsFromNsFilter: jest.fn(() => ({ projectsOrNamespaces: [], filters: [] })),
29
+ createParamsForPagination: jest.fn(() => ''),
30
+ },
31
+ }));
32
+
33
+ jest.mock('@shell/plugins/steve/projectAndNamespaceFiltering.utils', () => ({
34
+ __esModule: true,
35
+ default: { createParam: jest.fn(() => '') },
36
+ }));
37
+
38
+ const defaultGetters: Record<string, any> = {
39
+ clusterId: 'local',
40
+ isAllNamespaces: true,
41
+ namespaceFilters: [],
42
+ namespaceMode: 'both',
43
+ 'prefs/get': () => ({}),
44
+ 'cluster/all': () => [],
45
+ 'cluster/schemaFor': () => null,
46
+ currentCluster: { isLocal: true },
47
+ currentProduct: { hideSystemResources: false },
48
+ 'management/all': () => [],
49
+ };
50
+
51
+ Object.assign(mockGetters, defaultGetters);
52
+
53
+ function setupGetters(overrides: Record<string, any> = {}) {
54
+ Object.keys(mockGetters).forEach((key) => delete mockGetters[key]);
55
+ Object.assign(mockGetters, defaultGetters, overrides);
56
+ }
57
+
58
+ const summaryResponse = {
59
+ summary: [{
60
+ property: 'metadata.state.name',
61
+ counts: {
62
+ running: { total: 5, namespace: { default: 5 } },
63
+ error: { total: 2, namespace: { default: 2 } },
64
+ }
65
+ }],
66
+ data: [],
67
+ };
68
+
69
+ function mountComposable(getterOverrides: Record<string, any> = {}, dispatchResponse: any = summaryResponse) {
70
+ setupGetters({
71
+ 'cluster/schemaFor': () => ({ links: { collection: '/v1/test' } }),
72
+ 'cluster/urlFor': () => '/v1/test',
73
+ 'cluster/canList': () => true,
74
+ ...getterOverrides,
75
+ });
76
+
77
+ mockDispatch.mockImplementation((action: string) => {
78
+ if (action === 'cluster/request') {
79
+ return Promise.resolve(dispatchResponse);
80
+ }
81
+
82
+ return Promise.resolve();
83
+ });
84
+
85
+ let result: ReturnType<typeof useWorkloadDashboard>;
86
+
87
+ const wrapper = shallowMount(defineComponent({
88
+ setup() {
89
+ result = useWorkloadDashboard();
90
+
91
+ return {};
92
+ },
93
+ render: () => h('div'),
94
+ }));
95
+
96
+ return {
97
+ wrapper,
98
+ get result() {
99
+ return result!;
100
+ },
101
+ };
102
+ }
103
+
104
+ describe('composable: useWorkloadDashboard', () => {
105
+ beforeEach(() => {
106
+ jest.clearAllMocks();
107
+ setupGetters();
108
+ });
109
+
110
+ describe('namespaceSubtitle', () => {
111
+ it('should return allNamespaces subtitle with workloadCount suffix when isAllNamespaces is true', async() => {
112
+ const { wrapper, result } = mountComposable({ isAllNamespaces: true, namespaceMode: 'both' });
113
+
114
+ await flushPromises();
115
+
116
+ expect(result.namespaceSubtitle.value).toStrictEqual('%workloadDashboard.subtitle.allNamespaces% %workloadDashboard.workloadCount%{"count":42}');
117
+ wrapper.unmount();
118
+ });
119
+
120
+ it('should return userNamespaces subtitle with workloadCount suffix for ALL_USER filter', async() => {
121
+ const { wrapper, result } = mountComposable({
122
+ isAllNamespaces: false,
123
+ namespaceMode: 'both',
124
+ namespaceFilters: ['all://user'],
125
+ });
126
+
127
+ await flushPromises();
128
+
129
+ expect(result.namespaceSubtitle.value).toStrictEqual('%workloadDashboard.subtitle.userNamespaces% %workloadDashboard.workloadCount%{"count":42}');
130
+ wrapper.unmount();
131
+ });
132
+
133
+ it('should return systemNamespaces subtitle with workloadCount suffix for ALL_SYSTEM filter', async() => {
134
+ const { wrapper, result } = mountComposable({
135
+ isAllNamespaces: false,
136
+ namespaceMode: 'both',
137
+ namespaceFilters: ['all://system'],
138
+ });
139
+
140
+ await flushPromises();
141
+
142
+ expect(result.namespaceSubtitle.value).toStrictEqual('%workloadDashboard.subtitle.systemNamespaces% %workloadDashboard.workloadCount%{"count":42}');
143
+ wrapper.unmount();
144
+ });
145
+
146
+ it('should return project subtitle with workloadCount suffix for project filter', async() => {
147
+ const projectId = 'p-12345';
148
+
149
+ const { wrapper, result } = mountComposable({
150
+ isAllNamespaces: false,
151
+ namespaceMode: 'both',
152
+ namespaceFilters: [`project://${ projectId }`],
153
+ 'management/all': () => [{
154
+ id: `local/${ projectId }`, nameDisplay: 'My Project', metadata: { name: projectId }
155
+ }],
156
+ });
157
+
158
+ await flushPromises();
159
+
160
+ expect(result.namespaceSubtitle.value).toStrictEqual('%workloadDashboard.subtitle.project%{"name":"My Project"} %workloadDashboard.workloadCount%{"count":42}');
161
+ wrapper.unmount();
162
+ });
163
+
164
+ it('should return namespace subtitle with workloadCount suffix for namespace filter', async() => {
165
+ const { wrapper, result } = mountComposable({
166
+ isAllNamespaces: false,
167
+ namespaceMode: 'both',
168
+ namespaceFilters: ['ns://cattle-system'],
169
+ });
170
+
171
+ await flushPromises();
172
+
173
+ expect(result.namespaceSubtitle.value).toStrictEqual('%workloadDashboard.subtitle.namespace%{"name":"cattle-system"} %workloadDashboard.workloadCount%{"count":42}');
174
+ wrapper.unmount();
175
+ });
176
+
177
+ it('should return multipleSelected subtitle with workloadCount suffix for multiple filters', async() => {
178
+ const { wrapper, result } = mountComposable({
179
+ isAllNamespaces: false,
180
+ namespaceMode: 'both',
181
+ namespaceFilters: ['ns://default', 'ns://kube-system'],
182
+ });
183
+
184
+ await flushPromises();
185
+
186
+ expect(result.namespaceSubtitle.value).toStrictEqual('%workloadDashboard.subtitle.multipleSelected%{"selected":2} %workloadDashboard.workloadCount%{"count":42}');
187
+ wrapper.unmount();
188
+ });
189
+
190
+ it('should pass count 0 to workloadCount when no workloads exist', async() => {
191
+ const emptyResponse = {
192
+ summary: [{
193
+ property: 'metadata.state.name',
194
+ counts: {}
195
+ }],
196
+ data: [],
197
+ };
198
+
199
+ const { wrapper, result } = mountComposable({ isAllNamespaces: true }, emptyResponse);
200
+
201
+ await flushPromises();
202
+
203
+ expect(result.namespaceSubtitle.value).toStrictEqual('%workloadDashboard.subtitle.allNamespaces% %workloadDashboard.workloadCount%{"count":0}');
204
+ wrapper.unmount();
205
+ });
206
+
207
+ it('should pass count 1 to workloadCount for a single workload', async() => {
208
+ const singleResponse = {
209
+ summary: [{
210
+ property: 'metadata.state.name',
211
+ counts: { running: { total: 1, namespace: { default: 1 } } }
212
+ }],
213
+ data: [],
214
+ };
215
+
216
+ const { wrapper, result } = mountComposable({
217
+ isAllNamespaces: true,
218
+ 'cluster/canList': (type: string) => type === 'apps.deployment',
219
+ }, singleResponse);
220
+
221
+ await flushPromises();
222
+
223
+ expect(result.namespaceSubtitle.value).toStrictEqual('%workloadDashboard.subtitle.allNamespaces% %workloadDashboard.workloadCount%{"count":1}');
224
+ wrapper.unmount();
225
+ });
226
+
227
+ it('should not count entries with errors in the workloadCount total', async() => {
228
+ const errorResponse = {
229
+ summary: null,
230
+ error: 'No access',
231
+ };
232
+
233
+ const { wrapper, result } = mountComposable({ isAllNamespaces: true }, errorResponse);
234
+
235
+ await flushPromises();
236
+
237
+ expect(result.namespaceSubtitle.value).toStrictEqual('%workloadDashboard.subtitle.allNamespaces% %workloadDashboard.workloadCount%{"count":0}');
238
+ wrapper.unmount();
239
+ });
240
+ });
241
+
242
+ describe('hasWorkloads', () => {
243
+ it('should return false when there are no summaries', async() => {
244
+ const { wrapper, result } = mountComposable({ 'cluster/canList': () => false });
245
+
246
+ await flushPromises();
247
+
248
+ expect(result.hasWorkloads.value).toStrictEqual(false);
249
+ wrapper.unmount();
250
+ });
251
+
252
+ it('should return true when summaries contain workloads', async() => {
253
+ const { wrapper, result } = mountComposable();
254
+
255
+ await flushPromises();
256
+
257
+ expect(result.hasWorkloads.value).toStrictEqual(true);
258
+ wrapper.unmount();
259
+ });
260
+ });
261
+
262
+ describe('resourceRoute', () => {
263
+ it('should return route without query when no stateNames provided', async() => {
264
+ const { wrapper, result } = mountComposable();
265
+
266
+ await flushPromises();
267
+ const route = result.resourceRoute('apps.deployment');
268
+
269
+ expect(route).toStrictEqual({
270
+ name: 'c-cluster-product-resource',
271
+ params: {
272
+ cluster: 'local',
273
+ product: 'explorer',
274
+ resource: 'apps.deployment',
275
+ },
276
+ });
277
+ wrapper.unmount();
278
+ });
279
+
280
+ it('should include state filter query when stateNames are provided', async() => {
281
+ const { wrapper, result } = mountComposable();
282
+
283
+ await flushPromises();
284
+ const route = result.resourceRoute('apps.deployment', ['running', 'active']);
285
+
286
+ expect((route as any).query).toStrictEqual({ stateFilter: 'running,active' });
287
+ wrapper.unmount();
288
+ });
289
+
290
+ it('should not include query for empty stateNames array', async() => {
291
+ const { wrapper, result } = mountComposable();
292
+
293
+ await flushPromises();
294
+ const route = result.resourceRoute('apps.deployment', []);
295
+
296
+ expect((route as any).query).toBeUndefined();
297
+ wrapper.unmount();
298
+ });
299
+ });
300
+
301
+ describe('resetNamespaceFilter', () => {
302
+ it('should dispatch switchNamespaces with empty ids', async() => {
303
+ const { wrapper, result } = mountComposable();
304
+
305
+ await flushPromises();
306
+ result.resetNamespaceFilter();
307
+
308
+ expect(mockDispatch).toHaveBeenCalledWith('switchNamespaces', { ids: [], key: 'local' });
309
+ wrapper.unmount();
310
+ });
311
+ });
312
+
313
+ describe('byTypeCards', () => {
314
+ it('should return a card for each workload type with data', async() => {
315
+ const { wrapper, result } = mountComposable();
316
+
317
+ await flushPromises();
318
+
319
+ expect(result.byTypeCards.value).toHaveLength(WORKLOAD_RESOURCE_TYPES.length);
320
+ wrapper.unmount();
321
+ });
322
+
323
+ it('should include type and title on each card', async() => {
324
+ const { wrapper, result } = mountComposable();
325
+
326
+ await flushPromises();
327
+
328
+ const card = result.byTypeCards.value[0];
329
+
330
+ expect(card.type).toStrictEqual(WORKLOAD_RESOURCE_TYPES[0]);
331
+ expect(card.title).toBeTruthy();
332
+ wrapper.unmount();
333
+ });
334
+
335
+ it('should map resource states with correct colors', async() => {
336
+ const { wrapper, result } = mountComposable();
337
+
338
+ await flushPromises();
339
+
340
+ const card = result.byTypeCards.value[0];
341
+ const colors = card.resources.map((r) => r.stateSimpleColor);
342
+
343
+ expect(colors).toContain('success');
344
+ expect(colors).toContain('error');
345
+ wrapper.unmount();
346
+ });
347
+
348
+ it('should capitalize stateDisplay for each resource', async() => {
349
+ const { wrapper, result } = mountComposable();
350
+
351
+ await flushPromises();
352
+
353
+ const card = result.byTypeCards.value[0];
354
+ const runningResource = card.resources.find((r) => r.stateId === 'running');
355
+
356
+ expect(runningResource?.stateDisplay).toStrictEqual('Running');
357
+ wrapper.unmount();
358
+ });
359
+
360
+ it('should preserve counts from the summary response', async() => {
361
+ const { wrapper, result } = mountComposable();
362
+
363
+ await flushPromises();
364
+
365
+ const card = result.byTypeCards.value[0];
366
+ const runningResource = card.resources.find((r) => r.stateId === 'running');
367
+ const errorResource = card.resources.find((r) => r.stateId === 'error');
368
+
369
+ expect(runningResource?.count).toStrictEqual(5);
370
+ expect(errorResource?.count).toStrictEqual(2);
371
+ wrapper.unmount();
372
+ });
373
+ });
374
+
375
+ describe('byStateLayout', () => {
376
+ it('should assign the success card as hero', async() => {
377
+ const { wrapper, result } = mountComposable();
378
+
379
+ await flushPromises();
380
+
381
+ expect(result.byStateLayout.value.hero?.color).toStrictEqual('success');
382
+ wrapper.unmount();
383
+ });
384
+
385
+ it('should place non-hero cards in the cards array', async() => {
386
+ const { wrapper, result } = mountComposable();
387
+
388
+ await flushPromises();
389
+
390
+ const { cards } = result.byStateLayout.value;
391
+
392
+ expect(cards.length).toBeGreaterThan(0);
393
+ expect(cards.every((c) => c.color !== 'success')).toStrictEqual(true);
394
+ wrapper.unmount();
395
+ });
396
+
397
+ it('should set subHero to null when there are fewer than 2 other cards', async() => {
398
+ const { wrapper, result } = mountComposable();
399
+
400
+ await flushPromises();
401
+
402
+ expect(result.byStateLayout.value.subHero).toBeNull();
403
+ wrapper.unmount();
404
+ });
405
+ });
406
+
407
+ describe('byNamespaceCards', () => {
408
+ it('should return cards grouped by namespace', async() => {
409
+ const { wrapper, result } = mountComposable();
410
+
411
+ await flushPromises();
412
+
413
+ const cards = result.byNamespaceCards.value;
414
+
415
+ expect(cards.length).toBeGreaterThan(0);
416
+ expect(cards[0].title).toStrictEqual('default');
417
+ wrapper.unmount();
418
+ });
419
+
420
+ it('should have rows for each workload type present in the namespace', async() => {
421
+ const { wrapper, result } = mountComposable();
422
+
423
+ await flushPromises();
424
+
425
+ const defaultCard = result.byNamespaceCards.value.find((c) => c.title === 'default');
426
+
427
+ expect(defaultCard?.rows).toHaveLength(WORKLOAD_RESOURCE_TYPES.length);
428
+ expect(defaultCard?.rows.map((r) => r.type)).toStrictEqual(WORKLOAD_RESOURCE_TYPES);
429
+ wrapper.unmount();
430
+ });
431
+
432
+ it('should group counts by color within each row', async() => {
433
+ const { wrapper, result } = mountComposable();
434
+
435
+ await flushPromises();
436
+
437
+ const defaultCard = result.byNamespaceCards.value.find((c) => c.title === 'default');
438
+ const firstRow = defaultCard?.rows[0];
439
+ const colors = firstRow?.counts.map((c) => c.color);
440
+
441
+ expect(colors).toContain('error');
442
+ expect(colors).toContain('success');
443
+ wrapper.unmount();
444
+ });
445
+
446
+ it('should sort counts by color order (error first, success last)', async() => {
447
+ const { wrapper, result } = mountComposable();
448
+
449
+ await flushPromises();
450
+
451
+ const defaultCard = result.byNamespaceCards.value.find((c) => c.title === 'default');
452
+ const firstRow = defaultCard?.rows[0];
453
+ const colors = firstRow?.counts.map((c) => c.color);
454
+
455
+ expect(colors?.indexOf('error')).toBeLessThan(colors?.indexOf('success') as number);
456
+ wrapper.unmount();
457
+ });
458
+
459
+ it('should sort namespaces alphabetically', async() => {
460
+ const multiNsResponse = {
461
+ summary: [{
462
+ property: 'metadata.state.name',
463
+ counts: {
464
+ running: {
465
+ total: 3,
466
+ namespace: { zebra: 1, alpha: 2 },
467
+ },
468
+ }
469
+ }],
470
+ data: [],
471
+ };
472
+
473
+ const { wrapper, result } = mountComposable({}, multiNsResponse);
474
+
475
+ await flushPromises();
476
+
477
+ const titles = result.byNamespaceCards.value.map((c) => c.title);
478
+
479
+ expect(titles).toStrictEqual(['alpha', 'zebra']);
480
+ wrapper.unmount();
481
+ });
482
+
483
+ it('should include stateNames in each count entry for routing', async() => {
484
+ const { wrapper, result } = mountComposable();
485
+
486
+ await flushPromises();
487
+
488
+ const defaultCard = result.byNamespaceCards.value.find((c) => c.title === 'default');
489
+ const firstRow = defaultCard?.rows[0];
490
+ const successCount = firstRow?.counts.find((c) => c.color === 'success');
491
+
492
+ expect(successCount?.stateNames).toContain('running');
493
+ expect(successCount?.count).toStrictEqual(5);
494
+ wrapper.unmount();
495
+ });
496
+ });
497
+
498
+ describe('filterByNamespace', () => {
499
+ it('should dispatch switchNamespaces with the namespace filter', async() => {
500
+ const { wrapper, result } = mountComposable();
501
+
502
+ await flushPromises();
503
+ mockDispatch.mockClear();
504
+
505
+ result.filterByNamespace('cattle-system');
506
+
507
+ expect(mockDispatch).toHaveBeenCalledWith('switchNamespaces', {
508
+ ids: ['ns://cattle-system'],
509
+ key: 'local',
510
+ });
511
+ wrapper.unmount();
512
+ });
513
+ });
514
+
515
+ describe('navigateToNamespace', () => {
516
+ it('should switch namespace filter and navigate to resource page', async() => {
517
+ const { wrapper, result } = mountComposable();
518
+
519
+ await flushPromises();
520
+ mockDispatch.mockClear();
521
+ mockRouterPush.mockClear();
522
+
523
+ result.navigateToNamespace('apps.deployment', 'cattle-system');
524
+
525
+ expect(mockDispatch).toHaveBeenCalledWith('switchNamespaces', {
526
+ ids: ['ns://cattle-system'],
527
+ key: 'local',
528
+ });
529
+ expect(mockRouterPush).toHaveBeenCalledWith({
530
+ name: 'c-cluster-product-resource',
531
+ params: {
532
+ cluster: 'local',
533
+ product: 'explorer',
534
+ resource: 'apps.deployment',
535
+ },
536
+ });
537
+ wrapper.unmount();
538
+ });
539
+
540
+ it('should include state filter query when stateNames are provided', async() => {
541
+ const { wrapper, result } = mountComposable();
542
+
543
+ await flushPromises();
544
+ mockDispatch.mockClear();
545
+ mockRouterPush.mockClear();
546
+
547
+ result.navigateToNamespace('apps.deployment', 'default', ['running', 'active']);
548
+
549
+ expect(mockRouterPush).toHaveBeenCalledWith({
550
+ name: 'c-cluster-product-resource',
551
+ params: {
552
+ cluster: 'local',
553
+ product: 'explorer',
554
+ resource: 'apps.deployment',
555
+ },
556
+ query: { stateFilter: 'running,active' },
557
+ });
558
+ wrapper.unmount();
559
+ });
560
+ });
561
+ });