@rancher/shell 3.0.12-rc.5 → 3.0.12-rc.6
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/apis/intf/resources-api/resource-base.ts +42 -2
- package/apis/intf/resources-api/resource-instance.ts +101 -0
- package/apis/intf/resources-api/resources-api.ts +177 -23
- package/apis/intf/resources.ts +8 -6
- package/apis/resources/__tests__/resources-api-class.test.ts +309 -28
- package/apis/resources/resources-api-class.ts +232 -43
- package/assets/images/providers/traefik.png +0 -0
- package/assets/translations/en-us.yaml +10 -4
- package/chart/__tests__/rancher-monitoring-dashboards-index.test.ts +269 -0
- package/chart/rancher-monitoring-dashboards/index.vue +155 -0
- package/cloud-credential/__tests__/generic.test.ts +132 -0
- package/cloud-credential/generic.vue +33 -3
- package/components/EtcdInfoBanner.vue +2 -12
- package/components/GrafanaDashboard.vue +2 -8
- package/components/Resource/Detail/Masthead/__tests__/index.test.ts +68 -14
- package/components/Resource/Detail/Masthead/index.vue +8 -3
- package/components/form/Security.vue +1 -1
- package/components/formatter/Translate.vue +22 -1
- package/components/nav/Group.vue +5 -10
- package/components/nav/TopLevelMenu.vue +1 -30
- package/components/nav/__tests__/Group.test.ts +61 -0
- package/config/labels-annotations.js +1 -0
- package/config/table-headers.js +5 -2
- package/core/__tests__/plugin-products-apply.test.ts +787 -0
- package/core/__tests__/plugin-products-extend.test.ts +189 -0
- package/core/__tests__/plugin-products-helpers.test.ts +81 -78
- package/core/__tests__/plugin-products-new.test.ts +389 -0
- package/core/__tests__/plugin-products-scenarios.test.ts +980 -0
- package/core/__tests__/plugin-products-side-menu.test.ts +1935 -0
- package/core/plugin-products-base.ts +201 -93
- package/core/plugin-products-extending.ts +1 -1
- package/core/plugin-products-external.ts +380 -0
- package/core/plugin-products-helpers.ts +28 -24
- package/core/plugin-products-internal.ts +207 -0
- package/core/plugin-products-top-level.ts +10 -5
- package/core/plugin-products-type-guards.ts +17 -6
- package/core/plugin-products.ts +5 -7
- package/core/plugin-types.ts +1 -389
- package/core/plugin.ts +10 -11
- package/core/types.ts +6 -202
- package/detail/__tests__/provisioning.cattle.io.cluster.test.ts +52 -0
- package/detail/pod.vue +1 -1
- package/detail/provisioning.cattle.io.cluster.vue +0 -2
- package/edit/auth/AuthProviderWarningBanners.vue +12 -0
- package/edit/auth/__tests__/AuthProviderWarningBanners.test.ts +10 -1
- package/edit/auth/__tests__/azuread.test.ts +1 -0
- package/edit/auth/__tests__/github.test.ts +1 -0
- package/edit/auth/__tests__/oidc.test.ts +1 -0
- package/edit/auth/__tests__/saml.test.ts +1 -0
- package/edit/auth/ldap/__tests__/index.test.ts +1 -0
- package/edit/provisioning.cattle.io.cluster/index.vue +1 -1
- package/edit/provisioning.cattle.io.cluster/rke2.vue +1 -1
- package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +2 -2
- package/list/management.cattle.io.user.vue +24 -5
- package/list/utils/management.cattle.io.cluster.utils.ts +1 -1
- package/machine-config/__tests__/generic.test.ts +163 -0
- package/machine-config/components/EC2Networking.vue +31 -7
- package/machine-config/components/__tests__/EC2Networking.test.ts +38 -15
- package/machine-config/generic.vue +32 -5
- package/mixins/resource-fetch.js +1 -1
- package/models/__tests__/rke-machine.cattle.io.vmwarevspheremachinetemplate.test.ts +22 -0
- package/models/pod.js +12 -10
- package/models/rke-machine.cattle.io.vmwarevspheremachinetemplate.js +9 -0
- package/package.json +1 -1
- package/pages/auth/login.vue +1 -1
- package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +110 -0
- package/pages/c/_cluster/apps/charts/install.vue +29 -0
- package/pages/c/_cluster/auth/config/index.vue +11 -4
- package/pages/c/_cluster/monitoring/index.vue +6 -1
- package/plugins/dashboard-store/resource-class.js +10 -3
- package/plugins/steve/__tests__/steve-class-resource-api.test.ts +147 -0
- package/plugins/steve/steve-class.js +43 -0
- package/plugins/steve/subscribe.js +11 -0
- package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +115 -0
- package/rancher-components/Form/LabeledInput/LabeledInput.vue +83 -3
- package/store/__tests__/auth.test.ts +18 -0
- package/store/__tests__/notifications.test.ts +466 -1
- package/store/__tests__/ui-context.test.ts +255 -0
- package/store/auth.js +11 -1
- package/store/features.js +1 -0
- package/store/plugins.js +6 -0
- package/types/rancher/steve.api.ts +2 -2
- package/types/shell/index.d.ts +58 -0
- package/types/store/dashboard-store.types.ts +2 -2
- package/types/store/type-map.ts +262 -1
- package/utils/async.ts +2 -0
- package/utils/grafana.js +2 -17
- package/utils/monitoring.js +38 -1
- package/core/__tests__/plugin-products.test.ts +0 -4694
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { shallowMount, flushPromises } from '@vue/test-utils';
|
|
2
|
+
import RancherMonitoringDashboards from '@shell/chart/rancher-monitoring-dashboards/index.vue';
|
|
3
|
+
|
|
4
|
+
// The component keeps a module-level Map of `chartName -> last selected
|
|
5
|
+
// clusterType.id` so it can restore the user's pick after a Form → YAML → Form
|
|
6
|
+
// remount. To stop this cache from leaking between tests we generate a unique
|
|
7
|
+
// chart name per test.
|
|
8
|
+
let chartNameCounter = 0;
|
|
9
|
+
const uniqueChartName = (prefix = 'chart') => `${ prefix }-${ ++chartNameCounter }`;
|
|
10
|
+
|
|
11
|
+
describe('rancher-monitoring-dashboards: index', () => {
|
|
12
|
+
const createWrapper = (
|
|
13
|
+
{
|
|
14
|
+
provider = 'rke2',
|
|
15
|
+
value = {} as Record<string, any>,
|
|
16
|
+
chart,
|
|
17
|
+
mode = 'create',
|
|
18
|
+
}: {
|
|
19
|
+
provider?: string;
|
|
20
|
+
value?: Record<string, any>;
|
|
21
|
+
chart?: Record<string, any>;
|
|
22
|
+
mode?: string;
|
|
23
|
+
} = {}
|
|
24
|
+
) => {
|
|
25
|
+
return shallowMount(RancherMonitoringDashboards as any, {
|
|
26
|
+
props: {
|
|
27
|
+
value,
|
|
28
|
+
chart: chart ?? { chartName: uniqueChartName() },
|
|
29
|
+
mode,
|
|
30
|
+
},
|
|
31
|
+
global: {
|
|
32
|
+
mocks: {
|
|
33
|
+
$store: {
|
|
34
|
+
getters: {
|
|
35
|
+
currentCluster: { status: { provider } },
|
|
36
|
+
'i18n/t': (k: string) => k,
|
|
37
|
+
t: (k: string) => k,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
t: (k: string) => k,
|
|
41
|
+
},
|
|
42
|
+
stubs: { LabeledSelect: true },
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
describe('initial cluster type selection', () => {
|
|
48
|
+
it.each([
|
|
49
|
+
['aks'],
|
|
50
|
+
['eks'],
|
|
51
|
+
['gke'],
|
|
52
|
+
['k3s'],
|
|
53
|
+
['kubeadm'],
|
|
54
|
+
['rke2'],
|
|
55
|
+
])('defaults clusterType to the provider %s', (provider) => {
|
|
56
|
+
const wrapper = createWrapper({ provider });
|
|
57
|
+
|
|
58
|
+
expect((wrapper.vm as any).clusterType.id).toBe(provider);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('falls back to "other" when the provider does not match any cluster type', () => {
|
|
62
|
+
const wrapper = createWrapper({ provider: 'unknown-provider' });
|
|
63
|
+
|
|
64
|
+
expect((wrapper.vm as any).clusterType.id).toBe('other');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('lowercases the provider before matching', () => {
|
|
68
|
+
const wrapper = createWrapper({ provider: 'RKE2' });
|
|
69
|
+
|
|
70
|
+
expect((wrapper.vm as any).clusterType.id).toBe('rke2');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('applyClusterTypeValues', () => {
|
|
75
|
+
it('sets k3sServer.enabled=true and etcd.enabled=false for k3s', async() => {
|
|
76
|
+
const value: Record<string, any> = {};
|
|
77
|
+
|
|
78
|
+
createWrapper({ provider: 'k3s', value });
|
|
79
|
+
await flushPromises();
|
|
80
|
+
|
|
81
|
+
expect(value.k3sServer.enabled).toBe(true);
|
|
82
|
+
expect(value.etcd.enabled).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it.each([
|
|
86
|
+
['rke2'],
|
|
87
|
+
['eks'],
|
|
88
|
+
['kubeadm'],
|
|
89
|
+
['unknown-provider'],
|
|
90
|
+
])('sets etcd.enabled=true and k3sServer.enabled=false for non-k3s provider %s', async(provider) => {
|
|
91
|
+
const value: Record<string, any> = {};
|
|
92
|
+
|
|
93
|
+
createWrapper({ provider, value });
|
|
94
|
+
await flushPromises();
|
|
95
|
+
|
|
96
|
+
expect(value.etcd.enabled).toBe(true);
|
|
97
|
+
expect(value.k3sServer.enabled).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('creates the etcd and k3sServer objects when missing on value', async() => {
|
|
101
|
+
const value: Record<string, any> = {};
|
|
102
|
+
|
|
103
|
+
createWrapper({ provider: 'rke2', value });
|
|
104
|
+
await flushPromises();
|
|
105
|
+
|
|
106
|
+
expect(value.etcd).toStrictEqual({ enabled: true });
|
|
107
|
+
expect(value.k3sServer).toStrictEqual({ enabled: false });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('preserves unrelated fields on value', async() => {
|
|
111
|
+
const value: Record<string, any> = {
|
|
112
|
+
global: { foo: 'bar' },
|
|
113
|
+
etcd: { extraField: 'keep-me' },
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
createWrapper({ provider: 'k3s', value });
|
|
117
|
+
await flushPromises();
|
|
118
|
+
|
|
119
|
+
expect(value.global).toStrictEqual({ foo: 'bar' });
|
|
120
|
+
expect(value.etcd.extraField).toBe('keep-me');
|
|
121
|
+
expect(value.etcd.enabled).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('flips values when the user changes the selection', async() => {
|
|
125
|
+
const value: Record<string, any> = {};
|
|
126
|
+
const wrapper = createWrapper({ provider: 'rke2', value });
|
|
127
|
+
|
|
128
|
+
await flushPromises();
|
|
129
|
+
expect(value.etcd.enabled).toBe(true);
|
|
130
|
+
expect(value.k3sServer.enabled).toBe(false);
|
|
131
|
+
|
|
132
|
+
const k3sOption = (wrapper.vm as any).clusterTypes.find((c: any) => c.id === 'k3s');
|
|
133
|
+
|
|
134
|
+
(wrapper.vm as any).clusterType = k3sOption;
|
|
135
|
+
await flushPromises();
|
|
136
|
+
|
|
137
|
+
expect(value.etcd.enabled).toBe(false);
|
|
138
|
+
expect(value.k3sServer.enabled).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('watch: clusterType', () => {
|
|
143
|
+
it('ignores empty cluster type and does not mutate value', async() => {
|
|
144
|
+
const value: Record<string, any> = {};
|
|
145
|
+
const wrapper = createWrapper({ provider: 'rke2', value });
|
|
146
|
+
|
|
147
|
+
await flushPromises();
|
|
148
|
+
|
|
149
|
+
const snapshot = JSON.stringify(value);
|
|
150
|
+
|
|
151
|
+
(wrapper.vm as any).clusterType = null;
|
|
152
|
+
await flushPromises();
|
|
153
|
+
|
|
154
|
+
expect(JSON.stringify(value)).toBe(snapshot);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('cached selection across remounts', () => {
|
|
159
|
+
it('restores the last user selection when the same chart remounts', async() => {
|
|
160
|
+
const chart = { chartName: uniqueChartName('same-chart') };
|
|
161
|
+
|
|
162
|
+
const first = createWrapper({
|
|
163
|
+
provider: 'rke2', value: {}, chart
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
await flushPromises();
|
|
167
|
+
expect((first.vm as any).clusterType.id).toBe('rke2');
|
|
168
|
+
|
|
169
|
+
const k3sOption = (first.vm as any).clusterTypes.find((c: any) => c.id === 'k3s');
|
|
170
|
+
|
|
171
|
+
(first.vm as any).clusterType = k3sOption;
|
|
172
|
+
await flushPromises();
|
|
173
|
+
first.unmount();
|
|
174
|
+
|
|
175
|
+
// Simulate a remount on the same chart (e.g. user toggled to YAML and back).
|
|
176
|
+
const second = createWrapper({
|
|
177
|
+
provider: 'rke2',
|
|
178
|
+
value: { etcd: { enabled: false }, k3sServer: { enabled: true } },
|
|
179
|
+
chart,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
expect((second.vm as any).clusterType.id).toBe('k3s');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('preserves a non-k3s selection across remounts (the case value-derivation alone cannot recover)', async() => {
|
|
186
|
+
const chart = { chartName: uniqueChartName('non-k3s-pick') };
|
|
187
|
+
|
|
188
|
+
const first = createWrapper({
|
|
189
|
+
provider: 'k3s', value: {}, chart
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
await flushPromises();
|
|
193
|
+
expect((first.vm as any).clusterType.id).toBe('k3s');
|
|
194
|
+
|
|
195
|
+
const rke2Option = (first.vm as any).clusterTypes.find((c: any) => c.id === 'rke2');
|
|
196
|
+
|
|
197
|
+
(first.vm as any).clusterType = rke2Option;
|
|
198
|
+
await flushPromises();
|
|
199
|
+
first.unmount();
|
|
200
|
+
|
|
201
|
+
// Both rke2 and the k3s-provider default would write the same value
|
|
202
|
+
// object, so derivation from `value` alone cannot tell them apart — the
|
|
203
|
+
// module-level cache is the only thing that keeps "rke2" here.
|
|
204
|
+
const second = createWrapper({
|
|
205
|
+
provider: 'k3s',
|
|
206
|
+
value: { etcd: { enabled: true }, k3sServer: { enabled: false } },
|
|
207
|
+
chart,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
expect((second.vm as any).clusterType.id).toBe('rke2');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('does not leak a selection across different chart names', async() => {
|
|
214
|
+
const first = createWrapper({
|
|
215
|
+
provider: 'rke2',
|
|
216
|
+
value: {},
|
|
217
|
+
chart: { chartName: uniqueChartName('chart-a') },
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
await flushPromises();
|
|
221
|
+
|
|
222
|
+
const k3sOption = (first.vm as any).clusterTypes.find((c: any) => c.id === 'k3s');
|
|
223
|
+
|
|
224
|
+
(first.vm as any).clusterType = k3sOption;
|
|
225
|
+
await flushPromises();
|
|
226
|
+
first.unmount();
|
|
227
|
+
|
|
228
|
+
const second = createWrapper({
|
|
229
|
+
provider: 'rke2',
|
|
230
|
+
value: {},
|
|
231
|
+
chart: { chartName: uniqueChartName('chart-b') },
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Different cache key, so chart-b should still default from its provider.
|
|
235
|
+
expect((second.vm as any).clusterType.id).toBe('rke2');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('uses chart.name as the cache key when chartName is absent', async() => {
|
|
239
|
+
const chart = { name: uniqueChartName('fallback-name') };
|
|
240
|
+
|
|
241
|
+
const first = createWrapper({
|
|
242
|
+
provider: 'rke2', value: {}, chart
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
await flushPromises();
|
|
246
|
+
|
|
247
|
+
const k3sOption = (first.vm as any).clusterTypes.find((c: any) => c.id === 'k3s');
|
|
248
|
+
|
|
249
|
+
(first.vm as any).clusterType = k3sOption;
|
|
250
|
+
await flushPromises();
|
|
251
|
+
first.unmount();
|
|
252
|
+
|
|
253
|
+
const second = createWrapper({
|
|
254
|
+
provider: 'rke2', value: {}, chart
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
expect((second.vm as any).clusterType.id).toBe('k3s');
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe('clusterTypes data', () => {
|
|
262
|
+
it('sorts cluster types by id for the dropdown', () => {
|
|
263
|
+
const wrapper = createWrapper();
|
|
264
|
+
const ids = (wrapper.vm as any).clusterTypes.map((c: any) => c.id);
|
|
265
|
+
|
|
266
|
+
expect(ids).toStrictEqual([...ids].sort());
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { mapGetters } from 'vuex';
|
|
3
|
+
import isEmpty from 'lodash/isEmpty';
|
|
4
|
+
import sortBy from 'lodash/sortBy';
|
|
5
|
+
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
6
|
+
import { findBy } from '@shell/utils/array';
|
|
7
|
+
|
|
8
|
+
// Survives Form → YAML → Form tab switches (which unmount/remount this
|
|
9
|
+
// component) because the module is loaded once. Keyed by chart name so a
|
|
10
|
+
// different chart's install doesn't restore a stale selection.
|
|
11
|
+
const cachedSelections = new Map();
|
|
12
|
+
|
|
13
|
+
const CLUSTER_TYPES = [
|
|
14
|
+
{
|
|
15
|
+
group: 'managed',
|
|
16
|
+
id: 'aks',
|
|
17
|
+
label: 'cluster.provider.azureaks',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
group: 'managed',
|
|
21
|
+
id: 'eks',
|
|
22
|
+
label: 'cluster.provider.amazoneks',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
group: 'managed',
|
|
26
|
+
id: 'gke',
|
|
27
|
+
label: 'cluster.provider.googlegke',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
group: 'k3s',
|
|
31
|
+
id: 'k3s',
|
|
32
|
+
label: 'cluster.provider.k3s',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
group: 'kubeadm',
|
|
36
|
+
id: 'kubeadm',
|
|
37
|
+
label: 'cluster.provider.kubeAdmin',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
group: 'other',
|
|
41
|
+
id: 'other',
|
|
42
|
+
label: 'cluster.provider.other',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
group: 'rke',
|
|
46
|
+
id: 'rke2',
|
|
47
|
+
label: 'cluster.provider.rke2',
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
export default {
|
|
52
|
+
emits: ['input'],
|
|
53
|
+
|
|
54
|
+
components: { LabeledSelect },
|
|
55
|
+
|
|
56
|
+
props: {
|
|
57
|
+
chart: {
|
|
58
|
+
type: Object,
|
|
59
|
+
default: () => ({}),
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
mode: {
|
|
63
|
+
type: String,
|
|
64
|
+
default: 'create',
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
value: {
|
|
68
|
+
type: Object,
|
|
69
|
+
default: () => ({}),
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
data() {
|
|
74
|
+
return {
|
|
75
|
+
clusterType: null,
|
|
76
|
+
clusterTypes: sortBy(CLUSTER_TYPES, 'id'),
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
computed: {
|
|
81
|
+
...mapGetters(['currentCluster']),
|
|
82
|
+
provider() {
|
|
83
|
+
return this.currentCluster.status.provider.toLowerCase();
|
|
84
|
+
},
|
|
85
|
+
cacheKey() {
|
|
86
|
+
return this.chart?.chartName || this.chart?.name || '__default__';
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
watch: {
|
|
91
|
+
clusterType(clusterType) {
|
|
92
|
+
if (isEmpty(clusterType)) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
cachedSelections.set(this.cacheKey, clusterType.id);
|
|
97
|
+
this.applyClusterTypeValues(clusterType);
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
created() {
|
|
102
|
+
const cachedId = cachedSelections.get(this.cacheKey);
|
|
103
|
+
const cached = cachedId ? findBy(this.clusterTypes, 'id', cachedId) : null;
|
|
104
|
+
|
|
105
|
+
if (cached) {
|
|
106
|
+
this.clusterType = cached;
|
|
107
|
+
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const matched = findBy(this.clusterTypes, 'id', this.provider);
|
|
112
|
+
|
|
113
|
+
this.clusterType = matched || findBy(this.clusterTypes, 'id', 'other');
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
methods: {
|
|
117
|
+
applyClusterTypeValues(clusterType) {
|
|
118
|
+
const isK3s = clusterType.group === 'k3s';
|
|
119
|
+
|
|
120
|
+
if (!this.value.etcd) {
|
|
121
|
+
this.value.etcd = {};
|
|
122
|
+
}
|
|
123
|
+
if (!this.value.k3sServer) {
|
|
124
|
+
this.value.k3sServer = {};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this.value.etcd.enabled = !isK3s;
|
|
128
|
+
this.value.k3sServer.enabled = isK3s;
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
</script>
|
|
133
|
+
|
|
134
|
+
<template>
|
|
135
|
+
<div class="config-monitoring-dashboards-container">
|
|
136
|
+
<div class="row">
|
|
137
|
+
<div class="col span-6">
|
|
138
|
+
<LabeledSelect
|
|
139
|
+
v-model:value="clusterType"
|
|
140
|
+
:label="t('monitoring.clusterType.label')"
|
|
141
|
+
:placeholder="t('monitoring.clusterType.placeholder')"
|
|
142
|
+
:localized-label="true"
|
|
143
|
+
:options="clusterTypes"
|
|
144
|
+
:mode="mode"
|
|
145
|
+
/>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
</template>
|
|
150
|
+
|
|
151
|
+
<style lang="scss" scoped>
|
|
152
|
+
.config-monitoring-dashboards-container {
|
|
153
|
+
padding: 10px 0;
|
|
154
|
+
}
|
|
155
|
+
</style>
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { shallowMount } from '@vue/test-utils';
|
|
2
|
+
import Generic, { INPUT_TYPES } from '@shell/cloud-credential/generic.vue';
|
|
3
|
+
import { SCHEMA } from '@shell/config/types';
|
|
4
|
+
|
|
5
|
+
const DRIVER_NAME = 'testdriver';
|
|
6
|
+
const CONFIG_ID = `testdrivercredentialconfig`;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Build a normanSchema whose resourceFields include one field per INPUT_TYPE
|
|
10
|
+
* plus a plain "string" field (which should fall back to "text").
|
|
11
|
+
*/
|
|
12
|
+
const buildResourceFields = () => {
|
|
13
|
+
const fields: Record<string, { type: string }> = {};
|
|
14
|
+
|
|
15
|
+
INPUT_TYPES.forEach((t) => {
|
|
16
|
+
fields[`${ t }Field`] = { type: t };
|
|
17
|
+
});
|
|
18
|
+
fields.stringField = { type: 'string' };
|
|
19
|
+
|
|
20
|
+
return fields;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const normanSchema = { resourceFields: buildResourceFields() };
|
|
24
|
+
|
|
25
|
+
const mockedStore = () => ({
|
|
26
|
+
getters: {
|
|
27
|
+
'i18n/t': (key: string) => key,
|
|
28
|
+
t: (key: string) => key,
|
|
29
|
+
'plugins/credentialFieldForDriver': () => DRIVER_NAME,
|
|
30
|
+
'plugins/fieldNamesForDriver': () => [],
|
|
31
|
+
},
|
|
32
|
+
dispatch: jest.fn((_action: string, args: any) => {
|
|
33
|
+
if (args?.type === SCHEMA && args?.id === CONFIG_ID) {
|
|
34
|
+
return Promise.resolve(normanSchema);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return Promise.resolve(null);
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const mountGeneric = () => {
|
|
42
|
+
const store = mockedStore();
|
|
43
|
+
const value = {
|
|
44
|
+
decodedData: {} as Record<string, string>,
|
|
45
|
+
setData: jest.fn(),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return shallowMount(Generic, {
|
|
49
|
+
props: {
|
|
50
|
+
driverName: DRIVER_NAME,
|
|
51
|
+
value,
|
|
52
|
+
mode: 'edit',
|
|
53
|
+
},
|
|
54
|
+
global: {
|
|
55
|
+
mocks: {
|
|
56
|
+
$store: store,
|
|
57
|
+
$fetchState: { pending: false },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
data() {
|
|
61
|
+
return {
|
|
62
|
+
normanSchema,
|
|
63
|
+
keyOptions: Object.keys(normanSchema.resourceFields),
|
|
64
|
+
} as any;
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
describe('generic cloud credential', () => {
|
|
70
|
+
describe('typeForField', () => {
|
|
71
|
+
it.each(INPUT_TYPES)('should return "%s" for a field with type "%s"', (inputType) => {
|
|
72
|
+
const wrapper = mountGeneric();
|
|
73
|
+
|
|
74
|
+
expect((wrapper.vm as any).typeForField(`${ inputType }Field`)).toBe(inputType);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should fall back to "text" for a field type not in INPUT_TYPES', () => {
|
|
78
|
+
const wrapper = mountGeneric();
|
|
79
|
+
|
|
80
|
+
expect((wrapper.vm as any).typeForField('stringField')).toBe('text');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should fall back to "text" when no normanSchema could be located', () => {
|
|
84
|
+
const store = {
|
|
85
|
+
...mockedStore(),
|
|
86
|
+
dispatch: jest.fn(() => Promise.resolve(null)),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const wrapper = shallowMount(Generic, {
|
|
90
|
+
props: {
|
|
91
|
+
driverName: DRIVER_NAME,
|
|
92
|
+
value: { decodedData: {}, setData: jest.fn() },
|
|
93
|
+
mode: 'edit',
|
|
94
|
+
},
|
|
95
|
+
global: {
|
|
96
|
+
mocks: {
|
|
97
|
+
$store: store,
|
|
98
|
+
$fetchState: { pending: false },
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
data() {
|
|
102
|
+
return { normanSchema: null, keyOptions: [] };
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect((wrapper.vm as any).typeForField('passwordField')).toBe('text');
|
|
107
|
+
expect((wrapper.vm as any).typeForField('cronField')).toBe('text');
|
|
108
|
+
expect((wrapper.vm as any).typeForField('anyField')).toBe('text');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('LabeledInput rendering', () => {
|
|
113
|
+
it('should render a LabeledInput inside the KeyValue value slot', () => {
|
|
114
|
+
const wrapper = mountGeneric();
|
|
115
|
+
const keyValue = wrapper.findComponent({ name: 'KeyValue' });
|
|
116
|
+
|
|
117
|
+
expect(keyValue.exists()).toBe(true);
|
|
118
|
+
|
|
119
|
+
// The component registers LabeledInput and passes typeForField via the slot template
|
|
120
|
+
expect((wrapper.vm as any).$options.components.LabeledInput).toBeDefined();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should pass typeForField result as the type prop to LabeledInput', () => {
|
|
124
|
+
const wrapper = mountGeneric();
|
|
125
|
+
|
|
126
|
+
// Validate the integration: typeForField returns the correct type for each INPUT_TYPE field
|
|
127
|
+
INPUT_TYPES.forEach((inputType) => {
|
|
128
|
+
expect((wrapper.vm as any).typeForField(`${ inputType }Field`)).toBe(inputType);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -5,13 +5,24 @@ import { Banner } from '@components/Banner';
|
|
|
5
5
|
import { simplify, iffyFields, likelyFields } from '@shell/store/plugins';
|
|
6
6
|
import Loading from '@shell/components/Loading';
|
|
7
7
|
import { SCHEMA } from '@shell/config/types';
|
|
8
|
+
import { LabeledInput } from '@components/Form/LabeledInput';
|
|
9
|
+
|
|
10
|
+
export const INPUT_TYPES = [
|
|
11
|
+
'number',
|
|
12
|
+
'password',
|
|
13
|
+
'text',
|
|
14
|
+
];
|
|
8
15
|
|
|
9
16
|
export default {
|
|
10
17
|
emits: ['validationChanged'],
|
|
11
18
|
|
|
12
19
|
components: {
|
|
13
|
-
KeyValue,
|
|
20
|
+
KeyValue,
|
|
21
|
+
Banner,
|
|
22
|
+
Loading,
|
|
23
|
+
LabeledInput
|
|
14
24
|
},
|
|
25
|
+
|
|
15
26
|
mixins: [CreateEditView],
|
|
16
27
|
|
|
17
28
|
props: {
|
|
@@ -60,6 +71,7 @@ export default {
|
|
|
60
71
|
return {
|
|
61
72
|
errors: null,
|
|
62
73
|
normanSchema: null,
|
|
74
|
+
keyOptions: [],
|
|
63
75
|
};
|
|
64
76
|
},
|
|
65
77
|
|
|
@@ -72,7 +84,14 @@ export default {
|
|
|
72
84
|
methods: {
|
|
73
85
|
update(val) {
|
|
74
86
|
this.value.setData(val);
|
|
75
|
-
}
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
typeForField(fieldName = '') {
|
|
90
|
+
const fieldSchema = this.normanSchema?.resourceFields?.[fieldName] || {};
|
|
91
|
+
const fieldType = fieldSchema.type || '';
|
|
92
|
+
|
|
93
|
+
return INPUT_TYPES.includes(fieldType) ? fieldType : 'text';
|
|
94
|
+
},
|
|
76
95
|
},
|
|
77
96
|
|
|
78
97
|
computed: {
|
|
@@ -103,6 +122,17 @@ export default {
|
|
|
103
122
|
:remove-allowed="!hasSupport"
|
|
104
123
|
:initial-empty-row="true"
|
|
105
124
|
@update:value="update"
|
|
106
|
-
|
|
125
|
+
>
|
|
126
|
+
<template #value="{row, mode, queueUpdate}">
|
|
127
|
+
<LabeledInput
|
|
128
|
+
v-model:value="row.value"
|
|
129
|
+
:mode="mode"
|
|
130
|
+
:type="typeForField(row.key)"
|
|
131
|
+
:aria-label="row.key || t('generic.value')"
|
|
132
|
+
placeholder-key="keyValue.valuePlaceholder"
|
|
133
|
+
@update:value="queueUpdate"
|
|
134
|
+
/>
|
|
135
|
+
</template>
|
|
136
|
+
</KeyValue>
|
|
107
137
|
</div>
|
|
108
138
|
</template>
|
|
@@ -3,23 +3,13 @@ import { Banner } from '@components/Banner';
|
|
|
3
3
|
import Loading from '@shell/components/Loading';
|
|
4
4
|
import { mapGetters } from 'vuex';
|
|
5
5
|
import { hasLeader, leaderChanges, failedProposals } from '@shell/utils/grafana';
|
|
6
|
-
import {
|
|
6
|
+
import { fetchMonitoringVersion } from '@shell/utils/monitoring';
|
|
7
7
|
|
|
8
8
|
export default {
|
|
9
9
|
components: { Banner, Loading },
|
|
10
10
|
async fetch() {
|
|
11
11
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (this.$store.getters[`${ inStore }/canList`](CATALOG.APP)) {
|
|
15
|
-
try {
|
|
16
|
-
const res = await this.$store.dispatch(`${ inStore }/find`, { type: CATALOG.APP, id: 'cattle-monitoring-system/rancher-monitoring' });
|
|
17
|
-
|
|
18
|
-
monitoringVersion = res?.currentVersion;
|
|
19
|
-
} catch (err) {
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
}
|
|
12
|
+
const monitoringVersion = await fetchMonitoringVersion(this.$store, inStore);
|
|
23
13
|
|
|
24
14
|
const leader = await hasLeader(monitoringVersion, this.$store.dispatch, this.currentCluster.id);
|
|
25
15
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import Loading from '@shell/components/Loading';
|
|
3
3
|
import { Banner } from '@components/Banner';
|
|
4
4
|
import { computeDashboardUrl } from '@shell/utils/grafana';
|
|
5
|
-
import {
|
|
5
|
+
import { fetchMonitoringVersion } from '@shell/utils/monitoring';
|
|
6
6
|
|
|
7
7
|
export default {
|
|
8
8
|
components: { Banner, Loading },
|
|
@@ -41,13 +41,7 @@ export default {
|
|
|
41
41
|
async fetch() {
|
|
42
42
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
const res = await this.$store.dispatch(`${ inStore }/find`, { type: CATALOG.APP, id: 'cattle-monitoring-system/rancher-monitoring' });
|
|
47
|
-
|
|
48
|
-
this.monitoringVersion = res?.currentVersion;
|
|
49
|
-
} catch (err) {}
|
|
50
|
-
}
|
|
44
|
+
this.monitoringVersion = await fetchMonitoringVersion(this.$store, inStore);
|
|
51
45
|
},
|
|
52
46
|
data() {
|
|
53
47
|
return {
|