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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/assets/styles/global/_layout.scss +4 -0
  2. package/assets/translations/en-us.yaml +144 -41
  3. package/assets/translations/zh-hans.yaml +1 -7
  4. package/chart/monitoring/ClusterSelector.vue +0 -21
  5. package/chart/monitoring/prometheus/index.vue +6 -3
  6. package/components/CruResource.vue +161 -14
  7. package/components/ExplorerMembers.vue +8 -4
  8. package/components/ExplorerProjectsNamespaces.vue +10 -6
  9. package/components/GrowlManager.vue +4 -0
  10. package/components/MgmtNodeList.vue +184 -0
  11. package/components/Resource/Detail/Card/StateCard/__tests__/composables.test.ts +90 -1
  12. package/components/Resource/Detail/Card/StateCard/composables.ts +57 -87
  13. package/components/Resource/Detail/Card/StatusCard/__tests__/StatusCard.test.ts +61 -0
  14. package/components/Resource/Detail/Card/StatusCard/index.vue +61 -15
  15. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +2 -0
  16. package/components/Resource/Detail/Metadata/KeyValue.vue +5 -2
  17. package/components/Resource/Detail/Metadata/KeyValueRow.vue +2 -6
  18. package/components/ResourceDetail/index.vue +1 -1
  19. package/components/ResourceList/Masthead.vue +7 -1
  20. package/components/ResourceList/index.vue +82 -1
  21. package/components/RichTranslation.vue +5 -2
  22. package/components/Setting.vue +1 -0
  23. package/components/SubtleLink.vue +31 -6
  24. package/components/Tabbed/Tab.vue +29 -3
  25. package/components/Tabbed/index.vue +25 -3
  26. package/components/TableOfContents/TableOfContents.vue +109 -0
  27. package/components/TableOfContents/composables.ts +258 -0
  28. package/components/Window/ContainerShell.vue +21 -11
  29. package/components/Window/__tests__/ContainerShell.test.ts +107 -37
  30. package/components/Wizard.vue +9 -4
  31. package/components/fleet/AppCoChartGrid.vue +401 -0
  32. package/components/fleet/AppCoEmptyState.vue +127 -0
  33. package/components/fleet/AppCoPageHeader.vue +119 -0
  34. package/components/fleet/AppCoVersionSelect.vue +70 -0
  35. package/components/fleet/FleetClusterTargets/ClusterSelectionFields.vue +217 -0
  36. package/components/fleet/FleetClusterTargets/TargetsList.vue +123 -35
  37. package/components/fleet/FleetClusterTargets/index.vue +189 -146
  38. package/components/fleet/FleetIntro.vue +7 -3
  39. package/components/fleet/FleetNoWorkspaces.vue +7 -3
  40. package/components/fleet/FleetSecretSelector.vue +5 -3
  41. package/components/fleet/FleetValuesFrom.vue +8 -2
  42. package/components/fleet/GitRepoTargetTab.vue +0 -2
  43. package/components/fleet/HelmOpAdvancedTab.vue +19 -53
  44. package/components/fleet/HelmOpAppCoConfigTab.vue +593 -0
  45. package/components/fleet/HelmOpAppCoResourcesSection.vue +162 -0
  46. package/components/fleet/HelmOpResourcesSection.vue +82 -0
  47. package/components/fleet/HelmOpTargetOptionsSection.vue +89 -0
  48. package/components/fleet/HelmOpTargetTab.vue +64 -60
  49. package/components/fleet/HelmOpValuesTab.vue +129 -105
  50. package/components/fleet/__tests__/AppCoEmptyState.test.ts +71 -0
  51. package/components/fleet/__tests__/AppCoVersionSelect.test.ts +36 -0
  52. package/components/fleet/__tests__/ClusterSelectionFields.test.ts +62 -0
  53. package/components/fleet/__tests__/FleetClusterTargets.test.ts +253 -0
  54. package/components/fleet/__tests__/FleetSecretSelector.test.ts +16 -0
  55. package/components/fleet/__tests__/FleetValuesFrom.test.ts +44 -0
  56. package/components/fleet/__tests__/HelmOpAppCoConfigTab.test.ts +59 -0
  57. package/components/fleet/__tests__/HelmOpAppCoResourcesSection.test.ts +62 -0
  58. package/components/fleet/__tests__/HelmOpResourcesSection.test.ts +43 -0
  59. package/components/fleet/__tests__/HelmOpTargetOptionsSection.test.ts +34 -0
  60. package/components/fleet/__tests__/HelmOpValuesTab.test.ts +39 -0
  61. package/components/fleet/__tests__/__snapshots__/AppCoEmptyState.test.ts.snap +97 -0
  62. package/components/fleet/__tests__/__snapshots__/AppCoVersionSelect.test.ts.snap +30 -0
  63. package/components/fleet/__tests__/__snapshots__/ClusterSelectionFields.test.ts.snap +209 -0
  64. package/components/fleet/__tests__/__snapshots__/HelmOpTargetOptionsSection.test.ts.snap +140 -0
  65. package/components/fleet/dashboard/Empty.vue +8 -4
  66. package/components/fleet/dashboard/ResourceCard.vue +28 -0
  67. package/components/fleet/dashboard/ResourceDetails.vue +28 -0
  68. package/components/fleet/dashboard/__tests__/ResourceCard.test.ts +87 -0
  69. package/components/form/ArrayList.vue +61 -4
  70. package/components/form/KeyValue.vue +23 -2
  71. package/components/form/LabeledSelect.vue +39 -1
  72. package/components/form/Labels.vue +22 -3
  73. package/components/form/NameNsDescription.vue +13 -5
  74. package/components/form/ResourceTabs/index.vue +1 -0
  75. package/components/form/__tests__/NameNsDescription.test.ts +75 -0
  76. package/components/formatter/InternalExternalIP.vue +10 -4
  77. package/components/formatter/ServiceTargets.vue +26 -7
  78. package/components/formatter/__tests__/InternalExternalIP.test.ts +132 -0
  79. package/components/formatter/__tests__/ServiceTargets.test.ts +412 -0
  80. package/components/nav/Header.vue +4 -0
  81. package/components/nav/TopLevelMenu.vue +7 -2
  82. package/components/nav/__tests__/Header.test.ts +15 -0
  83. package/components/nav/__tests__/TopLevelMenu.test.ts +120 -2
  84. package/components/templates/default.vue +9 -4
  85. package/components/templates/home.vue +9 -4
  86. package/components/templates/plain.vue +9 -4
  87. package/composables/useHelmOpResources.test.ts +56 -0
  88. package/composables/useHelmOpResources.ts +32 -0
  89. package/composables/useStateColor.test.ts +325 -0
  90. package/composables/useStateColor.ts +128 -0
  91. package/config/home-links.js +1 -1
  92. package/config/labels-annotations.js +1 -0
  93. package/config/product/explorer.js +17 -4
  94. package/config/product/manager.js +2 -0
  95. package/config/router/index.js +16 -0
  96. package/config/router/navigation-guards/__tests__/authentication.test.ts +130 -0
  97. package/config/router/navigation-guards/authentication.js +10 -4
  98. package/config/router/routes.js +20 -6
  99. package/config/settings.ts +0 -2
  100. package/config/table-headers.js +3 -4
  101. package/config/types.js +9 -0
  102. package/core/plugin-products-base.ts +3 -3
  103. package/core/plugin-types.ts +83 -30
  104. package/core/plugin.ts +3 -0
  105. package/core/types-provisioning.ts +34 -1
  106. package/core/types.ts +15 -2
  107. package/detail/__tests__/provisioning.cattle.io.cluster.test.ts +114 -0
  108. package/detail/__tests__/workload.test.ts +3 -152
  109. package/detail/catalog.cattle.io.clusterrepo.vue +1 -1
  110. package/detail/provisioning.cattle.io.cluster.vue +30 -4
  111. package/detail/workload/index.vue +12 -55
  112. package/edit/__tests__/catalog.cattle.io.clusterrepo.test.ts +248 -0
  113. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +105 -0
  114. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +6 -0
  115. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/index.test.ts.snap +1 -0
  116. package/edit/auth/__tests__/azuread.test.ts +34 -9
  117. package/edit/auth/__tests__/github.test.ts +234 -0
  118. package/edit/auth/__tests__/oidc.test.ts +26 -6
  119. package/edit/auth/__tests__/saml.test.ts +196 -0
  120. package/edit/auth/azuread.vue +128 -95
  121. package/edit/auth/github.vue +72 -13
  122. package/edit/auth/ldap/__tests__/index.test.ts +206 -0
  123. package/edit/auth/ldap/config.vue +8 -0
  124. package/edit/auth/ldap/index.vue +75 -1
  125. package/edit/auth/oidc.vue +119 -73
  126. package/edit/auth/saml.vue +76 -12
  127. package/edit/catalog.cattle.io.clusterrepo.vue +140 -32
  128. package/edit/fleet.cattle.io.helmop.vue +491 -136
  129. package/edit/management.cattle.io.user.vue +5 -2
  130. package/edit/provisioning.cattle.io.cluster/rke2.vue +84 -10
  131. package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +11 -0
  132. package/list/group.principal.vue +5 -4
  133. package/list/harvesterhci.io.management.cluster.vue +8 -9
  134. package/list/management.cattle.io.user.vue +12 -9
  135. package/list/provisioning.cattle.io.cluster.vue +16 -10
  136. package/mixins/__tests__/auth-config.test.ts +90 -0
  137. package/mixins/__tests__/chart.test.ts +94 -0
  138. package/mixins/__tests__/resource-fetch-api-pagination.test.ts +48 -0
  139. package/mixins/auth-config.js +7 -0
  140. package/mixins/chart.js +11 -2
  141. package/mixins/child-hook.js +12 -6
  142. package/mixins/create-edit-view/impl.js +5 -3
  143. package/mixins/resource-fetch-api-pagination.js +21 -1
  144. package/models/__tests__/catalog.cattle.io.clusterrepo.test.ts +57 -0
  145. package/models/__tests__/compliance.cattle.io.clusterscan.test.ts +144 -0
  146. package/models/__tests__/fleet-application.test.ts +175 -0
  147. package/models/__tests__/fleet.cattle.io.bundle.test.ts +169 -0
  148. package/models/__tests__/fleet.cattle.io.helmop.test.ts +84 -0
  149. package/models/__tests__/management.cattle.io.node.ts +22 -0
  150. package/models/__tests__/namespace.test.ts +36 -0
  151. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +49 -0
  152. package/models/__tests__/workload.test.ts +401 -26
  153. package/models/catalog.cattle.io.clusterrepo.js +28 -4
  154. package/models/compliance.cattle.io.clusterscan.js +39 -4
  155. package/models/fleet-application.js +4 -0
  156. package/models/fleet.cattle.io.helmop.js +20 -1
  157. package/models/management.cattle.io.cluster.js +18 -2
  158. package/models/management.cattle.io.node.js +44 -3
  159. package/models/namespace.js +1 -1
  160. package/models/pod.js +33 -1
  161. package/models/provisioning.cattle.io.cluster.js +5 -5
  162. package/models/workload.js +108 -13
  163. package/models/workload.service.js +5 -0
  164. package/package.json +14 -13
  165. package/pages/about.vue +5 -6
  166. package/pages/auth/login.vue +0 -35
  167. package/pages/auth/setup.vue +11 -0
  168. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +2 -2
  169. package/pages/c/_cluster/apps/charts/AppChartCardSubHeader.vue +10 -1
  170. package/pages/c/_cluster/apps/charts/__tests__/index.test.ts +93 -0
  171. package/pages/c/_cluster/apps/charts/chart.vue +2 -1
  172. package/pages/c/_cluster/apps/charts/index.vue +48 -10
  173. package/pages/c/_cluster/apps/charts/install.vue +122 -116
  174. package/pages/c/_cluster/auth/roles/index.vue +5 -4
  175. package/pages/c/_cluster/explorer/workload-dashboard/ByNamespaceSection.vue +31 -0
  176. package/pages/c/_cluster/explorer/workload-dashboard/ByStateSection.vue +138 -0
  177. package/pages/c/_cluster/explorer/workload-dashboard/ByTypeSection.vue +30 -0
  178. package/pages/c/_cluster/explorer/workload-dashboard/WorkloadCard.vue +155 -0
  179. package/pages/c/_cluster/explorer/workload-dashboard/WorkloadNamespaceCard.vue +142 -0
  180. package/pages/c/_cluster/explorer/workload-dashboard/WorkloadTypeCard.vue +159 -0
  181. package/pages/c/_cluster/explorer/workload-dashboard/__tests__/composable.test.ts +561 -0
  182. package/pages/c/_cluster/explorer/workload-dashboard/composable.ts +440 -0
  183. package/pages/c/_cluster/explorer/workload-dashboard/index.vue +187 -0
  184. package/pages/c/_cluster/explorer/workload-dashboard/types.ts +80 -0
  185. package/pages/c/_cluster/fleet/application/create.vue +187 -136
  186. package/pages/c/_cluster/fleet/application/index.vue +5 -3
  187. package/pages/c/_cluster/fleet/application/suse-app-collection/ChartDetailBody.vue +338 -0
  188. package/pages/c/_cluster/fleet/application/suse-app-collection/ChartDetailHeader.vue +121 -0
  189. package/pages/c/_cluster/fleet/application/suse-app-collection/chart.vue +369 -0
  190. package/pages/c/_cluster/fleet/application/suse-app-collection/charts.vue +248 -0
  191. package/pages/c/_cluster/fleet/application/suse-app-collection/credentials.vue +310 -0
  192. package/pages/c/_cluster/fleet/index.vue +2 -2
  193. package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +96 -0
  194. package/pages/c/_cluster/uiplugins/index.vue +15 -0
  195. package/pages/fail-whale.vue +16 -11
  196. package/pages/home.vue +16 -46
  197. package/plugins/clean-html.d.ts +9 -0
  198. package/plugins/dashboard-store/__tests__/resource-class.test.ts +93 -0
  199. package/plugins/dashboard-store/resource-class.js +62 -7
  200. package/plugins/steve/__tests__/actions.test.ts +212 -0
  201. package/plugins/steve/actions.js +96 -0
  202. package/plugins/steve/steve-pagination-utils.ts +1 -1
  203. package/rancher-components/Accordion/Accordion.vue +53 -9
  204. package/rancher-components/Form/Checkbox/Checkbox.vue +14 -0
  205. package/rancher-components/Form/Radio/RadioButton.vue +17 -1
  206. package/rancher-components/Form/Radio/RadioGroup.vue +10 -0
  207. package/rancher-components/Pill/RcTag/RcTag.vue +3 -2
  208. package/rancher-components/RcButton/RcButton.test.ts +103 -0
  209. package/rancher-components/RcButton/RcButton.vue +94 -15
  210. package/rancher-components/RcButton/types.ts +3 -0
  211. package/rancher-components/RcItemCard/RcItemCard.test.ts +18 -0
  212. package/rancher-components/RcItemCard/RcItemCard.vue +2 -2
  213. package/rancher-components/RcSection/RcSection.vue +28 -3
  214. package/scripts/extension/helm/package/Dockerfile +1 -1
  215. package/scripts/test-plugins-build.sh +2 -1
  216. package/store/__tests__/notifications.test.ts +434 -0
  217. package/store/catalog.js +57 -0
  218. package/store/plugins.js +7 -4
  219. package/types/components/buttonGroup.ts +5 -0
  220. package/types/shell/index.d.ts +104 -70
  221. package/utils/__tests__/auth.test.ts +273 -0
  222. package/utils/__tests__/computed.test.ts +193 -0
  223. package/utils/__tests__/cspAdaptor.test.ts +163 -0
  224. package/utils/__tests__/dom.test.ts +81 -0
  225. package/utils/__tests__/duration.test.ts +37 -1
  226. package/utils/__tests__/dynamic-importer.test.ts +102 -0
  227. package/utils/__tests__/fleet-appco.test.ts +312 -0
  228. package/utils/__tests__/monitoring.test.ts +130 -0
  229. package/utils/__tests__/object.test.ts +22 -0
  230. package/utils/__tests__/platform.test.ts +91 -0
  231. package/utils/__tests__/position.test.ts +237 -0
  232. package/utils/__tests__/provider.test.ts +51 -1
  233. package/utils/__tests__/queue.test.ts +232 -0
  234. package/utils/__tests__/release-notes.test.ts +221 -0
  235. package/utils/__tests__/router.test.js +254 -1
  236. package/utils/__tests__/select.test.ts +208 -0
  237. package/utils/__tests__/time.test.ts +265 -1
  238. package/utils/__tests__/title.test.ts +47 -0
  239. package/utils/__tests__/width.test.ts +53 -0
  240. package/utils/__tests__/window.test.ts +158 -0
  241. package/utils/__tests__/xccdf.test.ts +126 -6
  242. package/utils/crypto/__tests__/browserHashUtils.test.ts +98 -0
  243. package/utils/crypto/__tests__/index.test.ts +144 -0
  244. package/utils/duration.ts +104 -0
  245. package/utils/dynamic-content/__tests__/notification-handler.test.ts +196 -0
  246. package/utils/dynamic-content/info.ts +2 -1
  247. package/utils/error.js +13 -0
  248. package/utils/fleet-appco.ts +323 -0
  249. package/utils/object.js +22 -2
  250. package/utils/provider.ts +12 -0
  251. package/utils/validators/__tests__/container-images.test.ts +104 -0
  252. package/utils/validators/__tests__/flow-output.test.ts +91 -0
  253. package/utils/validators/__tests__/logging-outputs.test.ts +58 -0
  254. package/utils/validators/__tests__/monitoring-route.test.ts +119 -0
  255. package/utils/xccdf.ts +39 -42
  256. package/vue.config.js +1 -1
  257. package/pages/support/index.vue +0 -264
  258. package/utils/duration.js +0 -43
@@ -1,4 +1,4 @@
1
- import { toMilliseconds, toSeconds } from '@shell/utils/duration';
1
+ import { toMilliseconds, toSeconds, secondsToLargestUnit, formatDuration } from '@shell/utils/duration';
2
2
 
3
3
  describe('toMilliseconds', () => {
4
4
  describe('falsy input', () => {
@@ -138,3 +138,39 @@ describe('toSeconds', () => {
138
138
  expect(toSeconds(input)).toStrictEqual(expected);
139
139
  });
140
140
  });
141
+
142
+ describe('function: secondsToLargestUnit', () => {
143
+ it.each([
144
+ [0, { value: 0, unit: 1 }],
145
+ [-1, { value: -1, unit: 1 }],
146
+ [-86400, { value: -86400, unit: 1 }],
147
+ [86400, { value: 1, unit: 86400 }],
148
+ [172800, { value: 2, unit: 86400 }],
149
+ [3600, { value: 1, unit: 3600 }],
150
+ [7200, { value: 2, unit: 3600 }],
151
+ [120, { value: 2, unit: 60 }],
152
+ [300, { value: 5, unit: 60 }],
153
+ [45, { value: 45, unit: 1 }],
154
+ [90, { value: 90, unit: 1 }],
155
+ [1, { value: 1, unit: 1 }],
156
+ ])('given %p seconds, returns %p', (seconds, expected) => {
157
+ expect(secondsToLargestUnit(seconds)).toStrictEqual(expected);
158
+ });
159
+ });
160
+
161
+ describe('function: formatDuration', () => {
162
+ it.each([
163
+ [0, '0s'],
164
+ [-1, '0s'],
165
+ [1, '1s'],
166
+ [60, '1m'],
167
+ [61, '1m 1s'],
168
+ [3600, '1h'],
169
+ [3661, '1h 1m 1s'],
170
+ [86400, '1d'],
171
+ [90061, '1d 1h 1m 1s'],
172
+ [100000, '1d 3h 46m 40s'],
173
+ ])('given %p seconds, returns %p', (seconds, expected) => {
174
+ expect(formatDuration(seconds)).toStrictEqual(expected);
175
+ });
176
+ });
@@ -0,0 +1,102 @@
1
+ import {
2
+ importCloudCredential,
3
+ importMachineConfig,
4
+ importLogin,
5
+ importChart,
6
+ importList,
7
+ importDetail,
8
+ importEdit,
9
+ importDialog,
10
+ importDrawer,
11
+ importWindowComponent,
12
+ loadProduct,
13
+ loadTranslation,
14
+ } from '@shell/utils/dynamic-importer';
15
+ import { defineAsyncComponent } from 'vue';
16
+
17
+ jest.mock('vue', () => ({ defineAsyncComponent: jest.fn(() => 'mockAsyncComponent') }));
18
+
19
+ const ASYNC_COMPONENT_IMPORTERS = [
20
+ {
21
+ desc: 'importCloudCredential',
22
+ fn: importCloudCredential,
23
+ },
24
+ {
25
+ desc: 'importMachineConfig',
26
+ fn: importMachineConfig,
27
+ },
28
+ {
29
+ desc: 'importLogin',
30
+ fn: importLogin,
31
+ },
32
+ {
33
+ desc: 'importChart',
34
+ fn: importChart,
35
+ },
36
+ {
37
+ desc: 'importList',
38
+ fn: importList,
39
+ },
40
+ {
41
+ desc: 'importDetail',
42
+ fn: importDetail,
43
+ },
44
+ {
45
+ desc: 'importEdit',
46
+ fn: importEdit,
47
+ },
48
+ {
49
+ desc: 'importDialog',
50
+ fn: importDialog,
51
+ },
52
+ {
53
+ desc: 'importDrawer',
54
+ fn: importDrawer,
55
+ },
56
+ {
57
+ desc: 'importWindowComponent',
58
+ fn: importWindowComponent,
59
+ },
60
+ ];
61
+
62
+ const DIRECT_IMPORTERS = [
63
+ {
64
+ desc: 'loadProduct',
65
+ fn: loadProduct,
66
+ },
67
+ {
68
+ desc: 'loadTranslation',
69
+ fn: loadTranslation,
70
+ },
71
+ ];
72
+
73
+ const ALL_GUARDED_IMPORTERS = [...ASYNC_COMPONENT_IMPORTERS, ...DIRECT_IMPORTERS];
74
+
75
+ describe('dynamic-importer', () => {
76
+ describe('name guard — throws when name is falsy', () => {
77
+ it.each(ALL_GUARDED_IMPORTERS)('$desc throws for null', ({ fn }) => {
78
+ expect(() => fn(null as any)).toThrow('Name required');
79
+ });
80
+
81
+ it.each(ALL_GUARDED_IMPORTERS)('$desc throws for undefined', ({ fn }) => {
82
+ expect(() => fn(undefined as any)).toThrow('Name required');
83
+ });
84
+
85
+ it.each(ALL_GUARDED_IMPORTERS)('$desc throws for empty string', ({ fn }) => {
86
+ expect(() => fn('')).toThrow('Name required');
87
+ });
88
+ });
89
+
90
+ describe('async component importers — delegates to defineAsyncComponent', () => {
91
+ beforeEach(() => {
92
+ (defineAsyncComponent as jest.Mock).mockClear();
93
+ });
94
+
95
+ it.each(ASYNC_COMPONENT_IMPORTERS)('$desc calls defineAsyncComponent with a loader function', ({ fn }) => {
96
+ const result = fn('some-name');
97
+
98
+ expect(defineAsyncComponent).toHaveBeenCalledWith(expect.any(Function));
99
+ expect(result).toStrictEqual('mockAsyncComponent');
100
+ });
101
+ });
102
+ });
@@ -0,0 +1,312 @@
1
+ import {
2
+ deriveRepoName, fetchAppCoCharts, ensureAppCoResources, ensureAppCoImagePullSecret, getDownstreamResourcesDocsUrl, FLEET_DOWNSTREAM_RESOURCES_DOCS_FALLBACK_URL
3
+ } from '@shell/utils/fleet-appco';
4
+ import { SECRET, CATALOG as CATALOG_TYPES } from '@shell/config/types';
5
+ import { SECRET_TYPES } from '@shell/config/secret';
6
+
7
+ /**
8
+ * Build a fake ClusterRepo resource. Its `waitForTestFn` emulates the real
9
+ * resource-class helper by synchronously polling the provided test function
10
+ * until it returns truthy (or "timing out" after a few attempts).
11
+ */
12
+ const buildRepo = (overrides: Record<string, any> = {}) => ({
13
+ metadata: { state: {} },
14
+ status: { conditions: [] },
15
+ stateDisplay: 'Active',
16
+ stateBackground: 'bg-success',
17
+ followLink: jest.fn().mockResolvedValue({ entries: { chartA: [{ name: 'chartA' }] } }),
18
+ waitForTestFn: (fn: () => boolean) => new Promise<void>((resolve, reject) => {
19
+ for (let attempt = 0; attempt < 5; attempt++) {
20
+ if (fn()) {
21
+ return resolve();
22
+ }
23
+ }
24
+
25
+ reject(new Error('timed out'));
26
+ }),
27
+ ...overrides,
28
+ });
29
+
30
+ const buildStore = (repo: any, { findRejects = false, findRejectStatus = 404 }: { findRejects?: boolean; findRejectStatus?: number } = {}) => ({
31
+ dispatch: jest.fn((action: string) => {
32
+ if (action.endsWith('/find')) {
33
+ return findRejects ? Promise.reject(Object.assign(new Error('not found'), { status: findRejectStatus })) : Promise.resolve(repo);
34
+ }
35
+
36
+ return Promise.resolve();
37
+ }),
38
+ getters: new Proxy({}, {
39
+ get: (_target, prop) => {
40
+ if (String(prop).endsWith('/byId')) {
41
+ return () => repo;
42
+ }
43
+
44
+ return undefined;
45
+ }
46
+ }),
47
+ });
48
+
49
+ describe('fleet-appco utils', () => {
50
+ describe('deriveRepoName', () => {
51
+ it.each([
52
+ ['my-auth-secret', 'my-repo-secret'],
53
+ ['auth-token', 'repo-token'],
54
+ ['some-auth', 'some-repo'],
55
+ ])('should replace "auth" with "repo" in %s', (input, expected) => {
56
+ expect(deriveRepoName(input)).toStrictEqual(expected);
57
+ });
58
+
59
+ it('should return empty string for empty input', () => {
60
+ expect(deriveRepoName('')).toStrictEqual('');
61
+ });
62
+ });
63
+
64
+ describe('getDownstreamResourcesDocsUrl', () => {
65
+ it.each([
66
+ ['v2.15.0', 'https://fleet.rancher.io/0.16/downstream-resources'],
67
+ ['2.15.0', 'https://fleet.rancher.io/0.16/downstream-resources'],
68
+ ['v2.16.3', 'https://fleet.rancher.io/0.17/downstream-resources'],
69
+ ['v2.15.0-rc1', 'https://fleet.rancher.io/0.16/downstream-resources'],
70
+ ['v2.15-head', 'https://fleet.rancher.io/0.16/downstream-resources'],
71
+ ['v2.20.0', 'https://fleet.rancher.io/0.21/downstream-resources'],
72
+ ])('should map Rancher %s to Fleet docs %s', (version, expected) => {
73
+ expect(getDownstreamResourcesDocsUrl(version)).toStrictEqual(expected);
74
+ });
75
+
76
+ it.each([
77
+ ['v2.14.0'],
78
+ ['v2.9.0'],
79
+ ['dev'],
80
+ [''],
81
+ [undefined],
82
+ ])('should fall back to the unversioned docs for %s', (version) => {
83
+ expect(getDownstreamResourcesDocsUrl(version)).toStrictEqual(FLEET_DOWNSTREAM_RESOURCES_DOCS_FALLBACK_URL);
84
+ });
85
+ });
86
+
87
+ describe('fetchAppCoCharts', () => {
88
+ it('should return entries once the repo OCIDownloaded condition is True', async() => {
89
+ const repo = buildRepo({ status: { conditions: [{ type: 'OCIDownloaded', status: 'True' }] } });
90
+ const store = buildStore(repo);
91
+ const onStateChange = jest.fn();
92
+
93
+ const result = await fetchAppCoCharts(store as any, 'my-repo', onStateChange);
94
+
95
+ expect(result.entries).toStrictEqual({ chartA: [{ name: 'chartA' }] });
96
+ expect(result.repoState).toStrictEqual(expect.objectContaining({
97
+ repoName: 'my-repo', transitioning: false, error: false
98
+ }));
99
+ expect(repo.followLink).toHaveBeenCalledWith('index');
100
+ expect(onStateChange).toHaveBeenCalledWith(expect.objectContaining({ repoName: 'my-repo', error: false }));
101
+ });
102
+
103
+ it('should return no entries and an error state when the repo reports an error', async() => {
104
+ const repo = buildRepo({ metadata: { state: { error: true, message: 'boom' } } });
105
+ const store = buildStore(repo);
106
+
107
+ const result = await fetchAppCoCharts(store as any, 'my-repo');
108
+
109
+ expect(result.entries).toBeNull();
110
+ expect(result.repoState).toStrictEqual(expect.objectContaining({
111
+ repoName: 'my-repo', error: true, errorMessage: 'boom'
112
+ }));
113
+ expect(repo.followLink).not.toHaveBeenCalled();
114
+ });
115
+
116
+ it('should flag notFound when the repo does not exist (404)', async() => {
117
+ const repo = buildRepo();
118
+ const store = buildStore(repo, { findRejects: true, findRejectStatus: 404 });
119
+
120
+ const result = await fetchAppCoCharts(store as any, 'my-repo');
121
+
122
+ expect(result.entries).toBeNull();
123
+ expect(result.repoState).toBeNull();
124
+ expect(result.notFound).toStrictEqual(true);
125
+ });
126
+
127
+ it('should not flag notFound when the repo lookup fails for another reason', async() => {
128
+ const repo = buildRepo();
129
+ const store = buildStore(repo, { findRejects: true, findRejectStatus: 500 });
130
+
131
+ const result = await fetchAppCoCharts(store as any, 'my-repo');
132
+
133
+ expect(result.entries).toBeNull();
134
+ expect(result.repoState).toBeNull();
135
+ expect(result.notFound).toStrictEqual(false);
136
+ });
137
+
138
+ it('should stop and return no entries when the signal is aborted', async() => {
139
+ const repo = buildRepo();
140
+ const store = buildStore(repo);
141
+ const controller = new AbortController();
142
+
143
+ controller.abort();
144
+
145
+ const result = await fetchAppCoCharts(store as any, 'my-repo', undefined, controller.signal);
146
+
147
+ expect(result.entries).toBeNull();
148
+ expect(result.repoState).toBeNull();
149
+ expect(repo.followLink).not.toHaveBeenCalled();
150
+ });
151
+ });
152
+
153
+ describe('ensureAppCoResources', () => {
154
+ const NAMESPACE = 'ns';
155
+ const AUTH_SECRET_NAME = 'fleet-appco-auth-x';
156
+ const AUTH_SECRET_ID = `${ NAMESPACE }/${ AUTH_SECRET_NAME }`;
157
+
158
+ const t = (key: string) => key;
159
+
160
+ const authSecret = { decodedData: { username: 'user', password: 'pass' } };
161
+
162
+ /**
163
+ * Build a store where the auth-secret existence can be controlled, while the
164
+ * image-pull secret and ClusterRepo are always absent (forcing their creation).
165
+ */
166
+ const buildResourcesStore = ({ secretInCache = false, secretFindResolves = false } = {}) => {
167
+ const newResource = () => ({
168
+ setData: jest.fn(),
169
+ save: jest.fn().mockResolvedValue(undefined),
170
+ });
171
+
172
+ const dispatch = jest.fn((action: string, payload?: any) => {
173
+ if (action.endsWith('/create')) {
174
+ return Promise.resolve(newResource());
175
+ }
176
+
177
+ if (action.endsWith('/find')) {
178
+ // The auth secret resolves via `find` whenever it exists (it is also
179
+ // re-fetched by ensureAppCoImagePullSecret to read its credentials).
180
+ if (payload?.id === AUTH_SECRET_ID) {
181
+ return secretInCache || secretFindResolves ? Promise.resolve(authSecret) : Promise.reject(new Error('not found'));
182
+ }
183
+
184
+ // Image-pull secret / ClusterRepo are absent so creation is triggered.
185
+ return Promise.reject(new Error('not found'));
186
+ }
187
+
188
+ return Promise.resolve();
189
+ });
190
+
191
+ const getters = new Proxy({}, {
192
+ get: (_target, prop) => {
193
+ if (String(prop).endsWith('/byId')) {
194
+ return (_type: string, id: string) => (secretInCache && id === AUTH_SECRET_ID ? authSecret : undefined);
195
+ }
196
+
197
+ return undefined;
198
+ }
199
+ });
200
+
201
+ return { dispatch, getters };
202
+ };
203
+
204
+ it('should return false and create nothing when the auth secret cannot be found', async() => {
205
+ const store = buildResourcesStore({ secretInCache: false, secretFindResolves: false });
206
+
207
+ const result = await ensureAppCoResources(store as any, AUTH_SECRET_NAME, NAMESPACE, t);
208
+
209
+ expect(result).toStrictEqual(false);
210
+ expect(store.dispatch).not.toHaveBeenCalledWith('management/create', expect.anything());
211
+ });
212
+
213
+ it('should ensure the image-pull secret and ClusterRepo when the auth secret is in cache', async() => {
214
+ const store = buildResourcesStore({ secretInCache: true });
215
+
216
+ const result = await ensureAppCoResources(store as any, AUTH_SECRET_NAME, NAMESPACE, t);
217
+
218
+ expect(result).toStrictEqual(true);
219
+ expect(store.dispatch).toHaveBeenCalledWith('management/create', expect.objectContaining({
220
+ type: SECRET,
221
+ _type: SECRET_TYPES.DOCKER_JSON,
222
+ }));
223
+ expect(store.dispatch).toHaveBeenCalledWith('management/create', expect.objectContaining({ type: CATALOG_TYPES.CLUSTER_REPO }));
224
+ });
225
+
226
+ it('should ensure resources when the auth secret resolves via find', async() => {
227
+ const store = buildResourcesStore({ secretInCache: false, secretFindResolves: true });
228
+
229
+ const result = await ensureAppCoResources(store as any, AUTH_SECRET_NAME, NAMESPACE, t);
230
+
231
+ expect(result).toStrictEqual(true);
232
+ expect(store.dispatch).toHaveBeenCalledWith('management/create', expect.objectContaining({ type: CATALOG_TYPES.CLUSTER_REPO }));
233
+ });
234
+ });
235
+
236
+ describe('ensureAppCoImagePullSecret', () => {
237
+ const NAMESPACE = 'ns';
238
+ const AUTH_SECRET_NAME = 'fleet-appco-auth-x';
239
+ const AUTH_SECRET_ID = `${ NAMESPACE }/${ AUTH_SECRET_NAME }`;
240
+ const PULL_SECRET_NAME = `${ AUTH_SECRET_NAME }-image-pull-secret`;
241
+ const PULL_SECRET_ID = `${ NAMESPACE }/${ PULL_SECRET_NAME }`;
242
+
243
+ const authSecret = { decodedData: { username: 'user', password: 'pass' } };
244
+
245
+ const buildPullSecretStore = ({ pullSecretExists = false, authFindResolves = true } = {}) => {
246
+ const newSecret = { setData: jest.fn(), save: jest.fn().mockResolvedValue(undefined) };
247
+
248
+ const dispatch = jest.fn((action: string, payload?: any) => {
249
+ if (action.endsWith('/create')) {
250
+ return Promise.resolve(newSecret);
251
+ }
252
+
253
+ if (action.endsWith('/find')) {
254
+ if (payload?.id === AUTH_SECRET_ID) {
255
+ return authFindResolves ? Promise.resolve(authSecret) : Promise.reject(new Error('not found'));
256
+ }
257
+
258
+ // The image-pull secret is never resolved via `find` here.
259
+ return Promise.reject(new Error('not found'));
260
+ }
261
+
262
+ return Promise.resolve();
263
+ });
264
+
265
+ const getters = new Proxy({}, {
266
+ get: (_target, prop) => {
267
+ if (String(prop).endsWith('/byId')) {
268
+ return (_type: string, id: string) => (pullSecretExists && id === PULL_SECRET_ID ? {} : undefined);
269
+ }
270
+
271
+ return undefined;
272
+ }
273
+ });
274
+
275
+ return {
276
+ dispatch, getters, newSecret
277
+ };
278
+ };
279
+
280
+ it('should no-op when the image-pull secret already exists', async() => {
281
+ const store = buildPullSecretStore({ pullSecretExists: true });
282
+
283
+ const result = await ensureAppCoImagePullSecret(store as any, AUTH_SECRET_NAME, NAMESPACE);
284
+
285
+ expect(result).toStrictEqual(PULL_SECRET_NAME);
286
+ expect(store.dispatch).not.toHaveBeenCalledWith('management/create', expect.anything());
287
+ });
288
+
289
+ it('should create the image-pull secret from the auth secret when it is missing', async() => {
290
+ const store = buildPullSecretStore({ pullSecretExists: false, authFindResolves: true });
291
+
292
+ const result = await ensureAppCoImagePullSecret(store as any, AUTH_SECRET_NAME, NAMESPACE);
293
+
294
+ expect(result).toStrictEqual(PULL_SECRET_NAME);
295
+ expect(store.dispatch).toHaveBeenCalledWith('management/create', expect.objectContaining({
296
+ type: SECRET,
297
+ _type: SECRET_TYPES.DOCKER_JSON,
298
+ }));
299
+ expect(store.newSecret.setData).toHaveBeenCalledWith('.dockerconfigjson', expect.stringContaining('"username":"user"'));
300
+ expect(store.newSecret.save).toHaveBeenCalledWith();
301
+ });
302
+
303
+ it('should skip creation when the auth secret cannot be found', async() => {
304
+ const store = buildPullSecretStore({ pullSecretExists: false, authFindResolves: false });
305
+
306
+ const result = await ensureAppCoImagePullSecret(store as any, AUTH_SECRET_NAME, NAMESPACE);
307
+
308
+ expect(result).toBeUndefined();
309
+ expect(store.dispatch).not.toHaveBeenCalledWith('management/create', expect.anything());
310
+ });
311
+ });
312
+ });
@@ -0,0 +1,130 @@
1
+ import {
2
+ haveV2Monitoring,
3
+ canViewGrafanaLink,
4
+ canViewAlertManagerLink,
5
+ canViewPrometheusLink,
6
+ CATTLE_MONITORING_NAMESPACE,
7
+ } from '@shell/utils/monitoring';
8
+ import { MONITORING, SCHEMA, ENDPOINTS } from '@shell/config/types';
9
+
10
+ const PODMONITOR_ID = MONITORING.PODMONITOR.toLowerCase();
11
+
12
+ function makeGetters(storeName: string, schemas: object[]) {
13
+ return {
14
+ getStoreNameByProductId: storeName,
15
+ [`${ storeName }/all`]: (type: string) => (type === SCHEMA ? schemas : []),
16
+ };
17
+ }
18
+
19
+ describe('haveV2Monitoring', () => {
20
+ it('returns true when podmonitor schema exists', () => {
21
+ const getters = makeGetters('cluster', [{ id: PODMONITOR_ID }]);
22
+
23
+ expect(haveV2Monitoring(getters)).toBe(true);
24
+ });
25
+
26
+ it('returns false when no schemas exist', () => {
27
+ const getters = makeGetters('cluster', []);
28
+
29
+ expect(haveV2Monitoring(getters)).toBe(false);
30
+ });
31
+
32
+ it('returns false when schemas exist but not podmonitor', () => {
33
+ const getters = makeGetters('cluster', [{ id: 'apps.deployment' }, { id: 'v1.pod' }]);
34
+
35
+ expect(haveV2Monitoring(getters)).toBe(false);
36
+ });
37
+
38
+ it('returns true when podmonitor is among multiple schemas', () => {
39
+ const getters = makeGetters('cluster', [
40
+ { id: 'apps.deployment' },
41
+ { id: PODMONITOR_ID },
42
+ { id: 'v1.pod' },
43
+ ]);
44
+
45
+ expect(haveV2Monitoring(getters)).toBe(true);
46
+ });
47
+
48
+ it('uses the store name from getStoreNameByProductId getter', () => {
49
+ const getters = makeGetters('mgmt', [{ id: PODMONITOR_ID }]);
50
+
51
+ expect(haveV2Monitoring(getters)).toBe(true);
52
+ });
53
+ });
54
+
55
+ describe('cATTLE_MONITORING_NAMESPACE', () => {
56
+ it('equals cattle-monitoring-system', () => {
57
+ expect(CATTLE_MONITORING_NAMESPACE).toStrictEqual('cattle-monitoring-system');
58
+ });
59
+ });
60
+
61
+ function makeStore(endpointId: string, subsets: object[] | undefined, hasSchema = true) {
62
+ return {
63
+ getters: { 'cluster/schemaFor': (type: string) => (type === ENDPOINTS ? (hasSchema ? {} : null) : null) },
64
+ dispatch: jest.fn().mockResolvedValue(
65
+ subsets !== undefined ? [{ id: endpointId, subsets }] : []
66
+ ),
67
+ };
68
+ }
69
+
70
+ describe('canViewGrafanaLink', () => {
71
+ it('returns true when grafana endpoint has subsets', async() => {
72
+ const id = `${ CATTLE_MONITORING_NAMESPACE }/rancher-monitoring-grafana`;
73
+ const store = makeStore(id, [{ addresses: [{ ip: '10.0.0.1' }] }]);
74
+
75
+ expect(await canViewGrafanaLink(store)).toBe(true);
76
+ });
77
+
78
+ it('returns false when grafana endpoint has empty subsets', async() => {
79
+ const id = `${ CATTLE_MONITORING_NAMESPACE }/rancher-monitoring-grafana`;
80
+ const store = makeStore(id, []);
81
+
82
+ expect(await canViewGrafanaLink(store)).toBe(false);
83
+ });
84
+
85
+ it('returns falsy when grafana endpoint is not found', async() => {
86
+ const store = makeStore('other/endpoint', [{ addresses: [] }]);
87
+
88
+ expect(await canViewGrafanaLink(store)).toBeFalsy();
89
+ });
90
+
91
+ it('returns false when endpoints schema is not available', async() => {
92
+ const id = `${ CATTLE_MONITORING_NAMESPACE }/rancher-monitoring-grafana`;
93
+ const store = makeStore(id, [{ addresses: [] }], false);
94
+
95
+ expect(await canViewGrafanaLink(store)).toBe(false);
96
+ expect(store.dispatch).not.toHaveBeenCalled();
97
+ });
98
+ });
99
+
100
+ describe('canViewAlertManagerLink', () => {
101
+ it('returns true when alertmanager endpoint has subsets', async() => {
102
+ const id = `${ CATTLE_MONITORING_NAMESPACE }/rancher-monitoring-alertmanager`;
103
+ const store = makeStore(id, [{ addresses: [{ ip: '10.0.0.2' }] }]);
104
+
105
+ expect(await canViewAlertManagerLink(store)).toBe(true);
106
+ });
107
+
108
+ it('returns falsy when alertmanager endpoint has no subsets', async() => {
109
+ const id = `${ CATTLE_MONITORING_NAMESPACE }/rancher-monitoring-alertmanager`;
110
+ const store = makeStore(id, undefined);
111
+
112
+ expect(await canViewAlertManagerLink(store)).toBeFalsy();
113
+ });
114
+ });
115
+
116
+ describe('canViewPrometheusLink', () => {
117
+ it('returns true when prometheus endpoint has subsets', async() => {
118
+ const id = `${ CATTLE_MONITORING_NAMESPACE }/rancher-monitoring-prometheus`;
119
+ const store = makeStore(id, [{ addresses: [{ ip: '10.0.0.3' }] }]);
120
+
121
+ expect(await canViewPrometheusLink(store)).toBe(true);
122
+ });
123
+
124
+ it('returns falsy when prometheus endpoint has no subsets', async() => {
125
+ const id = `${ CATTLE_MONITORING_NAMESPACE }/rancher-monitoring-prometheus`;
126
+ const store = makeStore(id, undefined);
127
+
128
+ expect(await canViewPrometheusLink(store)).toBeFalsy();
129
+ });
130
+ });
@@ -287,6 +287,28 @@ describe('fx: diff', () => {
287
287
  expect(result).toStrictEqual(expected);
288
288
  expect(result.parent.child.config).not.toHaveProperty('annotations', null);
289
289
  });
290
+
291
+ it('should handle type change from object in from to primitive in to without throwing', () => {
292
+ // from[k] is an object, to[k] is a string (e.g. a pre-existing secret name).
293
+ // The whole value was replaced; nested keys from from should not be nulled out.
294
+ const from = {
295
+ githubConfigSecret: { github_token: '' },
296
+ githubConfigUrl: '',
297
+ };
298
+ const to = {
299
+ githubConfigUrl: 'https://github.com/abc',
300
+ githubConfigSecret: 'preexisting-secret',
301
+ };
302
+
303
+ expect(() => diff(from, to)).not.toThrow();
304
+
305
+ const result = diff(from, to);
306
+
307
+ expect(result.githubConfigSecret).toStrictEqual('preexisting-secret');
308
+ expect(result.githubConfigUrl).toStrictEqual('https://github.com/abc');
309
+ // No garbage nested null should be injected under the replaced key
310
+ expect(Object.keys(result)).not.toContain('githubConfigSecret.github_token');
311
+ });
290
312
  });
291
313
 
292
314
  describe('fx: definedKeys', () => {