@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,5 +1,8 @@
1
+ import day from 'dayjs';
1
2
  import { DATE_FORMAT, TIME_FORMAT } from '@shell/store/prefs';
2
- import { dateTimeFormat, isMissingDate } from '@shell/utils/time';
3
+ import {
4
+ dateTimeFormat, diffFrom, elapsedTime, getSecondsDiff, isMissingDate, safeSetTimeout
5
+ } from '@shell/utils/time';
3
6
  import { type Store } from 'vuex';
4
7
  import { ZERO_TIME } from '@shell/config/types';
5
8
 
@@ -42,3 +45,264 @@ describe('function: dateTimeFormat', () => {
42
45
  expect(formattedDate).toBe('');
43
46
  });
44
47
  });
48
+
49
+ describe('function: elapsedTime', () => {
50
+ it.each([
51
+ {
52
+ desc: 'returns empty object for 0',
53
+ seconds: 0,
54
+ expected: {},
55
+ },
56
+ {
57
+ desc: 'returns empty object for null',
58
+ seconds: null,
59
+ expected: {},
60
+ },
61
+ {
62
+ desc: 'returns empty object for undefined',
63
+ seconds: undefined,
64
+ expected: {},
65
+ },
66
+ {
67
+ desc: 'returns seconds label for 1 second',
68
+ seconds: 1,
69
+ expected: { diff: 1, label: '1s' },
70
+ },
71
+ {
72
+ desc: 'returns seconds label for 60 seconds',
73
+ seconds: 60,
74
+ expected: { diff: 1, label: '60s' },
75
+ },
76
+ {
77
+ desc: 'returns seconds label for 119 seconds (upper bound)',
78
+ seconds: 119,
79
+ expected: { diff: 1, label: '119s' },
80
+ },
81
+ {
82
+ desc: 'returns minutes+seconds label for 120 seconds',
83
+ seconds: 120,
84
+ expected: { diff: 1, label: '2m0s' },
85
+ },
86
+ {
87
+ desc: 'returns minutes+seconds label for 599 seconds',
88
+ seconds: 599,
89
+ expected: { diff: 1, label: '9m59s' },
90
+ },
91
+ {
92
+ desc: 'returns minutes-only label for 600 seconds',
93
+ seconds: 600,
94
+ expected: { diff: 60, label: '10m' },
95
+ },
96
+ {
97
+ desc: 'returns minutes-only label for 10799 seconds (under 3 hours)',
98
+ seconds: 10799,
99
+ expected: { diff: 60, label: '179m' },
100
+ },
101
+ {
102
+ desc: 'returns hours+minutes label for 10800 seconds (3 hours)',
103
+ seconds: 10800,
104
+ expected: { diff: 60, label: '3h0m' },
105
+ },
106
+ {
107
+ desc: 'returns hours+minutes label for 25200 seconds (7 hours)',
108
+ seconds: 25200,
109
+ expected: { diff: 60, label: '7h0m' },
110
+ },
111
+ {
112
+ desc: 'returns hours-only label for 28800 seconds (8 hours)',
113
+ seconds: 28800,
114
+ expected: { diff: 60, label: '8h' },
115
+ },
116
+ {
117
+ desc: 'returns hours-only label for 86400 seconds (1 day as hours)',
118
+ seconds: 86400,
119
+ expected: { diff: 60, label: '24h' },
120
+ },
121
+ {
122
+ desc: 'returns days+hours label for 172800 seconds (2 days)',
123
+ seconds: 172800,
124
+ expected: { diff: 60, label: '2d0h' },
125
+ },
126
+ {
127
+ desc: 'returns days+hours label for 176400 seconds (2 days 1 hour)',
128
+ seconds: 176400,
129
+ expected: { diff: 60, label: '2d1h' },
130
+ },
131
+ ])('$desc', ({ seconds, expected }) => {
132
+ expect(elapsedTime(seconds)).toStrictEqual(expected);
133
+ });
134
+ });
135
+
136
+ describe('function: safeSetTimeout', () => {
137
+ beforeEach(() => {
138
+ jest.useFakeTimers();
139
+ });
140
+
141
+ afterEach(() => {
142
+ jest.clearAllTimers();
143
+ jest.useRealTimers();
144
+ });
145
+
146
+ it('invokes the callback after the given delay', () => {
147
+ const callback = jest.fn();
148
+
149
+ safeSetTimeout(100, callback, null);
150
+ jest.runAllTimers();
151
+
152
+ expect(callback).toHaveBeenCalledTimes(1);
153
+ });
154
+
155
+ it('invokes the callback with the provided context object', () => {
156
+ let capturedContext: unknown;
157
+
158
+ function cb(this: unknown) {
159
+ capturedContext = this;
160
+ }
161
+ const ctx = { id: 'ctx' };
162
+
163
+ safeSetTimeout(100, cb, ctx);
164
+ jest.runAllTimers();
165
+
166
+ expect(capturedContext).toStrictEqual(ctx);
167
+ });
168
+
169
+ it('does not schedule a timeout when delay exceeds 32-bit maximum', () => {
170
+ const callback = jest.fn();
171
+ const result = safeSetTimeout(2147483648, callback, null);
172
+
173
+ jest.runAllTimers();
174
+
175
+ expect(result).toBeUndefined();
176
+ expect(callback).not.toHaveBeenCalled();
177
+ });
178
+
179
+ it('schedules a timeout for delay equal to 32-bit maximum', () => {
180
+ const callback = jest.fn();
181
+
182
+ safeSetTimeout(2147483647, callback, null);
183
+ jest.runAllTimers();
184
+
185
+ expect(callback).toHaveBeenCalledTimes(1);
186
+ });
187
+ });
188
+
189
+ describe('function: getSecondsDiff', () => {
190
+ it.each([
191
+ {
192
+ desc: 'returns 0 for identical date strings',
193
+ startDate: '2024-01-01T00:00:00Z',
194
+ endDate: '2024-01-01T00:00:00Z',
195
+ expected: 0,
196
+ },
197
+ {
198
+ desc: 'returns 60 for dates one minute apart',
199
+ startDate: '2024-01-01T00:00:00Z',
200
+ endDate: '2024-01-01T00:01:00Z',
201
+ expected: 60,
202
+ },
203
+ {
204
+ desc: 'returns 3600 for dates one hour apart',
205
+ startDate: '2024-01-01T00:00:00Z',
206
+ endDate: '2024-01-01T01:00:00Z',
207
+ expected: 3600,
208
+ },
209
+ {
210
+ desc: 'returns absolute difference when end is before start',
211
+ startDate: '2024-01-01T01:00:00Z',
212
+ endDate: '2024-01-01T00:00:00Z',
213
+ expected: 3600,
214
+ },
215
+ {
216
+ desc: 'returns 86400 for dates one day apart',
217
+ startDate: '2024-01-01T00:00:00Z',
218
+ endDate: '2024-01-02T00:00:00Z',
219
+ expected: 86400,
220
+ },
221
+ ])('$desc', ({ startDate, endDate, expected }) => {
222
+ expect(getSecondsDiff(startDate, endDate)).toStrictEqual(expected);
223
+ });
224
+ });
225
+
226
+ describe('function: diffFrom', () => {
227
+ const from = day('2024-01-01T12:00:00Z');
228
+
229
+ it.each([
230
+ {
231
+ desc: '30 seconds reports seconds unit',
232
+ value: from.subtract(30, 'second'),
233
+ expected: {
234
+ diff: -30,
235
+ absDiff: 30,
236
+ label: 30,
237
+ unitsKey: 'unit.sec',
238
+ units: 'sec',
239
+ next: 1,
240
+ },
241
+ },
242
+ {
243
+ desc: '120 seconds reports minutes unit with small label',
244
+ value: from.subtract(120, 'second'),
245
+ expected: {
246
+ diff: -120,
247
+ absDiff: 2,
248
+ label: 2,
249
+ unitsKey: 'unit.min',
250
+ units: 'min',
251
+ next: 6,
252
+ },
253
+ },
254
+ {
255
+ desc: '600 seconds reports minutes unit with integer label',
256
+ value: from.subtract(600, 'second'),
257
+ expected: {
258
+ diff: -600,
259
+ absDiff: 10,
260
+ label: 10,
261
+ unitsKey: 'unit.min',
262
+ units: 'min',
263
+ next: 6,
264
+ },
265
+ },
266
+ {
267
+ desc: '5400 seconds reports hours unit with fractional label',
268
+ value: from.subtract(5400, 'second'),
269
+ expected: {
270
+ diff: -5400,
271
+ absDiff: 1.5,
272
+ label: 1.5,
273
+ unitsKey: 'unit.hour',
274
+ units: 'hour',
275
+ next: 36,
276
+ },
277
+ },
278
+ {
279
+ desc: '172800 seconds reports days unit',
280
+ value: from.subtract(172800, 'second'),
281
+ expected: {
282
+ diff: -172800,
283
+ absDiff: 2,
284
+ label: 2,
285
+ unitsKey: 'unit.day',
286
+ units: 'day',
287
+ next: 72,
288
+ },
289
+ },
290
+ ])('$desc', ({ value, expected }) => {
291
+ expect(diffFrom(value, from, null)).toStrictEqual(expected);
292
+ });
293
+
294
+ it('includes string property when t function is provided', () => {
295
+ const value = from.subtract(30, 'second');
296
+ const t = (key: string, args: unknown) => `${ key }:${ JSON.stringify(args) }`;
297
+ const result = diffFrom(value, from, t);
298
+
299
+ expect(result.string).toStrictEqual(`30 unit.sec:${ JSON.stringify({ count: 30 }) }`);
300
+ });
301
+
302
+ it('omits string property when t function is not provided', () => {
303
+ const value = from.subtract(30, 'second');
304
+ const result = diffFrom(value, from, null);
305
+
306
+ expect(result).not.toHaveProperty('string');
307
+ });
308
+ });
@@ -0,0 +1,47 @@
1
+ import { updatePageTitle } from '@shell/utils/title';
2
+
3
+ describe('updatePageTitle', () => {
4
+ afterEach(() => {
5
+ document.title = '';
6
+ });
7
+
8
+ it('sets document.title to a single breadcrumb', () => {
9
+ updatePageTitle('Home');
10
+ expect(document.title).toStrictEqual('Home');
11
+ });
12
+
13
+ it('joins multiple breadcrumbs with " - "', () => {
14
+ updatePageTitle('Rancher', 'Clusters', 'my-cluster');
15
+ expect(document.title).toStrictEqual('Rancher - Clusters - my-cluster');
16
+ });
17
+
18
+ it('filters out null values', () => {
19
+ updatePageTitle('Rancher', null, 'Clusters');
20
+ expect(document.title).toStrictEqual('Rancher - Clusters');
21
+ });
22
+
23
+ it('filters out undefined values', () => {
24
+ updatePageTitle('Rancher', undefined, 'Clusters');
25
+ expect(document.title).toStrictEqual('Rancher - Clusters');
26
+ });
27
+
28
+ it('filters out false values', () => {
29
+ updatePageTitle('Rancher', false, 'Clusters');
30
+ expect(document.title).toStrictEqual('Rancher - Clusters');
31
+ });
32
+
33
+ it('filters out empty string values', () => {
34
+ updatePageTitle('Rancher', '', 'Clusters');
35
+ expect(document.title).toStrictEqual('Rancher - Clusters');
36
+ });
37
+
38
+ it('sets document.title to empty string when all breadcrumbs are falsy', () => {
39
+ updatePageTitle(null, undefined, false);
40
+ expect(document.title).toStrictEqual('');
41
+ });
42
+
43
+ it('sets document.title to empty string when called with no arguments', () => {
44
+ updatePageTitle();
45
+ expect(document.title).toStrictEqual('');
46
+ });
47
+ });
@@ -0,0 +1,53 @@
1
+ import { setWidth, getWidth } from '@shell/utils/width';
2
+
3
+ describe('setWidth', () => {
4
+ it('does nothing when el is null', () => {
5
+ expect(() => setWidth(null, '100px')).not.toThrow();
6
+ });
7
+
8
+ it('does nothing when el is undefined', () => {
9
+ expect(() => setWidth(undefined, '100px')).not.toThrow();
10
+ });
11
+
12
+ it('sets style.width to a string value directly', () => {
13
+ const el = document.createElement('div');
14
+
15
+ setWidth(el, '200px');
16
+ expect(el.style.width).toStrictEqual('200px');
17
+ });
18
+
19
+ it('sets style.width with px suffix for a numeric value', () => {
20
+ const el = document.createElement('div');
21
+
22
+ setWidth(el, 150);
23
+ expect(el.style.width).toStrictEqual('150px');
24
+ });
25
+
26
+ it('sets style.width using the return value of a function', () => {
27
+ const el = document.createElement('div');
28
+
29
+ setWidth(el, () => '300px');
30
+ expect(el.style.width).toStrictEqual('300px');
31
+ });
32
+
33
+ it('sets style.width using the numeric return value of a function with px suffix', () => {
34
+ const el = document.createElement('div');
35
+
36
+ setWidth(el, () => 50);
37
+ expect(el.style.width).toStrictEqual('50px');
38
+ });
39
+ });
40
+
41
+ describe('getWidth', () => {
42
+ it('returns undefined when el is null', () => {
43
+ expect(getWidth(null)).toBeUndefined();
44
+ });
45
+
46
+ it('returns undefined when el is undefined', () => {
47
+ expect(getWidth(undefined)).toBeUndefined();
48
+ });
49
+
50
+ it('returns undefined for an empty array-like object with length 0', () => {
51
+ expect(getWidth({ length: 0 })).toBeUndefined();
52
+ });
53
+ });
@@ -0,0 +1,158 @@
1
+ import { open, Popup, popupWindowOptions } from '@shell/utils/window';
2
+
3
+ function mockScreen(width: number, height: number) {
4
+ Object.defineProperty(window, 'screen', {
5
+ configurable: true,
6
+ writable: true,
7
+ value: { width, height },
8
+ });
9
+ }
10
+
11
+ describe('window utils', () => {
12
+ describe('popupWindowOptions', () => {
13
+ afterEach(() => {
14
+ mockScreen(0, 0);
15
+ });
16
+
17
+ it.each([
18
+ {
19
+ desc: 'uses default 1040×768 and centers on a large screen when no size is given',
20
+ screenWidth: 2560,
21
+ screenHeight: 1440,
22
+ width: undefined as number | undefined,
23
+ height: undefined as number | undefined,
24
+ expected: 'width=1040,height=768,resizable=1,scrollbars=1,left=760,top=336',
25
+ },
26
+ {
27
+ desc: 'uses custom size and centers on a large screen',
28
+ screenWidth: 2560,
29
+ screenHeight: 1440,
30
+ width: 800,
31
+ height: 600,
32
+ expected: 'width=800,height=600,resizable=1,scrollbars=1,left=880,top=420',
33
+ },
34
+ {
35
+ desc: 'caps size to screen dimensions when requested size exceeds screen',
36
+ screenWidth: 2560,
37
+ screenHeight: 1440,
38
+ width: 3000,
39
+ height: 2000,
40
+ expected: 'width=2560,height=1440,resizable=1,scrollbars=1,left=0,top=0',
41
+ },
42
+ {
43
+ desc: 'caps size to screen dimensions and uses left/top 0 when screen is smaller than defaults',
44
+ screenWidth: 800,
45
+ screenHeight: 600,
46
+ width: undefined as number | undefined,
47
+ height: undefined as number | undefined,
48
+ expected: 'width=800,height=600,resizable=1,scrollbars=1,left=0,top=0',
49
+ },
50
+ ])('$desc', ({
51
+ screenWidth, screenHeight, width, height, expected
52
+ }) => {
53
+ mockScreen(screenWidth, screenHeight);
54
+ expect(popupWindowOptions(width, height)).toStrictEqual(expected);
55
+ });
56
+ });
57
+
58
+ describe('open', () => {
59
+ let mockWindowOpen: jest.SpyInstance;
60
+
61
+ beforeEach(() => {
62
+ mockWindowOpen = jest.spyOn(window, 'open').mockReturnValue(null);
63
+ });
64
+
65
+ afterEach(() => {
66
+ mockWindowOpen.mockRestore();
67
+ });
68
+
69
+ it('calls window.open with the given url, name, and options', () => {
70
+ open('https://example.com', '_blank', 'width=800');
71
+ expect(mockWindowOpen).toHaveBeenCalledWith('https://example.com', '_blank', 'width=800');
72
+ });
73
+
74
+ it('uses default name _blank and empty options when not specified', () => {
75
+ open('https://example.com');
76
+ expect(mockWindowOpen).toHaveBeenCalledWith('https://example.com', '_blank', '');
77
+ });
78
+ });
79
+
80
+ describe('Popup class', () => {
81
+ const mockPopup = { closed: false };
82
+ let mockWindowOpen: jest.SpyInstance;
83
+
84
+ beforeEach(() => {
85
+ mockPopup.closed = false;
86
+ mockWindowOpen = jest
87
+ .spyOn(window, 'open')
88
+ .mockReturnValue(mockPopup as unknown as Window);
89
+ });
90
+
91
+ afterEach(() => {
92
+ mockWindowOpen.mockRestore();
93
+ });
94
+
95
+ it('initialises with null popup reference', () => {
96
+ const popup = new Popup();
97
+
98
+ expect(popup.popup).toBeNull();
99
+ });
100
+
101
+ it('calls onOpen callback when open is called', () => {
102
+ const onOpen = jest.fn();
103
+ const popup = new Popup(onOpen);
104
+
105
+ popup.open('https://example.com', '_blank', '', true);
106
+
107
+ expect(onOpen).toHaveBeenCalledWith();
108
+ });
109
+
110
+ it('sets popup reference to the opened window', () => {
111
+ const popup = new Popup();
112
+
113
+ popup.open('https://example.com', '_blank', '', true);
114
+
115
+ expect(popup.popup).toStrictEqual(mockPopup);
116
+ });
117
+
118
+ it('throws when the popup is blocked by the browser', () => {
119
+ mockWindowOpen.mockReturnValue(null);
120
+ const popup = new Popup();
121
+
122
+ expect(() => popup.open('https://example.com', '_blank', '', true))
123
+ .toThrow('Please disable your popup blocker for this site');
124
+ });
125
+
126
+ describe('when polling for window closure', () => {
127
+ beforeEach(() => {
128
+ jest.useFakeTimers();
129
+ });
130
+
131
+ afterEach(() => {
132
+ jest.useRealTimers();
133
+ });
134
+
135
+ it('calls onClose callback when the popup window is closed', () => {
136
+ const onClose = jest.fn();
137
+ const popup = new Popup(undefined, onClose);
138
+
139
+ popup.open('https://example.com', '_blank', '', false);
140
+ mockPopup.closed = true;
141
+ jest.advanceTimersByTime(500);
142
+
143
+ expect(onClose).toHaveBeenCalledWith();
144
+ });
145
+
146
+ it('does not call onClose when doNotPollForClosure is true', () => {
147
+ const onClose = jest.fn();
148
+ const popup = new Popup(undefined, onClose);
149
+
150
+ popup.open('https://example.com', '_blank', '', true);
151
+ mockPopup.closed = true;
152
+ jest.advanceTimersByTime(1000);
153
+
154
+ expect(onClose).not.toHaveBeenCalled();
155
+ });
156
+ });
157
+ });
158
+ });
@@ -157,34 +157,154 @@ describe('xccdf util: generateXCCDF', () => {
157
157
  expect(xml).toContain('<Group id="not-applicable">');
158
158
  });
159
159
 
160
- it('uses STIG metadata to override rule id, version, severity, fix, check system, and CCI idents', () => {
160
+ it('applies a per-check decoration verbatim when the decoration key matches the check id exactly', () => {
161
161
  const xml = generateXCCDF({
162
162
  report: {
163
163
  ...baseReport,
164
164
  results: [{
165
- id: 'g1',
165
+ id: '5.1',
166
166
  description: 'g',
167
+ checks: [{
168
+ id: '5.1.1', description: 'exact', scored: true, state: 'pass',
169
+ }],
170
+ }],
171
+ },
172
+ benchmarkVersion: 'cis-1.7',
173
+ decorations: {
174
+ '5.1.1': {
175
+ ruleId: 'CIS-5.1.1-rule', version: '2024-01', severity: 'high', fixId: 'F-5.1.1', checkId: 'C-5.1.1',
176
+ },
177
+ },
178
+ });
179
+
180
+ expect(xml).toContain('id="CIS-5.1.1-rule"');
181
+ expect(xml).toContain('idref="CIS-5.1.1-rule"');
182
+ expect(xml).toMatch(/id="CIS-5\.1\.1-rule"[^>]*severity="high"/);
183
+ expect(xml).toContain('<version>2024-01</version>');
184
+ expect(xml).toContain('fixref="F-5.1.1"');
185
+ expect(xml).toContain('<check system="C-5.1.1">');
186
+ });
187
+
188
+ it('applies a group-keyed decoration with a suffix derived from the check id', () => {
189
+ const xml = generateXCCDF({
190
+ report: {
191
+ ...baseReport,
192
+ results: [{
193
+ id: 'V-254553',
194
+ description: 'STIG group',
167
195
  checks: [{
168
196
  id: 'V-254553-TLS-apiserver', description: 'STIG check', scored: true, state: 'pass',
169
197
  }],
170
198
  }],
171
199
  },
172
200
  benchmarkVersion: 'rke2-stig-1.31',
173
- stigChecks: {
201
+ decorations: {
174
202
  'V-254553': {
175
- ruleId: 'SV-254553r1016525_rule', version: 'SV-254553r1016525_rule', severity: 'high', fixId: 'F-254553', checkId: 'C-254553', cci: ['CCI-000366'],
203
+ ruleId: 'SV-254553r1016525_rule', version: 'SV-254553r1016525_rule', severity: 'high', fixId: 'F-254553', checkId: 'C-254553',
176
204
  },
177
205
  },
178
206
  });
179
207
 
208
+ expect(xml).toContain('id="SV-254553r1016525_rule_TLS-apiserver"');
180
209
  expect(xml).toContain('idref="SV-254553r1016525_rule_TLS-apiserver"');
181
210
  expect(xml).toContain('severity="high"');
182
- expect(xml).toContain('<ident system="http://cyber.mil/cci">CCI-000366</ident>');
183
211
  expect(xml).toContain('fixref="F-254553"');
184
212
  expect(xml).toContain('<check system="C-254553">');
185
213
  });
186
214
 
187
- it('preserves the operator-style rule id when there is no STIG metadata', () => {
215
+ it('falls back to the full check id as suffix when the check id does not start with the group id', () => {
216
+ const xml = generateXCCDF({
217
+ report: {
218
+ ...baseReport,
219
+ results: [{
220
+ id: 'group-a',
221
+ description: 'g',
222
+ checks: [{
223
+ id: 'unrelated-check', description: 'c', scored: true, state: 'pass',
224
+ }],
225
+ }],
226
+ },
227
+ benchmarkVersion: 'cis-1.7',
228
+ decorations: { 'group-a': { ruleId: 'GROUP-A-rule' } },
229
+ });
230
+
231
+ expect(xml).toContain('id="GROUP-A-rule_unrelated-check"');
232
+ expect(xml).toContain('idref="GROUP-A-rule_unrelated-check"');
233
+ });
234
+
235
+ it('emits generic XCCDF idents from the decoration with their declared system unchanged', () => {
236
+ const xml = generateXCCDF({
237
+ report: {
238
+ ...baseReport,
239
+ results: [{
240
+ id: '5.1.1',
241
+ description: 'g',
242
+ checks: [{
243
+ id: '5.1.1', description: 'c', scored: true, state: 'pass',
244
+ }],
245
+ }],
246
+ },
247
+ benchmarkVersion: 'cis-1.7',
248
+ decorations: {
249
+ '5.1.1': {
250
+ idents: [
251
+ { system: 'https://www.cisecurity.org/controls/', value: 'CIS-CSC-3' },
252
+ { system: 'http://cyber.mil/cci', value: 'CCI-000366' },
253
+ ],
254
+ },
255
+ },
256
+ });
257
+
258
+ expect(xml).toContain('<ident system="https://www.cisecurity.org/controls/">CIS-CSC-3</ident>');
259
+ expect(xml).toContain('<ident system="http://cyber.mil/cci">CCI-000366</ident>');
260
+ });
261
+
262
+ it('emits a not-applicable ident placeholder when the decoration has no idents', () => {
263
+ const xml = generateXCCDF({
264
+ report: baseReport,
265
+ benchmarkVersion: 'cis-1.7',
266
+ decorations: { '5.1.1': { ruleId: 'r-5.1.1' } },
267
+ });
268
+
269
+ expect(xml).toMatch(/id="r-5\.1\.1"[\s\S]*?<ident system="not-applicable">not-applicable<\/ident>/);
270
+ });
271
+
272
+ it('falls back to the operator-style rule id when no decoration matches either the check id or the group id', () => {
273
+ const xml = generateXCCDF({
274
+ report: baseReport,
275
+ benchmarkVersion: 'cis-1.7',
276
+ decorations: { 'no-match': { ruleId: 'should-not-apply' } },
277
+ });
278
+
279
+ expect(xml).toContain('id="xccdf_compliance-operator_rule_5.1.1"');
280
+ expect(xml).not.toContain('should-not-apply');
281
+ });
282
+
283
+ it('prefers a per-check decoration over a group-keyed decoration when both are present', () => {
284
+ const xml = generateXCCDF({
285
+ report: {
286
+ ...baseReport,
287
+ results: [{
288
+ id: '5.1',
289
+ description: 'g',
290
+ checks: [{
291
+ id: '5.1.1', description: 'c', scored: true, state: 'pass',
292
+ }],
293
+ }],
294
+ },
295
+ benchmarkVersion: 'cis-1.7',
296
+ decorations: {
297
+ 5.1: { ruleId: 'group-rule', severity: 'low' },
298
+ '5.1.1': { ruleId: 'check-rule', severity: 'high' },
299
+ },
300
+ });
301
+
302
+ expect(xml).toContain('id="check-rule"');
303
+ expect(xml).not.toContain('id="group-rule_5.1.1"');
304
+ expect(xml).toMatch(/id="check-rule"[^>]*severity="high"/);
305
+ });
306
+
307
+ it('preserves the operator-style rule id when no decorations are supplied', () => {
188
308
  const xml = generateXCCDF({
189
309
  report: baseReport,
190
310
  benchmarkVersion: 'cis-1.7',