@rancher/shell 0.3.19 → 0.3.21
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/translations/en-us.yaml +4 -1
- package/components/PromptModal.vue +4 -0
- package/components/Questions/Array.vue +2 -2
- package/components/Questions/Boolean.vue +7 -1
- package/components/Questions/CloudCredential.vue +1 -0
- package/components/Questions/Enum.vue +21 -2
- package/components/Questions/Float.vue +8 -3
- package/components/Questions/Int.vue +8 -3
- package/components/Questions/Question.js +72 -0
- package/components/Questions/QuestionMap.vue +2 -1
- package/components/Questions/Radio.vue +33 -0
- package/components/Questions/Reference.vue +2 -0
- package/components/Questions/String.vue +8 -3
- package/components/Questions/Yaml.vue +46 -0
- package/components/Questions/__tests__/Boolean.test.ts +123 -0
- package/components/Questions/__tests__/Float.test.ts +123 -0
- package/components/Questions/__tests__/Int.test.ts +123 -0
- package/components/Questions/__tests__/String.test.ts +123 -0
- package/components/Questions/__tests__/Yaml.test.ts +123 -0
- package/components/Questions/index.vue +8 -1
- package/components/ResourceTable.vue +10 -13
- package/components/SideNav.vue +634 -0
- package/components/__tests__/NamespaceFilter.test.ts +3 -4
- package/components/form/UnitInput.vue +1 -0
- package/components/form/__tests__/KeyValue.test.ts +2 -1
- package/components/form/__tests__/UnitInput.test.ts +2 -2
- package/components/formatter/LinkName.vue +12 -1
- package/components/nav/WorkspaceSwitcher.vue +4 -1
- package/core/plugin-helpers.js +4 -1
- package/core/types.ts +25 -1
- package/detail/node.vue +2 -2
- package/edit/fleet.cattle.io.gitrepo.vue +7 -0
- package/layouts/default.vue +11 -597
- package/middleware/authenticated.js +2 -14
- package/models/fleet.cattle.io.gitrepo.js +3 -1
- package/package.json +1 -1
- package/pages/auth/login.vue +1 -1
- package/pages/c/_cluster/fleet/index.vue +4 -0
- package/pages/c/_cluster/uiplugins/index.vue +3 -3
- package/rancher-components/components/Form/LabeledInput/LabeledInput.vue +8 -0
- package/rancher-components/components/Form/Radio/RadioButton.test.ts +7 -3
- package/store/auth.js +2 -0
- package/types/shell/index.d.ts +2 -0
- package/utils/auth.js +17 -0
- package/utils/object.js +0 -1
- package/utils/validators/__tests__/cidr.test.ts +33 -0
- package/utils/validators/cidr.js +5 -0
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import debounce from 'lodash/debounce';
|
|
3
|
+
import isEqual from 'lodash/isEqual';
|
|
4
|
+
import { mapGetters, mapState } from 'vuex';
|
|
5
|
+
import {
|
|
6
|
+
mapPref,
|
|
7
|
+
FAVORITE_TYPES
|
|
8
|
+
} from '@shell/store/prefs';
|
|
9
|
+
import { getVersionInfo } from '@shell/utils/version';
|
|
10
|
+
import { addObjects, replaceWith, clear, addObject } from '@shell/utils/array';
|
|
11
|
+
import { sortBy } from '@shell/utils/sort';
|
|
12
|
+
import { ucFirst } from '@shell/utils/string';
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
HCI, CATALOG, UI, SCHEMA, COUNT
|
|
16
|
+
} from '@shell/config/types';
|
|
17
|
+
import { HARVESTER_NAME as HARVESTER } from '@shell/config/features';
|
|
18
|
+
import { NAME as EXPLORER } from '@shell/config/product/explorer';
|
|
19
|
+
import { BASIC, FAVORITE, USED } from '@shell/store/type-map';
|
|
20
|
+
import { NAME as NAVLINKS } from '@shell/config/product/navlinks';
|
|
21
|
+
import Group from '@shell/components/nav/Group';
|
|
22
|
+
|
|
23
|
+
export default {
|
|
24
|
+
name: 'SideNav',
|
|
25
|
+
components: { Group },
|
|
26
|
+
data() {
|
|
27
|
+
return {
|
|
28
|
+
groups: [],
|
|
29
|
+
gettingGroups: false
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
created() {
|
|
34
|
+
this.queueUpdate = debounce(this.getGroups, 500);
|
|
35
|
+
|
|
36
|
+
this.getGroups();
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
mounted() {
|
|
40
|
+
// Sync the navigation tree on fresh load
|
|
41
|
+
this.$nextTick(() => this.syncNav());
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
watch: {
|
|
45
|
+
counts(a, b) {
|
|
46
|
+
if ( a !== b ) {
|
|
47
|
+
this.queueUpdate();
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
allSchemas(a, b) {
|
|
52
|
+
if ( a !== b ) {
|
|
53
|
+
this.queueUpdate();
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
allNavLinks(a, b) {
|
|
58
|
+
if ( a !== b ) {
|
|
59
|
+
this.queueUpdate();
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
favoriteTypes(a, b) {
|
|
64
|
+
if ( !isEqual(a, b) ) {
|
|
65
|
+
this.queueUpdate();
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
locale(a, b) {
|
|
70
|
+
if ( !isEqual(a, b) ) {
|
|
71
|
+
this.getGroups();
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
productId(a, b) {
|
|
76
|
+
if ( !isEqual(a, b) ) {
|
|
77
|
+
// Immediately update because you'll see it come in later
|
|
78
|
+
this.getGroups();
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
namespaceMode(a, b) {
|
|
83
|
+
if ( !isEqual(a, b) ) {
|
|
84
|
+
// Immediately update because you'll see it come in later
|
|
85
|
+
this.getGroups();
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
namespaces(a, b) {
|
|
90
|
+
if ( !isEqual(a, b) ) {
|
|
91
|
+
// Immediately update because you'll see it come in later
|
|
92
|
+
this.getGroups();
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
clusterReady(a, b) {
|
|
97
|
+
if ( !isEqual(a, b) ) {
|
|
98
|
+
// Immediately update because you'll see it come in later
|
|
99
|
+
this.getGroups();
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
product(a, b) {
|
|
104
|
+
if ( !isEqual(a, b) ) {
|
|
105
|
+
// Immediately update because you'll see it come in later
|
|
106
|
+
this.getGroups();
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
$route(a, b) {
|
|
111
|
+
this.$nextTick(() => this.syncNav());
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
computed: {
|
|
117
|
+
...mapState(['managementReady', 'clusterReady']),
|
|
118
|
+
...mapGetters(['productId', 'clusterId', 'currentProduct', 'isSingleProduct', 'namespaceMode', 'isExplorer', 'isVirtualCluster']),
|
|
119
|
+
...mapGetters({ locale: 'i18n/selectedLocaleLabel', availableLocales: 'i18n/availableLocales' }),
|
|
120
|
+
...mapGetters('type-map', ['activeProducts']),
|
|
121
|
+
|
|
122
|
+
favoriteTypes: mapPref(FAVORITE_TYPES),
|
|
123
|
+
|
|
124
|
+
showClusterTools() {
|
|
125
|
+
return this.isExplorer &&
|
|
126
|
+
this.$store.getters['cluster/canList'](CATALOG.CLUSTER_REPO) &&
|
|
127
|
+
this.$store.getters['cluster/canList'](CATALOG.APP);
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
supportLink() {
|
|
131
|
+
const product = this.currentProduct;
|
|
132
|
+
|
|
133
|
+
if (product?.supportRoute) {
|
|
134
|
+
return { ...product.supportRoute, params: { ...product.supportRoute.params, cluster: this.clusterId } };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { name: `c-cluster-${ product?.name }-support` };
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
displayVersion() {
|
|
141
|
+
if (this.isSingleProduct?.getVersionInfo) {
|
|
142
|
+
return this.isSingleProduct?.getVersionInfo(this.$store);
|
|
143
|
+
}
|
|
144
|
+
const { displayVersion } = getVersionInfo(this.$store);
|
|
145
|
+
|
|
146
|
+
return displayVersion;
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
singleProductAbout() {
|
|
150
|
+
return this.isSingleProduct?.aboutPage;
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
harvesterVersion() {
|
|
154
|
+
return this.$store.getters['cluster/byId'](HCI.SETTING, 'server-version')?.value || 'unknown';
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
showProductFooter() {
|
|
158
|
+
if (this.isVirtualProduct) {
|
|
159
|
+
return true;
|
|
160
|
+
} else {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
isVirtualProduct() {
|
|
166
|
+
return this.currentProduct.name === HARVESTER;
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
allNavLinks() {
|
|
170
|
+
if ( !this.clusterId || !this.$store.getters['cluster/schemaFor'](UI.NAV_LINK) ) {
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return this.$store.getters['cluster/all'](UI.NAV_LINK);
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
allSchemas() {
|
|
178
|
+
const managementReady = this.managementReady;
|
|
179
|
+
const product = this.currentProduct;
|
|
180
|
+
|
|
181
|
+
if ( !managementReady || !product ) {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return this.$store.getters[`${ product.inStore }/all`](SCHEMA);
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
counts() {
|
|
189
|
+
const managementReady = this.managementReady;
|
|
190
|
+
const product = this.currentProduct;
|
|
191
|
+
|
|
192
|
+
if ( !managementReady || !product ) {
|
|
193
|
+
return {};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const inStore = product.inStore;
|
|
197
|
+
|
|
198
|
+
// So that there's something to watch for updates
|
|
199
|
+
if ( this.$store.getters[`${ inStore }/haveAll`](COUNT) ) {
|
|
200
|
+
const counts = this.$store.getters[`${ inStore }/all`](COUNT)[0].counts;
|
|
201
|
+
|
|
202
|
+
return counts;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {};
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
namespaces() {
|
|
209
|
+
return this.$store.getters['activeNamespaceCache'];
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
methods: {
|
|
213
|
+
/**
|
|
214
|
+
* Fetch navigation by creating groups from product schemas
|
|
215
|
+
*/
|
|
216
|
+
getGroups() {
|
|
217
|
+
if ( this.gettingGroups ) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
this.gettingGroups = true;
|
|
221
|
+
|
|
222
|
+
if ( !this.clusterReady ) {
|
|
223
|
+
clear(this.groups);
|
|
224
|
+
this.gettingGroups = false;
|
|
225
|
+
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const currentProduct = this.$store.getters['productId'];
|
|
230
|
+
let namespaces = null;
|
|
231
|
+
|
|
232
|
+
if ( !this.$store.getters['isAllNamespaces'] ) {
|
|
233
|
+
const namespacesObject = this.$store.getters['namespaces']();
|
|
234
|
+
|
|
235
|
+
namespaces = Object.keys(namespacesObject);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Always show cluster-level types, regardless of the namespace filter
|
|
239
|
+
const namespaceMode = 'both';
|
|
240
|
+
const out = [];
|
|
241
|
+
const loadProducts = this.isExplorer ? [EXPLORER] : [];
|
|
242
|
+
|
|
243
|
+
const productMap = this.activeProducts.reduce((acc, p) => {
|
|
244
|
+
return { ...acc, [p.name]: p };
|
|
245
|
+
}, {});
|
|
246
|
+
|
|
247
|
+
if ( this.isExplorer ) {
|
|
248
|
+
for ( const product of this.activeProducts ) {
|
|
249
|
+
if ( product.inStore === 'cluster' ) {
|
|
250
|
+
addObject(loadProducts, product.name);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// This should already have come into the list from above, but in case it hasn't...
|
|
256
|
+
addObject(loadProducts, currentProduct);
|
|
257
|
+
|
|
258
|
+
this.getProductsGroups(out, loadProducts, namespaceMode, namespaces, productMap);
|
|
259
|
+
this.getExplorerGroups(out);
|
|
260
|
+
|
|
261
|
+
replaceWith(this.groups, ...sortBy(out, ['weight:desc', 'label']));
|
|
262
|
+
|
|
263
|
+
this.gettingGroups = false;
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
getProductsGroups(out, loadProducts, namespaceMode, namespaces, productMap) {
|
|
267
|
+
const clusterId = this.$store.getters['clusterId'];
|
|
268
|
+
const currentType = this.$route.params.resource || '';
|
|
269
|
+
|
|
270
|
+
for ( const productId of loadProducts ) {
|
|
271
|
+
const modes = [BASIC];
|
|
272
|
+
|
|
273
|
+
if ( productId === NAVLINKS ) {
|
|
274
|
+
// Navlinks produce their own top-level nav items so don't need to show it as a product.
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if ( productId === EXPLORER ) {
|
|
279
|
+
modes.push(FAVORITE);
|
|
280
|
+
modes.push(USED);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
for ( const mode of modes ) {
|
|
284
|
+
const types = this.$store.getters['type-map/allTypes'](productId, mode) || {};
|
|
285
|
+
|
|
286
|
+
const more = this.$store.getters['type-map/getTree'](productId, mode, types, clusterId, namespaceMode, namespaces, currentType);
|
|
287
|
+
|
|
288
|
+
if ( productId === EXPLORER || !this.isExplorer ) {
|
|
289
|
+
addObjects(out, more);
|
|
290
|
+
} else {
|
|
291
|
+
const root = more.find((x) => x.name === 'root');
|
|
292
|
+
const other = more.filter((x) => x.name !== 'root');
|
|
293
|
+
|
|
294
|
+
const group = {
|
|
295
|
+
name: productId,
|
|
296
|
+
label: this.$store.getters['i18n/withFallback'](`product.${ productId }`, null, ucFirst(productId)),
|
|
297
|
+
children: [...(root?.children || []), ...other],
|
|
298
|
+
weight: productMap[productId]?.weight || 0,
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
addObject(out, group);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
getExplorerGroups(out) {
|
|
308
|
+
if ( this.isExplorer ) {
|
|
309
|
+
const allNavLinks = this.allNavLinks;
|
|
310
|
+
const toAdd = [];
|
|
311
|
+
const haveGroup = {};
|
|
312
|
+
|
|
313
|
+
for ( const obj of allNavLinks ) {
|
|
314
|
+
if ( !obj.link ) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const groupLabel = obj.spec.group;
|
|
319
|
+
const groupSlug = obj.normalizedGroup;
|
|
320
|
+
|
|
321
|
+
const entry = {
|
|
322
|
+
name: `link-${ obj._key }`,
|
|
323
|
+
link: obj.link,
|
|
324
|
+
target: obj.actualTarget,
|
|
325
|
+
label: obj.labelDisplay,
|
|
326
|
+
sideLabel: obj.spec.sideLabel,
|
|
327
|
+
iconSrc: obj.spec.iconSrc,
|
|
328
|
+
description: obj.spec.description,
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// If there's a spec.group (groupLabel), all entries with that name go under one nav group
|
|
332
|
+
if ( groupSlug ) {
|
|
333
|
+
if ( haveGroup[groupSlug] ) {
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
haveGroup[groupSlug] = true;
|
|
338
|
+
|
|
339
|
+
toAdd.push({
|
|
340
|
+
name: `navlink-group-${ groupSlug }`,
|
|
341
|
+
label: groupLabel,
|
|
342
|
+
isRoot: true,
|
|
343
|
+
// This is the item that actually shows up in the nav, since this outer group will be invisible
|
|
344
|
+
children: [
|
|
345
|
+
{
|
|
346
|
+
name: `navlink-child-${ groupSlug }`,
|
|
347
|
+
label: groupLabel,
|
|
348
|
+
route: {
|
|
349
|
+
name: 'c-cluster-navlinks-group',
|
|
350
|
+
params: {
|
|
351
|
+
cluster: this.clusterId,
|
|
352
|
+
group: groupSlug,
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
}
|
|
356
|
+
],
|
|
357
|
+
weight: -100,
|
|
358
|
+
});
|
|
359
|
+
} else {
|
|
360
|
+
toAdd.push({
|
|
361
|
+
name: `navlink-${ entry.name }`,
|
|
362
|
+
label: entry.label,
|
|
363
|
+
isRoot: true,
|
|
364
|
+
// This is the item that actually shows up in the nav, since this outer group will be invisible
|
|
365
|
+
children: [entry],
|
|
366
|
+
weight: -100,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
addObjects(out, toAdd);
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
groupSelected(selected) {
|
|
376
|
+
this.$refs.groups.forEach((grp) => {
|
|
377
|
+
if (grp.canCollapse) {
|
|
378
|
+
grp.isExpanded = (grp.group.name === selected.name);
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
},
|
|
382
|
+
|
|
383
|
+
collapseAll() {
|
|
384
|
+
this.$refs.groups.forEach((grp) => {
|
|
385
|
+
grp.isExpanded = false;
|
|
386
|
+
});
|
|
387
|
+
},
|
|
388
|
+
|
|
389
|
+
switchLocale(locale) {
|
|
390
|
+
this.$store.dispatch('i18n/switchTo', locale);
|
|
391
|
+
},
|
|
392
|
+
|
|
393
|
+
syncNav() {
|
|
394
|
+
const refs = this.$refs.groups;
|
|
395
|
+
|
|
396
|
+
if (refs) {
|
|
397
|
+
// Only expand one group - so after the first has been expanded, no more will
|
|
398
|
+
// This prevents the 'More Resources' group being expanded in addition to the normal group
|
|
399
|
+
let canExpand = true;
|
|
400
|
+
const expanded = refs.filter((grp) => grp.isExpanded)[0];
|
|
401
|
+
|
|
402
|
+
if (expanded && expanded.hasActiveRoute()) {
|
|
403
|
+
this.$nextTick(() => expanded.syncNav());
|
|
404
|
+
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
refs.forEach((grp) => {
|
|
408
|
+
if (!grp.group.isRoot) {
|
|
409
|
+
grp.isExpanded = false;
|
|
410
|
+
if (canExpand) {
|
|
411
|
+
const isActive = grp.hasActiveRoute();
|
|
412
|
+
|
|
413
|
+
if (isActive) {
|
|
414
|
+
grp.isExpanded = true;
|
|
415
|
+
canExpand = false;
|
|
416
|
+
this.$nextTick(() => grp.syncNav());
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
};
|
|
425
|
+
</script>
|
|
426
|
+
|
|
427
|
+
<template>
|
|
428
|
+
<nav class="side-nav">
|
|
429
|
+
<!-- Actual nav -->
|
|
430
|
+
<div class="nav">
|
|
431
|
+
<template v-for="(g) in groups">
|
|
432
|
+
<Group
|
|
433
|
+
ref="groups"
|
|
434
|
+
:key="g.name"
|
|
435
|
+
id-prefix=""
|
|
436
|
+
class="package"
|
|
437
|
+
:group="g"
|
|
438
|
+
:can-collapse="!g.isRoot"
|
|
439
|
+
:show-header="!g.isRoot"
|
|
440
|
+
@selected="groupSelected($event)"
|
|
441
|
+
@expand="groupSelected($event)"
|
|
442
|
+
/>
|
|
443
|
+
</template>
|
|
444
|
+
</div>
|
|
445
|
+
<!-- Cluster tools -->
|
|
446
|
+
<n-link
|
|
447
|
+
v-if="showClusterTools"
|
|
448
|
+
tag="div"
|
|
449
|
+
class="tools"
|
|
450
|
+
:to="{name: 'c-cluster-explorer-tools', params: {cluster: clusterId}}"
|
|
451
|
+
>
|
|
452
|
+
<a
|
|
453
|
+
class="tools-button"
|
|
454
|
+
@click="collapseAll()"
|
|
455
|
+
>
|
|
456
|
+
<i class="icon icon-gear" />
|
|
457
|
+
<span>{{ t('nav.clusterTools') }}</span>
|
|
458
|
+
</a>
|
|
459
|
+
</n-link>
|
|
460
|
+
<!-- SideNav footer area (seems to be tied to harvester) -->
|
|
461
|
+
<div
|
|
462
|
+
v-if="showProductFooter"
|
|
463
|
+
class="footer"
|
|
464
|
+
>
|
|
465
|
+
<!-- support link -->
|
|
466
|
+
<nuxt-link
|
|
467
|
+
:to="supportLink"
|
|
468
|
+
class="pull-right"
|
|
469
|
+
>
|
|
470
|
+
{{ t('nav.support', {hasSupport: true}) }}
|
|
471
|
+
</nuxt-link>
|
|
472
|
+
<!-- version number -->
|
|
473
|
+
<span
|
|
474
|
+
v-clean-tooltip="{content: displayVersion, placement: 'top'}"
|
|
475
|
+
class="clip version text-muted"
|
|
476
|
+
>
|
|
477
|
+
{{ displayVersion }}
|
|
478
|
+
</span>
|
|
479
|
+
|
|
480
|
+
<!-- locale selector -->
|
|
481
|
+
<span v-if="isSingleProduct">
|
|
482
|
+
<v-popover
|
|
483
|
+
popover-class="localeSelector"
|
|
484
|
+
placement="top"
|
|
485
|
+
trigger="click"
|
|
486
|
+
>
|
|
487
|
+
<a
|
|
488
|
+
data-testid="locale-selector"
|
|
489
|
+
class="locale-chooser"
|
|
490
|
+
>
|
|
491
|
+
{{ locale }}
|
|
492
|
+
</a>
|
|
493
|
+
|
|
494
|
+
<template slot="popover">
|
|
495
|
+
<ul
|
|
496
|
+
class="list-unstyled dropdown"
|
|
497
|
+
style="margin: -1px;"
|
|
498
|
+
>
|
|
499
|
+
<li
|
|
500
|
+
v-for="(label, name) in availableLocales"
|
|
501
|
+
:key="name"
|
|
502
|
+
class="hand"
|
|
503
|
+
@click="switchLocale(name)"
|
|
504
|
+
>
|
|
505
|
+
{{ label }}
|
|
506
|
+
</li>
|
|
507
|
+
</ul>
|
|
508
|
+
</template>
|
|
509
|
+
</v-popover>
|
|
510
|
+
</span>
|
|
511
|
+
</div>
|
|
512
|
+
<!-- SideNav footer alternative -->
|
|
513
|
+
<div
|
|
514
|
+
v-else
|
|
515
|
+
class="version text-muted flex"
|
|
516
|
+
>
|
|
517
|
+
<nuxt-link
|
|
518
|
+
v-if="singleProductAbout"
|
|
519
|
+
:to="singleProductAbout"
|
|
520
|
+
>
|
|
521
|
+
{{ displayVersion }}
|
|
522
|
+
</nuxt-link>
|
|
523
|
+
<template v-else>
|
|
524
|
+
<span>{{ displayVersion }}</span>
|
|
525
|
+
<span
|
|
526
|
+
v-if="isVirtualCluster && isExplorer"
|
|
527
|
+
v-tooltip="{content: harvesterVersion, placement: 'top'}"
|
|
528
|
+
class="clip text-muted ml-5"
|
|
529
|
+
>
|
|
530
|
+
(Harvester-{{ harvesterVersion }})
|
|
531
|
+
</span>
|
|
532
|
+
</template>
|
|
533
|
+
</div>
|
|
534
|
+
</nav>
|
|
535
|
+
</template>
|
|
536
|
+
|
|
537
|
+
<style lang="scss" scoped>
|
|
538
|
+
.side-nav {
|
|
539
|
+
display: flex;
|
|
540
|
+
flex-direction: column;
|
|
541
|
+
.nav {
|
|
542
|
+
flex: 1;
|
|
543
|
+
overflow-y: auto;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
position: relative;
|
|
547
|
+
background-color: var(--nav-bg);
|
|
548
|
+
border-right: var(--nav-border-size) solid var(--nav-border);
|
|
549
|
+
overflow-y: auto;
|
|
550
|
+
|
|
551
|
+
// h6 is used in Group element
|
|
552
|
+
::v-deep h6 {
|
|
553
|
+
margin: 0;
|
|
554
|
+
letter-spacing: normal;
|
|
555
|
+
line-height: initial;
|
|
556
|
+
|
|
557
|
+
A { padding-left: 0; }
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
.tools {
|
|
561
|
+
display: flex;
|
|
562
|
+
margin: 10px;
|
|
563
|
+
text-align: center;
|
|
564
|
+
|
|
565
|
+
A {
|
|
566
|
+
align-items: center;
|
|
567
|
+
border: 1px solid var(--border);
|
|
568
|
+
border-radius: 5px;
|
|
569
|
+
color: var(--body-text);
|
|
570
|
+
display: flex;
|
|
571
|
+
justify-content: center;
|
|
572
|
+
outline: 0;
|
|
573
|
+
flex: 1;
|
|
574
|
+
padding: 10px;
|
|
575
|
+
|
|
576
|
+
&:hover {
|
|
577
|
+
background: var(--nav-hover);
|
|
578
|
+
text-decoration: none;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
> I {
|
|
582
|
+
margin-right: 4px;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
&.nuxt-link-active:not(:hover) {
|
|
587
|
+
A {
|
|
588
|
+
background-color: var(--nav-active);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.version {
|
|
594
|
+
cursor: default;
|
|
595
|
+
margin: 0 10px 10px 10px;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.footer {
|
|
599
|
+
margin: 20px;
|
|
600
|
+
|
|
601
|
+
display: flex;
|
|
602
|
+
flex: 0;
|
|
603
|
+
flex-direction: row;
|
|
604
|
+
> * {
|
|
605
|
+
flex: 1;
|
|
606
|
+
color: var(--link);
|
|
607
|
+
|
|
608
|
+
&:last-child {
|
|
609
|
+
text-align: right;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
&:first-child {
|
|
613
|
+
text-align: left;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
text-align: center;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
.version {
|
|
620
|
+
cursor: default;
|
|
621
|
+
margin: 0px;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
.locale-chooser {
|
|
625
|
+
cursor: pointer;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
.flex {
|
|
631
|
+
display: flex;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
</style>
|
|
@@ -203,10 +203,9 @@ describe('component: NamespaceFilter', () => {
|
|
|
203
203
|
jest.spyOn(NamespaceFilter.computed.value, 'get').mockReturnValue([]);
|
|
204
204
|
const wrapper = mount(NamespaceFilter, {
|
|
205
205
|
computed: {
|
|
206
|
-
options:
|
|
207
|
-
currentProduct:
|
|
208
|
-
|
|
209
|
-
key: () => key,
|
|
206
|
+
options: () => [],
|
|
207
|
+
currentProduct: () => undefined,
|
|
208
|
+
key: () => key,
|
|
210
209
|
},
|
|
211
210
|
mocks: {
|
|
212
211
|
$store: {
|
|
@@ -19,7 +19,8 @@ describe('component: KeyValue', () => {
|
|
|
19
19
|
it('should display a markdown-multiline field with new lines visible', () => {
|
|
20
20
|
const wrapper = mount(KeyValue, {
|
|
21
21
|
propsData: {
|
|
22
|
-
value:
|
|
22
|
+
value:
|
|
23
|
+
{ value: 'test' },
|
|
23
24
|
valueMarkdownMultiline: true,
|
|
24
25
|
},
|
|
25
26
|
mocks: { $store: { getters: { 'i18n/t': jest.fn() } } },
|
|
@@ -10,13 +10,13 @@ describe('component: UnitInput', () => {
|
|
|
10
10
|
expect(wrapper.isVisible()).toBe(true);
|
|
11
11
|
});
|
|
12
12
|
|
|
13
|
-
it('should emit input event
|
|
13
|
+
it.each(['blur', 'change'])('should emit input event when "%p" is fired', async(event) => {
|
|
14
14
|
const wrapper = mount(UnitInput, { propsData: { value: 1, delay: 0 } });
|
|
15
15
|
const input = wrapper.find('input');
|
|
16
16
|
|
|
17
17
|
await input.setValue(2);
|
|
18
18
|
await input.setValue(4);
|
|
19
|
-
input.trigger(
|
|
19
|
+
input.trigger(event);
|
|
20
20
|
|
|
21
21
|
expect(wrapper.emitted('input')).toHaveLength(1);
|
|
22
22
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { NAME as EXPLORER } from '@shell/config/product/explorer';
|
|
3
|
+
import { canViewResource } from '@shell/utils/auth';
|
|
3
4
|
|
|
4
5
|
export default {
|
|
5
6
|
props: {
|
|
@@ -41,6 +42,10 @@ export default {
|
|
|
41
42
|
};
|
|
42
43
|
|
|
43
44
|
return { name, params };
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
canViewResource() {
|
|
48
|
+
return canViewResource(this.$store, this.type);
|
|
44
49
|
}
|
|
45
50
|
}
|
|
46
51
|
};
|
|
@@ -48,8 +53,14 @@ export default {
|
|
|
48
53
|
|
|
49
54
|
<template>
|
|
50
55
|
<span v-if="value">
|
|
51
|
-
<nuxt-link
|
|
56
|
+
<nuxt-link
|
|
57
|
+
v-if="canViewResource"
|
|
58
|
+
:to="url"
|
|
59
|
+
>
|
|
52
60
|
{{ value }}
|
|
53
61
|
</nuxt-link>
|
|
62
|
+
<template v-else>
|
|
63
|
+
{{ value }}
|
|
64
|
+
</template>
|
|
54
65
|
</span>
|
|
55
66
|
</template>
|