@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
@@ -31,6 +31,10 @@ export default {
31
31
  type: Number,
32
32
  default: 200000
33
33
  },
34
+ accept: {
35
+ type: String,
36
+ default: 'image/*'
37
+ }
34
38
  },
35
39
  computed: {
36
40
  isView() {
@@ -44,6 +48,9 @@ export default {
44
48
  */
45
49
  setIcon(event) {
46
50
  this.$emit('input', event);
51
+ },
52
+ setError(error) {
53
+ this.$emit('error', error);
47
54
  }
48
55
  }
49
56
  };
@@ -58,7 +65,9 @@ export default {
58
65
  :read-as-data-url="true"
59
66
  :byte-limit="byteLimit"
60
67
  :label="label"
68
+ :accept="accept"
61
69
  @selected="setIcon"
70
+ @error="setError"
62
71
  />
63
72
 
64
73
  <div
@@ -63,7 +63,8 @@ export default {
63
63
  accept: {
64
64
  type: String,
65
65
  default: '*'
66
- }
66
+ },
67
+
67
68
  },
68
69
 
69
70
  computed: {
@@ -218,12 +218,10 @@ export default {
218
218
 
219
219
  if ( rule.operator === 'Exists' || rule.operator === 'DoesNotExist') {
220
220
  val = null;
221
- } else if (!val) {
222
- return;
223
221
  }
224
222
 
225
223
  if ( val !== null ) {
226
- expression.values = val.split(/\s*,\s*/).filter((x) => !!x);
224
+ expression.values = val.split(/\s*,\s*/);
227
225
  }
228
226
 
229
227
  return expression;
@@ -8,16 +8,7 @@ import { DESCRIPTION } from '@shell/config/labels-annotations';
8
8
  import { _VIEW, _EDIT, _CREATE } from '@shell/config/query-params';
9
9
  import { LabeledInput } from '@components/Form/LabeledInput';
10
10
  import LabeledSelect from '@shell/components/form/LabeledSelect';
11
-
12
- export function normalizeName(str) {
13
- return (str || '')
14
- .trim()
15
- .toLowerCase()
16
- .replace(/\s+/g, '-')
17
- .replace(/-+/g, '-')
18
- .replace(/^-+/, '')
19
- .replace(/-+$/, '');
20
- }
11
+ import { normalizeName } from '@shell/utils/kube';
21
12
 
22
13
  export default {
23
14
  name: 'NameNsDescription',
@@ -101,10 +92,20 @@ export default {
101
92
  type: Boolean,
102
93
  default: false
103
94
  },
95
+ /**
96
+ * Use these objects instead of namespaces
97
+ */
104
98
  namespacesOverride: {
105
99
  type: Array,
106
100
  default: null,
107
101
  },
102
+ /**
103
+ * User these namespaces instead of determining list within component
104
+ */
105
+ namespaceOptions: {
106
+ type: Array,
107
+ default: null,
108
+ },
108
109
  createNamespaceOverride: {
109
110
  type: Boolean,
110
111
  default: false,
@@ -228,8 +229,23 @@ export default {
228
229
  * Map namespaces from the store to options, adding divider and create button
229
230
  */
230
231
  options() {
231
- const namespaces = this.namespacesOverride ||
232
- (Object.keys(this.isCreate ? this.allowedNamespaces() : this.namespaces()));
232
+ let namespaces;
233
+
234
+ if (this.namespacesOverride) {
235
+ // Use the resources provided
236
+ namespaces = this.namespacesOverride;
237
+ } else {
238
+ if (this.namespaceOptions) {
239
+ // Use the namespaces provided
240
+ namespaces = (this.namespaceOptions.map((ns) => ns.name) || []).sort();
241
+ } else {
242
+ // Determine the namespaces
243
+ const namespaceObjs = this.isCreate ? this.allowedNamespaces() : this.namespaces();
244
+
245
+ namespaces = Object.keys(namespaceObjs);
246
+ }
247
+ }
248
+
233
249
  const options = namespaces
234
250
  .map((namespace) => ({ nameDisplay: namespace, id: namespace }))
235
251
  .map(this.namespaceMapper || ((obj) => ({
@@ -156,7 +156,7 @@ export default {
156
156
  },
157
157
 
158
158
  priorityDisplay(term) {
159
- return term.weight ? this.t('workload.scheduling.affinity.preferred') : this.t('workload.scheduling.affinity.required');
159
+ return 'weight' in term ? this.t('workload.scheduling.affinity.preferred') : this.t('workload.scheduling.affinity.required');
160
160
  },
161
161
 
162
162
  updateExpressions(row, expressions) {
@@ -212,7 +212,7 @@ export default {
212
212
  />
213
213
  </div>
214
214
  <div
215
- v-if="props.row.value.weight"
215
+ v-if="'weight' in props.row.value"
216
216
  class="col span-3"
217
217
  >
218
218
  <LabeledInput
@@ -286,7 +286,7 @@ export default {
286
286
  },
287
287
 
288
288
  priorityDisplay(term) {
289
- return term.weight ? this.t('workload.scheduling.affinity.preferred') : this.t('workload.scheduling.affinity.required');
289
+ return 'weight' in term ? this.t('workload.scheduling.affinity.preferred') : this.t('workload.scheduling.affinity.required');
290
290
  },
291
291
 
292
292
  changeNamespaceMode(val, term, idx) {
@@ -468,7 +468,7 @@ export default {
468
468
  />
469
469
  </div>
470
470
  <div
471
- v-if="props.row.value.weight"
471
+ v-if="'weight' in props.row.value"
472
472
  class="col span-3"
473
473
  >
474
474
  <LabeledInput
@@ -13,6 +13,7 @@ import { _VIEW } from '@shell/config/query-params';
13
13
  import RelatedResources from '@shell/components/RelatedResources';
14
14
  import { ExtensionPoint, TabLocation } from '@shell/core/types';
15
15
  import { getApplicableExtensionEnhancements } from '@shell/core/plugin-helpers';
16
+ import { isConditionReadyAndWaiting } from '@shell/plugins/dashboard-store/resource-class';
16
17
 
17
18
  export default {
18
19
 
@@ -60,6 +61,11 @@ export default {
60
61
  needRelated: {
61
62
  type: Boolean,
62
63
  default: true
64
+ },
65
+
66
+ extensionParams: {
67
+ type: Object,
68
+ default: null
63
69
  }
64
70
  },
65
71
 
@@ -71,7 +77,7 @@ export default {
71
77
  allEvents: [],
72
78
  selectedTab: this.defaultTab,
73
79
  didLoadEvents: false,
74
- extensionTabs: getApplicableExtensionEnhancements(this, ExtensionPoint.TAB, TabLocation.RESOURCE_DETAIL, this.$route),
80
+ extensionTabs: getApplicableExtensionEnhancements(this, ExtensionPoint.TAB, TabLocation.RESOURCE_DETAIL, this.$route, this, this.extensionParams),
75
81
  };
76
82
  },
77
83
 
@@ -140,7 +146,7 @@ export default {
140
146
  },
141
147
  conditionsHaveIssues() {
142
148
  if (this.showConditions) {
143
- return this.value.status?.conditions?.some((cond) => cond.error);
149
+ return this.value.status?.conditions?.filter((cond) => !isConditionReadyAndWaiting(cond)).some((cond) => cond.error);
144
150
  }
145
151
 
146
152
  return false;
@@ -134,11 +134,27 @@ export default {
134
134
 
135
135
  return true;
136
136
  },
137
+ /**
138
+ * Get a unique value to represent the option
139
+ */
137
140
  getOptionKey(opt) {
141
+ // Use the property from a component level key
142
+ if (opt && this.optionKey) {
143
+ return get(opt, this.optionKey);
144
+ }
145
+
146
+ // Use the property from an option level key
147
+ // This doesn't seem right, think it was meant to represent the actual option key... rather than the key to find the option key
148
+ // This approach also doesn't appear in LabeledSelect
138
149
  if (opt?.optionKey) {
150
+ // opt.optionKey should in theory be optionKeyKey
139
151
  return get(opt, opt.optionKey);
140
152
  }
141
153
 
154
+ // There's no configuration to help us get a sensible key. Fall back on ..
155
+ // - the label
156
+ // - something random
157
+
142
158
  const label = this.getOptionLabel(opt);
143
159
 
144
160
  // label may be type of object
@@ -0,0 +1,42 @@
1
+ /* eslint-disable jest/no-hooks */
2
+ import FileImageSelector from '@shell/components/form/FileImageSelector';
3
+ import { mount } from '@vue/test-utils';
4
+ import FileSelector from '@shell/components/form/FileSelector';
5
+
6
+ describe('component: FileImageSelector', () => {
7
+ let wrapper: any;
8
+
9
+ beforeEach(() => {
10
+ wrapper = mount(FileImageSelector, {
11
+ propsData: { label: 'upload' },
12
+ mocks: {},
13
+ methods: {},
14
+ });
15
+ });
16
+
17
+ afterEach(() => {
18
+ wrapper.destroy();
19
+ });
20
+
21
+ it('should render', () => {
22
+ const uploadButton = wrapper.find('.btn');
23
+
24
+ expect(wrapper.isVisible()).toBe(true);
25
+ expect(uploadButton.exists()).toBeTruthy();
26
+ });
27
+ it('should throw error if file could not be uploaded', async() => {
28
+ const fs = wrapper.findComponent(FileSelector);
29
+
30
+ expect(fs.exists()).toBeTruthy();
31
+ await fs.vm.$emit('error');
32
+
33
+ expect(wrapper.emitted('error')).toHaveLength(1);
34
+ });
35
+
36
+ it('should emit input on image upload', async() => {
37
+ const fs = wrapper.findComponent(FileSelector);
38
+
39
+ await fs.vm.$emit('selected');
40
+ expect(wrapper.emitted('input')).toHaveLength(1);
41
+ });
42
+ });
@@ -0,0 +1,76 @@
1
+ /* eslint-disable jest/no-hooks */
2
+ import FileSelector from '@shell/components/form/FileSelector';
3
+ import { mount } from '@vue/test-utils';
4
+
5
+ describe('component: FileSelector', () => {
6
+ let wrapper: any;
7
+
8
+ beforeEach(() => {
9
+ jest.restoreAllMocks();
10
+ });
11
+ afterEach(() => {
12
+ wrapper.destroy();
13
+ });
14
+
15
+ const binaryString = Buffer.from('/9j/4AAQSkZJRgABAQAASABIAAD/4QCARXhpZgAATU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIAAIdpAAQAAAABAAAAWgAAAAAAAABIAAAAAQAAAEgAAAABAAKgAgAEAAAAAQAAADmgAwAEAAAAAQAAAFEAAAAA/+0AOFBob3Rvc2hvcCAzLjAAOEJJTQQEAAAAAAAAOEJJTQQlAAAAAAAQ1B2M2Y8AsgTpgAmY7PhCfv/iAihJQ0NfUFJPRklMRQABAQAAAhgAAAAABDAAAG1udHJSR0IgWFlaIAAAAAAAAAAAAAAAAGFjc3AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD21gABAAAAANMtAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACWRlc2MAAADwAAAAdHJYWVoAAAFkAAAAFGdYWVoAAAF4AAAAFGJYWVoAAAGMAAAAFHJUUkMAAAGgAAAAKGdUUkMAAAGgAAAAKGJUUkMAAAGgAAAAKHd0cHQAAAHIAAAAFGNwcnQAAAHcAAAAPG1sdWMAAAAAAAAAAQAAAAxlblVTAAAAWAAAABwAcwBSAEcAQgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAG+iAAA49QAAA5BYWVogAAAAAAAAYpkAALeFAAAY2lhZWiAAAAAAAAAkoAAAD4QAALbPcGFyYQAAAAAABAAAAAJmZgAA8qcAAA1ZAAAT0AAAClsAAAAAAAAAAFhZWiAAAAAAAAD21gABAAAAANMtbWx1YwAAAAAAAAABAAAADGVuVVMAAAAgAAAAHABHAG8AbwBnAGwAZQAgAEkAbgBjAC4AIAAyADAAMQA2/8AAEQgAUQA5AwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAEAsMDgwKEA4NDhIREBMYKBoYFhYYMSMlHSg6Mz08OTM4N0BIXE5ARFdFNzhQbVFXX2JnaGc+TXF5cGR4XGVnY//bAEMBERISGBUYLxoaL2NCOEJjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY//dAAQABP/aAAwDAQACEQMRAD8AdRRRX0B4gUUUUAFFFFABRRRQB//QdRRRX0B4gUUUUAFFFFABRRRQB//RdRRRX0B4gUUUUAFFFFABRRRQB//SdRRRX0B4gUUUUAFFFFABRRRQB//TdRRRX0B4gUUUUAFFFFAwooooA//UdRRRX0B4gUUUUAFFFFAwooooA//Z', 'base64'); // Binary data string
16
+ const jpegBlobFile = new Blob([binaryString], { type: 'image/jpeg' });
17
+ const obj = { hello: 'world' };
18
+ const jsonBlobFile = new Blob([JSON.stringify(obj, null, 2)], { type: 'application/json' });
19
+
20
+ it('should render', () => {
21
+ wrapper = mount(FileSelector, {
22
+ propsData: { label: 'upload' },
23
+ mocks: {},
24
+ methods: {},
25
+ });
26
+
27
+ const uploadButton = wrapper.find('.btn');
28
+
29
+ expect(wrapper.isVisible()).toBe(true);
30
+ expect(uploadButton.exists()).toBeTruthy();
31
+ });
32
+
33
+ it('should succeed when loading an image', async() => {
34
+ wrapper = mount(FileSelector, {
35
+ propsData: { label: 'upload', accept: 'image/jpeg,image/png,image/svg+xml' },
36
+ mocks: {},
37
+ methods: {},
38
+ });
39
+ const readAsTextSpy = jest.spyOn(FileReader.prototype, 'readAsText');
40
+
41
+ const event = {
42
+ target: {
43
+ files: [
44
+ jpegBlobFile
45
+ ]
46
+ }
47
+ };
48
+
49
+ await wrapper.vm.fileChange(event);
50
+ expect(wrapper.emitted('selected')).toHaveLength(1);
51
+ expect(readAsTextSpy).toHaveBeenCalledWith(jpegBlobFile);
52
+ });
53
+
54
+ it('should fail when file is too big', async() => {
55
+ wrapper = mount(FileSelector, {
56
+ propsData: {
57
+ label: 'upload', accept: 'image/jpeg,image/png,image/svg+xml', byteLimit: 10
58
+ },
59
+ mocks: {},
60
+ methods: {},
61
+ });
62
+ const readAsTextSpy = jest.spyOn(FileReader.prototype, 'readAsText');
63
+
64
+ const event = {
65
+ target: {
66
+ files: [
67
+ jpegBlobFile
68
+ ]
69
+ }
70
+ };
71
+
72
+ await wrapper.vm.fileChange(event);
73
+ expect(wrapper.emitted('error')).toHaveLength(1);
74
+ expect(readAsTextSpy).not.toHaveBeenCalledWith(jsonBlobFile);
75
+ });
76
+ });
@@ -0,0 +1,38 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import NodeAffinity from '@shell/components/form/NodeAffinity.vue';
3
+ import { _CREATE } from '@shell/config/query-params';
4
+
5
+ describe('component: NodeAffinity', () => {
6
+ it('should display the weight input when the priority is preferred', () => {
7
+ const nodeAffinity = {
8
+ preferredDuringSchedulingIgnoredDuringExecution: [{
9
+ preference: { matchExpressions: [] },
10
+ weight: 1
11
+ }],
12
+ requiredDuringSchedulingIgnoredDuringExecution: { nodeSelectorTerms: [{ matchExpressions: [] }] }
13
+ };
14
+ const wrapper = mount(NodeAffinity, { propsData: { mode: _CREATE, value: nodeAffinity } });
15
+
16
+ expect(wrapper.find('[data-testid="node-affinity-weight-index0"]').exists()).toBeTruthy();
17
+ expect(wrapper.find('[data-testid="node-affinity-weight-index1"]').exists()).toBeFalsy();
18
+ });
19
+
20
+ it('should display the weight input when the value is cleared', async() => {
21
+ const nodeAffinity = {
22
+ preferredDuringSchedulingIgnoredDuringExecution: [{
23
+ preference: { matchExpressions: [] },
24
+ weight: 1
25
+ }],
26
+ };
27
+
28
+ const wrapper = mount(NodeAffinity, { propsData: { mode: _CREATE, value: nodeAffinity } });
29
+
30
+ const weightInput = wrapper.find('[data-testid="node-affinity-weight-index0"]');
31
+
32
+ weightInput.setValue('');
33
+
34
+ await wrapper.vm.$nextTick();
35
+
36
+ expect(wrapper.find('[data-testid="node-affinity-weight-index0"]').exists()).toBeTruthy();
37
+ });
38
+ });
@@ -0,0 +1,46 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import PodAffinity from '@shell/components/form/PodAffinity.vue';
3
+ import { _CREATE } from '@shell/config/query-params';
4
+
5
+ describe('component: PodAffinity', () => {
6
+ it('should display the weight input when the priority is preferred', () => {
7
+ const podAffinity = {
8
+ preferredDuringSchedulingIgnoredDuringExecution: [{
9
+ podAffinityTerm: { topologyKey: 'test topology key 1' },
10
+ weight: 1
11
+ }],
12
+ requiredDuringSchedulingIgnoredDuringExecution: [{ topologyKey: 'test topology key 2' }]
13
+ };
14
+ const wrapper = mount(PodAffinity, {
15
+ propsData: {
16
+ mode: _CREATE, field: 'overrideAffinity', value: { overrideAffinity: { podAffinity } }
17
+ }
18
+ });
19
+
20
+ expect(wrapper.find('[data-testid="pod-affinity-weight-index0"]').exists()).toBeTruthy();
21
+ expect(wrapper.find('[data-testid="pod-affinity-weight-index1"]').exists()).toBeFalsy();
22
+ });
23
+
24
+ it('should display the weight input when the value is cleared', async() => {
25
+ const podAffinity = {
26
+ preferredDuringSchedulingIgnoredDuringExecution: [{
27
+ podAffinityTerm: { topologyKey: 'test topology key 1' },
28
+ weight: 1
29
+ }],
30
+ };
31
+
32
+ const wrapper = mount(PodAffinity, {
33
+ propsData: {
34
+ mode: _CREATE, field: 'overrideAffinity', value: { overrideAffinity: { podAffinity } }
35
+ }
36
+ });
37
+
38
+ const weightInput = wrapper.find('[data-testid="pod-affinity-weight-index0"]');
39
+
40
+ weightInput.setValue('');
41
+
42
+ await wrapper.vm.$nextTick();
43
+
44
+ expect(wrapper.find('[data-testid="pod-affinity-weight-index0"]').exists()).toBeTruthy();
45
+ });
46
+ });
@@ -1,5 +1,6 @@
1
1
  <script>
2
2
  import { get } from '@shell/utils/object';
3
+ import { isConditionReadyAndWaiting } from '@shell/plugins/dashboard-store/resource-class';
3
4
 
4
5
  export default {
5
6
  props: {
@@ -27,10 +28,10 @@ export default {
27
28
 
28
29
  statusErrorConditions() {
29
30
  if (this.row.hasError) {
30
- return this.row?.status.conditions.filter((condition) => condition.error === true);
31
+ return this.row?.status.conditions.filter((cond) => cond.error === true && !isConditionReadyAndWaiting(cond));
31
32
  }
32
33
 
33
- return false;
34
+ return [];
34
35
  },
35
36
 
36
37
  formattedConditions() {
@@ -46,7 +47,7 @@ export default {
46
47
  return formattedTooltip.toString().replaceAll(',', '');
47
48
  }
48
49
 
49
- return false;
50
+ return '';
50
51
  },
51
52
  },
52
53
 
@@ -66,16 +67,19 @@ export default {
66
67
  v-if="row.unavailableMachines"
67
68
  v-clean-tooltip="row.unavailableMachines"
68
69
  class="conditions-alert-icon icon-alert icon"
70
+ data-testid="unavailable-machines-alert-icon"
69
71
  />
70
72
  <i
71
73
  v-if="row.rkeTemplateUpgrade"
72
74
  v-clean-tooltip="t('cluster.rkeTemplateUpgrade', { name: row.rkeTemplateUpgrade })"
73
75
  class="template-upgrade-icon icon-alert icon"
76
+ data-testid="rke-template-upgrade-alert-icon"
74
77
  />
75
78
  <i
76
- v-if="row.hasError"
79
+ v-if="row.hasError && statusErrorConditions.length > 0"
77
80
  v-clean-tooltip="{ content: `<div>${formattedConditions}</div>`, html: true }"
78
81
  class="conditions-alert-icon icon-error icon-lg"
82
+ data-testid="conditions-has-error-icon"
79
83
  />
80
84
  </span>
81
85
  </template>
@@ -12,7 +12,9 @@ export default {
12
12
  // model doesn't work for imported K3s clusters, in
13
13
  // which case it returns 'k3s' instead of 'imported.'
14
14
  // This is the workaround.
15
- isImported: props.row.mgmt?.providerForEmberParam === 'import'
15
+ isImported: props.row?.mgmt?.providerForEmberParam === 'import' ||
16
+ // when imported cluster is Google GKE
17
+ props.row?.mgmt?.spec?.gkeConfig?.imported
16
18
  };
17
19
  },
18
20
  };
@@ -0,0 +1,23 @@
1
+ <script>
2
+ export default {
3
+ props:
4
+ {
5
+ value: {
6
+ type: String,
7
+ default: ''
8
+ }
9
+ }
10
+ };
11
+ </script>
12
+
13
+ <template>
14
+ <span class="formatter-image">
15
+ {{ value }}
16
+ </span>
17
+ </template>
18
+
19
+ <style scoped>
20
+ .formatter-image {
21
+ word-break: break-all;
22
+ }
23
+ </style>
@@ -55,7 +55,7 @@ export default {
55
55
  </script>
56
56
 
57
57
  <template>
58
- <span>
58
+ <span class="formatter-pod-images">
59
59
  <span>{{ mainImage }}</span><br>
60
60
  <span
61
61
  v-if="images.length-1>0"
@@ -64,3 +64,9 @@ export default {
64
64
  >{{ t('generic.plusMore', {n:images.length-1}) }}</span>
65
65
  </span>
66
66
  </template>
67
+
68
+ <style scoped>
69
+ .formatter-pod-images {
70
+ word-break: break-all;
71
+ }
72
+ </style>
@@ -0,0 +1,101 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import ClusterLink from '@shell/components/formatter/ClusterLink.vue';
3
+
4
+ describe('component: ClusterLink', () => {
5
+ const UNAVAILABLE_MACHINES_ICON_SELECTOR = '[data-testid="unavailable-machines-alert-icon"]';
6
+ const TEMPLATE_UPGRADE_ICON_SELECTOR = '[data-testid="rke-template-upgrade-alert-icon"]';
7
+ const CONDITION_HAS_ERROR_ICON_SELECTOR = '[data-testid="conditions-has-error-icon"]';
8
+
9
+ describe('unavailable machines alert icon', () => {
10
+ const testCases = [
11
+ [undefined, false],
12
+ [0, false],
13
+ [1, true],
14
+ [5, true],
15
+ ];
16
+
17
+ it.each(testCases)(
18
+ 'should show/hide properly based on on the number of unavailable machines',
19
+ (unavailableMachines, expected) => {
20
+ const wrapper = mount(ClusterLink, {
21
+ propsData: {
22
+ row: {
23
+ hasError: false,
24
+ status: {},
25
+ unavailableMachines,
26
+ rkeTemplateUpgrade: undefined
27
+ },
28
+ reference: 'any',
29
+ value: 'any'
30
+ }
31
+ });
32
+ const el = wrapper.find(UNAVAILABLE_MACHINES_ICON_SELECTOR);
33
+
34
+ expect(el.exists()).toBe(expected);
35
+ }
36
+ );
37
+ });
38
+
39
+ describe('template upgrade alert icon', () => {
40
+ const testCases = [
41
+ [undefined, false],
42
+ ['any', true],
43
+ ];
44
+
45
+ it.each(testCases)(
46
+ 'should show/hide properly based on rkeTemplateUpgrade',
47
+ (rkeTemplateUpgrade, expected) => {
48
+ const wrapper = mount(ClusterLink, {
49
+ propsData: {
50
+ row: {
51
+ hasError: false,
52
+ status: {},
53
+ unavailableMachines: 0,
54
+ rkeTemplateUpgrade
55
+ },
56
+ reference: 'any',
57
+ value: 'any'
58
+ }
59
+ });
60
+ const el = wrapper.find(TEMPLATE_UPGRADE_ICON_SELECTOR);
61
+
62
+ expect(el.exists()).toBe(expected);
63
+ }
64
+ );
65
+ });
66
+
67
+ describe('conditions has error icon', () => {
68
+ const MOCKED_CONDITIONS_1 = [{
69
+ status: '', type: 'Ready', reason: 'Waiting', error: true // When the only existing error has a type "Ready" and reason "Waiting"
70
+ }];
71
+ const MOCKED_CONDITIONS_2 = [{
72
+ status: 'any', type: 'any', error: true
73
+ }];
74
+
75
+ const testCases = [
76
+ [[], false],
77
+ [MOCKED_CONDITIONS_1, false],
78
+ [MOCKED_CONDITIONS_2, true],
79
+ ];
80
+
81
+ it.each(testCases)(
82
+ 'should show/hide properly based on the status conditions',
83
+ (conditions, expected) => {
84
+ const wrapper = mount(ClusterLink, {
85
+ propsData: {
86
+ row: {
87
+ hasError: true,
88
+ status: { conditions },
89
+ unavailableMachines: 0
90
+ },
91
+ reference: 'any',
92
+ value: 'any'
93
+ }
94
+ });
95
+ const el = wrapper.find(CONDITION_HAS_ERROR_ICON_SELECTOR);
96
+
97
+ expect(el.exists()).toBe(expected);
98
+ }
99
+ );
100
+ });
101
+ });
@@ -0,0 +1,24 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import ClusterProvider from '@shell/components/formatter/ClusterProvider.vue';
3
+
4
+ describe('component: ClusterProvider', () => {
5
+ const importedGkeClusterInfo = { mgmt: { spec: { gkeConfig: { imported: true } } } };
6
+ const notImportedGkeClusterInfo = { mgmt: { spec: { gkeConfig: { imported: false } } } };
7
+ const importedClusterInfoWithProviderForEmberParam = { mgmt: { providerForEmberParam: 'import' } };
8
+
9
+ describe('isImported', () => {
10
+ const testCases = [
11
+ [importedGkeClusterInfo, true],
12
+ [notImportedGkeClusterInfo, false],
13
+ [importedClusterInfoWithProviderForEmberParam, true],
14
+ [{}, undefined],
15
+ ];
16
+
17
+ it.each(testCases)('should return the isImported value properly based on the props data', (row, expected) => {
18
+ const wrapper = mount(ClusterProvider, { propsData: { row } });
19
+
20
+ expect(wrapper.vm.$data.isImported).toBe(expected);
21
+ }
22
+ );
23
+ });
24
+ });