@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,98 @@
1
+ import { hashObj, isEmptyData, convertToBuffer } from '../browserHashUtils';
2
+
3
+ describe('crypto/browserHashUtils', () => {
4
+ describe('hashObj', () => {
5
+ it.each([
6
+ {
7
+ desc: 'empty object',
8
+ input: {},
9
+ expected: '31e',
10
+ },
11
+ {
12
+ desc: 'object with string value',
13
+ input: { a: 1 },
14
+ expected: '1b0fmfe',
15
+ },
16
+ ])('returns deterministic base-36 hash for $desc', ({ input, expected }) => {
17
+ expect(hashObj(input)).toStrictEqual(expected);
18
+ });
19
+
20
+ it('is idempotent — same input always produces same hash', () => {
21
+ const obj = {
22
+ a: 1, b: 'hello', c: [1, 2, 3]
23
+ };
24
+
25
+ expect(hashObj(obj)).toStrictEqual(hashObj(obj));
26
+ });
27
+
28
+ it('produces different hashes for different objects', () => {
29
+ expect(hashObj({ a: 1 })).not.toStrictEqual(hashObj({ b: 1 }));
30
+ });
31
+
32
+ it('returns a base-36 string (only digits and lowercase a-z)', () => {
33
+ expect(hashObj({ key: 'value' })).toMatch(/^[0-9a-z]+$/);
34
+ });
35
+ });
36
+
37
+ describe('isEmptyData', () => {
38
+ it.each([
39
+ {
40
+ desc: 'empty string',
41
+ input: '',
42
+ expected: true,
43
+ },
44
+ {
45
+ desc: 'non-empty string',
46
+ input: 'hello',
47
+ expected: false,
48
+ },
49
+ {
50
+ desc: 'zero-length ArrayBuffer',
51
+ input: new ArrayBuffer(0),
52
+ expected: true,
53
+ },
54
+ {
55
+ desc: 'non-empty ArrayBuffer',
56
+ input: new ArrayBuffer(5),
57
+ expected: false,
58
+ },
59
+ {
60
+ desc: 'zero-length Uint8Array',
61
+ input: new Uint8Array(0),
62
+ expected: true,
63
+ },
64
+ {
65
+ desc: 'non-empty Uint8Array',
66
+ input: new Uint8Array([1, 2, 3]),
67
+ expected: false,
68
+ },
69
+ ])('$desc → $expected', ({ input, expected }) => {
70
+ expect(isEmptyData(input)).toStrictEqual(expected);
71
+ });
72
+ });
73
+
74
+ describe('convertToBuffer', () => {
75
+ it('converts a string to a Uint8Array with the correct byte values', () => {
76
+ const result = convertToBuffer('hello');
77
+
78
+ expect(result).toBeInstanceOf(Uint8Array);
79
+ expect(Array.from(result)).toStrictEqual([104, 101, 108, 108, 111]);
80
+ });
81
+
82
+ it('converts a Uint8Array view to a Uint8Array with the same bytes', () => {
83
+ const input = new Uint8Array([1, 2, 3]);
84
+ const result = convertToBuffer(input);
85
+
86
+ expect(result).toBeInstanceOf(Uint8Array);
87
+ expect(Array.from(result)).toStrictEqual([1, 2, 3]);
88
+ });
89
+
90
+ it('wraps a plain ArrayBuffer in a Uint8Array', () => {
91
+ const buf = new Uint8Array([10, 20, 30]).buffer;
92
+ const result = convertToBuffer(buf);
93
+
94
+ expect(result).toBeInstanceOf(Uint8Array);
95
+ expect(Array.from(result)).toStrictEqual([10, 20, 30]);
96
+ });
97
+ });
98
+ });
@@ -0,0 +1,144 @@
1
+ import { base64Encode, base64DecodeToBuffer, base64Decode, binarySize } from '../index';
2
+
3
+ describe('crypto/index', () => {
4
+ describe('base64Encode', () => {
5
+ it.each([
6
+ {
7
+ desc: 'null input',
8
+ input: null as any,
9
+ expected: null,
10
+ },
11
+ {
12
+ desc: 'undefined input',
13
+ input: undefined as any,
14
+ expected: undefined,
15
+ },
16
+ {
17
+ desc: 'empty string',
18
+ input: '',
19
+ expected: '',
20
+ },
21
+ {
22
+ desc: 'hello',
23
+ input: 'hello',
24
+ expected: 'aGVsbG8=',
25
+ },
26
+ {
27
+ desc: 'abc (no padding needed)',
28
+ input: 'abc',
29
+ expected: 'YWJj',
30
+ },
31
+ ])('returns correct base64 for $desc', ({ input, expected }) => {
32
+ expect(base64Encode(input)).toStrictEqual(expected);
33
+ });
34
+
35
+ it.each([
36
+ {
37
+ desc: 'single-byte input (strips two padding chars)',
38
+ input: 'a',
39
+ expected: 'YQ',
40
+ },
41
+ {
42
+ desc: 'two-byte input (strips one padding char)',
43
+ input: 'ab',
44
+ expected: 'YWI',
45
+ },
46
+ {
47
+ desc: 'three-byte input (no padding to strip)',
48
+ input: 'abc',
49
+ expected: 'YWJj',
50
+ },
51
+ ])('url alphabet: $desc', ({ input, expected }) => {
52
+ expect(base64Encode(input, 'url')).toStrictEqual(expected);
53
+ });
54
+ });
55
+
56
+ describe('base64DecodeToBuffer', () => {
57
+ it.each([
58
+ {
59
+ desc: 'null',
60
+ input: null as any,
61
+ expected: null,
62
+ },
63
+ {
64
+ desc: 'undefined',
65
+ input: undefined as any,
66
+ expected: undefined,
67
+ },
68
+ ])('returns $desc unchanged', ({ input, expected }) => {
69
+ expect(base64DecodeToBuffer(input)).toStrictEqual(expected);
70
+ });
71
+
72
+ it('decodes a valid base64 string to a buffer', () => {
73
+ const result = base64DecodeToBuffer('aGVsbG8=');
74
+
75
+ expect(result.toString()).toStrictEqual('hello');
76
+ });
77
+
78
+ it('decodes base64 without padding', () => {
79
+ const result = base64DecodeToBuffer('YWJj');
80
+
81
+ expect(result.toString()).toStrictEqual('abc');
82
+ });
83
+ });
84
+
85
+ describe('base64Decode', () => {
86
+ it.each([
87
+ {
88
+ desc: 'null',
89
+ input: null as any,
90
+ expected: null,
91
+ },
92
+ {
93
+ desc: 'empty string',
94
+ input: '',
95
+ expected: '',
96
+ },
97
+ {
98
+ desc: 'padded base64 for hello',
99
+ input: 'aGVsbG8=',
100
+ expected: 'hello',
101
+ },
102
+ {
103
+ desc: 'unpadded base64 for abc',
104
+ input: 'YWJj',
105
+ expected: 'abc',
106
+ },
107
+ ])('$desc', ({ input, expected }) => {
108
+ expect(base64Decode(input)).toStrictEqual(expected);
109
+ });
110
+
111
+ it('decodes url-safe base64 without padding (url-encoded "a")', () => {
112
+ // base64Encode('a', 'url') → 'YQ' (stripped ==)
113
+ // base64Decode must handle missing padding
114
+ expect(base64Decode('YQ')).toStrictEqual('a');
115
+ });
116
+ });
117
+
118
+ describe('binarySize', () => {
119
+ it.each([
120
+ {
121
+ desc: 'empty string',
122
+ input: '',
123
+ expected: 0,
124
+ },
125
+ {
126
+ desc: 'no padding (3 bytes — "abc")',
127
+ input: 'YWJj',
128
+ expected: 3,
129
+ },
130
+ {
131
+ desc: 'one padding char (2 bytes — "ab")',
132
+ input: 'YWI=',
133
+ expected: 2,
134
+ },
135
+ {
136
+ desc: 'two padding chars (1 byte — "a")',
137
+ input: 'YQ==',
138
+ expected: 1,
139
+ },
140
+ ])('$desc → $expected bytes', ({ input, expected }) => {
141
+ expect(binarySize(input)).toStrictEqual(expected);
142
+ });
143
+ });
144
+ });
@@ -0,0 +1,104 @@
1
+ export const SECONDS_PER = {
2
+ s: 1,
3
+ m: 60,
4
+ h: 3600,
5
+ d: 86400,
6
+ } as const;
7
+
8
+ export const UNIT_TO_MS = {
9
+ ms: 1,
10
+ s: 1000,
11
+ m: 60 * 1000,
12
+ h: 60 * 60 * 1000,
13
+ d: 24 * 60 * 60 * 1000,
14
+ w: 7 * 24 * 60 * 60 * 1000,
15
+ y: 365 * 24 * 60 * 60 * 1000
16
+ } as const;
17
+
18
+ const DURATION_REGEX = /^(?:([0-9]+)y)?(?:([0-9]+)w)?(?:([0-9]+)d)?(?:([0-9]+)h)?(?:([0-9]+)m)?(?:([0-9]+)s)?(?:([0-9]+)ms)?$/;
19
+
20
+ export function toMilliseconds(input: string | number | null | undefined): number {
21
+ if (!input) {
22
+ return 0;
23
+ }
24
+ const d = `${ input }`.match(DURATION_REGEX);
25
+
26
+ if (d) {
27
+ const properties = d.slice(1);
28
+ const numberD = properties.map((value) => ([null, undefined].includes(value) ? 0 : Number(value)));
29
+ const data: Record<string, number> = {};
30
+
31
+ [
32
+ data.y,
33
+ data.w,
34
+ data.d,
35
+ data.h,
36
+ data.m,
37
+ data.s,
38
+ data.ms
39
+ ] = numberD;
40
+
41
+ return Object.keys(data).reduce((total, unit) => (total + ((data[unit] || 0) * (UNIT_TO_MS[unit as keyof typeof UNIT_TO_MS] || 0))), 0);
42
+ }
43
+
44
+ return 0;
45
+ }
46
+
47
+ export function toSeconds(input: string | number | null | undefined): number {
48
+ return Math.floor(toMilliseconds(input) / 1000);
49
+ }
50
+
51
+ const DURATION_UNITS = [
52
+ { divisor: SECONDS_PER.d, suffix: 'd' },
53
+ { divisor: SECONDS_PER.h, suffix: 'h' },
54
+ { divisor: SECONDS_PER.m, suffix: 'm' },
55
+ { divisor: SECONDS_PER.s, suffix: 's' },
56
+ ];
57
+
58
+ /**
59
+ * Decomposes a total number of seconds into the largest whole time unit.
60
+ * For example, 7200 returns { value: 2, unit: 3600 } (2 hours).
61
+ * @param seconds - Total seconds to decompose
62
+ * @returns An object with `value` (the count) and `unit` (the multiplier in seconds: 86400, 3600, 60, or 1)
63
+ */
64
+ export function secondsToLargestUnit(seconds: number): { value: number, unit: number } {
65
+ if (seconds <= 0) {
66
+ return { value: seconds, unit: SECONDS_PER.s };
67
+ }
68
+
69
+ if (seconds % SECONDS_PER.d === 0) {
70
+ return { value: seconds / SECONDS_PER.d, unit: SECONDS_PER.d };
71
+ }
72
+ if (seconds % SECONDS_PER.h === 0) {
73
+ return { value: seconds / SECONDS_PER.h, unit: SECONDS_PER.h };
74
+ }
75
+ if (seconds % SECONDS_PER.m === 0) {
76
+ return { value: seconds / SECONDS_PER.m, unit: SECONDS_PER.m };
77
+ }
78
+
79
+ return { value: seconds, unit: SECONDS_PER.s };
80
+ }
81
+
82
+ /**
83
+ * Formats a duration in seconds into a human-readable string (e.g. "1d 3h 46m 40s").
84
+ * Zero-value components are omitted. Returns "0s" for non-positive values.
85
+ * @param seconds - Duration in seconds
86
+ */
87
+ export function formatDuration(seconds: number): string {
88
+ if (seconds <= 0) {
89
+ return '0s';
90
+ }
91
+
92
+ let remaining = seconds;
93
+
94
+ return DURATION_UNITS
95
+ .map(({ divisor, suffix }) => {
96
+ const count = Math.floor(remaining / divisor);
97
+
98
+ remaining %= divisor;
99
+
100
+ return count > 0 ? `${ count }${ suffix }` : '';
101
+ })
102
+ .filter(Boolean)
103
+ .join(' ');
104
+ }
@@ -0,0 +1,196 @@
1
+ import { Notification } from '@shell/types/notifications';
2
+ import { READ_ANNOUNCEMENTS } from '@shell/store/prefs';
3
+ import { createHandler, DynamicContentAnnouncementHandlerName } from '../notification-handler';
4
+ import { ANNOUNCEMENT_PREFIX } from '../announcement';
5
+
6
+ function makeStore(prefValue: string, notificationsAll: any[] = []) {
7
+ const dispatch = jest.fn();
8
+ const store = {
9
+ getters: {
10
+ 'notifications/all': notificationsAll,
11
+ 'prefs/get': jest.fn().mockReturnValue(prefValue),
12
+ },
13
+ dispatch,
14
+ };
15
+
16
+ return { store, dispatch };
17
+ }
18
+
19
+ function makeNotification(id: string): Notification {
20
+ return {
21
+ id,
22
+ level: 0,
23
+ title: 'Test',
24
+ message: '',
25
+ };
26
+ }
27
+
28
+ describe('notification-handler', () => {
29
+ describe('DynamicContentAnnouncementHandlerName', () => {
30
+ it('has the expected constant value', () => {
31
+ expect(DynamicContentAnnouncementHandlerName).toStrictEqual('dc-announcements');
32
+ });
33
+ });
34
+
35
+ describe('createHandler', () => {
36
+ it('returns an object with an onReadUpdated method', () => {
37
+ const { store } = makeStore('');
38
+ const handler = createHandler(store);
39
+
40
+ expect(typeof handler.onReadUpdated).toStrictEqual('function');
41
+ });
42
+
43
+ describe('onReadUpdated', () => {
44
+ describe('non-announcement notifications', () => {
45
+ it.each([
46
+ {
47
+ desc: 'arbitrary id',
48
+ id: 'some-notification-id',
49
+ },
50
+ {
51
+ desc: 'empty id',
52
+ id: '',
53
+ },
54
+ {
55
+ desc: 'id with announcement substring but not prefix',
56
+ id: `not-${ ANNOUNCEMENT_PREFIX }foo`,
57
+ },
58
+ ])('does not dispatch when notification id is "$id" ($desc)', async({ id }) => {
59
+ const { store, dispatch } = makeStore('');
60
+ const handler = createHandler(store);
61
+
62
+ await handler.onReadUpdated(makeNotification(id), true);
63
+
64
+ expect(dispatch).not.toHaveBeenCalled();
65
+ });
66
+ });
67
+
68
+ describe('marking announcement as read (read=true)', () => {
69
+ it.each([
70
+ {
71
+ desc: 'empty pref stores single id',
72
+ prefValue: '',
73
+ announcementId: 'foo',
74
+ expected: 'foo',
75
+ },
76
+ {
77
+ desc: 'pref with other values adds id sorted',
78
+ prefValue: 'abc,xyz',
79
+ announcementId: 'bar',
80
+ expected: 'abc,bar,xyz',
81
+ },
82
+ {
83
+ desc: 'id already present is not duplicated',
84
+ prefValue: 'foo',
85
+ announcementId: 'foo',
86
+ expected: 'foo',
87
+ },
88
+ {
89
+ desc: 'new value sorts into correct lexicographic position',
90
+ prefValue: 'alpha,zeta',
91
+ announcementId: 'beta',
92
+ expected: 'alpha,beta,zeta',
93
+ },
94
+ {
95
+ desc: 'single char id added to empty pref',
96
+ prefValue: '',
97
+ announcementId: 'a',
98
+ expected: 'a',
99
+ },
100
+ ])('$desc', async({ prefValue, announcementId, expected }) => {
101
+ const { store, dispatch } = makeStore(prefValue);
102
+ const handler = createHandler(store);
103
+ const notification = makeNotification(`${ ANNOUNCEMENT_PREFIX }${ announcementId }`);
104
+
105
+ await handler.onReadUpdated(notification, true);
106
+
107
+ expect(dispatch).toHaveBeenCalledWith('prefs/set', {
108
+ key: READ_ANNOUNCEMENTS,
109
+ value: expected,
110
+ });
111
+ });
112
+ });
113
+
114
+ describe('marking announcement as unread (read=false)', () => {
115
+ it.each([
116
+ {
117
+ desc: 'id in pref is removed',
118
+ prefValue: 'foo',
119
+ announcementId: 'foo',
120
+ expected: '',
121
+ },
122
+ {
123
+ desc: 'id in multi-value pref is removed leaving others',
124
+ prefValue: 'bar,foo,xyz',
125
+ announcementId: 'foo',
126
+ expected: 'bar,xyz',
127
+ },
128
+ {
129
+ desc: 'id not in pref leaves pref unchanged',
130
+ prefValue: 'bar,baz',
131
+ announcementId: 'foo',
132
+ expected: 'bar,baz',
133
+ },
134
+ {
135
+ desc: 'empty pref dispatches empty string',
136
+ prefValue: '',
137
+ announcementId: 'foo',
138
+ expected: '',
139
+ },
140
+ ])('$desc', async({ prefValue, announcementId, expected }) => {
141
+ const { store, dispatch } = makeStore(prefValue);
142
+ const handler = createHandler(store);
143
+ const notification = makeNotification(`${ ANNOUNCEMENT_PREFIX }${ announcementId }`);
144
+
145
+ await handler.onReadUpdated(notification, false);
146
+
147
+ expect(dispatch).toHaveBeenCalledWith('prefs/set', {
148
+ key: READ_ANNOUNCEMENTS,
149
+ value: expected,
150
+ });
151
+ });
152
+ });
153
+
154
+ describe('dispatch call details', () => {
155
+ it('uses READ_ANNOUNCEMENTS as the preference key', async() => {
156
+ const { store, dispatch } = makeStore('');
157
+ const handler = createHandler(store);
158
+
159
+ await handler.onReadUpdated(makeNotification(`${ ANNOUNCEMENT_PREFIX }test-id`), true);
160
+
161
+ expect(dispatch).toHaveBeenCalledWith('prefs/set', expect.objectContaining({ key: READ_ANNOUNCEMENTS }));
162
+ });
163
+
164
+ it('strips the announcement prefix from the stored id', async() => {
165
+ const { store, dispatch } = makeStore('');
166
+ const handler = createHandler(store);
167
+
168
+ await handler.onReadUpdated(makeNotification(`${ ANNOUNCEMENT_PREFIX }my-announcement`), true);
169
+
170
+ expect(dispatch).toHaveBeenCalledWith('prefs/set', {
171
+ key: READ_ANNOUNCEMENTS,
172
+ value: 'my-announcement',
173
+ });
174
+ });
175
+ });
176
+
177
+ describe('interaction with existing notifications', () => {
178
+ it('calls notifications/all getter to retrieve current announcements', async() => {
179
+ const notificationsAll = [
180
+ { id: `${ ANNOUNCEMENT_PREFIX }existing` },
181
+ { id: 'other-notification' },
182
+ ];
183
+ const { store, dispatch } = makeStore('existing', notificationsAll);
184
+ const handler = createHandler(store);
185
+
186
+ await handler.onReadUpdated(makeNotification(`${ ANNOUNCEMENT_PREFIX }new`), true);
187
+
188
+ expect(dispatch).toHaveBeenCalledWith('prefs/set', {
189
+ key: READ_ANNOUNCEMENTS,
190
+ value: 'existing,new',
191
+ });
192
+ });
193
+ });
194
+ });
195
+ });
196
+ });
@@ -24,7 +24,8 @@ const SUSE_EXTENSIONS = [
24
24
  'neuvector-ui-ext',
25
25
  'observability',
26
26
  'supportability-review-app',
27
- 'virtual-clusters'
27
+ 'virtual-clusters',
28
+ 'rancher-ai-ui'
28
29
  ];
29
30
 
30
31
  type FeatureFlagInfos = {
package/utils/error.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { isArray } from '@shell/utils/array';
2
2
  const AWS_SDK_UNHANDLED_ERROR = 'Deserialization error:';
3
+ const DO_NOT_LOG_ERROR = '__doNotLogError';
3
4
 
4
5
  export class ClusterNotFoundError extends Error {
5
6
  static NAME = 'ClusterNotFoundError'
@@ -106,6 +107,18 @@ export function exceptionToErrorsArray(err) {
106
107
  }
107
108
  }
108
109
 
110
+ export function createDoNotLogError(message) {
111
+ const err = new Error(message);
112
+
113
+ err[DO_NOT_LOG_ERROR] = true;
114
+
115
+ return err;
116
+ }
117
+
118
+ export function isDoNotLogError(err) {
119
+ return !!(err?.[DO_NOT_LOG_ERROR] || err?.doNotLog);
120
+ }
121
+
109
122
  /**
110
123
  * Imported from path-to-regexp
111
124
  * @param {*} err