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

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 (315) hide show
  1. package/assets/styles/global/_button.scss +1 -1
  2. package/assets/styles/global/_layout.scss +4 -0
  3. package/assets/translations/en-us.yaml +183 -51
  4. package/assets/translations/zh-hans.yaml +1 -7
  5. package/chart/monitoring/ClusterSelector.vue +0 -21
  6. package/chart/monitoring/prometheus/index.vue +6 -3
  7. package/components/ActionDropdownShell.vue +5 -3
  8. package/components/ButtonGroup.vue +26 -1
  9. package/components/CruResource.vue +212 -16
  10. package/components/ExplorerMembers.vue +8 -4
  11. package/components/ExplorerProjectsNamespaces.vue +10 -6
  12. package/components/GrowlManager.vue +4 -0
  13. package/components/MgmtNodeList.vue +184 -0
  14. package/components/PromptRestore.vue +93 -32
  15. package/components/Questions/index.vue +1 -0
  16. package/components/Resource/Detail/Card/StateCard/__tests__/composables.test.ts +90 -1
  17. package/components/Resource/Detail/Card/StateCard/composables.ts +57 -87
  18. package/components/Resource/Detail/Card/StatusCard/__tests__/StatusCard.test.ts +61 -0
  19. package/components/Resource/Detail/Card/StatusCard/index.vue +61 -15
  20. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +2 -0
  21. package/components/Resource/Detail/Metadata/KeyValue.vue +5 -2
  22. package/components/Resource/Detail/Metadata/KeyValueRow.vue +2 -6
  23. package/components/ResourceDetail/index.vue +1 -1
  24. package/components/ResourceList/Masthead.vue +7 -1
  25. package/components/ResourceList/index.vue +82 -1
  26. package/components/ResourceTable.vue +1 -0
  27. package/components/RichTranslation.vue +5 -2
  28. package/components/Setting.vue +1 -0
  29. package/components/SortableTable/index.vue +4 -3
  30. package/components/SubtleLink.vue +31 -6
  31. package/components/Tabbed/Tab.vue +29 -3
  32. package/components/Tabbed/index.vue +25 -3
  33. package/components/TableOfContents/TableOfContents.vue +109 -0
  34. package/components/TableOfContents/composables.ts +258 -0
  35. package/components/Window/ContainerShell.vue +21 -11
  36. package/components/Window/__tests__/ContainerShell.test.ts +107 -37
  37. package/components/Wizard.vue +23 -5
  38. package/components/__tests__/ButtonGroup.test.ts +56 -0
  39. package/components/__tests__/PromptRestore.test.ts +169 -19
  40. package/components/fleet/AppCoChartGrid.vue +401 -0
  41. package/components/fleet/AppCoEmptyState.vue +127 -0
  42. package/components/fleet/AppCoPageHeader.vue +119 -0
  43. package/components/fleet/AppCoVersionSelect.vue +70 -0
  44. package/components/fleet/FleetClusterTargets/ClusterSelectionFields.vue +217 -0
  45. package/components/fleet/FleetClusterTargets/TargetsList.vue +123 -35
  46. package/components/fleet/FleetClusterTargets/index.vue +189 -146
  47. package/components/fleet/FleetIntro.vue +7 -3
  48. package/components/fleet/FleetNoWorkspaces.vue +7 -3
  49. package/components/fleet/FleetSecretSelector.vue +5 -3
  50. package/components/fleet/FleetValuesFrom.vue +8 -2
  51. package/components/fleet/GitRepoAdvancedTab.vue +1 -0
  52. package/components/fleet/GitRepoMetadataTab.vue +5 -0
  53. package/components/fleet/GitRepoTargetTab.vue +0 -2
  54. package/components/fleet/HelmOpAdvancedTab.vue +19 -53
  55. package/components/fleet/HelmOpAppCoConfigTab.vue +597 -0
  56. package/components/fleet/HelmOpAppCoResourcesSection.vue +162 -0
  57. package/components/fleet/HelmOpMetadataTab.vue +5 -0
  58. package/components/fleet/HelmOpResourcesSection.vue +82 -0
  59. package/components/fleet/HelmOpTargetOptionsSection.vue +89 -0
  60. package/components/fleet/HelmOpTargetTab.vue +64 -60
  61. package/components/fleet/HelmOpValuesTab.vue +129 -105
  62. package/components/fleet/__tests__/AppCoEmptyState.test.ts +71 -0
  63. package/components/fleet/__tests__/AppCoVersionSelect.test.ts +36 -0
  64. package/components/fleet/__tests__/ClusterSelectionFields.test.ts +62 -0
  65. package/components/fleet/__tests__/FleetClusterTargets.test.ts +253 -0
  66. package/components/fleet/__tests__/FleetSecretSelector.test.ts +16 -0
  67. package/components/fleet/__tests__/FleetValuesFrom.test.ts +44 -0
  68. package/components/fleet/__tests__/HelmOpAppCoConfigTab.test.ts +59 -0
  69. package/components/fleet/__tests__/HelmOpAppCoResourcesSection.test.ts +62 -0
  70. package/components/fleet/__tests__/HelmOpResourcesSection.test.ts +43 -0
  71. package/components/fleet/__tests__/HelmOpTargetOptionsSection.test.ts +34 -0
  72. package/components/fleet/__tests__/HelmOpValuesTab.test.ts +39 -0
  73. package/components/fleet/__tests__/__snapshots__/AppCoEmptyState.test.ts.snap +97 -0
  74. package/components/fleet/__tests__/__snapshots__/AppCoVersionSelect.test.ts.snap +30 -0
  75. package/components/fleet/__tests__/__snapshots__/ClusterSelectionFields.test.ts.snap +209 -0
  76. package/components/fleet/__tests__/__snapshots__/HelmOpTargetOptionsSection.test.ts.snap +140 -0
  77. package/components/fleet/dashboard/Empty.vue +8 -4
  78. package/components/fleet/dashboard/ResourceCard.vue +28 -0
  79. package/components/fleet/dashboard/ResourceDetails.vue +28 -0
  80. package/components/fleet/dashboard/__tests__/ResourceCard.test.ts +87 -0
  81. package/components/form/ArrayList.vue +61 -4
  82. package/components/form/FileSelector.vue +39 -1
  83. package/components/form/KeyValue.vue +23 -2
  84. package/components/form/LabeledSelect.vue +39 -1
  85. package/components/form/Labels.vue +22 -3
  86. package/components/form/NameNsDescription.vue +13 -5
  87. package/components/form/PrivateRegistry.constants.ts +7 -0
  88. package/components/form/PrivateRegistry.vue +253 -18
  89. package/components/form/ResourceTabs/index.vue +1 -0
  90. package/components/form/SelectOrCreateAuthSecret.vue +140 -17
  91. package/components/form/__tests__/FileSelector.test.ts +23 -0
  92. package/components/form/__tests__/NameNsDescription.test.ts +75 -0
  93. package/components/form/__tests__/PrivateRegistry.test.ts +463 -73
  94. package/components/form/__tests__/SelectOrCreateAuthSecret.test.ts +122 -0
  95. package/components/formatter/EtcdSnapshotName.vue +73 -0
  96. package/components/formatter/InternalExternalIP.vue +10 -4
  97. package/components/formatter/ServiceTargets.vue +26 -7
  98. package/components/formatter/__tests__/InternalExternalIP.test.ts +132 -0
  99. package/components/formatter/__tests__/ServiceTargets.test.ts +412 -0
  100. package/components/nav/Header.vue +12 -1
  101. package/components/nav/TopLevelMenu.vue +7 -2
  102. package/components/nav/__tests__/Header.test.ts +15 -0
  103. package/components/nav/__tests__/TopLevelMenu.test.ts +120 -2
  104. package/components/templates/default.vue +16 -4
  105. package/components/templates/home.vue +9 -4
  106. package/components/templates/plain.vue +9 -4
  107. package/composables/useHelmOpResources.test.ts +56 -0
  108. package/composables/useHelmOpResources.ts +32 -0
  109. package/composables/useStateColor.test.ts +325 -0
  110. package/composables/useStateColor.ts +128 -0
  111. package/config/features.js +1 -0
  112. package/config/home-links.js +1 -1
  113. package/config/labels-annotations.js +3 -0
  114. package/config/product/explorer.js +17 -4
  115. package/config/product/manager.js +8 -0
  116. package/config/router/index.js +16 -0
  117. package/config/router/navigation-guards/__tests__/authentication.test.ts +130 -0
  118. package/config/router/navigation-guards/authentication.js +10 -4
  119. package/config/router/routes.js +20 -6
  120. package/config/secret.ts +10 -0
  121. package/config/settings.ts +6 -4
  122. package/config/table-headers.js +3 -4
  123. package/config/types.js +16 -0
  124. package/core/plugin-products-base.ts +3 -3
  125. package/core/plugin-types.ts +83 -30
  126. package/core/plugin.ts +3 -0
  127. package/core/types-provisioning.ts +34 -1
  128. package/core/types.ts +15 -2
  129. package/detail/__tests__/provisioning.cattle.io.cluster.test.ts +114 -0
  130. package/detail/__tests__/workload.test.ts +3 -152
  131. package/detail/catalog.cattle.io.clusterrepo.vue +1 -1
  132. package/detail/provisioning.cattle.io.cluster.vue +109 -7
  133. package/detail/workload/index.vue +12 -55
  134. package/dialog/RotateEncryptionKeyDialog.vue +33 -9
  135. package/dialog/__tests__/RotateEncryptionKeyDialog.test.ts +78 -0
  136. package/edit/__tests__/catalog.cattle.io.clusterrepo.test.ts +248 -0
  137. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +92 -0
  138. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +206 -0
  139. package/edit/__tests__/management.cattle.io.setting.test.ts +2 -1
  140. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +6 -0
  141. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/index.test.ts.snap +1 -0
  142. package/edit/auth/__tests__/azuread.test.ts +34 -9
  143. package/edit/auth/__tests__/github.test.ts +234 -0
  144. package/edit/auth/__tests__/oidc.test.ts +26 -6
  145. package/edit/auth/__tests__/saml.test.ts +196 -0
  146. package/edit/auth/azuread.vue +128 -95
  147. package/edit/auth/github.vue +72 -13
  148. package/edit/auth/ldap/__tests__/index.test.ts +206 -0
  149. package/edit/auth/ldap/config.vue +8 -0
  150. package/edit/auth/ldap/index.vue +75 -1
  151. package/edit/auth/oidc.vue +119 -73
  152. package/edit/auth/saml.vue +76 -12
  153. package/edit/catalog.cattle.io.clusterrepo.vue +140 -32
  154. package/edit/compliance.cattle.io.clusterscanprofile.vue +39 -41
  155. package/edit/fleet.cattle.io.gitrepo.vue +70 -16
  156. package/edit/fleet.cattle.io.helmop.vue +542 -141
  157. package/edit/helm.cattle.io.projecthelmchart.vue +1 -0
  158. package/edit/{management.cattle.io.setting.vue → management.cattle.io.setting/index.vue} +32 -9
  159. package/edit/management.cattle.io.setting/system-default-registry-pull-secrets.vue +81 -0
  160. package/edit/management.cattle.io.user.vue +5 -2
  161. package/edit/provisioning.cattle.io.cluster/SelectCredential.vue +3 -12
  162. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +18 -0
  163. package/edit/provisioning.cattle.io.cluster/rke2.vue +89 -11
  164. package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +11 -0
  165. package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +0 -1
  166. package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +14 -55
  167. package/list/group.principal.vue +5 -4
  168. package/list/harvesterhci.io.management.cluster.vue +8 -9
  169. package/list/management.cattle.io.user.vue +12 -9
  170. package/list/provisioning.cattle.io.cluster.vue +16 -10
  171. package/mixins/__tests__/auth-config.test.ts +90 -0
  172. package/mixins/__tests__/chart.test.ts +94 -0
  173. package/mixins/__tests__/resource-fetch-api-pagination.test.ts +48 -0
  174. package/mixins/auth-config.js +7 -0
  175. package/mixins/chart.js +11 -2
  176. package/mixins/child-hook.js +12 -6
  177. package/mixins/create-edit-view/impl.js +5 -3
  178. package/mixins/resource-fetch-api-pagination.js +21 -1
  179. package/models/__tests__/catalog.cattle.io.clusterrepo.test.ts +57 -0
  180. package/models/__tests__/compliance.cattle.io.clusterscan.test.ts +144 -0
  181. package/models/__tests__/fleet-application.test.ts +175 -0
  182. package/models/__tests__/fleet.cattle.io.bundle.test.ts +169 -0
  183. package/models/__tests__/fleet.cattle.io.helmop.test.ts +84 -0
  184. package/models/__tests__/management.cattle.io.node.ts +22 -0
  185. package/models/__tests__/namespace.test.ts +36 -0
  186. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +205 -0
  187. package/models/__tests__/secret.test.ts +68 -1
  188. package/models/__tests__/workload.test.ts +401 -26
  189. package/models/catalog.cattle.io.clusterrepo.js +28 -4
  190. package/models/compliance.cattle.io.clusterscan.js +39 -4
  191. package/models/fleet-application.js +4 -0
  192. package/models/fleet.cattle.io.helmop.js +20 -1
  193. package/models/management.cattle.io.cluster.js +39 -5
  194. package/models/management.cattle.io.node.js +44 -3
  195. package/models/namespace.js +1 -1
  196. package/models/pod.js +46 -3
  197. package/models/provisioning.cattle.io.cluster.js +64 -14
  198. package/models/rke.cattle.io.etcdsnapshot.js +17 -9
  199. package/models/secret.js +19 -0
  200. package/models/workload.js +120 -20
  201. package/models/workload.service.js +5 -0
  202. package/package.json +14 -13
  203. package/pages/about.vue +5 -6
  204. package/pages/auth/login.vue +0 -35
  205. package/pages/auth/setup.vue +11 -0
  206. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +2 -2
  207. package/pages/c/_cluster/apps/charts/AppChartCardSubHeader.vue +10 -1
  208. package/pages/c/_cluster/apps/charts/__tests__/index.test.ts +93 -0
  209. package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +485 -107
  210. package/pages/c/_cluster/apps/charts/chart.vue +2 -1
  211. package/pages/c/_cluster/apps/charts/index.vue +48 -10
  212. package/pages/c/_cluster/apps/charts/install.vue +236 -144
  213. package/pages/c/_cluster/auth/roles/index.vue +5 -4
  214. package/pages/c/_cluster/explorer/workload-dashboard/ByNamespaceSection.vue +31 -0
  215. package/pages/c/_cluster/explorer/workload-dashboard/ByStateSection.vue +138 -0
  216. package/pages/c/_cluster/explorer/workload-dashboard/ByTypeSection.vue +30 -0
  217. package/pages/c/_cluster/explorer/workload-dashboard/WorkloadCard.vue +155 -0
  218. package/pages/c/_cluster/explorer/workload-dashboard/WorkloadNamespaceCard.vue +142 -0
  219. package/pages/c/_cluster/explorer/workload-dashboard/WorkloadTypeCard.vue +159 -0
  220. package/pages/c/_cluster/explorer/workload-dashboard/__tests__/composable.test.ts +561 -0
  221. package/pages/c/_cluster/explorer/workload-dashboard/composable.ts +440 -0
  222. package/pages/c/_cluster/explorer/workload-dashboard/index.vue +187 -0
  223. package/pages/c/_cluster/explorer/workload-dashboard/types.ts +80 -0
  224. package/pages/c/_cluster/fleet/application/create.vue +187 -136
  225. package/pages/c/_cluster/fleet/application/index.vue +5 -3
  226. package/pages/c/_cluster/fleet/application/suse-app-collection/ChartDetailBody.vue +338 -0
  227. package/pages/c/_cluster/fleet/application/suse-app-collection/ChartDetailHeader.vue +121 -0
  228. package/pages/c/_cluster/fleet/application/suse-app-collection/chart.vue +369 -0
  229. package/pages/c/_cluster/fleet/application/suse-app-collection/charts.vue +248 -0
  230. package/pages/c/_cluster/fleet/application/suse-app-collection/credentials.vue +310 -0
  231. package/pages/c/_cluster/fleet/index.vue +2 -2
  232. package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +96 -0
  233. package/pages/c/_cluster/uiplugins/index.vue +15 -0
  234. package/pages/fail-whale.vue +16 -11
  235. package/pages/home.vue +16 -46
  236. package/pkg/require-asset.lib.js +25 -0
  237. package/pkg/vue.config.js +7 -0
  238. package/plugins/clean-html.d.ts +9 -0
  239. package/plugins/dashboard-store/__tests__/resource-class.test.ts +177 -0
  240. package/plugins/dashboard-store/getters.js +0 -1
  241. package/plugins/dashboard-store/resource-class.js +114 -19
  242. package/plugins/steve/__tests__/actions.test.ts +212 -0
  243. package/plugins/steve/actions.js +96 -0
  244. package/plugins/steve/steve-pagination-utils.ts +1 -1
  245. package/rancher-components/Accordion/Accordion.vue +53 -9
  246. package/rancher-components/Form/Checkbox/Checkbox.vue +14 -0
  247. package/rancher-components/Form/Radio/RadioButton.vue +17 -1
  248. package/rancher-components/Form/Radio/RadioGroup.vue +10 -0
  249. package/rancher-components/Form/TextArea/TextAreaAutoGrow.vue +30 -0
  250. package/rancher-components/Form/TextArea/__tests__/TextAreaAutoGrow.test.ts +95 -0
  251. package/rancher-components/Pill/RcTag/RcTag.vue +3 -2
  252. package/rancher-components/RcButton/RcButton.test.ts +103 -0
  253. package/rancher-components/RcButton/RcButton.vue +94 -15
  254. package/rancher-components/RcButton/index.ts +1 -1
  255. package/rancher-components/RcButton/types.ts +3 -0
  256. package/rancher-components/RcDropdown/RcDropdownTrigger.vue +6 -1
  257. package/rancher-components/RcItemCard/RcItemCard.test.ts +18 -0
  258. package/rancher-components/RcItemCard/RcItemCard.vue +2 -2
  259. package/rancher-components/RcSection/RcSection.vue +28 -3
  260. package/scripts/extension/helm/package/Dockerfile +1 -1
  261. package/scripts/test-plugins-build.sh +2 -1
  262. package/store/__tests__/features.test.ts +131 -0
  263. package/store/__tests__/growl.test.ts +374 -0
  264. package/store/__tests__/modal.test.ts +131 -0
  265. package/store/__tests__/notifications.test.ts +434 -0
  266. package/store/__tests__/slideInPanel.test.ts +88 -0
  267. package/store/__tests__/type-map.utils.test.ts +433 -0
  268. package/store/catalog.js +57 -0
  269. package/store/features.js +4 -0
  270. package/store/plugins.js +7 -4
  271. package/types/components/buttonGroup.ts +5 -0
  272. package/types/shell/index.d.ts +166 -70
  273. package/utils/__tests__/auth.test.ts +273 -0
  274. package/utils/__tests__/computed.test.ts +193 -0
  275. package/utils/__tests__/cspAdaptor.test.ts +163 -0
  276. package/utils/__tests__/dom.test.ts +81 -0
  277. package/utils/__tests__/duration.test.ts +37 -1
  278. package/utils/__tests__/dynamic-importer.test.ts +102 -0
  279. package/utils/__tests__/fleet-appco.test.ts +312 -0
  280. package/utils/__tests__/monitoring.test.ts +130 -0
  281. package/utils/__tests__/object.test.ts +22 -0
  282. package/utils/__tests__/operation-cr.test.ts +34 -0
  283. package/utils/__tests__/platform.test.ts +91 -0
  284. package/utils/__tests__/position.test.ts +237 -0
  285. package/utils/__tests__/provider.test.ts +51 -1
  286. package/utils/__tests__/queue.test.ts +232 -0
  287. package/utils/__tests__/release-notes.test.ts +221 -0
  288. package/utils/__tests__/router.test.js +254 -1
  289. package/utils/__tests__/select.test.ts +208 -0
  290. package/utils/__tests__/time.test.ts +265 -1
  291. package/utils/__tests__/title.test.ts +47 -0
  292. package/utils/__tests__/width.test.ts +53 -0
  293. package/utils/__tests__/window.test.ts +158 -0
  294. package/utils/__tests__/xccdf.test.ts +126 -6
  295. package/utils/crypto/__tests__/browserHashUtils.test.ts +98 -0
  296. package/utils/crypto/__tests__/index.test.ts +144 -0
  297. package/utils/duration.ts +104 -0
  298. package/utils/dynamic-content/__tests__/notification-handler.test.ts +196 -0
  299. package/utils/dynamic-content/info.ts +2 -1
  300. package/utils/error.js +13 -0
  301. package/utils/fleet-appco.ts +323 -0
  302. package/utils/object.js +22 -2
  303. package/utils/operation-cr.js +19 -0
  304. package/utils/provider.ts +12 -0
  305. package/utils/require-asset.ts +7 -0
  306. package/utils/validators/__tests__/container-images.test.ts +104 -0
  307. package/utils/validators/__tests__/flow-output.test.ts +91 -0
  308. package/utils/validators/__tests__/logging-outputs.test.ts +58 -0
  309. package/utils/validators/__tests__/monitoring-route.test.ts +119 -0
  310. package/utils/validators/__tests__/private-registry.test.ts +27 -15
  311. package/utils/validators/private-registry.ts +15 -4
  312. package/utils/xccdf.ts +39 -42
  313. package/vue.config.js +1 -1
  314. package/pages/support/index.vue +0 -264
  315. package/utils/duration.js +0 -43
@@ -0,0 +1,25 @@
1
+ // Stub for extension library builds — prevents require.context() from bundling
2
+ // all shell images into every extension. At runtime, delegates to the host
3
+ // dashboard's asset resolver (exposed on window by the real require-asset.ts).
4
+
5
+ export function toContextKey(path) {
6
+ return `./${ path.replace(/^[~@]shell\/assets\//, '') }`;
7
+ }
8
+
9
+ export function requireAsset(path) {
10
+ if (typeof window !== 'undefined' && window.__shell_requireAsset) {
11
+ return window.__shell_requireAsset(path);
12
+ }
13
+
14
+ throw new Error(`Asset context not available for: ${ path }`);
15
+ }
16
+
17
+ export function requireJson(path) {
18
+ if (typeof window !== 'undefined' && window.__shell_requireJson) {
19
+ return window.__shell_requireJson(path);
20
+ }
21
+
22
+ throw new Error(`JSON context not available for: ${ path }`);
23
+ }
24
+
25
+ export function _setContexts() {}
package/pkg/vue.config.js CHANGED
@@ -72,11 +72,18 @@ module.exports = function(dir) {
72
72
  resource.request = fs.existsSync(pkgModelLoaderRequire) ? pkgModelLoaderRequire : path.join(__dirname, fileName);
73
73
  });
74
74
 
75
+ // Prevent require.context('@shell/assets') from bundling all shell images into extensions.
76
+ // The stub delegates to the host dashboard's asset resolver at runtime via window.__shell_requireAsset.
77
+ const requireAssetOverride = new webpack.NormalModuleReplacementPlugin(/require-asset$/, (resource) => {
78
+ resource.request = path.join(__dirname, 'require-asset.lib.js');
79
+ });
80
+
75
81
  // Auto-generate module to import the types (model, detail, edit etc)
76
82
  const autoImportPlugin = new VirtualModulesPlugin({ 'node_modules/@rancher/auto-import': generateTypeImport('@pkg', dir) });
77
83
 
78
84
  config.plugins.unshift(dynamicImporterOverride);
79
85
  config.plugins.unshift(modelLoaderImporterOverride);
86
+ config.plugins.unshift(requireAssetOverride);
80
87
  config.plugins.unshift(autoImportPlugin);
81
88
  config.plugins.unshift(new NodePolyfillPlugin()); // required from Webpack 5 to polyfill node modules
82
89
  // config.plugins.unshift(debug);
@@ -0,0 +1,9 @@
1
+ import { Config } from 'dompurify';
2
+
3
+ export function purifyHTML(value: string, options?: Config): string;
4
+
5
+ export function addLinkInterceptor(fn: (link: string) => string | undefined | void, name?: string): void;
6
+
7
+ export function removeLinkInterceptor(fn: (link: string) => string | undefined | void): void;
8
+
9
+ export function processLink(link: string): string;
@@ -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', () => {
@@ -585,4 +678,88 @@ describe('class: Resource', () => {
585
678
  expect(viewYaml.enabled).toBe(true);
586
679
  });
587
680
  });
681
+
682
+ describe('method: dryRunCreate', () => {
683
+ const collectionUrl = '/v1/test.resources';
684
+
685
+ it('should dispatch a request with dryRun=All query param', async() => {
686
+ const dispatch = jest.fn().mockResolvedValue({});
687
+ const resource = new Resource({
688
+ type: 'test.resource',
689
+ metadata: {
690
+ name: 'my-resource',
691
+ namespace: 'my-ns',
692
+ },
693
+ }, {
694
+ getters: {
695
+ schemaFor: () => ({
696
+ linkFor: (link: string) => (link === 'collection' ? collectionUrl : ''),
697
+ attributes: { namespaced: true },
698
+ })
699
+ },
700
+ dispatch,
701
+ rootGetters: { 'i18n/t': jest.fn() },
702
+ });
703
+
704
+ await resource.dryRunCreate();
705
+
706
+ expect(dispatch).toHaveBeenCalledWith('request', {
707
+ opt: expect.objectContaining({
708
+ method: 'post',
709
+ url: `${ collectionUrl }/my-ns?dryRun=All`,
710
+ }),
711
+ type: 'test.resource'
712
+ });
713
+ });
714
+
715
+ it('should use provided data instead of resource state when given', async() => {
716
+ const dispatch = jest.fn().mockResolvedValue({});
717
+ const resource = new Resource({
718
+ type: 'test.resource',
719
+ metadata: { name: 'original', namespace: 'ns' },
720
+ }, {
721
+ getters: {
722
+ schemaFor: () => ({
723
+ linkFor: () => collectionUrl,
724
+ attributes: { namespaced: true },
725
+ })
726
+ },
727
+ dispatch,
728
+ rootGetters: { 'i18n/t': jest.fn() },
729
+ });
730
+
731
+ const customData = {
732
+ type: 'test.resource',
733
+ metadata: { name: 'custom' },
734
+ spec: {}
735
+ };
736
+
737
+ await resource.dryRunCreate(customData);
738
+
739
+ expect(dispatch).toHaveBeenCalledWith('request', {
740
+ opt: expect.objectContaining({ data: customData }),
741
+ type: 'test.resource'
742
+ });
743
+ });
744
+
745
+ it('should propagate API errors', async() => {
746
+ const apiError = { _status: 409, message: 'already exists' };
747
+ const dispatch = jest.fn().mockRejectedValue(apiError);
748
+ const resource = new Resource({
749
+ type: 'test.resource',
750
+ metadata: { name: 'dup', namespace: 'ns' },
751
+ }, {
752
+ getters: {
753
+ schemaFor: () => ({
754
+ linkFor: () => collectionUrl,
755
+ attributes: { namespaced: true },
756
+ })
757
+ },
758
+ dispatch,
759
+ rootGetters: { 'i18n/t': jest.fn() },
760
+ });
761
+
762
+ await expect(resource.dryRunCreate()).rejects.toStrictEqual(apiError);
763
+ });
764
+ });
588
765
  });
@@ -278,7 +278,6 @@ export default {
278
278
  const schemas = state.types[SCHEMA];
279
279
 
280
280
  type = getters.normalizeType(type);
281
-
282
281
  if ( !schemas ) {
283
282
  if ( allowThrow ) {
284
283
  throw new Error("Schemas aren't loaded yet");
@@ -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
 
@@ -79,6 +79,7 @@ export const STATES_ENUM = {
79
79
  BUILDING: 'building',
80
80
  COMPLETED: 'completed',
81
81
  CORDONED: 'cordoned',
82
+ CANCELLED: 'cancelled',
82
83
  COUNT: 'count',
83
84
  CREATED: 'created',
84
85
  CREATING: 'creating',
@@ -207,6 +208,9 @@ export const STATES = {
207
208
  [STATES_ENUM.CORDONED]: {
208
209
  color: 'info', icon: 'tag', label: 'Cordoned', compoundIcon: 'info'
209
210
  },
211
+ [STATES_ENUM.CANCELLED]: {
212
+ color: 'warning', icon: 'error', label: 'Cancelled', compoundIcon: 'warning'
213
+ },
210
214
  [STATES_ENUM.COUNT]: {
211
215
  color: 'success', icon: 'dot-open', label: 'Count', compoundIcon: 'checkmark'
212
216
  },
@@ -507,7 +511,11 @@ export function colorForState(state, isError, isTransitioning) {
507
511
  return `text-${ color }`;
508
512
  }
509
513
 
510
- export function stateDisplay(state) {
514
+ export function simpleColorForState(state, isError = false, isTransitioning = false) {
515
+ return colorForState(state, isError, isTransitioning).replace('text-', '') || 'disabled';
516
+ }
517
+
518
+ export function stateDisplay(state, preserveOriginal = false) {
511
519
  // @TODO use translations
512
520
  const key = (state || 'active').toLowerCase();
513
521
 
@@ -515,6 +523,11 @@ export function stateDisplay(state) {
515
523
  return REMAP_STATE[key];
516
524
  }
517
525
 
526
+ // Preserves the original state name returned by the
527
+ if ( preserveOriginal ) {
528
+ return ucFirst(state);
529
+ }
530
+
518
531
  return key.split(/-/).map(ucFirst).join('-');
519
532
  }
520
533
 
@@ -754,7 +767,7 @@ export default class Resource {
754
767
  }
755
768
 
756
769
  get stateSimpleColor() {
757
- return this.stateColor.replace('text-', '');
770
+ return simpleColorForState(this.state, this.stateObj?.error, this.stateObj?.transitioning);
758
771
  }
759
772
 
760
773
  get stateBackground() {
@@ -1182,6 +1195,46 @@ export default class Resource {
1182
1195
  return this._save(...arguments);
1183
1196
  }
1184
1197
 
1198
+ _collectionUrl() {
1199
+ const schema = this.$getters['schemaFor'](this.type);
1200
+
1201
+ if ( !schema ) {
1202
+ // Schema not found - likely due to lack of permissions to view this resource type
1203
+ throw new Error(`${ this.type }: ${ this.t('validation.createResourceFailed', { type: this.typeDisplay }, true) }`);
1204
+ }
1205
+
1206
+ let url = schema.linkFor('collection');
1207
+
1208
+ if ( schema.attributes && schema.attributes.namespaced && this.metadata && this.metadata.namespace ) {
1209
+ url += `/${ this.metadata.namespace }`;
1210
+ }
1211
+
1212
+ return url;
1213
+ }
1214
+
1215
+ async dryRunCreate(data) {
1216
+ try {
1217
+ const url = this._collectionUrl();
1218
+ const separator = url.includes('?') ? '&' : '?';
1219
+ const body = data || this.cleanForSave(this.toSave() || JSON.parse(JSON.stringify(this)), true);
1220
+
1221
+ return this.$dispatch('request', {
1222
+ opt: {
1223
+ method: 'post',
1224
+ url: `${ url }${ separator }dryRun=All`,
1225
+ data: body,
1226
+ headers: {
1227
+ 'content-type': 'application/json',
1228
+ accept: 'application/json'
1229
+ }
1230
+ },
1231
+ type: this.type
1232
+ });
1233
+ } catch (e) {
1234
+ return Promise.reject(e);
1235
+ }
1236
+ }
1237
+
1185
1238
  /**
1186
1239
  * Remove any unwanted properties from the object that will be saved
1187
1240
  */
@@ -1210,20 +1263,16 @@ export default class Resource {
1210
1263
  if ( this.metadata?.resourceVersion ) {
1211
1264
  this.metadata.resourceVersion = `${ this.metadata.resourceVersion }`;
1212
1265
  }
1213
-
1214
- if ( !opt.url ) {
1215
- if ( forNew ) {
1216
- const schema = this.$getters['schemaFor'](this.type);
1217
- let url = schema.linkFor('collection');
1218
-
1219
- if ( schema.attributes && schema.attributes.namespaced && this.metadata && this.metadata.namespace ) {
1220
- url += `/${ this.metadata.namespace }`;
1266
+ try {
1267
+ if ( !opt.url ) {
1268
+ if ( forNew ) {
1269
+ opt.url = this._collectionUrl();
1270
+ } else {
1271
+ opt.url = this.linkFor('update') || this.linkFor('self');
1221
1272
  }
1222
-
1223
- opt.url = url;
1224
- } else {
1225
- opt.url = this.linkFor('update') || this.linkFor('self');
1226
1273
  }
1274
+ } catch (e) {
1275
+ return Promise.reject(e);
1227
1276
  }
1228
1277
 
1229
1278
  if ( !opt.method ) {
@@ -2073,7 +2122,7 @@ export default class Resource {
2073
2122
 
2074
2123
  if ( r.selector ) {
2075
2124
  // 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, {
2125
+ addObject(out.selectors, {
2077
2126
  type: r.toType,
2078
2127
  namespace: r.toNamespace,
2079
2128
  selector: r.selector
@@ -2083,7 +2132,7 @@ export default class Resource {
2083
2132
  let namespace = r[`${ direction }Namespace`];
2084
2133
  let name = r[`${ direction }Id`];
2085
2134
 
2086
- if ( !namespace && name.includes('/') ) {
2135
+ if ( !namespace && name?.includes('/') ) {
2087
2136
  const idx = name.indexOf('/');
2088
2137
 
2089
2138
  namespace = name.substr(0, idx);
@@ -2245,12 +2294,58 @@ export default class Resource {
2245
2294
  };
2246
2295
  }
2247
2296
 
2297
+ get _resourcesCardRows() {
2298
+ const rows = [];
2299
+ const relationships = this.metadata?.relationships || [];
2300
+
2301
+ const referredToByRels = relationships.filter((r) => r.fromType && r.fromId && !r.selector);
2302
+ const refersToRels = relationships.filter((r) => r.toType && r.toId && !r.selector && !r.fromType);
2303
+
2304
+ if (referredToByRels.length) {
2305
+ rows.push(useResourceCardRowFromRelationships(
2306
+ this.t('component.resource.detail.card.resourcesCard.rows.referredToBy'),
2307
+ referredToByRels,
2308
+ { hash: '#related' }
2309
+ ));
2310
+ }
2311
+
2312
+ if (refersToRels.length) {
2313
+ rows.push(useResourceCardRowFromRelationships(
2314
+ this.t('component.resource.detail.card.resourcesCard.rows.refersTo'),
2315
+ refersToRels,
2316
+ { hash: '#related' }
2317
+ ));
2318
+ }
2319
+
2320
+ return rows;
2321
+ }
2322
+
2323
+ get resourcesCardRows() {
2324
+ return this._resourcesCardRows;
2325
+ }
2326
+
2327
+ get resourcesCard() {
2328
+ const rows = this.resourcesCardRows;
2329
+
2330
+ if (!rows.length) {
2331
+ return null;
2332
+ }
2333
+
2334
+ return {
2335
+ component: markRaw(defineAsyncComponent(() => import('@shell/components/Resource/Detail/Card/StateCard/index.vue'))),
2336
+ props: {
2337
+ title: this.t('component.resource.detail.card.resourcesCard.title'),
2338
+ rows
2339
+ }
2340
+ };
2341
+ }
2342
+
2248
2343
  get _cards() {
2249
2344
  // 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
2345
  return [];
2251
2346
  }
2252
2347
 
2253
2348
  get cards() {
2254
- return this._cards;
2349
+ return [this.resourcesCard, ...this._cards].filter((c) => c);
2255
2350
  }
2256
2351
  }
@@ -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
+ });