@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,434 @@
1
+ import { getters, mutations, state } from '@shell/store/notifications';
2
+ import { NotificationLevel, Notification, StoredNotification } from '@shell/types/notifications';
3
+
4
+ jest.mock('@shell/utils/string', () => ({ randomStr: jest.fn(() => 'mock-random-id') }));
5
+ jest.mock('@shell/utils/crypto', () => ({ md5: jest.fn((v: string) => `hash-${ v }`) }));
6
+ jest.mock('@shell/utils/crypto/encryption', () => ({
7
+ encrypt: jest.fn(),
8
+ decrypt: jest.fn(),
9
+ deriveKey: jest.fn(),
10
+ }));
11
+
12
+ function makeNotification(overrides: Partial<Notification> = {}): Notification {
13
+ return {
14
+ id: 'test-id',
15
+ title: 'test notification',
16
+ level: NotificationLevel.Info,
17
+ ...overrides,
18
+ };
19
+ }
20
+
21
+ function makeStoredNotification(overrides: Partial<StoredNotification> = {}): StoredNotification {
22
+ return {
23
+ ...makeNotification(),
24
+ read: false,
25
+ created: new Date('2026-01-01'),
26
+ ...overrides,
27
+ };
28
+ }
29
+
30
+ describe('notifications store', () => {
31
+ describe('state', () => {
32
+ it('returns initial state with empty notifications', () => {
33
+ const result = state();
34
+
35
+ expect(result.notifications).toStrictEqual([]);
36
+ expect(result.localStorageKey).toStrictEqual('');
37
+ expect(result.userId).toStrictEqual('');
38
+ expect(result.encryptionKey).toBeUndefined();
39
+ });
40
+ });
41
+
42
+ describe('getters', () => {
43
+ let mockState: ReturnType<typeof state>;
44
+ const hidden = makeStoredNotification({
45
+ id: 'h1', level: NotificationLevel.Hidden, read: false
46
+ });
47
+ const unreadVisible = makeStoredNotification({
48
+ id: 'u1', level: NotificationLevel.Info, read: false
49
+ });
50
+ const readVisible = makeStoredNotification({
51
+ id: 'r1', level: NotificationLevel.Warning, read: true
52
+ });
53
+
54
+ beforeEach(() => {
55
+ mockState = state();
56
+ mockState.notifications = [hidden, unreadVisible, readVisible];
57
+ });
58
+
59
+ describe('all', () => {
60
+ it('returns all notifications including hidden', () => {
61
+ expect(getters.all(mockState)).toStrictEqual([hidden, unreadVisible, readVisible]);
62
+ });
63
+
64
+ it('returns empty array when there are no notifications', () => {
65
+ mockState.notifications = [];
66
+ expect(getters.all(mockState)).toStrictEqual([]);
67
+ });
68
+ });
69
+
70
+ describe('visible', () => {
71
+ it('returns only non-hidden notifications', () => {
72
+ expect(getters.visible(mockState)).toStrictEqual([unreadVisible, readVisible]);
73
+ });
74
+
75
+ it('returns empty array when all notifications are hidden', () => {
76
+ mockState.notifications = [hidden];
77
+ expect(getters.visible(mockState)).toStrictEqual([]);
78
+ });
79
+ });
80
+
81
+ describe('hidden', () => {
82
+ it('returns only hidden notifications', () => {
83
+ expect(getters.hidden(mockState)).toStrictEqual([hidden]);
84
+ });
85
+
86
+ it('returns empty array when no notifications are hidden', () => {
87
+ mockState.notifications = [unreadVisible, readVisible];
88
+ expect(getters.hidden(mockState)).toStrictEqual([]);
89
+ });
90
+ });
91
+
92
+ describe('item', () => {
93
+ it('returns a function that finds a notification by id', () => {
94
+ const itemFn = getters.item(mockState);
95
+
96
+ expect(itemFn('u1')).toStrictEqual(unreadVisible);
97
+ });
98
+
99
+ it('returns undefined when the notification id does not exist', () => {
100
+ const itemFn = getters.item(mockState);
101
+
102
+ expect(itemFn('nonexistent')).toBeUndefined();
103
+ });
104
+ });
105
+
106
+ describe('unreadCount', () => {
107
+ it('counts only unread visible notifications', () => {
108
+ expect(getters.unreadCount(mockState)).toStrictEqual(1);
109
+ });
110
+
111
+ it('returns zero when all visible notifications are read', () => {
112
+ mockState.notifications = [readVisible];
113
+ expect(getters.unreadCount(mockState)).toStrictEqual(0);
114
+ });
115
+
116
+ it('excludes hidden notifications from the unread count', () => {
117
+ mockState.notifications = [hidden]; // hidden and unread
118
+ expect(getters.unreadCount(mockState)).toStrictEqual(0);
119
+ });
120
+ });
121
+
122
+ describe('localStorageKey', () => {
123
+ it('returns the localStorageKey from state', () => {
124
+ mockState.localStorageKey = 'rancher-notifications-abc';
125
+ expect(getters.localStorageKey(mockState)).toStrictEqual('rancher-notifications-abc');
126
+ });
127
+ });
128
+
129
+ describe('userId', () => {
130
+ it('returns the userId from state', () => {
131
+ mockState.userId = 'user-123';
132
+ expect(getters.userId(mockState)).toStrictEqual('user-123');
133
+ });
134
+ });
135
+
136
+ describe('encryptionKey', () => {
137
+ it('returns undefined by default', () => {
138
+ expect(getters.encryptionKey(mockState)).toBeUndefined();
139
+ });
140
+
141
+ it('returns the encryptionKey when set', () => {
142
+ const mockKey = {} as CryptoKey;
143
+
144
+ mockState.encryptionKey = mockKey;
145
+ expect(getters.encryptionKey(mockState)).toStrictEqual(mockKey);
146
+ });
147
+ });
148
+ });
149
+
150
+ describe('mutations', () => {
151
+ let mockState: ReturnType<typeof state>;
152
+
153
+ beforeEach(() => {
154
+ mockState = state();
155
+ mockState.localStorageKey = 'rancher-notifications-test';
156
+ localStorage.clear();
157
+ });
158
+
159
+ describe('localStorageKey', () => {
160
+ it('sets localStorageKey with the store prefix', () => {
161
+ mutations.localStorageKey(mockState, 'abc123');
162
+ expect(mockState.localStorageKey).toStrictEqual('rancher-notifications-abc123');
163
+ });
164
+ });
165
+
166
+ describe('userId', () => {
167
+ it('sets the userId on state', () => {
168
+ mutations.userId(mockState, 'user-456');
169
+ expect(mockState.userId).toStrictEqual('user-456');
170
+ });
171
+ });
172
+
173
+ describe('encryptionKey', () => {
174
+ it('sets the encryptionKey on state', () => {
175
+ const mockKey = {} as CryptoKey;
176
+
177
+ mutations.encryptionKey(mockState, mockKey);
178
+ expect(mockState.encryptionKey).toStrictEqual(mockKey);
179
+ });
180
+ });
181
+
182
+ describe('load', () => {
183
+ it('replaces all notifications with the provided list', () => {
184
+ mockState.notifications = [makeStoredNotification({ id: 'old' })];
185
+ const newNotifications = [makeStoredNotification({ id: 'n1' }), makeStoredNotification({ id: 'n2' })];
186
+
187
+ mutations.load(mockState, newNotifications);
188
+ expect(mockState.notifications).toStrictEqual(newNotifications);
189
+ });
190
+
191
+ it('clears notifications when given an empty array', () => {
192
+ mockState.notifications = [makeStoredNotification({ id: 'old' })];
193
+ mutations.load(mockState, []);
194
+ expect(mockState.notifications).toStrictEqual([]);
195
+ });
196
+ });
197
+
198
+ describe('add', () => {
199
+ it('adds a notification with the provided id to the front of the list', () => {
200
+ mutations.add(mockState, makeNotification({ id: 'custom-id' }));
201
+ expect(mockState.notifications[0].id).toStrictEqual('custom-id');
202
+ expect(mockState.notifications).toHaveLength(1);
203
+ });
204
+
205
+ it('generates an id via randomStr when none is provided', () => {
206
+ mutations.add(mockState, makeNotification({ id: '' as any }));
207
+ expect(mockState.notifications[0].id).toStrictEqual('mock-random-id');
208
+ });
209
+
210
+ it('sets read to false on the newly added notification', () => {
211
+ mutations.add(mockState, makeNotification({ id: 'n1' }));
212
+ expect(mockState.notifications[0].read).toStrictEqual(false);
213
+ });
214
+
215
+ it('prepends the new notification before existing ones', () => {
216
+ mockState.notifications = [makeStoredNotification({ id: 'existing' })];
217
+ mutations.add(mockState, makeNotification({ id: 'new' }));
218
+ expect(mockState.notifications[0].id).toStrictEqual('new');
219
+ expect(mockState.notifications[1].id).toStrictEqual('existing');
220
+ });
221
+
222
+ it('does not add a notification whose id already exists', () => {
223
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
224
+
225
+ mockState.notifications = [makeStoredNotification({ id: 'dup' })];
226
+ mutations.add(mockState, makeNotification({ id: 'dup' }));
227
+ expect(mockState.notifications).toHaveLength(1);
228
+ consoleErrorSpy.mockRestore();
229
+ });
230
+
231
+ it('enforces the maximum of 50 notifications by removing the oldest', () => {
232
+ for (let i = 0; i < 50; i++) {
233
+ mockState.notifications.push(makeStoredNotification({ id: `n${ i }` }));
234
+ }
235
+ mutations.add(mockState, makeNotification({ id: 'newest' }));
236
+ expect(mockState.notifications).toHaveLength(50);
237
+ expect(mockState.notifications[0].id).toStrictEqual('newest');
238
+ });
239
+
240
+ it('removes the oldest notification from localStorage when the limit is exceeded', () => {
241
+ const removeItemSpy = jest.spyOn(Storage.prototype, 'removeItem');
242
+
243
+ for (let i = 0; i < 50; i++) {
244
+ mockState.notifications.push(makeStoredNotification({ id: `n${ i }` }));
245
+ }
246
+ mutations.add(mockState, makeNotification({ id: 'newest' }));
247
+ expect(removeItemSpy).toHaveBeenCalledWith('rancher-notifications-test-n49');
248
+ removeItemSpy.mockRestore();
249
+ });
250
+
251
+ it('syncs the notifications index to localStorage after adding', () => {
252
+ const setItemSpy = jest.spyOn(Storage.prototype, 'setItem');
253
+
254
+ mutations.add(mockState, makeNotification({ id: 'n1' }));
255
+ expect(setItemSpy).toHaveBeenCalledWith('rancher-notifications-test', expect.any(String));
256
+ setItemSpy.mockRestore();
257
+ });
258
+
259
+ it('stores id, created, read, and progress fields in the localStorage index', () => {
260
+ mutations.add(mockState, makeNotification({ id: 'n1', progress: 42 }));
261
+ const stored = JSON.parse(localStorage.getItem('rancher-notifications-test') || '[]');
262
+
263
+ expect(stored[0]).toStrictEqual({
264
+ id: 'n1',
265
+ created: expect.any(String),
266
+ read: false,
267
+ progress: 42,
268
+ });
269
+ });
270
+ });
271
+
272
+ describe('markRead', () => {
273
+ it('marks a notification as read', () => {
274
+ mockState.notifications = [makeStoredNotification({ id: 'n1', read: false })];
275
+ mutations.markRead(mockState, 'n1');
276
+ expect(mockState.notifications[0].read).toStrictEqual(true);
277
+ });
278
+
279
+ it('does not modify state when the notification is not found', () => {
280
+ mockState.notifications = [makeStoredNotification({ id: 'n1', read: false })];
281
+ mutations.markRead(mockState, 'nonexistent');
282
+ expect(mockState.notifications[0].read).toStrictEqual(false);
283
+ });
284
+
285
+ it('does not change a notification that is already read', () => {
286
+ mockState.notifications = [makeStoredNotification({ id: 'n1', read: true })];
287
+ mutations.markRead(mockState, 'n1');
288
+ expect(mockState.notifications[0].read).toStrictEqual(true);
289
+ });
290
+ });
291
+
292
+ describe('markUnread', () => {
293
+ it('marks a notification as unread', () => {
294
+ mockState.notifications = [makeStoredNotification({ id: 'n1', read: true })];
295
+ mutations.markUnread(mockState, 'n1');
296
+ expect(mockState.notifications[0].read).toStrictEqual(false);
297
+ });
298
+
299
+ it('does not modify state when the notification is not found', () => {
300
+ mockState.notifications = [makeStoredNotification({ id: 'n1', read: true })];
301
+ mutations.markUnread(mockState, 'nonexistent');
302
+ expect(mockState.notifications[0].read).toStrictEqual(true);
303
+ });
304
+
305
+ it('does not change a notification that is already unread', () => {
306
+ mockState.notifications = [makeStoredNotification({ id: 'n1', read: false })];
307
+ mutations.markUnread(mockState, 'n1');
308
+ expect(mockState.notifications[0].read).toStrictEqual(false);
309
+ });
310
+ });
311
+
312
+ describe('markAllRead', () => {
313
+ it('marks all visible unread notifications as read', () => {
314
+ mockState.notifications = [
315
+ makeStoredNotification({
316
+ id: 'n1', level: NotificationLevel.Info, read: false
317
+ }),
318
+ makeStoredNotification({
319
+ id: 'n2', level: NotificationLevel.Warning, read: false
320
+ }),
321
+ ];
322
+ mutations.markAllRead(mockState);
323
+ expect(mockState.notifications[0].read).toStrictEqual(true);
324
+ expect(mockState.notifications[1].read).toStrictEqual(true);
325
+ });
326
+
327
+ it('does not mark hidden notifications as read', () => {
328
+ mockState.notifications = [
329
+ makeStoredNotification({
330
+ id: 'h1', level: NotificationLevel.Hidden, read: false
331
+ }),
332
+ ];
333
+ mutations.markAllRead(mockState);
334
+ expect(mockState.notifications[0].read).toStrictEqual(false);
335
+ });
336
+
337
+ it('leaves already-read visible notifications unchanged', () => {
338
+ mockState.notifications = [
339
+ makeStoredNotification({
340
+ id: 'n1', level: NotificationLevel.Info, read: true
341
+ }),
342
+ ];
343
+ mutations.markAllRead(mockState);
344
+ expect(mockState.notifications[0].read).toStrictEqual(true);
345
+ });
346
+ });
347
+
348
+ describe('update', () => {
349
+ it('updates notification fields by id', () => {
350
+ mockState.notifications = [makeStoredNotification({ id: 'n1', title: 'original' })];
351
+ mutations.update(mockState, { id: 'n1', title: 'updated' } as any);
352
+ expect(mockState.notifications[0].title).toStrictEqual('updated');
353
+ });
354
+
355
+ it('preserves fields not included in the update', () => {
356
+ mockState.notifications = [makeStoredNotification({
357
+ id: 'n1',
358
+ title: 'original',
359
+ level: NotificationLevel.Success,
360
+ })];
361
+ mutations.update(mockState, { id: 'n1', title: 'updated' } as any);
362
+ expect(mockState.notifications[0].level).toStrictEqual(NotificationLevel.Success);
363
+ });
364
+
365
+ it('does not modify state when the id is not found', () => {
366
+ mockState.notifications = [makeStoredNotification({ id: 'n1', title: 'original' })];
367
+ mutations.update(mockState, { id: 'nonexistent', title: 'updated' } as any);
368
+ expect(mockState.notifications[0].title).toStrictEqual('original');
369
+ });
370
+
371
+ it('does nothing when no id is provided', () => {
372
+ mockState.notifications = [makeStoredNotification({ id: 'n1', title: 'original' })];
373
+ mutations.update(mockState, {} as any);
374
+ expect(mockState.notifications[0].title).toStrictEqual('original');
375
+ });
376
+ });
377
+
378
+ describe('remove', () => {
379
+ it('removes the notification with the given id', () => {
380
+ mockState.notifications = [
381
+ makeStoredNotification({ id: 'n1' }),
382
+ makeStoredNotification({ id: 'n2' }),
383
+ ];
384
+ mutations.remove(mockState, 'n1');
385
+ expect(mockState.notifications).toHaveLength(1);
386
+ expect(mockState.notifications[0].id).toStrictEqual('n2');
387
+ });
388
+
389
+ it('removes the encrypted notification entry from localStorage', () => {
390
+ const removeItemSpy = jest.spyOn(Storage.prototype, 'removeItem');
391
+
392
+ mockState.notifications = [makeStoredNotification({ id: 'n1' })];
393
+ mutations.remove(mockState, 'n1');
394
+ expect(removeItemSpy).toHaveBeenCalledWith('rancher-notifications-test-n1');
395
+ removeItemSpy.mockRestore();
396
+ });
397
+
398
+ it('does not change state when the id is not found', () => {
399
+ mockState.notifications = [makeStoredNotification({ id: 'n1' })];
400
+ mutations.remove(mockState, 'nonexistent');
401
+ expect(mockState.notifications).toHaveLength(1);
402
+ });
403
+ });
404
+
405
+ describe('clearAll', () => {
406
+ it('removes all notifications from state', () => {
407
+ mockState.notifications = [
408
+ makeStoredNotification({ id: 'n1' }),
409
+ makeStoredNotification({ id: 'n2' }),
410
+ ];
411
+ mutations.clearAll(mockState);
412
+ expect(mockState.notifications).toStrictEqual([]);
413
+ });
414
+
415
+ it('removes each notification encrypted entry from localStorage', () => {
416
+ const removeItemSpy = jest.spyOn(Storage.prototype, 'removeItem');
417
+
418
+ mockState.notifications = [
419
+ makeStoredNotification({ id: 'n1' }),
420
+ makeStoredNotification({ id: 'n2' }),
421
+ ];
422
+ mutations.clearAll(mockState);
423
+ expect(removeItemSpy).toHaveBeenCalledWith('rancher-notifications-test-n1');
424
+ expect(removeItemSpy).toHaveBeenCalledWith('rancher-notifications-test-n2');
425
+ removeItemSpy.mockRestore();
426
+ });
427
+
428
+ it('does nothing when there are no notifications', () => {
429
+ mutations.clearAll(mockState);
430
+ expect(mockState.notifications).toStrictEqual([]);
431
+ });
432
+ });
433
+ });
434
+ });
package/store/catalog.js CHANGED
@@ -302,6 +302,16 @@ export const mutations = {
302
302
  state.namespacedRepos = namespaced;
303
303
  },
304
304
 
305
+ addClusterRepo(state, repo) {
306
+ if (!state.clusterRepos) {
307
+ state.clusterRepos = [];
308
+ }
309
+
310
+ if (!state.clusterRepos.find((r) => r.metadata?.name === repo.metadata?.name)) {
311
+ state.clusterRepos.push(repo);
312
+ }
313
+ },
314
+
305
315
  setCharts(state, { charts, errors = [], loaded = [] }) {
306
316
  state.charts = charts;
307
317
  state.errors = errors;
@@ -445,6 +455,53 @@ export const actions = {
445
455
  }
446
456
  },
447
457
 
458
+ async loadRepo(ctx, { repoName }) {
459
+ const {
460
+ state, getters, rootGetters, commit, dispatch
461
+ } = ctx;
462
+
463
+ const inStore = rootGetters['currentCluster'] ? rootGetters['currentProduct'].inStore : 'management';
464
+
465
+ let repo = rootGetters[`${ inStore }/byId`](CATALOG.CLUSTER_REPO, repoName);
466
+
467
+ if (!repo) {
468
+ try {
469
+ repo = await dispatch(`${ inStore }/find`, { type: CATALOG.CLUSTER_REPO, id: repoName }, { root: true });
470
+ } catch (e) {
471
+ return;
472
+ }
473
+ }
474
+
475
+ if (!repo) {
476
+ return;
477
+ }
478
+
479
+ commit('addClusterRepo', repo);
480
+
481
+ if (getters.isLoaded(repo)) {
482
+ return;
483
+ }
484
+
485
+ try {
486
+ const index = await repo.followLink('index');
487
+ const charts = { ...state.charts };
488
+
489
+ for (const k in index?.entries) {
490
+ for (const entry of index.entries[k]) {
491
+ addChart(ctx, charts, entry, repo);
492
+ }
493
+ }
494
+
495
+ commit('setCharts', {
496
+ charts,
497
+ errors: state.errors,
498
+ loaded: [repo],
499
+ });
500
+ } catch (e) {
501
+ console.error(`Failed to load repo ${ repoName }:`, e); // eslint-disable-line no-console
502
+ }
503
+ },
504
+
448
505
  /**
449
506
  * Globally refreshes all loaded repositories by triggering their refresh actions concurrently,
450
507
  * bypassing individual catalog loads, and then performs a single, global catalog/load.
package/store/plugins.js CHANGED
@@ -172,14 +172,14 @@ export const getters = {
172
172
  return async(name) => {
173
173
  const schema = getters.schemaForDriver(name);
174
174
 
175
- await schema.fetchResourceFields();
176
-
177
175
  if ( !schema ) {
178
176
  // eslint-disable-next-line no-console
179
177
  console.error(`Machine Driver Config schema not found for ${ name }`);
180
178
 
181
179
  return [];
182
180
  }
181
+ await schema.fetchResourceFields();
182
+
183
183
  // This is used in places where `createPopulated` has been called, which has called fetchResourceFields to populate resourceFields
184
184
  const out = Object.keys(schema?.resourceFields || {});
185
185
 
@@ -191,13 +191,16 @@ export const getters = {
191
191
 
192
192
  fieldsForDriver(state, getters) {
193
193
  return async(name) => {
194
+ const out = {};
194
195
  const schema = getters.schemaForDriver(name);
195
196
 
197
+ if ( !schema ) {
198
+ return out;
199
+ }
200
+
196
201
  await schema.fetchResourceFields();
197
202
  const names = await getters.fieldNamesForDriver(name);
198
203
 
199
- const out = {};
200
-
201
204
  for ( const n of names ) {
202
205
  out[n] = schema.resourceFields[n];
203
206
  }
@@ -0,0 +1,5 @@
1
+ export interface ButtonGroupOption {
2
+ labelKey: string;
3
+ value: string;
4
+ disabled?: boolean;
5
+ }