@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,48 @@
1
+ import { parseStateFilter } from '@shell/mixins/resource-fetch-api-pagination';
2
+ import { PaginationFilterEquality } from '@shell/types/store/pagination.types';
3
+
4
+ describe('parseStateFilter', () => {
5
+ it('should return null for null input', () => {
6
+ expect(parseStateFilter(null)).toBeNull();
7
+ });
8
+
9
+ it('should return null for undefined input', () => {
10
+ expect(parseStateFilter(undefined)).toBeNull();
11
+ });
12
+
13
+ it('should return null for empty string', () => {
14
+ expect(parseStateFilter('')).toBeNull();
15
+ });
16
+
17
+ it('should parse a single state', () => {
18
+ const result = parseStateFilter('running');
19
+
20
+ expect(result).toHaveLength(1);
21
+ expect(result?.[0].field).toStrictEqual('metadata.state.name');
22
+ expect(result?.[0].value).toStrictEqual('running');
23
+ expect(result?.[0].equality).toStrictEqual(PaginationFilterEquality.IN);
24
+ });
25
+
26
+ it('should parse multiple comma-separated states', () => {
27
+ const result = parseStateFilter('running,active');
28
+
29
+ expect(result).toHaveLength(1);
30
+ expect(result?.[0].field).toStrictEqual('metadata.state.name');
31
+ expect(result?.[0].value).toStrictEqual('running,active');
32
+ expect(result?.[0].equality).toStrictEqual(PaginationFilterEquality.IN);
33
+ });
34
+
35
+ it('should parse three comma-separated states', () => {
36
+ const result = parseStateFilter('running,active,waiting');
37
+
38
+ expect(result).toHaveLength(1);
39
+ expect(result?.[0].value).toStrictEqual('running,active,waiting');
40
+ });
41
+
42
+ it('should ignore empty segments from trailing commas', () => {
43
+ const result = parseStateFilter('running,,active,');
44
+
45
+ expect(result).toHaveLength(1);
46
+ expect(result?.[0].value).toStrictEqual('running,active');
47
+ });
48
+ });
@@ -201,6 +201,9 @@ export default {
201
201
  if (!this.model.accessMode) {
202
202
  this.model.accessMode = 'unrestricted';
203
203
  }
204
+ if (this.model.id === 'github' || this.model.id === 'githubapp') {
205
+ this.model.accessMode = 'restricted';
206
+ }
204
207
  await this.model.doAction('testAndApply', obj, { redirectUnauthorized: false });
205
208
  }
206
209
 
@@ -241,6 +244,10 @@ export default {
241
244
  } else {
242
245
  console.warn(`Unable to find principal marked as 'me'`); // eslint-disable-line no-console
243
246
  }
247
+
248
+ if (!wasEnabled) {
249
+ this.model.accessMode = 'required';
250
+ }
244
251
  }
245
252
  if (wasEnabled && configType === 'oauth') {
246
253
  await this.model.save({ ignoreFields: ['oauthCredential', 'serviceAccountCredential'] } );
package/mixins/chart.js CHANGED
@@ -71,6 +71,8 @@ export default {
71
71
 
72
72
  const selectedVersion = this.targetVersion;
73
73
  const OSs = this.currentCluster?.workerOSs;
74
+ const isRancher = isRancherRepo(this.repo, this.chart);
75
+ const permittedSystemsByVersion = new Map();
74
76
  const out = [];
75
77
 
76
78
  versions.forEach((version) => {
@@ -84,9 +86,10 @@ export default {
84
86
  keywords: version.keywords
85
87
  };
86
88
 
87
- const isRancher = isRancherRepo(this.repo, this.chart);
88
89
  const permittedSystems = getPermittedOSs(version?.annotations, isRancher);
89
90
 
91
+ permittedSystemsByVersion.set(version.version, permittedSystems);
92
+
90
93
  if (permittedSystems.length > 0 && difference(OSs, permittedSystems).length > 0) {
91
94
  nue.disabled = true;
92
95
  }
@@ -118,7 +121,13 @@ export default {
118
121
  const currentVersion = out.find((v) => v.originalVersion === this.currentVersion);
119
122
 
120
123
  if (currentVersion) {
121
- currentVersion.label = this.t('catalog.install.versions.current', { ver: this.currentVersion });
124
+ const permittedSystems = permittedSystemsByVersion.get(currentVersion.originalVersion) || [];
125
+
126
+ if (permittedSystems.length === 1) {
127
+ currentVersion.label = this.t(`catalog.install.versions.current_${ permittedSystems[0] }`, { ver: this.currentVersion });
128
+ } else {
129
+ currentVersion.label = this.t('catalog.install.versions.current', { ver: this.currentVersion });
130
+ }
122
131
  }
123
132
 
124
133
  return out;
@@ -8,20 +8,26 @@ export const AFTER_SAVE_HOOKS = '_afterSaveHooks';
8
8
 
9
9
  export default {
10
10
  methods: {
11
+ registerHook(key, boundFn, name, priority = 99, boundFnContext) {
12
+ this._registerHook(key, boundFn, name, priority, boundFnContext);
13
+ },
14
+
11
15
  registerBeforeHook(boundFn, name, priority = 99, boundFnContext) {
12
- this._registerHook(BEFORE_SAVE_HOOKS, boundFn, name, priority, boundFnContext);
16
+ this.registerHook(BEFORE_SAVE_HOOKS, boundFn, name, priority, boundFnContext);
13
17
  },
14
18
 
15
- unregisterBeforeSaveHook(name) {
16
- this[BEFORE_SAVE_HOOKS] = this[BEFORE_SAVE_HOOKS].filter((hook) => {
17
- // BEFORE_SAVE_HOOKS is an array of objects with keys
18
- // fn, name and priority.
19
+ unregisterHook(key, name) {
20
+ this[key] = (this[key] || []).filter((hook) => {
19
21
  return hook.name !== name;
20
22
  });
21
23
  },
22
24
 
25
+ unregisterBeforeSaveHook(name) {
26
+ this.unregisterHook(BEFORE_SAVE_HOOKS, name);
27
+ },
28
+
23
29
  registerAfterHook(boundFn, name, priority = 99, boundFnContext) {
24
- this._registerHook(AFTER_SAVE_HOOKS, boundFn, name, priority, boundFnContext);
30
+ this.registerHook(AFTER_SAVE_HOOKS, boundFn, name, priority, boundFnContext);
25
31
  },
26
32
 
27
33
  async applyHooks(key, ...args) {
@@ -1,6 +1,6 @@
1
1
  import { _CREATE, _EDIT, _VIEW } from '@shell/config/query-params';
2
2
  import { LAST_NAMESPACE } from '@shell/store/prefs';
3
- import { exceptionToErrorsArray } from '@shell/utils/error';
3
+ import { exceptionToErrorsArray, isDoNotLogError } from '@shell/utils/error';
4
4
  import ChildHook, { BEFORE_SAVE_HOOKS, AFTER_SAVE_HOOKS } from '@shell/mixins/child-hook';
5
5
  import { clear } from '@shell/utils/array';
6
6
  import { DEFAULT_WORKSPACE } from '@shell/config/types';
@@ -184,8 +184,10 @@ export default {
184
184
  } else {
185
185
  this.errors = exceptionToErrorsArray(err);
186
186
  }
187
- // Provide a stack trace for easier debugging of save errors
188
- console.error('CreateEditView mixin failed to save: ', err); // eslint-disable-line no-console
187
+ if (!isDoNotLogError(err)) {
188
+ // Provide a stack trace for easier debugging of save errors
189
+ console.error('CreateEditView mixin failed to save: ', err); // eslint-disable-line no-console
190
+ }
189
191
  buttonDone && buttonDone(false);
190
192
  }
191
193
  },
@@ -5,10 +5,28 @@ import { mapGetters } from 'vuex';
5
5
  import { ResourceListComponentName } from '../components/ResourceList/resource-list.config';
6
6
  import paginationUtils from '@shell/utils/pagination-utils';
7
7
  import debounce from 'lodash/debounce';
8
- import { PaginationParamFilter, PaginationFilterField, PaginationArgs } from '@shell/types/store/pagination.types';
8
+ import { PaginationParamFilter, PaginationFilterField, PaginationFilterEquality, PaginationArgs } from '@shell/types/store/pagination.types';
9
9
  import stevePaginationUtils from '@shell/plugins/steve/steve-pagination-utils';
10
10
  import { STEVE_WATCH_MODE } from '@shell/types/store/subscribe.types';
11
11
 
12
+ export function parseStateFilter(stateFilter) {
13
+ if (!stateFilter) {
14
+ return null;
15
+ }
16
+
17
+ const states = stateFilter.split(',').filter(Boolean);
18
+
19
+ if (!states.length) {
20
+ return null;
21
+ }
22
+
23
+ return [new PaginationFilterField({
24
+ field: 'metadata.state.name',
25
+ value: states.join(','),
26
+ equality: PaginationFilterEquality.IN,
27
+ })];
28
+ }
29
+
12
30
  /**
13
31
  * Companion mixin used with `resource-fetch` for `ResourceList` to determine if the user needs to filter the list by a single namespace
14
32
  */
@@ -75,6 +93,7 @@ export default {
75
93
  const {
76
94
  page, perPage, filter, sort, descending
77
95
  } = event;
96
+ const stateFilters = parseStateFilter(this.$route?.query?.stateFilter) || [];
78
97
  const searchFilters = filter.searchQuery ? filter.searchFields.map((field) => new PaginationFilterField({
79
98
  field,
80
99
  value: filter.searchQuery,
@@ -91,6 +110,7 @@ export default {
91
110
  projectsOrNamespaces: this.requestFilters.projectsOrNamespaces,
92
111
  filters: [
93
112
  new PaginationParamFilter({ fields: searchFilters }),
113
+ new PaginationParamFilter({ fields: stateFilters }),
94
114
  ...this.requestFilters.filters, // Apply the additional filters. these aren't from the user but from ns filtering
95
115
  ]
96
116
  });
@@ -17,6 +17,7 @@ describe('clusterRepo', () => {
17
17
  jest.spyOn(model, 'save').mockImplementation().mockResolvedValue(true);
18
18
  jest.spyOn(model, 'waitForState').mockImplementation().mockResolvedValue(true);
19
19
  jest.spyOn(model, '$dispatch', 'get').mockReturnValue(jest.fn());
20
+ jest.spyOn(model, 't', 'get').mockReturnValue((key: string) => key);
20
21
  });
21
22
 
22
23
  describe('refresh', () => {
@@ -59,6 +60,62 @@ describe('clusterRepo', () => {
59
60
  });
60
61
  });
61
62
 
63
+ describe('defaultRefreshIntervalHours', () => {
64
+ it('returns 24 for OCI repos', () => {
65
+ model.spec.url = 'oci://example.com/chart';
66
+ model.spec.insecurePlainHttp = false;
67
+ expect(model.defaultRefreshIntervalHours).toStrictEqual(24);
68
+ });
69
+
70
+ it('returns 1 for git repos', () => {
71
+ model.spec = { gitRepo: 'https://github.com/example/charts' };
72
+ expect(model.defaultRefreshIntervalHours).toStrictEqual(1);
73
+ });
74
+
75
+ it('returns 1 for helm repos', () => {
76
+ model.spec = { url: 'https://charts.example.com' };
77
+ expect(model.defaultRefreshIntervalHours).toStrictEqual(1);
78
+ });
79
+ });
80
+
81
+ describe('defaultRefreshInterval', () => {
82
+ it('returns seconds for OCI repos (24 hours)', () => {
83
+ model.spec.url = 'oci://example.com/chart';
84
+ model.spec.insecurePlainHttp = false;
85
+ expect(model.defaultRefreshInterval).toStrictEqual(86400);
86
+ });
87
+
88
+ it('returns seconds for non-OCI repos (1 hour)', () => {
89
+ model.spec = { gitRepo: 'https://github.com/example/charts' };
90
+ expect(model.defaultRefreshInterval).toStrictEqual(3600);
91
+ });
92
+ });
93
+
94
+ describe('refreshIntervalDisplay', () => {
95
+ it.each([
96
+ -1,
97
+ -100,
98
+ ])('returns "Disabled" when value is %p', (val) => {
99
+ model.spec.refreshInterval = val;
100
+ expect(model.refreshIntervalDisplay).toStrictEqual('generic.disabled');
101
+ });
102
+
103
+ it('returns formatted duration for positive values', () => {
104
+ model.spec.refreshInterval = 3661;
105
+ expect(model.refreshIntervalDisplay).toStrictEqual('1h 1m 1s');
106
+ });
107
+
108
+ it('returns default formatted duration when not set', () => {
109
+ model.spec = { url: 'https://charts.example.com' };
110
+ expect(model.refreshIntervalDisplay).toStrictEqual('1h');
111
+ });
112
+
113
+ it('returns default formatted duration for OCI when not set', () => {
114
+ model.spec = { url: 'oci://example.com/chart', insecurePlainHttp: false };
115
+ expect(model.refreshIntervalDisplay).toStrictEqual('1d');
116
+ });
117
+ });
118
+
62
119
  describe('refreshBulk', () => {
63
120
  it('calls refresh(false) on all items and then dispatches a single catalog/load with all repoKeys', async() => {
64
121
  const mockItem1 = {
@@ -0,0 +1,144 @@
1
+ import ClusterScan from '@shell/models/compliance.cattle.io.clusterscan';
2
+
3
+ /**
4
+ * Build a ClusterScan model wired to a mocked store `dispatch`.
5
+ * `$dispatch` resolves to `this.$ctx.dispatch`, so injecting it via the
6
+ * constructor context is enough to exercise `_resolveExportMetadata`.
7
+ */
8
+ const makeScan = (dispatch: jest.Mock) => new ClusterScan({}, {
9
+ getters: {},
10
+ rootGetters: {},
11
+ dispatch,
12
+ } as any) as any;
13
+
14
+ const benchmarkWithConfigMap = {
15
+ spec: {
16
+ customBenchmarkConfigMapName: 'metadata-cm',
17
+ customBenchmarkConfigMapNamespace: 'compliance',
18
+ },
19
+ };
20
+
21
+ describe('class: ClusterScan', () => {
22
+ describe('method: _resolveExportMetadata', () => {
23
+ it('returns empty metadata and decorations when the benchmark is undefined', async() => {
24
+ const dispatch = jest.fn();
25
+ const scan = makeScan(dispatch);
26
+
27
+ const result = await scan._resolveExportMetadata(undefined);
28
+
29
+ expect(result).toStrictEqual({ metadata: {}, decorations: {} });
30
+ expect(dispatch).not.toHaveBeenCalled();
31
+ });
32
+
33
+ it('returns empty when the ConfigMap name is missing', async() => {
34
+ const dispatch = jest.fn();
35
+ const scan = makeScan(dispatch);
36
+
37
+ const result = await scan._resolveExportMetadata({ spec: { customBenchmarkConfigMapNamespace: 'compliance' } });
38
+
39
+ expect(result).toStrictEqual({ metadata: {}, decorations: {} });
40
+ expect(dispatch).not.toHaveBeenCalled();
41
+ });
42
+
43
+ it('returns empty when the ConfigMap namespace is missing', async() => {
44
+ const dispatch = jest.fn();
45
+ const scan = makeScan(dispatch);
46
+
47
+ const result = await scan._resolveExportMetadata({ spec: { customBenchmarkConfigMapName: 'metadata-cm' } });
48
+
49
+ expect(result).toStrictEqual({ metadata: {}, decorations: {} });
50
+ expect(dispatch).not.toHaveBeenCalled();
51
+ });
52
+
53
+ it('fetches the ConfigMap by namespace/name from the configmap type', async() => {
54
+ const dispatch = jest.fn().mockResolvedValue({ data: {} });
55
+ const scan = makeScan(dispatch);
56
+
57
+ await scan._resolveExportMetadata(benchmarkWithConfigMap);
58
+
59
+ expect(dispatch).toHaveBeenCalledWith('find', { type: 'configmap', id: 'compliance/metadata-cm' });
60
+ });
61
+
62
+ it('returns empty when the ConfigMap has no metadata.yaml data key', async() => {
63
+ const dispatch = jest.fn().mockResolvedValue({ data: { 'other.yaml': 'title: x' } });
64
+ const scan = makeScan(dispatch);
65
+
66
+ const result = await scan._resolveExportMetadata(benchmarkWithConfigMap);
67
+
68
+ expect(result).toStrictEqual({ metadata: {}, decorations: {} });
69
+ });
70
+
71
+ it('returns empty when the ConfigMap itself is not found', async() => {
72
+ const dispatch = jest.fn().mockResolvedValue(undefined);
73
+ const scan = makeScan(dispatch);
74
+
75
+ const result = await scan._resolveExportMetadata(benchmarkWithConfigMap);
76
+
77
+ expect(result).toStrictEqual({ metadata: {}, decorations: {} });
78
+ });
79
+
80
+ it('splits top-level fields into metadata and the checks map into decorations', async() => {
81
+ const yaml = [
82
+ 'title: CIS Kubernetes Benchmark',
83
+ 'description: A CIS profile',
84
+ 'referenceType: cis',
85
+ 'checks:',
86
+ ' "5.1.1":',
87
+ ' ruleId: CIS-5.1.1-rule',
88
+ ' severity: high',
89
+ ' idents:',
90
+ ' - system: https://www.cisecurity.org/controls/',
91
+ ' value: CIS-CSC-3',
92
+ ' "5.1.2":',
93
+ ' ruleId: CIS-5.1.2-rule',
94
+ ].join('\n');
95
+ const dispatch = jest.fn().mockResolvedValue({ data: { 'metadata.yaml': yaml } });
96
+ const scan = makeScan(dispatch);
97
+
98
+ const result = await scan._resolveExportMetadata(benchmarkWithConfigMap);
99
+
100
+ expect(result.metadata).toStrictEqual({
101
+ title: 'CIS Kubernetes Benchmark',
102
+ description: 'A CIS profile',
103
+ referenceType: 'cis',
104
+ });
105
+ expect(result.decorations).toStrictEqual({
106
+ '5.1.1': {
107
+ ruleId: 'CIS-5.1.1-rule',
108
+ severity: 'high',
109
+ idents: [{ system: 'https://www.cisecurity.org/controls/', value: 'CIS-CSC-3' }],
110
+ },
111
+ '5.1.2': { ruleId: 'CIS-5.1.2-rule' },
112
+ });
113
+ });
114
+
115
+ it('returns empty decorations when metadata.yaml has no checks map', async() => {
116
+ const yaml = ['title: STIG', 'source: disa'].join('\n');
117
+ const dispatch = jest.fn().mockResolvedValue({ data: { 'metadata.yaml': yaml } });
118
+ const scan = makeScan(dispatch);
119
+
120
+ const result = await scan._resolveExportMetadata(benchmarkWithConfigMap);
121
+
122
+ expect(result.metadata).toStrictEqual({ title: 'STIG', source: 'disa' });
123
+ expect(result.decorations).toStrictEqual({});
124
+ });
125
+
126
+ it('returns empty when the ConfigMap lookup rejects', async() => {
127
+ const dispatch = jest.fn().mockRejectedValue(new Error('not found'));
128
+ const scan = makeScan(dispatch);
129
+
130
+ const result = await scan._resolveExportMetadata(benchmarkWithConfigMap);
131
+
132
+ expect(result).toStrictEqual({ metadata: {}, decorations: {} });
133
+ });
134
+
135
+ it('returns empty when metadata.yaml is malformed YAML', async() => {
136
+ const dispatch = jest.fn().mockResolvedValue({ data: { 'metadata.yaml': 'title: "unterminated' } });
137
+ const scan = makeScan(dispatch);
138
+
139
+ const result = await scan._resolveExportMetadata(benchmarkWithConfigMap);
140
+
141
+ expect(result).toStrictEqual({ metadata: {}, decorations: {} });
142
+ });
143
+ });
144
+ });
@@ -0,0 +1,175 @@
1
+ import FleetApplication from '@shell/models/fleet-application.js';
2
+
3
+ describe('class FleetApplication', () => {
4
+ afterEach(() => {
5
+ jest.restoreAllMocks();
6
+ });
7
+
8
+ describe('applicationType', () => {
9
+ it('should return the kind property', () => {
10
+ const instance = new FleetApplication({ kind: 'GitRepo' });
11
+
12
+ expect(instance.applicationType).toStrictEqual('GitRepo');
13
+ });
14
+ });
15
+
16
+ describe('targetClusters', () => {
17
+ function createFleetApplication(targets: any[] | undefined, clusters: any[], workspaceId = 'fleet-default', groups: any[] = []) {
18
+ const workspace = {
19
+ id: workspaceId,
20
+ clusters,
21
+ clusterGroups: groups,
22
+ };
23
+
24
+ jest.spyOn(FleetApplication.prototype, '$getters', 'get').mockReturnValue({ byId: () => workspace });
25
+
26
+ return new FleetApplication({
27
+ metadata: { namespace: workspaceId },
28
+ spec: { targets },
29
+ });
30
+ }
31
+
32
+ it.each([
33
+ [
34
+ 'metadata.name',
35
+ [{ clusterName: 'c-m-abc123' }],
36
+ [{
37
+ id: 'fleet-default/c-m-abc123', metadata: { name: 'c-m-abc123' }, nameDisplay: 'my-cluster'
38
+ }],
39
+ [{
40
+ id: 'fleet-default/c-m-abc123', metadata: { name: 'c-m-abc123' }, nameDisplay: 'my-cluster'
41
+ }],
42
+ ],
43
+ [
44
+ 'nameDisplay when metadata.name does not match',
45
+ [{ clusterName: 'my-display-name' }],
46
+ [{
47
+ id: 'fleet-default/c-m-abc123', metadata: { name: 'c-m-abc123' }, nameDisplay: 'my-display-name'
48
+ }],
49
+ [{
50
+ id: 'fleet-default/c-m-abc123', metadata: { name: 'c-m-abc123' }, nameDisplay: 'my-display-name'
51
+ }],
52
+ ],
53
+ ])('should find cluster by %s', (_label, targets, clusters, expected) => {
54
+ const app = createFleetApplication(targets, clusters);
55
+
56
+ expect(app.targetClusters).toStrictEqual(expected);
57
+ });
58
+
59
+ it('should prefer metadata.name match over nameDisplay match', () => {
60
+ const clusters = [
61
+ {
62
+ id: 'fleet-default/exact-match',
63
+ metadata: { name: 'exact-match' },
64
+ nameDisplay: 'display-a',
65
+ },
66
+ {
67
+ id: 'fleet-default/c-m-other',
68
+ metadata: { name: 'c-m-other' },
69
+ nameDisplay: 'exact-match',
70
+ }
71
+ ];
72
+
73
+ const app = createFleetApplication([{ clusterName: 'exact-match' }], clusters);
74
+
75
+ expect(app.targetClusters).toStrictEqual([clusters[0]]);
76
+ });
77
+
78
+ it('should return empty array when no cluster matches by name or nameDisplay', () => {
79
+ const clusters = [
80
+ {
81
+ id: 'fleet-default/c-m-abc123',
82
+ metadata: { name: 'c-m-abc123' },
83
+ nameDisplay: 'my-cluster',
84
+ }
85
+ ];
86
+
87
+ const app = createFleetApplication([{ clusterName: 'non-existent' }], clusters);
88
+
89
+ expect(app.targetClusters).toStrictEqual([]);
90
+ });
91
+
92
+ it('should handle multiple targets with mixed name and nameDisplay matches', () => {
93
+ const clusters = [
94
+ {
95
+ id: 'fleet-default/c-m-abc123',
96
+ metadata: { name: 'c-m-abc123' },
97
+ nameDisplay: 'cluster-alpha',
98
+ },
99
+ {
100
+ id: 'fleet-default/c-m-def456',
101
+ metadata: { name: 'c-m-def456' },
102
+ nameDisplay: 'cluster-beta',
103
+ }
104
+ ];
105
+
106
+ const targets = [
107
+ { clusterName: 'c-m-abc123' },
108
+ { clusterName: 'cluster-beta' },
109
+ ];
110
+
111
+ const app = createFleetApplication(targets, clusters);
112
+
113
+ expect(app.targetClusters).toStrictEqual([clusters[0], clusters[1]]);
114
+ });
115
+
116
+ it('should return empty array when targets is empty', () => {
117
+ const app = createFleetApplication([], []);
118
+
119
+ expect(app.targetClusters).toStrictEqual([]);
120
+ });
121
+
122
+ it('should return empty array when targets is undefined', () => {
123
+ const app = createFleetApplication(undefined, []);
124
+
125
+ expect(app.targetClusters).toStrictEqual([]);
126
+ });
127
+
128
+ it('should return empty array when workspace has no clusters', () => {
129
+ const app = createFleetApplication([{ clusterName: 'any-name' }], []);
130
+
131
+ expect(app.targetClusters).toStrictEqual([]);
132
+ });
133
+
134
+ it('should handle cluster with undefined nameDisplay gracefully', () => {
135
+ const clusters = [
136
+ {
137
+ id: 'fleet-default/c-m-abc123',
138
+ metadata: { name: 'c-m-abc123' },
139
+ nameDisplay: undefined,
140
+ }
141
+ ];
142
+
143
+ const app = createFleetApplication([{ clusterName: 'c-m-abc123' }], clusters);
144
+
145
+ expect(app.targetClusters).toStrictEqual([clusters[0]]);
146
+ });
147
+
148
+ it('should return local cluster targets when workspace is fleet-local', () => {
149
+ const localTargetClusters = [
150
+ {
151
+ id: 'fleet-local/local',
152
+ metadata: { name: 'local' },
153
+ nameDisplay: 'local',
154
+ }
155
+ ];
156
+
157
+ const groups = [
158
+ {
159
+ id: 'fleet-local/default',
160
+ targetClusters: localTargetClusters,
161
+ }
162
+ ];
163
+
164
+ const app = createFleetApplication([], [], 'fleet-local', groups);
165
+
166
+ expect(app.targetClusters).toStrictEqual(localTargetClusters);
167
+ });
168
+
169
+ it('should return empty array when workspace is fleet-local and default group is missing', () => {
170
+ const app = createFleetApplication([], [], 'fleet-local', []);
171
+
172
+ expect(app.targetClusters).toStrictEqual([]);
173
+ });
174
+ });
175
+ });