@rancher/shell 3.0.11 → 3.0.12-rc.2

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 (219) hide show
  1. package/assets/images/providers/entraid-black.svg +4 -0
  2. package/assets/images/providers/entraid.svg +9 -0
  3. package/assets/images/vendor/entraid.svg +9 -0
  4. package/assets/styles/app.scss +0 -1
  5. package/assets/styles/base/_mixins.scss +31 -0
  6. package/assets/styles/base/_variables.scss +2 -0
  7. package/assets/styles/themes/_modern.scss +6 -5
  8. package/assets/translations/en-us.yaml +24 -21
  9. package/assets/translations/zh-hans.yaml +4 -11
  10. package/chart/__tests__/S3.test.ts +10 -3
  11. package/components/CountBox.vue +20 -0
  12. package/components/CreateDriver.vue +0 -12
  13. package/components/DetailText.vue +12 -3
  14. package/components/EmptyProductPage.vue +76 -0
  15. package/components/Resource/Detail/CopyToClipboard.vue +1 -2
  16. package/components/Resource/Detail/Metadata/KeyValueRow.vue +9 -3
  17. package/components/Resource/Detail/TitleBar/__tests__/__snapshots__/index.test.ts.snap +31 -0
  18. package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +45 -1
  19. package/components/Resource/Detail/TitleBar/index.vue +1 -1
  20. package/components/Resource/Detail/ViewOptions/__tests__/__snapshots__/index.test.ts.snap +9 -0
  21. package/components/Resource/Detail/ViewOptions/__tests__/index.test.ts +62 -0
  22. package/components/Resource/Detail/ViewOptions/index.vue +2 -1
  23. package/components/ResourceList/Masthead.vue +25 -2
  24. package/components/SelectIconGrid.vue +5 -0
  25. package/components/SideNav.vue +13 -0
  26. package/components/__tests__/CountBox.test.ts +72 -0
  27. package/components/__tests__/DetailText.test.ts +113 -0
  28. package/components/__tests__/PromptModal.test.ts +2 -0
  29. package/components/fleet/FleetClusterTargets/index.vue +18 -1
  30. package/components/fleet/FleetClusters.vue +1 -0
  31. package/components/fleet/__tests__/FleetClusters.test.ts +71 -0
  32. package/components/form/InputWithSelect.vue +18 -10
  33. package/components/form/KeyValue.vue +17 -1
  34. package/components/form/LabeledSelect.vue +82 -24
  35. package/components/form/NodeScheduling.vue +17 -3
  36. package/components/form/PrivateRegistry.vue +69 -0
  37. package/components/form/Select.vue +73 -56
  38. package/components/form/ServiceNameSelect.vue +13 -11
  39. package/components/form/__tests__/KeyValue.test.ts +66 -0
  40. package/components/form/__tests__/NodeScheduling.test.ts +9 -0
  41. package/components/form/__tests__/PrivateRegistry.test.ts +133 -0
  42. package/components/form/labeled-select-utils/useLabeledSelectPagination.ts +138 -0
  43. package/components/formatter/WorkloadHealthScale.vue +3 -1
  44. package/components/nav/Group.vue +33 -9
  45. package/components/nav/Header.vue +56 -10
  46. package/components/nav/NotificationCenter/Notification.vue +4 -1
  47. package/components/nav/NotificationCenter/NotificationHeader.vue +20 -8
  48. package/components/nav/NotificationCenter/__tests__/NotificationHeader.test.ts +80 -0
  49. package/components/nav/TopLevelMenu.vue +15 -1
  50. package/components/nav/Type.vue +8 -7
  51. package/components/nav/WindowManager/index.vue +2 -1
  52. package/components/nav/WorkspaceSwitcher.vue +13 -0
  53. package/components/nav/__tests__/Group.test.ts +67 -0
  54. package/components/nav/__tests__/Header.test.ts +235 -0
  55. package/components/nav/__tests__/Type.test.ts +20 -3
  56. package/components/templates/default.vue +34 -4
  57. package/components/templates/home.vue +12 -25
  58. package/components/templates/plain.vue +13 -26
  59. package/composables/useLabeledFormElement.ts +10 -2
  60. package/composables/useLabeledSelect.ts +60 -0
  61. package/composables/useUserRetentionValidation.ts +1 -49
  62. package/config/cookies.js +0 -1
  63. package/config/labels-annotations.js +1 -0
  64. package/config/pagination-table-headers.js +8 -1
  65. package/config/product/apps.js +2 -1
  66. package/config/product/auth.js +1 -0
  67. package/config/product/backup.js +1 -0
  68. package/config/product/compliance.js +1 -1
  69. package/config/product/explorer.js +25 -6
  70. package/config/product/fleet.js +1 -0
  71. package/config/product/gatekeeper.js +1 -0
  72. package/config/product/istio.js +1 -0
  73. package/config/product/logging.js +1 -0
  74. package/config/product/longhorn.js +2 -1
  75. package/config/product/manager.js +1 -0
  76. package/config/product/monitoring.js +1 -0
  77. package/config/product/navlinks.js +1 -0
  78. package/config/product/neuvector.js +2 -1
  79. package/config/product/settings.js +1 -0
  80. package/config/product/uiplugins.js +1 -0
  81. package/config/query-params.js +1 -0
  82. package/config/router/routes.js +0 -8
  83. package/core/__tests__/plugin-products-helpers.test.ts +454 -0
  84. package/core/__tests__/plugin-products.test.ts +3810 -0
  85. package/core/extension-manager-impl.js +30 -1
  86. package/core/plugin-products-base.ts +392 -0
  87. package/core/plugin-products-extending.ts +44 -0
  88. package/core/plugin-products-helpers.ts +263 -0
  89. package/core/plugin-products-top-level.ts +66 -0
  90. package/core/plugin-products-type-guards.ts +33 -0
  91. package/core/plugin-products.ts +50 -0
  92. package/core/plugin-types.ts +237 -0
  93. package/core/plugin.ts +45 -10
  94. package/core/productDebugger.js +48 -0
  95. package/core/types.ts +97 -11
  96. package/detail/__tests__/__snapshots__/fleet.cattle.io.bundle.test.ts.snap +52 -0
  97. package/detail/__tests__/fleet.cattle.io.bundle.test.ts +171 -0
  98. package/detail/__tests__/management.cattle.io.fleetworkspace.test.ts +128 -0
  99. package/detail/fleet.cattle.io.bundle.vue +21 -34
  100. package/detail/management.cattle.io.fleetworkspace.vue +49 -0
  101. package/dialog/ExtensionCatalogInstallDialog.vue +1 -1
  102. package/dialog/InstallExtensionDialog.vue +6 -27
  103. package/dialog/UninstallExistingExtensionDialog.vue +141 -0
  104. package/dialog/UninstallExtensionDialog.vue +4 -26
  105. package/dialog/__tests__/UninstallExistingExtensionDialog.test.ts +114 -0
  106. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +1 -0
  107. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +9 -0
  108. package/edit/__tests__/kontainerDriver.test.ts +0 -13
  109. package/edit/__tests__/nodeDriver.test.ts +5 -11
  110. package/edit/__tests__/resources.cattle.io.restore.test.ts +9 -0
  111. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +6 -0
  112. package/edit/auth/__tests__/oidc.test.ts +54 -0
  113. package/edit/auth/azuread.vue +1 -1
  114. package/edit/auth/oidc.vue +8 -0
  115. package/edit/kontainerDriver.vue +1 -2
  116. package/edit/nodeDriver.vue +0 -2
  117. package/edit/provisioning.cattle.io.cluster/AgentEnv.vue +1 -0
  118. package/edit/provisioning.cattle.io.cluster/__tests__/AgentEnv.test.ts +25 -0
  119. package/edit/provisioning.cattle.io.cluster/__tests__/Ingress.test.ts +176 -0
  120. package/edit/provisioning.cattle.io.cluster/index.vue +70 -99
  121. package/edit/provisioning.cattle.io.cluster/rke2.vue +4 -1
  122. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +6 -0
  123. package/edit/provisioning.cattle.io.cluster/tabs/Ingress.vue +7 -2
  124. package/initialize/App.vue +29 -2
  125. package/initialize/install-plugins.js +0 -2
  126. package/list/__tests__/management.cattle.io.feature.test.ts +105 -0
  127. package/list/catalog.cattle.io.app.vue +25 -5
  128. package/list/management.cattle.io.feature.vue +1 -1
  129. package/list/management.cattle.io.fleetworkspace.vue +8 -0
  130. package/list/provisioning.cattle.io.cluster.vue +0 -1
  131. package/list/workload.vue +11 -4
  132. package/machine-config/amazonec2.vue +1 -0
  133. package/mixins/chart.js +40 -9
  134. package/mixins/resource-fetch.js +12 -3
  135. package/models/__tests__/catalog.cattle.io.app.test.ts +15 -1
  136. package/models/__tests__/catalog.cattle.io.clusterrepo.test.ts +84 -0
  137. package/models/__tests__/chart.test.ts +99 -6
  138. package/models/__tests__/management.cattle.io.feature.test.ts +131 -0
  139. package/models/__tests__/monitoring.coreos.com.alertmanagerconfig.test.ts +98 -0
  140. package/models/catalog.cattle.io.app.js +21 -17
  141. package/models/catalog.cattle.io.clusterrepo.js +39 -11
  142. package/models/chart.js +33 -19
  143. package/models/fleet-application.js +1 -1
  144. package/models/fleet.cattle.io.bundle.js +1 -1
  145. package/models/kontainerdriver.js +11 -0
  146. package/models/management.cattle.io.authconfig.js +5 -1
  147. package/models/management.cattle.io.cluster.js +0 -53
  148. package/models/management.cattle.io.feature.js +3 -3
  149. package/models/management.cattle.io.kontainerdriver.js +1 -26
  150. package/models/monitoring.coreos.com.alertmanagerconfig.js +31 -17
  151. package/models/nodedriver.js +7 -0
  152. package/models/pod.js +18 -0
  153. package/models/workload.js +20 -2
  154. package/package.json +13 -13
  155. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +0 -1
  156. package/pages/c/_cluster/apps/charts/__tests__/chart.test.ts +189 -0
  157. package/pages/c/_cluster/apps/charts/__tests__/index.test.ts +55 -0
  158. package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +53 -0
  159. package/pages/c/_cluster/apps/charts/chart.vue +217 -33
  160. package/pages/c/_cluster/apps/charts/index.vue +2 -2
  161. package/pages/c/_cluster/apps/charts/install.vue +8 -3
  162. package/pages/c/_cluster/auth/user.retention/index.vue +55 -22
  163. package/pages/c/_cluster/manager/drivers/kontainerDriver/index.vue +5 -7
  164. package/pages/c/_cluster/settings/brand.vue +4 -4
  165. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +39 -2
  166. package/pages/c/_cluster/uiplugins/__tests__/PluginInfoPanel.test.ts +61 -0
  167. package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +246 -23
  168. package/pages/c/_cluster/uiplugins/index.vue +166 -62
  169. package/plugins/dashboard-store/__tests__/resource-class.test.ts +1 -0
  170. package/plugins/dashboard-store/actions.js +3 -2
  171. package/plugins/dashboard-store/resource-class.js +62 -6
  172. package/plugins/plugin.js +16 -0
  173. package/plugins/steve/steve-pagination-utils.ts +7 -0
  174. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +205 -1
  175. package/rancher-components/Form/LabeledInput/LabeledInput.vue +82 -4
  176. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +1 -1
  177. package/scripts/test-plugins-build.sh +5 -2
  178. package/scripts/typegen.sh +13 -1
  179. package/server/server-middleware.js +2 -2
  180. package/static/humans.txt +1 -0
  181. package/static/robots.txt +34 -0
  182. package/static/welcome-cow.svg +18 -0
  183. package/store/__tests__/catalog.test.ts +161 -11
  184. package/store/__tests__/type-map.test.ts +84 -24
  185. package/store/auth.js +0 -3
  186. package/store/catalog.js +60 -8
  187. package/store/type-map.js +42 -3
  188. package/tsconfig.paths.json +1 -0
  189. package/types/resources/pod.ts +18 -0
  190. package/types/shell/index.d.ts +8539 -2938
  191. package/types/store/dashboard-store.types.ts +5 -0
  192. package/types/store/pagination.types.ts +6 -0
  193. package/utils/__tests__/git.test.ts +270 -0
  194. package/utils/__tests__/inactivity.test.ts +316 -0
  195. package/utils/__tests__/object.test.ts +77 -0
  196. package/utils/__tests__/time.test.ts +14 -1
  197. package/utils/__tests__/url.test.ts +246 -0
  198. package/utils/axios.js +1 -4
  199. package/utils/dynamic-importer.js +3 -2
  200. package/utils/object.js +33 -2
  201. package/utils/pagination-utils.ts +1 -1
  202. package/utils/time.ts +5 -0
  203. package/utils/uiplugins.ts +12 -16
  204. package/utils/validators/__tests__/private-registry.test.ts +76 -0
  205. package/utils/validators/private-registry.ts +28 -0
  206. package/vue.config.js +0 -9
  207. package/assets/images/providers/azuread-black.svg +0 -22
  208. package/assets/images/providers/azuread.svg +0 -25
  209. package/assets/images/vendor/azuread.svg +0 -18
  210. package/assets/styles/fonts/_dots.scss +0 -18
  211. package/components/EmberPage.vue +0 -622
  212. package/components/EmberPageView.vue +0 -39
  213. package/components/form/labeled-select-utils/labeled-select-pagination.ts +0 -116
  214. package/mixins/labeled-form-element.ts +0 -225
  215. package/pages/c/_cluster/explorer/tools/pages/_page.vue +0 -28
  216. package/pages/c/_cluster/manager/pages/_page.vue +0 -22
  217. package/pages/c/_cluster/mcapps/pages/_page.vue +0 -22
  218. package/plugins/ember-cookie.js +0 -17
  219. package/utils/ember-page.js +0 -30
@@ -83,6 +83,11 @@ export interface ActionFindPageArgs extends ActionCoreFindArgs {
83
83
 
84
84
  saveCountAs?: string,
85
85
 
86
+ /**
87
+ * When making a supporting HTTP request include associated resource data
88
+ */
89
+ includeAssociatedData?: boolean,
90
+
86
91
  /**
87
92
  * The target minimum revision for the resource.
88
93
  *
@@ -607,6 +607,7 @@ export interface StorePaginationRequest {
607
607
  * The single namespace to filter results by (as part of url path, not pagination params)
608
608
  */
609
609
  namespace?: string,
610
+
610
611
  /**
611
612
  * The set of pagination args used to create the request
612
613
  */
@@ -616,6 +617,11 @@ export interface StorePaginationRequest {
616
617
  * Does this request stem from a list with manual refresh?
617
618
  */
618
619
  hasManualRefresh?: boolean,
620
+
621
+ /**
622
+ * When making a supporting HTTP request include associated resource data
623
+ */
624
+ includeAssociatedData?: boolean,
619
625
  }
620
626
 
621
627
  /**
@@ -0,0 +1,270 @@
1
+ import { GitUtils, Commit } from '@shell/utils/git';
2
+
3
+ describe('git utils', () => {
4
+ describe('gitUtils.github.normalize.repo', () => {
5
+ it('maps owner fields from GitHub API response', () => {
6
+ const data = {
7
+ owner: {
8
+ login: 'octocat',
9
+ html_url: 'https://github.com/octocat',
10
+ avatar_url: 'https://avatars.githubusercontent.com/u/1?v=4'
11
+ },
12
+ description: 'Hello World',
13
+ created_at: '2021-01-01T00:00:00Z',
14
+ updated_at: '2021-06-01T00:00:00Z',
15
+ html_url: 'https://github.com/octocat/hello-world',
16
+ name: 'hello-world'
17
+ };
18
+
19
+ const result = GitUtils.github.normalize.repo(data);
20
+
21
+ expect(result.owner).toStrictEqual({
22
+ name: 'octocat',
23
+ htmlUrl: 'https://github.com/octocat',
24
+ avatarUrl: 'https://avatars.githubusercontent.com/u/1?v=4'
25
+ });
26
+ });
27
+
28
+ it('maps repo-level fields from GitHub API response', () => {
29
+ const data = {
30
+ owner: {
31
+ login: 'octocat', html_url: '', avatar_url: ''
32
+ },
33
+ description: 'A repository description',
34
+ created_at: '2021-01-01T00:00:00Z',
35
+ updated_at: '2022-03-15T12:00:00Z',
36
+ html_url: 'https://github.com/octocat/repo',
37
+ name: 'my-repo'
38
+ };
39
+
40
+ const result = GitUtils.github.normalize.repo(data);
41
+
42
+ expect(result.description).toStrictEqual('A repository description');
43
+ expect(result.created_at).toStrictEqual('2021-01-01T00:00:00Z');
44
+ expect(result.updated_at).toStrictEqual('2022-03-15T12:00:00Z');
45
+ expect(result.htmlUrl).toStrictEqual('https://github.com/octocat/repo');
46
+ expect(result.name).toStrictEqual('my-repo');
47
+ });
48
+
49
+ it('handles missing owner gracefully', () => {
50
+ const data = {
51
+ owner: undefined,
52
+ description: 'No owner',
53
+ created_at: '2021-01-01T00:00:00Z',
54
+ updated_at: '2021-01-01T00:00:00Z',
55
+ html_url: 'https://github.com/no/owner',
56
+ name: 'no-owner'
57
+ };
58
+
59
+ const result = GitUtils.github.normalize.repo(data);
60
+
61
+ expect(result.owner).toStrictEqual({
62
+ name: undefined,
63
+ htmlUrl: undefined,
64
+ avatarUrl: undefined
65
+ });
66
+ });
67
+ });
68
+
69
+ describe('gitUtils.github.normalize.commit', () => {
70
+ it('maps commit fields from GitHub API response', () => {
71
+ const data = {
72
+ commit: {
73
+ message: 'fix: resolve issue',
74
+ committer: { date: '2022-01-15T10:30:00Z' }
75
+ },
76
+ html_url: 'https://github.com/octocat/repo/commit/abc1234567890',
77
+ sha: 'abc1234567890abcdef',
78
+ author: {
79
+ login: 'octocat',
80
+ avatar_url: 'https://avatars.githubusercontent.com/u/1?v=4',
81
+ htmlUrl: 'https://github.com/octocat'
82
+ }
83
+ };
84
+
85
+ const result = GitUtils.github.normalize.commit(data);
86
+
87
+ expect(result.message).toStrictEqual('fix: resolve issue');
88
+ expect(result.htmlUrl).toStrictEqual('https://github.com/octocat/repo/commit/abc1234567890');
89
+ expect(result.sha).toStrictEqual('abc1234');
90
+ expect(result.commitId).toStrictEqual('abc1234567890abcdef');
91
+ expect(result.date).toStrictEqual('2022-01-15T10:30:00Z');
92
+ expect(result.isChecked).toStrictEqual(false);
93
+ });
94
+
95
+ it('truncates sha to 7 characters', () => {
96
+ const data = {
97
+ commit: { message: 'chore: update deps', committer: { date: '2022-01-01T00:00:00Z' } },
98
+ html_url: 'https://github.com/octocat/repo/commit/1234567890',
99
+ sha: '1234567890abcdef1234',
100
+ author: {
101
+ login: 'user', avatar_url: '', htmlUrl: ''
102
+ }
103
+ };
104
+
105
+ const result = GitUtils.github.normalize.commit(data);
106
+
107
+ expect(result.sha).toStrictEqual('1234567');
108
+ });
109
+
110
+ it('returns undefined sha when sha is empty string', () => {
111
+ const data = {
112
+ commit: { message: 'fix: bug', committer: { date: '2022-01-01T00:00:00Z' } },
113
+ html_url: 'https://github.com/octocat/repo/commit/x',
114
+ sha: '',
115
+ author: {
116
+ login: 'user', avatar_url: '', htmlUrl: ''
117
+ }
118
+ };
119
+
120
+ const result: Commit = GitUtils.github.normalize.commit(data);
121
+
122
+ expect(result.sha).toBeUndefined();
123
+ });
124
+
125
+ it('maps author fields from GitHub API response', () => {
126
+ const data = {
127
+ commit: { message: 'feat: new feature', committer: { date: '2022-01-01T00:00:00Z' } },
128
+ html_url: 'https://github.com/octocat/repo/commit/abc',
129
+ sha: 'abcdef1234567890',
130
+ author: {
131
+ login: 'contributor',
132
+ avatar_url: 'https://avatars.githubusercontent.com/u/2?v=4',
133
+ htmlUrl: 'https://github.com/contributor'
134
+ }
135
+ };
136
+
137
+ const result = GitUtils.github.normalize.commit(data);
138
+
139
+ expect(result.author).toStrictEqual({
140
+ name: 'contributor',
141
+ avatarUrl: 'https://avatars.githubusercontent.com/u/2?v=4',
142
+ htmlUrl: 'https://github.com/contributor'
143
+ });
144
+ });
145
+ });
146
+
147
+ describe('gitUtils.gitlab.normalize.repo', () => {
148
+ it('maps owner fields from GitLab API response', () => {
149
+ const data = {
150
+ namespace: {
151
+ name: 'my-group',
152
+ web_url: 'https://gitlab.com/my-group',
153
+ avatar_url: 'https://gitlab.com/uploads/group.png'
154
+ },
155
+ description: 'GitLab repo description',
156
+ created_at: '2021-02-01T00:00:00Z',
157
+ last_activity_at: '2022-07-20T00:00:00Z',
158
+ web_url: 'https://gitlab.com/my-group/project',
159
+ name: 'project'
160
+ };
161
+
162
+ const result = GitUtils.gitlab.normalize.repo(data);
163
+
164
+ expect(result.owner).toStrictEqual({
165
+ name: 'my-group',
166
+ htmlUrl: 'https://gitlab.com/my-group',
167
+ avatarUrl: 'https://gitlab.com/uploads/group.png'
168
+ });
169
+ });
170
+
171
+ it('maps repo-level fields from GitLab API response', () => {
172
+ const data = {
173
+ namespace: {
174
+ name: 'ns', web_url: '', avatar_url: ''
175
+ },
176
+ description: 'My GitLab project',
177
+ created_at: '2020-05-01T00:00:00Z',
178
+ last_activity_at: '2023-01-10T00:00:00Z',
179
+ web_url: 'https://gitlab.com/ns/myproject',
180
+ name: 'myproject'
181
+ };
182
+
183
+ const result = GitUtils.gitlab.normalize.repo(data);
184
+
185
+ expect(result.description).toStrictEqual('My GitLab project');
186
+ expect(result.created_at).toStrictEqual('2020-05-01T00:00:00Z');
187
+ expect(result.updated_at).toStrictEqual('2023-01-10T00:00:00Z');
188
+ expect(result.htmlUrl).toStrictEqual('https://gitlab.com/ns/myproject');
189
+ expect(result.name).toStrictEqual('myproject');
190
+ });
191
+
192
+ it('maps updated_at from last_activity_at (not updated_at)', () => {
193
+ const data = {
194
+ namespace: {
195
+ name: 'ns', web_url: '', avatar_url: ''
196
+ },
197
+ description: '',
198
+ created_at: '2020-01-01T00:00:00Z',
199
+ last_activity_at: '2023-06-15T08:00:00Z',
200
+ updated_at: 'should-be-ignored',
201
+ web_url: 'https://gitlab.com/ns/proj',
202
+ name: 'proj'
203
+ };
204
+
205
+ const result = GitUtils.gitlab.normalize.repo(data);
206
+
207
+ expect(result.updated_at).toStrictEqual('2023-06-15T08:00:00Z');
208
+ });
209
+ });
210
+
211
+ describe('gitUtils.gitlab.normalize.commit', () => {
212
+ it('maps commit fields from GitLab API response', () => {
213
+ const data = {
214
+ message: 'refactor: clean up code',
215
+ web_url: 'https://gitlab.com/ns/proj/-/commit/abc1234',
216
+ short_id: 'abc1234',
217
+ id: 'abc1234567890abcdef',
218
+ author_name: 'Jane Doe',
219
+ avatar_url: 'https://gitlab.com/uploads/jane.png',
220
+ committed_date: '2022-03-10T14:00:00Z'
221
+ };
222
+
223
+ const result = GitUtils.gitlab.normalize.commit(data);
224
+
225
+ expect(result.message).toStrictEqual('refactor: clean up code');
226
+ expect(result.htmlUrl).toStrictEqual('https://gitlab.com/ns/proj/-/commit/abc1234');
227
+ expect(result.sha).toStrictEqual('abc1234');
228
+ expect(result.commitId).toStrictEqual('abc1234567890abcdef');
229
+ expect(result.date).toStrictEqual('2022-03-10T14:00:00Z');
230
+ expect(result.isChecked).toStrictEqual(false);
231
+ });
232
+
233
+ it('maps author fields from GitLab API response', () => {
234
+ const data = {
235
+ message: 'feat: add feature',
236
+ web_url: 'https://gitlab.com/ns/proj/-/commit/xyz',
237
+ short_id: 'xyz',
238
+ id: 'xyzabcdef',
239
+ author_name: 'John Smith',
240
+ avatar_url: 'https://gitlab.com/uploads/john.png',
241
+ committed_date: '2022-04-01T00:00:00Z'
242
+ };
243
+
244
+ const result = GitUtils.gitlab.normalize.commit(data);
245
+
246
+ expect(result.author).toStrictEqual({
247
+ name: 'John Smith',
248
+ avatarUrl: 'https://gitlab.com/uploads/john.png',
249
+ htmlUrl: 'https://gitlab.com/ns/proj/-/commit/xyz'
250
+ });
251
+ });
252
+
253
+ it('uses web_url for both htmlUrl and author.htmlUrl', () => {
254
+ const data = {
255
+ message: 'fix: something',
256
+ web_url: 'https://gitlab.com/ns/proj/-/commit/def456',
257
+ short_id: 'def456',
258
+ id: 'def456abc',
259
+ author_name: 'Dev',
260
+ avatar_url: '',
261
+ committed_date: '2022-01-01T00:00:00Z'
262
+ };
263
+
264
+ const result = GitUtils.gitlab.normalize.commit(data);
265
+
266
+ expect(result.htmlUrl).toStrictEqual('https://gitlab.com/ns/proj/-/commit/def456');
267
+ expect((result.author as any).htmlUrl).toStrictEqual('https://gitlab.com/ns/proj/-/commit/def456');
268
+ });
269
+ });
270
+ });
@@ -0,0 +1,316 @@
1
+ import { Inactivity } from '@shell/utils/inactivity';
2
+
3
+ describe('inactivity', () => {
4
+ describe('inactivity class', () => {
5
+ let inactivity: Inactivity;
6
+
7
+ beforeEach(() => {
8
+ inactivity = new Inactivity();
9
+ });
10
+
11
+ describe('getSessionTokenName', () => {
12
+ it('returns undefined by default', () => {
13
+ expect(inactivity.getSessionTokenName()).toBeUndefined();
14
+ });
15
+
16
+ it('returns the token name after setSessionTokenName', () => {
17
+ inactivity.setSessionTokenName('my-token');
18
+ expect(inactivity.getSessionTokenName()).toStrictEqual('my-token');
19
+ });
20
+ });
21
+
22
+ describe('setSessionTokenName', () => {
23
+ it('sets the session token name', () => {
24
+ inactivity.setSessionTokenName('token-abc');
25
+ expect(inactivity.getSessionTokenName()).toStrictEqual('token-abc');
26
+ });
27
+
28
+ it('overwrites a previously set token name', () => {
29
+ inactivity.setSessionTokenName('first');
30
+ inactivity.setSessionTokenName('second');
31
+ expect(inactivity.getSessionTokenName()).toStrictEqual('second');
32
+ });
33
+ });
34
+
35
+ describe('getUserActivity', () => {
36
+ it('dispatches management/find with correct parameters', async() => {
37
+ const mockResult = { status: { expiresAt: '2026-01-01T00:00:00Z' } };
38
+ const mockStore = { dispatch: jest.fn().mockResolvedValue(mockResult) };
39
+
40
+ const result = await inactivity.getUserActivity(mockStore, 'my-token');
41
+
42
+ expect(mockStore.dispatch).toHaveBeenCalledWith('management/find', {
43
+ type: 'ext.cattle.io.useractivity',
44
+ id: 'my-token',
45
+ opt: {
46
+ force: true, watch: false, logoutOnError: false
47
+ }
48
+ });
49
+ expect(result).toStrictEqual(mockResult);
50
+ });
51
+
52
+ it('passes force=false when specified', async() => {
53
+ const mockResult = { status: { expiresAt: '2026-01-01T00:00:00Z' } };
54
+ const mockStore = { dispatch: jest.fn().mockResolvedValue(mockResult) };
55
+
56
+ await inactivity.getUserActivity(mockStore, 'my-token', false);
57
+
58
+ expect(mockStore.dispatch).toHaveBeenCalledWith('management/find', {
59
+ type: 'ext.cattle.io.useractivity',
60
+ id: 'my-token',
61
+ opt: {
62
+ force: false, watch: false, logoutOnError: false
63
+ }
64
+ });
65
+ });
66
+
67
+ it('dispatches auth/logout with sessionIdle=true on 401 error', async() => {
68
+ const mockStore = {
69
+ dispatch: jest.fn()
70
+ .mockRejectedValueOnce({ _status: 401 })
71
+ .mockResolvedValue('logged out')
72
+ };
73
+
74
+ const result = await inactivity.getUserActivity(mockStore, 'my-token');
75
+
76
+ expect(mockStore.dispatch).toHaveBeenCalledWith('auth/logout', { sessionIdle: true });
77
+ expect(result).toStrictEqual('logged out');
78
+ });
79
+
80
+ it('re-throws non-401 errors', async() => {
81
+ const mockStore = { dispatch: jest.fn().mockRejectedValue({ _status: 500, message: 'Server Error' }) };
82
+
83
+ await expect(inactivity.getUserActivity(mockStore, 'my-token')).rejects.toThrow(Error);
84
+ });
85
+ });
86
+
87
+ describe('updateUserActivity', () => {
88
+ it('sets spec with tokenId and seenAt and calls save', async() => {
89
+ const mockSaved = { status: { expiresAt: '2026-01-01T00:00:00Z' } };
90
+ const mockResource: any = { save: jest.fn().mockResolvedValue(mockSaved) };
91
+
92
+ const result = await inactivity.updateUserActivity(mockResource, 'my-token', '2026-01-01T00:00:00Z');
93
+
94
+ expect(mockResource.spec).toStrictEqual({
95
+ tokenId: 'my-token',
96
+ seenAt: '2026-01-01T00:00:00Z'
97
+ });
98
+ expect(mockResource.save).toHaveBeenCalledWith({ force: true });
99
+ expect(result).toStrictEqual(mockSaved);
100
+ });
101
+
102
+ it('omits seenAt from spec when empty string', async() => {
103
+ const mockSaved = { status: { expiresAt: '2026-01-01T00:00:00Z' } };
104
+ const mockResource: any = { save: jest.fn().mockResolvedValue(mockSaved) };
105
+
106
+ await inactivity.updateUserActivity(mockResource, 'my-token', '');
107
+
108
+ expect(mockResource.spec).toStrictEqual({ tokenId: 'my-token' });
109
+ });
110
+
111
+ it('re-throws errors from save', async() => {
112
+ const mockResource: any = { save: jest.fn().mockRejectedValue(new Error('save failed')) };
113
+
114
+ await expect(inactivity.updateUserActivity(mockResource, 'my-token', '2026-01-01T00:00:00Z')).rejects.toThrow(Error);
115
+ });
116
+ });
117
+
118
+ describe('parseTTLData', () => {
119
+ afterEach(() => {
120
+ jest.restoreAllMocks();
121
+ });
122
+
123
+ it('returns expiresAt from userActivityData.status', () => {
124
+ const expiresAt = '2030-01-01T01:00:00.000Z';
125
+ const now = new Date('2030-01-01T00:00:00.000Z').getTime();
126
+
127
+ jest.spyOn(Date, 'now').mockReturnValue(now);
128
+
129
+ const data: any = {
130
+ status: { expiresAt },
131
+ metadata: { name: 'token-1' }
132
+ };
133
+
134
+ const result = inactivity.parseTTLData(data);
135
+
136
+ expect(result.expiresAt).toStrictEqual(expiresAt);
137
+ });
138
+
139
+ it('returns sessionTokenName from userActivityData.metadata.name', () => {
140
+ const expiresAt = '2030-01-01T01:00:00.000Z';
141
+ const now = new Date('2030-01-01T00:00:00.000Z').getTime();
142
+
143
+ jest.spyOn(Date, 'now').mockReturnValue(now);
144
+
145
+ const data: any = {
146
+ status: { expiresAt },
147
+ metadata: { name: 'token-abc' }
148
+ };
149
+
150
+ const result = inactivity.parseTTLData(data);
151
+
152
+ expect(result.sessionTokenName).toStrictEqual('token-abc');
153
+ });
154
+
155
+ it('calculates courtesyTimer as 20% of threshold, capped at 300s', () => {
156
+ // 1 hour = 3600 seconds; threshold = 3600 - 3 = 3597s
157
+ // courtesyTimerVal = floor(3597 * 0.2) = floor(719.4) = 719
158
+ // courtesyTimer = min(719, 300) = 300
159
+ const now = new Date('2030-01-01T00:00:00.000Z').getTime();
160
+ const expiresAt = new Date(now + 3600 * 1000).toISOString(); // 1 hour later
161
+
162
+ jest.spyOn(Date, 'now').mockReturnValue(now);
163
+
164
+ const data: any = {
165
+ status: { expiresAt },
166
+ metadata: { name: 'token-1' }
167
+ };
168
+
169
+ const result = inactivity.parseTTLData(data);
170
+
171
+ expect(result.courtesyTimer).toStrictEqual(300);
172
+ });
173
+
174
+ it('calculates courtesyTimer as 20% when under 300s cap', () => {
175
+ // 100 seconds until expiry; threshold = 100 - 3 = 97s
176
+ // courtesyTimerVal = floor(97 * 0.2) = floor(19.4) = 19
177
+ // courtesyTimer = min(19, 300) = 19
178
+ const now = new Date('2030-01-01T00:00:00.000Z').getTime();
179
+ const expiresAt = new Date(now + 100 * 1000).toISOString();
180
+
181
+ jest.spyOn(Date, 'now').mockReturnValue(now);
182
+
183
+ const data: any = {
184
+ status: { expiresAt },
185
+ metadata: { name: 'token-1' }
186
+ };
187
+
188
+ const result = inactivity.parseTTLData(data);
189
+
190
+ expect(result.courtesyTimer).toStrictEqual(19);
191
+ });
192
+
193
+ it('courtesyCountdown equals courtesyTimer', () => {
194
+ const now = new Date('2030-01-01T00:00:00.000Z').getTime();
195
+ const expiresAt = new Date(now + 100 * 1000).toISOString();
196
+
197
+ jest.spyOn(Date, 'now').mockReturnValue(now);
198
+
199
+ const data: any = {
200
+ status: { expiresAt },
201
+ metadata: { name: 'token-1' }
202
+ };
203
+
204
+ const result = inactivity.parseTTLData(data);
205
+
206
+ expect(result.courtesyCountdown).toStrictEqual(result.courtesyTimer);
207
+ });
208
+
209
+ it('calculates showModalAfter = thresholdSeconds - courtesyTimer', () => {
210
+ // 100s; threshold = 97s; courtesyTimer = 19s; showModalAfter = 97 - 19 = 78
211
+ const now = new Date('2030-01-01T00:00:00.000Z').getTime();
212
+ const expiresAt = new Date(now + 100 * 1000).toISOString();
213
+
214
+ jest.spyOn(Date, 'now').mockReturnValue(now);
215
+
216
+ const data: any = {
217
+ status: { expiresAt },
218
+ metadata: { name: 'token-1' }
219
+ };
220
+
221
+ const result = inactivity.parseTTLData(data);
222
+
223
+ expect(result.showModalAfter).toStrictEqual(78);
224
+ });
225
+
226
+ it('calculates showModalAfter correctly with large TTL (1 hour)', () => {
227
+ // 3600s; threshold = 3597s; courtesyTimer = 300s (capped); showModalAfter = 3597 - 300 = 3297
228
+ const now = new Date('2030-01-01T00:00:00.000Z').getTime();
229
+ const expiresAt = new Date(now + 3600 * 1000).toISOString();
230
+
231
+ jest.spyOn(Date, 'now').mockReturnValue(now);
232
+
233
+ const data: any = {
234
+ status: { expiresAt },
235
+ metadata: { name: 'token-1' }
236
+ };
237
+
238
+ const result = inactivity.parseTTLData(data);
239
+
240
+ expect(result.showModalAfter).toStrictEqual(3297);
241
+ });
242
+
243
+ it('handles expired session (negative thresholdSeconds)', () => {
244
+ // Already expired by 10 seconds; threshold = -10 - 3 = -13s
245
+ // courtesyTimerVal = floor(-13 * 0.2) = floor(-2.6) = -3
246
+ // courtesyTimer = min(-3, 300) = -3
247
+ // showModalAfter = -13 - (-3) = -10
248
+ const now = new Date('2030-01-01T00:00:10.000Z').getTime();
249
+ const expiresAt = new Date('2030-01-01T00:00:00.000Z').toISOString();
250
+
251
+ jest.spyOn(Date, 'now').mockReturnValue(now);
252
+
253
+ const data: any = {
254
+ status: { expiresAt },
255
+ metadata: { name: 'token-1' }
256
+ };
257
+
258
+ const result = inactivity.parseTTLData(data);
259
+
260
+ expect(result.courtesyTimer).toStrictEqual(-3);
261
+ expect(result.showModalAfter).toStrictEqual(-10);
262
+ });
263
+
264
+ it('returns undefined for expiresAt when status.expiresAt is absent', () => {
265
+ const now = new Date('2030-01-01T00:00:00.000Z').getTime();
266
+
267
+ jest.spyOn(Date, 'now').mockReturnValue(now);
268
+
269
+ const data: any = {
270
+ status: {},
271
+ metadata: { name: 'token-1' }
272
+ };
273
+
274
+ const result = inactivity.parseTTLData(data);
275
+
276
+ expect(result.expiresAt).toBeUndefined();
277
+ });
278
+
279
+ it('returns undefined for sessionTokenName when metadata.name is absent', () => {
280
+ const now = new Date('2030-01-01T00:00:00.000Z').getTime();
281
+ const expiresAt = new Date(now + 100 * 1000).toISOString();
282
+
283
+ jest.spyOn(Date, 'now').mockReturnValue(now);
284
+
285
+ const data: any = {
286
+ status: { expiresAt },
287
+ metadata: {}
288
+ };
289
+
290
+ const result = inactivity.parseTTLData(data);
291
+
292
+ expect(result.sessionTokenName).toBeUndefined();
293
+ });
294
+
295
+ it('handles exactly 5 minute threshold (boundary: courtesyTimer not capped)', () => {
296
+ // 300s; threshold = 300 - 3 = 297s
297
+ // courtesyTimerVal = floor(297 * 0.2) = floor(59.4) = 59
298
+ // courtesyTimer = min(59, 300) = 59
299
+ const now = new Date('2030-01-01T00:00:00.000Z').getTime();
300
+ const expiresAt = new Date(now + 300 * 1000).toISOString();
301
+
302
+ jest.spyOn(Date, 'now').mockReturnValue(now);
303
+
304
+ const data: any = {
305
+ status: { expiresAt },
306
+ metadata: { name: 'token-1' }
307
+ };
308
+
309
+ const result = inactivity.parseTTLData(data);
310
+
311
+ expect(result.courtesyTimer).toStrictEqual(59);
312
+ expect(result.showModalAfter).toStrictEqual(238);
313
+ });
314
+ });
315
+ });
316
+ });