@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.
- package/assets/images/wechat-qr-code.jpg +0 -0
- package/assets/translations/en-us.yaml +75 -16
- package/assets/translations/zh-hans.yaml +151 -15
- package/chart/__tests__/S3.test.ts +50 -0
- package/chart/rancher-backup/S3.vue +21 -0
- package/chart/rancher-backup/index.vue +4 -0
- package/components/AsyncButton.vue +1 -1
- package/components/CommunityLinks.vue +1 -0
- package/components/FileDiff.vue +92 -85
- package/components/Inactivity.vue +10 -0
- package/components/LazyImage.vue +2 -2
- package/components/PromptRestore.vue +7 -5
- package/components/ResourceDetail/Masthead.vue +1 -1
- package/components/ResourceDetail/index.vue +8 -14
- package/components/ResourceList/index.vue +1 -1
- package/components/ResourceTable.vue +50 -2
- package/components/YamlEditor.vue +1 -0
- package/components/__tests__/PromptRestore.test.ts +72 -0
- package/components/auth/AzureWarning.vue +1 -1
- package/components/auth/RoleDetailEdit.vue +1 -0
- package/components/fleet/FleetResources.vue +3 -64
- package/components/form/FileImageSelector.vue +9 -0
- package/components/form/FileSelector.vue +2 -1
- package/components/form/MatchExpressions.vue +1 -3
- package/components/form/NameNsDescription.vue +28 -12
- package/components/form/NodeAffinity.vue +2 -2
- package/components/form/PodAffinity.vue +2 -2
- package/components/form/ResourceTabs/index.vue +8 -2
- package/components/form/Select.vue +16 -0
- package/components/form/__tests__/FileImageSelector.test.ts +42 -0
- package/components/form/__tests__/FileSelector.test.ts +76 -0
- package/components/form/__tests__/NodeAffinity.test.ts +38 -0
- package/components/form/__tests__/PodAffinity.test.ts +46 -0
- package/components/formatter/ClusterLink.vue +8 -4
- package/components/formatter/ClusterProvider.vue +3 -1
- package/components/formatter/ImageName.vue +23 -0
- package/components/formatter/PodImages.vue +7 -1
- package/components/formatter/__tests__/ClusterLink.test.ts +101 -0
- package/components/formatter/__tests__/ClusterProvider.test.ts +24 -0
- package/components/nav/Header.vue +2 -2
- package/components/nav/WindowManager/ContainerShell.vue +60 -36
- package/components/nav/WindowManager/__tests__/ContainerShell.test.ts +561 -0
- package/config/__test__/home-links.test.ts +62 -0
- package/config/home-links.js +15 -3
- package/config/labels-annotations.js +7 -2
- package/config/persistentVolume.ts +108 -0
- package/config/product/manager.js +5 -1
- package/config/router.js +0 -4
- package/config/settings.ts +4 -0
- package/config/table-headers.js +6 -5
- package/config/types.js +2 -0
- package/config/uiplugins.js +50 -5
- package/core/plugin-helpers.js +39 -15
- package/core/plugin.ts +9 -0
- package/core/plugins.js +1 -1
- package/core/types-provisioning.ts +253 -0
- package/core/types.ts +21 -3
- package/detail/autoscaling.horizontalpodautoscaler/index.vue +50 -1
- package/detail/fleet.cattle.io.gitrepo.vue +10 -2
- package/detail/node.vue +6 -6
- package/detail/pod.vue +38 -9
- package/detail/provisioning.cattle.io.cluster.vue +46 -7
- package/detail/workload/index.vue +49 -18
- package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +62 -0
- package/edit/__tests__/ui.cattle.io.navlink.test.ts +110 -0
- package/edit/auth/github.vue +1 -0
- package/edit/autoscaling.horizontalpodautoscaler/hpa-scaling-rule.vue +130 -0
- package/edit/autoscaling.horizontalpodautoscaler/index.vue +79 -0
- package/edit/fleet.cattle.io.clustergroup.vue +14 -3
- package/edit/fleet.cattle.io.gitrepo.vue +18 -1
- package/edit/namespace.vue +9 -1
- package/edit/networking.k8s.io.ingress/RulePath.vue +0 -2
- package/edit/persistentvolume/__tests__/persistentvolume.test.ts +82 -0
- package/edit/persistentvolume/index.vue +2 -1
- package/edit/persistentvolume/plugins/csi.vue +3 -1
- package/edit/persistentvolume/plugins/longhorn.vue +12 -12
- package/edit/provisioning.cattle.io.cluster/AgentConfiguration.vue +1 -30
- package/edit/provisioning.cattle.io.cluster/RegistryConfigs.vue +15 -11
- package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +79 -1
- package/edit/provisioning.cattle.io.cluster/index.vue +53 -1
- package/edit/provisioning.cattle.io.cluster/rke2.vue +335 -151
- package/edit/storage.k8s.io.storageclass/index.vue +1 -2
- package/edit/ui.cattle.io.navlink.vue +213 -186
- package/initialize/App.js +3 -13
- package/initialize/layouts.ts +26 -0
- package/layouts/default.vue +1 -1
- package/list/group.principal.vue +1 -1
- package/list/provisioning.cattle.io.cluster.vue +8 -1
- package/middleware/authenticated.js +101 -5
- package/mixins/brand.js +39 -3
- package/mixins/child-hook.js +2 -2
- package/mixins/create-edit-view/impl.js +4 -4
- package/models/chart.js +1 -1
- package/models/fleet.cattle.io.cluster.js +33 -4
- package/models/fleet.cattle.io.gitrepo.js +113 -38
- package/models/management.cattle.io.kontainerdriver.js +14 -0
- package/models/persistentvolume.js +2 -111
- package/models/pod.js +30 -0
- package/models/provisioning.cattle.io.cluster.js +9 -1
- package/models/rke.cattle.io.etcdsnapshot.js +10 -7
- package/package.json +2 -2
- package/pages/about.vue +8 -2
- package/pages/auth/login.vue +1 -1
- package/pages/auth/logout.vue +11 -3
- package/pages/c/_cluster/apps/charts/index.vue +5 -2
- package/pages/c/_cluster/apps/charts/install.vue +5 -0
- package/pages/c/_cluster/auth/group.principal/assign-edit.vue +1 -1
- package/pages/c/_cluster/auth/roles/index.vue +1 -1
- package/pages/c/_cluster/explorer/index.vue +2 -11
- package/pages/c/_cluster/manager/cloudCredential/_id.vue +0 -1
- package/pages/c/_cluster/manager/cloudCredential/create.vue +0 -1
- package/pages/c/_cluster/settings/brand.vue +11 -8
- package/pages/c/_cluster/uiplugins/AddExtensionRepos.vue +177 -0
- package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +19 -3
- package/pages/c/_cluster/uiplugins/RemoveUIPlugins.vue +90 -21
- package/pages/c/_cluster/uiplugins/SetupUIPlugins.vue +107 -37
- package/pages/c/_cluster/uiplugins/index.vue +160 -44
- package/pages/docs/_doc.vue +9 -3
- package/pages/home.vue +6 -6
- package/pages/support/index.vue +10 -4
- package/pkg/auto-import.js +1 -1
- package/plugins/clean-tooltip-directive.js +1 -1
- package/plugins/dashboard-store/__tests__/actions.spec.ts +165 -0
- package/plugins/dashboard-store/__tests__/getters.spec.ts +100 -0
- package/plugins/dashboard-store/__tests__/{mutations.spec.js → mutations.spec.ts} +2 -2
- package/plugins/dashboard-store/actions.js +1 -1
- package/plugins/dashboard-store/resource-class.js +39 -2
- package/plugins/plugin.js +9 -1
- package/plugins/steve/__tests__/getters.spec.ts +93 -0
- package/plugins/steve/getters.js +21 -1
- package/plugins/steve/subscribe.js +1 -3
- package/rancher-components/BadgeState/BadgeState.vue +5 -1
- package/rancher-components/Banner/Banner.test.ts +51 -1
- package/rancher-components/Banner/Banner.vue +134 -53
- package/rancher-components/Card/Card.test.ts +37 -0
- package/rancher-components/Card/Card.vue +24 -7
- package/rancher-components/Form/Checkbox/Checkbox.test.ts +20 -29
- package/rancher-components/Form/Checkbox/Checkbox.vue +45 -20
- package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +2 -8
- package/rancher-components/Form/LabeledInput/LabeledInput.vue +22 -10
- package/rancher-components/Form/Radio/RadioButton.test.ts +31 -0
- package/rancher-components/Form/Radio/RadioButton.vue +30 -13
- package/rancher-components/Form/Radio/RadioGroup.vue +26 -7
- package/rancher-components/Form/TextArea/TextAreaAutoGrow.vue +7 -6
- package/rancher-components/Form/ToggleSwitch/ToggleSwitch.test.ts +25 -38
- package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +23 -11
- package/rancher-components/LabeledTooltip/LabeledTooltip.vue +19 -5
- package/rancher-components/StringList/StringList.test.ts +453 -49
- package/rancher-components/StringList/StringList.vue +44 -26
- package/scripts/extension/publish +2 -2
- package/scripts/typegen.sh +11 -2
- package/server/server-middleware.js +4 -12
- package/store/index.js +14 -3
- package/store/prefs.js +0 -3
- package/store/store-types.js +2 -0
- package/store/type-map.js +17 -29
- package/types/api.d.ts +1 -0
- package/types/fleet.d.ts +1 -0
- package/types/shell/index.d.ts +931 -85
- package/types/userPreferences.d.ts +1 -1
- package/utils/__mocks__/socket.js +21 -0
- package/utils/grafana.js +23 -11
- package/utils/kube.js +9 -0
- package/utils/object.js +27 -0
- package/utils/selector.js +2 -1
- package/utils/settings.ts +2 -2
- package/utils/validators/formRules/index.ts +3 -3
- package/vue.config.js +3 -2
- package/components/.DS_Store +0 -0
- package/components/__tests__/.DS_Store +0 -0
- package/creators/pkg/package-lock.json +0 -37
- package/pages/safeMode.vue +0 -17
- package/plugins/steve/urloptions.js +0 -47
- 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
|
|
@@ -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*/)
|
|
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
|
-
|
|
232
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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((
|
|
31
|
+
return this.row?.status.conditions.filter((cond) => cond.error === true && !isConditionReadyAndWaiting(cond));
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
return
|
|
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
|
|
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
|
|
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
|
+
});
|