@rancher/shell 0.3.16 → 0.3.18

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 (174) hide show
  1. package/assets/images/wechat-qr-code.jpg +0 -0
  2. package/assets/translations/en-us.yaml +75 -16
  3. package/assets/translations/zh-hans.yaml +151 -15
  4. package/chart/__tests__/S3.test.ts +50 -0
  5. package/chart/rancher-backup/S3.vue +21 -0
  6. package/chart/rancher-backup/index.vue +4 -0
  7. package/components/AsyncButton.vue +1 -1
  8. package/components/CommunityLinks.vue +1 -0
  9. package/components/FileDiff.vue +92 -85
  10. package/components/Inactivity.vue +10 -0
  11. package/components/LazyImage.vue +2 -2
  12. package/components/PromptRestore.vue +7 -5
  13. package/components/ResourceDetail/Masthead.vue +1 -1
  14. package/components/ResourceDetail/index.vue +8 -14
  15. package/components/ResourceList/index.vue +1 -1
  16. package/components/ResourceTable.vue +50 -2
  17. package/components/YamlEditor.vue +1 -0
  18. package/components/__tests__/PromptRestore.test.ts +72 -0
  19. package/components/auth/AzureWarning.vue +1 -1
  20. package/components/auth/RoleDetailEdit.vue +1 -0
  21. package/components/fleet/FleetResources.vue +3 -64
  22. package/components/form/FileImageSelector.vue +9 -0
  23. package/components/form/FileSelector.vue +2 -1
  24. package/components/form/MatchExpressions.vue +1 -3
  25. package/components/form/NameNsDescription.vue +28 -12
  26. package/components/form/NodeAffinity.vue +2 -2
  27. package/components/form/PodAffinity.vue +2 -2
  28. package/components/form/ResourceTabs/index.vue +8 -2
  29. package/components/form/Select.vue +16 -0
  30. package/components/form/__tests__/FileImageSelector.test.ts +42 -0
  31. package/components/form/__tests__/FileSelector.test.ts +76 -0
  32. package/components/form/__tests__/NodeAffinity.test.ts +38 -0
  33. package/components/form/__tests__/PodAffinity.test.ts +46 -0
  34. package/components/formatter/ClusterLink.vue +8 -4
  35. package/components/formatter/ClusterProvider.vue +3 -1
  36. package/components/formatter/ImageName.vue +23 -0
  37. package/components/formatter/PodImages.vue +7 -1
  38. package/components/formatter/__tests__/ClusterLink.test.ts +101 -0
  39. package/components/formatter/__tests__/ClusterProvider.test.ts +24 -0
  40. package/components/nav/Header.vue +2 -2
  41. package/components/nav/WindowManager/ContainerShell.vue +60 -36
  42. package/components/nav/WindowManager/__tests__/ContainerShell.test.ts +561 -0
  43. package/config/__test__/home-links.test.ts +62 -0
  44. package/config/home-links.js +15 -3
  45. package/config/labels-annotations.js +7 -2
  46. package/config/persistentVolume.ts +108 -0
  47. package/config/product/manager.js +5 -1
  48. package/config/router.js +0 -4
  49. package/config/settings.ts +4 -0
  50. package/config/table-headers.js +6 -5
  51. package/config/types.js +2 -0
  52. package/config/uiplugins.js +50 -5
  53. package/core/plugin-helpers.js +39 -15
  54. package/core/plugin.ts +9 -0
  55. package/core/plugins.js +1 -1
  56. package/core/types-provisioning.ts +253 -0
  57. package/core/types.ts +21 -3
  58. package/detail/autoscaling.horizontalpodautoscaler/index.vue +50 -1
  59. package/detail/fleet.cattle.io.gitrepo.vue +10 -2
  60. package/detail/node.vue +6 -6
  61. package/detail/pod.vue +38 -9
  62. package/detail/provisioning.cattle.io.cluster.vue +46 -7
  63. package/detail/workload/index.vue +49 -18
  64. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +62 -0
  65. package/edit/__tests__/ui.cattle.io.navlink.test.ts +110 -0
  66. package/edit/auth/github.vue +1 -0
  67. package/edit/autoscaling.horizontalpodautoscaler/hpa-scaling-rule.vue +130 -0
  68. package/edit/autoscaling.horizontalpodautoscaler/index.vue +79 -0
  69. package/edit/fleet.cattle.io.clustergroup.vue +14 -3
  70. package/edit/fleet.cattle.io.gitrepo.vue +18 -1
  71. package/edit/namespace.vue +9 -1
  72. package/edit/networking.k8s.io.ingress/RulePath.vue +0 -2
  73. package/edit/persistentvolume/__tests__/persistentvolume.test.ts +82 -0
  74. package/edit/persistentvolume/index.vue +2 -1
  75. package/edit/persistentvolume/plugins/csi.vue +3 -1
  76. package/edit/persistentvolume/plugins/longhorn.vue +12 -12
  77. package/edit/provisioning.cattle.io.cluster/AgentConfiguration.vue +1 -30
  78. package/edit/provisioning.cattle.io.cluster/RegistryConfigs.vue +15 -11
  79. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +79 -1
  80. package/edit/provisioning.cattle.io.cluster/index.vue +53 -1
  81. package/edit/provisioning.cattle.io.cluster/rke2.vue +335 -151
  82. package/edit/storage.k8s.io.storageclass/index.vue +1 -2
  83. package/edit/ui.cattle.io.navlink.vue +213 -186
  84. package/initialize/App.js +3 -13
  85. package/initialize/layouts.ts +26 -0
  86. package/layouts/default.vue +1 -1
  87. package/list/group.principal.vue +1 -1
  88. package/list/provisioning.cattle.io.cluster.vue +8 -1
  89. package/middleware/authenticated.js +101 -5
  90. package/mixins/brand.js +39 -3
  91. package/mixins/child-hook.js +2 -2
  92. package/mixins/create-edit-view/impl.js +4 -4
  93. package/models/chart.js +1 -1
  94. package/models/fleet.cattle.io.cluster.js +33 -4
  95. package/models/fleet.cattle.io.gitrepo.js +113 -38
  96. package/models/management.cattle.io.kontainerdriver.js +14 -0
  97. package/models/persistentvolume.js +2 -111
  98. package/models/pod.js +30 -0
  99. package/models/provisioning.cattle.io.cluster.js +9 -1
  100. package/models/rke.cattle.io.etcdsnapshot.js +10 -7
  101. package/package.json +2 -2
  102. package/pages/about.vue +8 -2
  103. package/pages/auth/login.vue +1 -1
  104. package/pages/auth/logout.vue +11 -3
  105. package/pages/c/_cluster/apps/charts/index.vue +5 -2
  106. package/pages/c/_cluster/apps/charts/install.vue +5 -0
  107. package/pages/c/_cluster/auth/group.principal/assign-edit.vue +1 -1
  108. package/pages/c/_cluster/auth/roles/index.vue +1 -1
  109. package/pages/c/_cluster/explorer/index.vue +2 -11
  110. package/pages/c/_cluster/manager/cloudCredential/_id.vue +0 -1
  111. package/pages/c/_cluster/manager/cloudCredential/create.vue +0 -1
  112. package/pages/c/_cluster/settings/brand.vue +11 -8
  113. package/pages/c/_cluster/uiplugins/AddExtensionRepos.vue +177 -0
  114. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +19 -3
  115. package/pages/c/_cluster/uiplugins/RemoveUIPlugins.vue +90 -21
  116. package/pages/c/_cluster/uiplugins/SetupUIPlugins.vue +107 -37
  117. package/pages/c/_cluster/uiplugins/index.vue +160 -44
  118. package/pages/docs/_doc.vue +9 -3
  119. package/pages/home.vue +6 -6
  120. package/pages/support/index.vue +10 -4
  121. package/pkg/auto-import.js +1 -1
  122. package/plugins/clean-tooltip-directive.js +1 -1
  123. package/plugins/dashboard-store/__tests__/actions.spec.ts +165 -0
  124. package/plugins/dashboard-store/__tests__/getters.spec.ts +100 -0
  125. package/plugins/dashboard-store/__tests__/{mutations.spec.js → mutations.spec.ts} +2 -2
  126. package/plugins/dashboard-store/actions.js +1 -1
  127. package/plugins/dashboard-store/resource-class.js +39 -2
  128. package/plugins/plugin.js +9 -1
  129. package/plugins/steve/__tests__/getters.spec.ts +93 -0
  130. package/plugins/steve/getters.js +21 -1
  131. package/plugins/steve/subscribe.js +1 -3
  132. package/rancher-components/BadgeState/BadgeState.vue +5 -1
  133. package/rancher-components/Banner/Banner.test.ts +51 -1
  134. package/rancher-components/Banner/Banner.vue +134 -53
  135. package/rancher-components/Card/Card.test.ts +37 -0
  136. package/rancher-components/Card/Card.vue +24 -7
  137. package/rancher-components/Form/Checkbox/Checkbox.test.ts +20 -29
  138. package/rancher-components/Form/Checkbox/Checkbox.vue +45 -20
  139. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +2 -8
  140. package/rancher-components/Form/LabeledInput/LabeledInput.vue +22 -10
  141. package/rancher-components/Form/Radio/RadioButton.test.ts +31 -0
  142. package/rancher-components/Form/Radio/RadioButton.vue +30 -13
  143. package/rancher-components/Form/Radio/RadioGroup.vue +26 -7
  144. package/rancher-components/Form/TextArea/TextAreaAutoGrow.vue +7 -6
  145. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.test.ts +25 -38
  146. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +23 -11
  147. package/rancher-components/LabeledTooltip/LabeledTooltip.vue +19 -5
  148. package/rancher-components/StringList/StringList.test.ts +453 -49
  149. package/rancher-components/StringList/StringList.vue +44 -26
  150. package/scripts/extension/publish +2 -2
  151. package/scripts/typegen.sh +11 -2
  152. package/server/server-middleware.js +4 -12
  153. package/store/index.js +14 -3
  154. package/store/prefs.js +0 -3
  155. package/store/store-types.js +2 -0
  156. package/store/type-map.js +17 -29
  157. package/types/api.d.ts +1 -0
  158. package/types/fleet.d.ts +1 -0
  159. package/types/shell/index.d.ts +931 -85
  160. package/types/userPreferences.d.ts +1 -1
  161. package/utils/__mocks__/socket.js +21 -0
  162. package/utils/grafana.js +23 -11
  163. package/utils/kube.js +9 -0
  164. package/utils/object.js +27 -0
  165. package/utils/selector.js +2 -1
  166. package/utils/settings.ts +2 -2
  167. package/utils/validators/formRules/index.ts +3 -3
  168. package/vue.config.js +3 -2
  169. package/components/.DS_Store +0 -0
  170. package/components/__tests__/.DS_Store +0 -0
  171. package/creators/pkg/package-lock.json +0 -37
  172. package/pages/safeMode.vue +0 -17
  173. package/plugins/steve/urloptions.js +0 -47
  174. package/yarn-error.log +0 -196
@@ -0,0 +1,561 @@
1
+ import { mount, Wrapper } from '@vue/test-utils';
2
+ import ContainerShell from '@shell/components/nav/WindowManager/ContainerShell.vue';
3
+ import Socket, {
4
+ addEventListener, EVENT_CONNECTED, EVENT_CONNECTING, EVENT_DISCONNECTED, EVENT_MESSAGE, EVENT_CONNECT_ERROR
5
+ } from '@shell/utils/socket';
6
+
7
+ jest.mock('@shell/utils/socket');
8
+ jest.mock('@shell/utils/crypto', () => {
9
+ const originalModule = jest.requireActual('@shell/utils/crypto');
10
+
11
+ return {
12
+ __esModule: true,
13
+ ...originalModule,
14
+ base64Decode: jest.fn().mockImplementation((str:String) => str)
15
+ };
16
+ });
17
+
18
+ describe('component: ContainerShell', () => {
19
+ const action = jest.fn();
20
+ const translate = jest.fn();
21
+ const schemaFor = jest.fn();
22
+ const onData = jest.fn();
23
+ const loadAddon = jest.fn();
24
+ const open = jest.fn();
25
+ const focus = jest.fn();
26
+ const fit = jest.fn();
27
+ const proposeDimensions = jest.fn().mockImplementation(() => {
28
+ return { rows: 1 };
29
+ });
30
+ const write = jest.fn();
31
+ const reset = jest.fn();
32
+
33
+ jest.mock(/* webpackChunkName: "xterm" */ 'xterm', () => {
34
+ return {
35
+ Terminal: class {
36
+ onData = onData;
37
+ loadAddon = loadAddon;
38
+ open = open;
39
+ focus = focus;
40
+ write = write;
41
+ reset = reset
42
+ }
43
+ };
44
+ });
45
+ jest.mock(/* webpackChunkName: "xterm" */ 'xterm-addon-fit', () => {
46
+ return {
47
+ FitAddon: class {
48
+ fit = fit
49
+ proposeDimensions = proposeDimensions
50
+ }
51
+ };
52
+ });
53
+
54
+ const defaultContainerShellParams = {
55
+ propsData: {
56
+ tab: {},
57
+ active: true,
58
+ height: 1000,
59
+ pod: {
60
+ spec: { nodeName: 'nodeId' },
61
+ links: { view: 'url' },
62
+ os: 'linux'
63
+ },
64
+ initialContainer: 'containerId'
65
+ },
66
+ stubs: ['resize-observer'],
67
+ mocks: {
68
+ $store: {
69
+ dispatch: action,
70
+ getters: {
71
+ 'i18n/t': translate,
72
+ 'cluster/schemaFor': schemaFor
73
+ }
74
+ }
75
+ }
76
+ };
77
+
78
+ const resetMocks = () => {
79
+ // Clear all instances and calls to constructor and all methods:
80
+ jest.clearAllMocks();
81
+ defaultContainerShellParams.propsData.pod.os = 'linux';
82
+ };
83
+
84
+ const wrapperPostMounted = async(params: Object) => {
85
+ const wrapper = await mount(ContainerShell, params);
86
+
87
+ // these awaits are all associated with the various async dyamic imports on xterm
88
+ await wrapper.vm.$nextTick();
89
+ await wrapper.vm.$nextTick();
90
+ await wrapper.vm.$nextTick();
91
+ await wrapper.vm.$nextTick();
92
+
93
+ return wrapper;
94
+ };
95
+
96
+ it.todo('test that we are calling the xterm terminal and fitAddon class method mocks correctly');
97
+
98
+ it('creates a window on the page', async() => {
99
+ resetMocks();
100
+ const wrapper: Wrapper<InstanceType<typeof ContainerShell> & { [key: string]: any }> = await wrapperPostMounted(defaultContainerShellParams);
101
+ const windowElement = wrapper.find('div.window');
102
+
103
+ expect(windowElement.exists()).toBe(true);
104
+ });
105
+
106
+ it('the find action for the node is called if schemaFor finds a schema for NODE', async() => {
107
+ resetMocks();
108
+ const testSchemaFindsSchemaParams = {
109
+ ...defaultContainerShellParams,
110
+ mocks: {
111
+ ...defaultContainerShellParams.mocks,
112
+ $store: {
113
+ ...defaultContainerShellParams.mocks.$store,
114
+ getters: {
115
+ ...defaultContainerShellParams.mocks.$store.getters,
116
+ 'cluster/schemaFor': jest.fn().mockImplementation(() => true)
117
+ }
118
+ }
119
+ }
120
+ };
121
+
122
+ await wrapperPostMounted(testSchemaFindsSchemaParams);
123
+
124
+ const actionParams = action.mock.calls[0];
125
+
126
+ expect(action.mock.calls).toHaveLength(1);
127
+ expect(actionParams[0]).toBe('cluster/find');
128
+ expect(actionParams[1]).toStrictEqual({
129
+ id: 'nodeId',
130
+ type: 'node'
131
+ });
132
+ });
133
+
134
+ it('the translate getter for the ...', async() => {
135
+ resetMocks();
136
+ await wrapperPostMounted(defaultContainerShellParams);
137
+ const firstTranslate = translate.mock.calls[0];
138
+ const secondTranslate = translate.mock.calls[1];
139
+
140
+ expect(translate.mock.calls).toHaveLength(2);
141
+ expect(firstTranslate[0]).toBe('wm.containerShell.clear');
142
+ expect(firstTranslate[1]).toStrictEqual({});
143
+ expect(secondTranslate[0]).toBe('wm.connection.disconnected');
144
+ expect(secondTranslate[1]).toStrictEqual({});
145
+ });
146
+
147
+ it('the socket is instantiated', async() => {
148
+ resetMocks();
149
+ const wrapper = await wrapperPostMounted(defaultContainerShellParams);
150
+
151
+ const socketParams = Socket.mock.calls[0][0]
152
+ .split('?')[1]
153
+ .split('&')
154
+ .reduce((paramMap: Object, param: [String, String]) => {
155
+ const [key, value] = param.split('=');
156
+
157
+ return {
158
+ ...paramMap,
159
+ [key]: decodeURIComponent(value)
160
+ };
161
+ }, {});
162
+
163
+ expect(Socket.mock.calls).toHaveLength(1);
164
+ expect(socketParams.command).toBe('TERM=xterm-256color; export TERM; [ -x /bin/bash ] && ([ -x /usr/bin/script ] && /usr/bin/script -q -c "/bin/bash" /dev/null || exec /bin/bash) || exec /bin/sh');
165
+ expect(wrapper.vm.os).toBe('linux');
166
+ });
167
+
168
+ it('the sockets events are bound', async() => {
169
+ resetMocks();
170
+ await wrapperPostMounted(defaultContainerShellParams);
171
+
172
+ const addEventListenerCalls = addEventListener.mock.calls;
173
+
174
+ expect(addEventListenerCalls).toHaveLength(5);
175
+ expect(addEventListenerCalls[0][0]).toBe(EVENT_CONNECTING);
176
+ expect(addEventListenerCalls[1][0]).toBe(EVENT_CONNECT_ERROR);
177
+ expect(addEventListenerCalls[2][0]).toBe(EVENT_CONNECTED);
178
+ expect(addEventListenerCalls[3][0]).toBe(EVENT_DISCONNECTED);
179
+ expect(addEventListenerCalls[4][0]).toBe(EVENT_MESSAGE);
180
+ });
181
+
182
+ it('the socket connecting event sets data props correctly', async() => {
183
+ resetMocks();
184
+ const wrapper = await wrapperPostMounted(defaultContainerShellParams);
185
+
186
+ const addEventListenerCalls = addEventListener.mock.calls;
187
+ const eventConnecting = addEventListenerCalls[0][1];
188
+
189
+ eventConnecting();
190
+
191
+ expect(wrapper.vm.isOpen).toBe(false);
192
+ expect(wrapper.vm.isOpening).toBe(true);
193
+ expect(wrapper.vm.errorMsg).toBe('');
194
+ expect(wrapper.vm.os).toBe('linux');
195
+ });
196
+
197
+ it('the socket connect error event sets data props correctly and calls the console', async() => {
198
+ resetMocks();
199
+ const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
200
+ const wrapper = await wrapperPostMounted(defaultContainerShellParams);
201
+ const errorMessage = 'eventConnectError';
202
+
203
+ const addEventListenerCalls = addEventListener.mock.calls;
204
+ const eventConnecting = addEventListenerCalls[0][1];
205
+ const eventConnectError = addEventListenerCalls[1][1];
206
+
207
+ eventConnecting();
208
+ eventConnectError(errorMessage);
209
+
210
+ expect(consoleError.mock.calls[0][0]).toBe('Connect Error');
211
+ expect(consoleError.mock.calls[0][1]).toBe(errorMessage);
212
+ expect(wrapper.vm.isOpen).toBe(false);
213
+ expect(wrapper.vm.isOpening).toBe(false);
214
+ expect(wrapper.vm.errorMsg).toBe('');
215
+ expect(wrapper.vm.os).toBe('linux');
216
+ });
217
+
218
+ it('the socket connected event sets data props correctly', async() => {
219
+ resetMocks();
220
+ const wrapper = await wrapperPostMounted(defaultContainerShellParams);
221
+
222
+ const addEventListenerCalls = addEventListener.mock.calls;
223
+ const eventConnecting = addEventListenerCalls[0][1];
224
+ const eventConnected = addEventListenerCalls[2][1];
225
+
226
+ eventConnecting();
227
+ eventConnected();
228
+
229
+ expect(wrapper.vm.isOpen).toBe(true);
230
+ expect(wrapper.vm.isOpening).toBe(false);
231
+ expect(wrapper.vm.errorMsg).toBe('');
232
+ expect(wrapper.vm.os).toBe('linux');
233
+ });
234
+
235
+ it.todo('test that fit and flush are operating properly');
236
+ it.todo('test that we are properly feeding the terminal the commandOnFirstConnect prop correctly on connected');
237
+
238
+ it('the socket message event sets data props correctly', async() => {
239
+ resetMocks();
240
+ const wrapper = await wrapperPostMounted(defaultContainerShellParams);
241
+
242
+ const addEventListenerCalls = addEventListener.mock.calls;
243
+ const eventConnecting = addEventListenerCalls[0][1];
244
+ const eventConnected = addEventListenerCalls[2][1];
245
+ const eventMessage = addEventListenerCalls[4][1];
246
+
247
+ eventConnecting();
248
+ eventConnected();
249
+ eventMessage({ detail: { data: '1noError' } });
250
+
251
+ expect(wrapper.vm.isOpen).toBe(true);
252
+ expect(wrapper.vm.isOpening).toBe(false);
253
+ expect(wrapper.vm.errorMsg).toBe('');
254
+ expect(wrapper.vm.os).toBe('linux');
255
+ });
256
+
257
+ it('the socket message event sets data props correctly and call the console on error', async() => {
258
+ resetMocks();
259
+ const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
260
+ const wrapper = await wrapperPostMounted(defaultContainerShellParams);
261
+ const errorMessage = 'eventMessageError';
262
+
263
+ const addEventListenerCalls = addEventListener.mock.calls;
264
+ const eventConnecting = addEventListenerCalls[0][1];
265
+ const eventConnected = addEventListenerCalls[2][1];
266
+ const eventMessage = addEventListenerCalls[4][1];
267
+
268
+ eventConnecting();
269
+ eventConnected();
270
+ eventMessage({ detail: { data: `3${ errorMessage }` } });
271
+
272
+ expect(consoleError.mock.calls[0][0]).toBe(errorMessage);
273
+ expect(wrapper.vm.isOpen).toBe(true);
274
+ expect(wrapper.vm.isOpening).toBe(false);
275
+ expect(wrapper.vm.errorMsg).toBe(errorMessage);
276
+ expect(wrapper.vm.os).toBe('linux');
277
+ });
278
+
279
+ it('the socket disconnect event without an error sets data props correctly', async() => {
280
+ resetMocks();
281
+ const wrapper = await wrapperPostMounted(defaultContainerShellParams);
282
+
283
+ const addEventListenerCalls = addEventListener.mock.calls;
284
+ const eventConnecting = addEventListenerCalls[0][1];
285
+ const eventConnected = addEventListenerCalls[2][1];
286
+ const eventDisconnected = addEventListenerCalls[3][1];
287
+
288
+ eventConnecting();
289
+ eventConnected();
290
+ eventDisconnected();
291
+
292
+ expect(wrapper.vm.isOpen).toBe(false);
293
+ expect(wrapper.vm.isOpening).toBe(false);
294
+ expect(wrapper.vm.errorMsg).toBe('');
295
+ expect(wrapper.vm.os).toBe('linux');
296
+ });
297
+
298
+ it('the socket disconnect event with an error sets data props correctly and attempts a second connect', async() => {
299
+ resetMocks();
300
+ const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
301
+ const connect = jest.spyOn(ContainerShell.methods, 'connect');
302
+ const wrapper = await wrapperPostMounted(defaultContainerShellParams);
303
+ const errorMessage = 'eventMessageError';
304
+
305
+ const addEventListenerCalls = addEventListener.mock.calls;
306
+ const eventConnecting = addEventListenerCalls[0][1];
307
+ const eventConnected = addEventListenerCalls[2][1];
308
+ const eventMessage = addEventListenerCalls[4][1];
309
+ const eventDisconnected = addEventListenerCalls[3][1];
310
+
311
+ eventConnecting();
312
+ eventConnected(); // for whatever reason, when this is called the mock on addEventListener doesn't clear it's calls...
313
+ eventMessage({ detail: { data: `3${ errorMessage }` } });
314
+
315
+ // we start with 2 backup shells but remove whichever one we already used
316
+ expect(wrapper.vm.backupShells).toHaveLength(1);
317
+
318
+ eventDisconnected();
319
+
320
+ expect(consoleError.mock.calls[0][0]).toBe(errorMessage);
321
+ expect(wrapper.vm.isOpen).toBe(false);
322
+ expect(wrapper.vm.isOpening).toBe(false);
323
+ expect(wrapper.vm.errorMsg).toBe('eventMessageError');
324
+ // the backup shell that was leftover was windows so it became the new os in dataprops
325
+ expect(wrapper.vm.os).toBe('windows');
326
+ // but we still didn't write it to the pod itself since we don't know if it worked
327
+ expect(defaultContainerShellParams.propsData.pod.os).toBeUndefined();
328
+ // we can see here that we removed that last backup shell because we're attempting to use it now
329
+ expect(wrapper.vm.backupShells).toHaveLength(0);
330
+ expect(connect.mock.calls).toHaveLength(2);
331
+ });
332
+
333
+ it('the socket disconnect event fires twice, sets data props correctly, and only attempts two connects if the pod os is linux', async() => {
334
+ resetMocks();
335
+ const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
336
+ const connect = jest.spyOn(ContainerShell.methods, 'connect');
337
+ const wrapper = await wrapperPostMounted(defaultContainerShellParams);
338
+ const linuxErrorMessage = 'eventLinuxMessageError';
339
+ const windowsErrorMessage = 'eventWindowsMessageError';
340
+
341
+ const addEventListenerCalls = addEventListener.mock.calls;
342
+ const eventConnecting = addEventListenerCalls[0][1];
343
+ const eventConnected = addEventListenerCalls[2][1];
344
+ const eventMessage = addEventListenerCalls[4][1];
345
+ const eventDisconnected = addEventListenerCalls[3][1];
346
+
347
+ expect(wrapper.vm.backupShells).toHaveLength(1);
348
+
349
+ eventConnecting();
350
+ eventConnected();
351
+ eventMessage({ detail: { data: `3${ linuxErrorMessage }` } });
352
+ eventDisconnected();
353
+
354
+ expect(wrapper.vm.backupShells).toHaveLength(0);
355
+ expect(wrapper.vm.os).toBe('windows');
356
+ // the pod os was 'linux' but we cleared it out since that didn't work
357
+ expect(defaultContainerShellParams.propsData.pod.os).toBeUndefined();
358
+ expect(connect.mock.calls).toHaveLength(2);
359
+
360
+ eventConnecting();
361
+ eventConnected();
362
+ eventMessage({ detail: { data: `3${ windowsErrorMessage }` } });
363
+ eventDisconnected();
364
+
365
+ expect(consoleError.mock.calls[0][0]).toBe(linuxErrorMessage);
366
+ expect(consoleError.mock.calls[1][0]).toBe(windowsErrorMessage);
367
+ expect(wrapper.vm.isOpen).toBe(false);
368
+ expect(wrapper.vm.isOpening).toBe(false);
369
+ expect(wrapper.vm.errorMsg).toBe(windowsErrorMessage);
370
+ expect(wrapper.vm.os).toBe('windows');
371
+ // we never found a shell that worked so we're going to leave the pod os as undefined
372
+ expect(defaultContainerShellParams.propsData.pod.os).toBeUndefined();
373
+ // we're out of backupShells now so we're not going to retry after that second disconnect
374
+ expect(connect.mock.calls).toHaveLength(2);
375
+
376
+ resetMocks();
377
+ });
378
+
379
+ it('the socket disconnect event fires twice, sets data props correctly, and only attempts two connects if the pod os is undefined', async() => {
380
+ resetMocks();
381
+ const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
382
+ const connect = jest.spyOn(ContainerShell.methods, 'connect');
383
+ const testUndefinedOsParams = {
384
+ ...defaultContainerShellParams,
385
+ propsData: {
386
+ ...defaultContainerShellParams.propsData,
387
+ pod: {
388
+ ...defaultContainerShellParams.propsData.pod,
389
+ os: undefined
390
+ }
391
+ }
392
+ };
393
+ const wrapper = await wrapperPostMounted(testUndefinedOsParams);
394
+ const linuxErrorMessage = 'eventLinuxMessageError';
395
+ const windowsErrorMessage = 'eventWindowsMessageError';
396
+
397
+ const addEventListenerCalls = addEventListener.mock.calls;
398
+ const eventConnecting = addEventListenerCalls[0][1];
399
+ const eventConnected = addEventListenerCalls[2][1];
400
+ const eventMessage = addEventListenerCalls[4][1];
401
+ const eventDisconnected = addEventListenerCalls[3][1];
402
+
403
+ expect(wrapper.vm.backupShells).toHaveLength(1);
404
+ expect(wrapper.vm.os).toBe('linux');
405
+ expect(testUndefinedOsParams.propsData.pod.os).toBeUndefined();
406
+
407
+ eventConnecting();
408
+ eventConnected();
409
+ eventMessage({ detail: { data: `3${ linuxErrorMessage }` } });
410
+ eventDisconnected();
411
+
412
+ expect(wrapper.vm.backupShells).toHaveLength(0);
413
+ expect(wrapper.vm.os).toBe('windows');
414
+ expect(testUndefinedOsParams.propsData.pod.os).toBeUndefined();
415
+
416
+ eventConnecting();
417
+ eventConnected();
418
+ eventMessage({ detail: { data: `3${ windowsErrorMessage }` } });
419
+ eventDisconnected();
420
+
421
+ expect(consoleError.mock.calls[0][0]).toBe(linuxErrorMessage);
422
+ expect(consoleError.mock.calls[1][0]).toBe(windowsErrorMessage);
423
+ expect(wrapper.vm.isOpen).toBe(false);
424
+ expect(wrapper.vm.isOpening).toBe(false);
425
+ expect(wrapper.vm.errorMsg).toBe(windowsErrorMessage);
426
+ expect(wrapper.vm.os).toBe('windows');
427
+ expect(testUndefinedOsParams.propsData.pod.os).toBeUndefined();
428
+ expect(connect.mock.calls).toHaveLength(2);
429
+
430
+ resetMocks();
431
+ });
432
+
433
+ it('the socket disconnect event fires twice, sets data props correctly, and only attempts two connects, and sets the pod os if the pod os is initially undefined and connects on the second attempt', async() => {
434
+ resetMocks();
435
+ const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
436
+ const connect = jest.spyOn(ContainerShell.methods, 'connect');
437
+ const testUndefinedOsParams = {
438
+ ...defaultContainerShellParams,
439
+ propsData: {
440
+ ...defaultContainerShellParams.propsData,
441
+ pod: {
442
+ ...defaultContainerShellParams.propsData.pod,
443
+ os: undefined
444
+ }
445
+ }
446
+ };
447
+ const wrapper = await wrapperPostMounted(testUndefinedOsParams);
448
+ const linuxErrorMessage = 'eventLinuxMessageError';
449
+ const windowsShellMessage = 'eventWindowsMessageShell';
450
+
451
+ const addEventListenerCalls = addEventListener.mock.calls;
452
+ const eventConnecting = addEventListenerCalls[0][1];
453
+ const eventConnected = addEventListenerCalls[2][1];
454
+ const eventMessage = addEventListenerCalls[4][1];
455
+ const eventDisconnected = addEventListenerCalls[3][1];
456
+
457
+ expect(wrapper.vm.backupShells).toHaveLength(1);
458
+
459
+ eventConnecting();
460
+ eventConnected();
461
+ eventMessage({ detail: { data: `3${ linuxErrorMessage }` } });
462
+ eventDisconnected();
463
+
464
+ expect(wrapper.vm.backupShells).toHaveLength(0);
465
+ expect(wrapper.vm.os).toBe('windows');
466
+ expect(testUndefinedOsParams.propsData.pod.os).toBeUndefined();
467
+ expect(wrapper.vm.errorMsg).toBe(linuxErrorMessage);
468
+
469
+ eventConnecting();
470
+ eventConnected();
471
+ eventMessage({ detail: { data: `1${ windowsShellMessage }` } });
472
+
473
+ expect(consoleError.mock.calls[0][0]).toBe(linuxErrorMessage);
474
+ expect(consoleError.mock.calls[1]).toBeUndefined();
475
+ expect(wrapper.vm.isOpen).toBe(true);
476
+ expect(wrapper.vm.isOpening).toBe(false);
477
+ expect(wrapper.vm.errorMsg).toBe('');
478
+ expect(wrapper.vm.os).toBe('windows');
479
+ // the second shell worked so we're going to set it on the pod itself so if we need to connect again we'll just use the correct shell on the first attempt
480
+ expect(testUndefinedOsParams.propsData.pod.os).toBe('windows');
481
+ expect(connect.mock.calls).toHaveLength(2);
482
+
483
+ resetMocks();
484
+ });
485
+
486
+ it('the socket disconnect event fires 3 times, sets data props correctly, and only attempts 3 connects if the pod os is defined at the pods parent node', async() => {
487
+ resetMocks();
488
+ const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
489
+ const connect = jest.spyOn(ContainerShell.methods, 'connect');
490
+ const testNodeDefinedOsParams = {
491
+ ...defaultContainerShellParams,
492
+ propsData: {
493
+ ...defaultContainerShellParams.propsData,
494
+ pod: {
495
+ ...defaultContainerShellParams.propsData.pod,
496
+ _os: 'linux',
497
+ get os(): string {
498
+ return 'linux';
499
+ },
500
+ set os(os: string) {
501
+ this._os = os;
502
+ }
503
+ }
504
+ }
505
+ };
506
+
507
+ const wrapper = await wrapperPostMounted(testNodeDefinedOsParams);
508
+ const linuxErrorMessage = 'eventLinuxMessageError';
509
+
510
+ const addEventListenerCalls = addEventListener.mock.calls;
511
+ const eventConnecting = addEventListenerCalls[0][1];
512
+ const eventConnected = addEventListenerCalls[2][1];
513
+ const eventMessage = addEventListenerCalls[4][1];
514
+ const eventDisconnected = addEventListenerCalls[3][1];
515
+
516
+ expect(wrapper.vm.backupShells).toHaveLength(1);
517
+
518
+ eventConnecting();
519
+ eventConnected();
520
+ eventMessage({ detail: { data: `3${ linuxErrorMessage }` } });
521
+ eventDisconnected();
522
+
523
+ // the parent node's os overrides the _os field in the pod so it didn't change on the previous failure and we know it is correct, thus we're not burning down our backup shells and just retrying the same shell
524
+ expect(wrapper.vm.backupShells).toHaveLength(1);
525
+ expect(wrapper.vm.os).toBe('linux');
526
+ expect(testNodeDefinedOsParams.propsData.pod.os).toBe('linux');
527
+ expect(wrapper.vm.errorMsg).toBe(linuxErrorMessage);
528
+
529
+ eventConnecting();
530
+ eventConnected();
531
+ eventMessage({ detail: { data: `3${ linuxErrorMessage }` } });
532
+ eventDisconnected();
533
+
534
+ expect(wrapper.vm.backupShells).toHaveLength(1);
535
+ expect(wrapper.vm.isOpen).toBe(false);
536
+ expect(wrapper.vm.isOpening).toBe(false);
537
+ expect(wrapper.vm.errorMsg).toBe(linuxErrorMessage);
538
+ expect(wrapper.vm.os).toBe('linux');
539
+ expect(testNodeDefinedOsParams.propsData.pod.os).toBe('linux');
540
+ expect(connect.mock.calls).toHaveLength(3);
541
+
542
+ eventConnecting();
543
+ eventConnected();
544
+ eventMessage({ detail: { data: `3${ linuxErrorMessage }` } });
545
+ eventDisconnected();
546
+
547
+ expect(consoleError.mock.calls[0][0]).toBe(linuxErrorMessage);
548
+ expect(consoleError.mock.calls[1][0]).toBe(linuxErrorMessage);
549
+ expect(consoleError.mock.calls[2][0]).toBe(linuxErrorMessage);
550
+ expect(wrapper.vm.backupShells).toHaveLength(1);
551
+ expect(wrapper.vm.isOpen).toBe(false);
552
+ expect(wrapper.vm.isOpening).toBe(false);
553
+ expect(wrapper.vm.errorMsg).toBe(linuxErrorMessage);
554
+ expect(wrapper.vm.os).toBe('linux');
555
+ expect(testNodeDefinedOsParams.propsData.pod.os).toBe('linux');
556
+ // at some point we have to stop retying and if we're not burning through backup shells, there's a retry limit of 2 for a total of 3 attempts
557
+ expect(connect.mock.calls).toHaveLength(3);
558
+
559
+ resetMocks();
560
+ });
561
+ });
@@ -0,0 +1,62 @@
1
+ import Vuex from 'vuex';
2
+ import { createLocalVue } from '@vue/test-utils';
3
+ import { ensureSupportLink } from '@shell/config/home-links.js';
4
+ import { getters, state, mutations } from '@shell/store/i18n.js';
5
+
6
+ jest.mock('@shell/assets/translations/en-us.yaml', () => ({
7
+ locale: {
8
+ 'en-us': 'English',
9
+ 'zh-hans': '简体中文',
10
+ none: '(None)',
11
+ }
12
+ }));
13
+
14
+ describe('fx: ensureSupportLink', () => {
15
+ const localVue = createLocalVue();
16
+
17
+ localVue.use(Vuex);
18
+
19
+ const store = new Vuex.Store({
20
+ state,
21
+ getters: {
22
+ 'i18n/selectedLocaleLabel': getters.selectedLocaleLabel,
23
+ 'i18n/t': getters.t,
24
+ },
25
+ mutations,
26
+ });
27
+
28
+ store.commit('loadTranslations', {
29
+ locale: 'zh-zhans',
30
+ translations: {
31
+ locale: {
32
+ 'en-us': 'English',
33
+ 'zh-hans': '简体中文',
34
+ none: '(None)',
35
+ }
36
+ }
37
+ });
38
+
39
+ const testCases = [
40
+ ['en-us', false],
41
+ ['zh-hans', true],
42
+ ['none', false],
43
+ [null, false],
44
+ ];
45
+
46
+ it.each(testCases)('should return cn forum link if the language is zh-hans', (language:String, value) => {
47
+ store.commit('setSelected', language);
48
+
49
+ const links = { defaults: [], custom: [] };
50
+ const hasSupport = true;
51
+ const isSupportPage = true;
52
+
53
+ const localThis = {
54
+ $store: store,
55
+ t: store.getters['i18n/t'],
56
+ };
57
+
58
+ const result = ensureSupportLink(links, hasSupport, isSupportPage, localThis.t, store);
59
+
60
+ expect(!!result.defaults.find((link) => link.key === 'cnforums')).toBe(value);
61
+ });
62
+ });
@@ -38,6 +38,12 @@ const SUPPORT_LINK = {
38
38
  readonly: true
39
39
  };
40
40
 
41
+ const CN_FORUMS_LINK = {
42
+ key: 'cnforums',
43
+ value: 'https://forums.rancher.cn/',
44
+ enabled: true,
45
+ };
46
+
41
47
  // We add a version attribute to the setting so we know what has been migrated and which version of the setting we have
42
48
  export const CUSTOM_LINKS_VERSION = 'v1';
43
49
 
@@ -72,7 +78,7 @@ export async function fetchLinks(store, hasSupport, isSupportPage, t) {
72
78
  uiLinks.defaults = defaults;
73
79
  }
74
80
 
75
- return ensureSupportLink(uiLinks, hasSupport, isSupportPage, t);
81
+ return ensureSupportLink(uiLinks, hasSupport, isSupportPage, t, store);
76
82
  }
77
83
 
78
84
  // No new setting, so return the required structure
@@ -117,11 +123,11 @@ export async function fetchLinks(store, hasSupport, isSupportPage, t) {
117
123
  console.warn('Could not parse legacy link settings', e); // eslint-disable-line no-console
118
124
  }
119
125
 
120
- return ensureSupportLink(links, hasSupport, isSupportPage, t);
126
+ return ensureSupportLink(links, hasSupport, isSupportPage, t, store);
121
127
  }
122
128
 
123
129
  // Ensure the support link is added if needed
124
- function ensureSupportLink(links, hasSupport, isSupportPage, t) {
130
+ export function ensureSupportLink(links, hasSupport, isSupportPage, t, store) {
125
131
  if (!hasSupport && !isSupportPage) {
126
132
  const supportLink = links.defaults?.find((link) => link.key === 'commercialSupport');
127
133
 
@@ -130,6 +136,12 @@ function ensureSupportLink(links, hasSupport, isSupportPage, t) {
130
136
  }
131
137
  }
132
138
 
139
+ const selectedLocaleLabel = store.getters['i18n/selectedLocaleLabel'];
140
+
141
+ if (selectedLocaleLabel === t('locale.zh-hans')) {
142
+ links.defaults.push(CN_FORUMS_LINK);
143
+ }
144
+
133
145
  // Localise the default links
134
146
  links.defaults = links.defaults.map((link) => {
135
147
  return {
@@ -54,7 +54,11 @@ export const CAPI = {
54
54
  DELETE_MACHINE: 'cluster.x-k8s.io/delete-machine',
55
55
  PROVIDER: 'provider.cattle.io',
56
56
  SECRET_AUTH: 'v2prov-secret-authorized-for-cluster',
57
- SECRET_WILL_DELETE: 'v2prov-authorized-secret-deletes-on-cluster-removal'
57
+ SECRET_WILL_DELETE: 'v2prov-authorized-secret-deletes-on-cluster-removal',
58
+ /**
59
+ * Annotation for overriding the cluster provider,
60
+ */
61
+ UI_CUSTOM_PROVIDER: 'ui.rancher/provider'
58
62
  };
59
63
 
60
64
  export const CATALOG = {
@@ -105,7 +109,8 @@ export const FLEET = {
105
109
  CLUSTER_DISPLAY_NAME: 'management.cattle.io/cluster-display-name',
106
110
  CLUSTER_NAME: 'management.cattle.io/cluster-name',
107
111
  BUNDLE_ID: 'fleet.cattle.io/bundle-id',
108
- MANAGED: 'fleet.cattle.io/managed'
112
+ MANAGED: 'fleet.cattle.io/managed',
113
+ CLUSTER: 'fleet.cattle.io/cluster'
109
114
  };
110
115
 
111
116
  export const RBAC = { PRODUCT: 'management.cattle.io/ui-product' };