@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.
Files changed (47) hide show
  1. package/assets/translations/en-us.yaml +4 -1
  2. package/components/PromptModal.vue +4 -0
  3. package/components/Questions/Array.vue +2 -2
  4. package/components/Questions/Boolean.vue +7 -1
  5. package/components/Questions/CloudCredential.vue +1 -0
  6. package/components/Questions/Enum.vue +21 -2
  7. package/components/Questions/Float.vue +8 -3
  8. package/components/Questions/Int.vue +8 -3
  9. package/components/Questions/Question.js +72 -0
  10. package/components/Questions/QuestionMap.vue +2 -1
  11. package/components/Questions/Radio.vue +33 -0
  12. package/components/Questions/Reference.vue +2 -0
  13. package/components/Questions/String.vue +8 -3
  14. package/components/Questions/Yaml.vue +46 -0
  15. package/components/Questions/__tests__/Boolean.test.ts +123 -0
  16. package/components/Questions/__tests__/Float.test.ts +123 -0
  17. package/components/Questions/__tests__/Int.test.ts +123 -0
  18. package/components/Questions/__tests__/String.test.ts +123 -0
  19. package/components/Questions/__tests__/Yaml.test.ts +123 -0
  20. package/components/Questions/index.vue +8 -1
  21. package/components/ResourceTable.vue +10 -13
  22. package/components/SideNav.vue +634 -0
  23. package/components/__tests__/NamespaceFilter.test.ts +3 -4
  24. package/components/form/UnitInput.vue +1 -0
  25. package/components/form/__tests__/KeyValue.test.ts +2 -1
  26. package/components/form/__tests__/UnitInput.test.ts +2 -2
  27. package/components/formatter/LinkName.vue +12 -1
  28. package/components/nav/WorkspaceSwitcher.vue +4 -1
  29. package/core/plugin-helpers.js +4 -1
  30. package/core/types.ts +25 -1
  31. package/detail/node.vue +2 -2
  32. package/edit/fleet.cattle.io.gitrepo.vue +7 -0
  33. package/layouts/default.vue +11 -597
  34. package/middleware/authenticated.js +2 -14
  35. package/models/fleet.cattle.io.gitrepo.js +3 -1
  36. package/package.json +1 -1
  37. package/pages/auth/login.vue +1 -1
  38. package/pages/c/_cluster/fleet/index.vue +4 -0
  39. package/pages/c/_cluster/uiplugins/index.vue +3 -3
  40. package/rancher-components/components/Form/LabeledInput/LabeledInput.vue +8 -0
  41. package/rancher-components/components/Form/Radio/RadioButton.test.ts +7 -3
  42. package/store/auth.js +2 -0
  43. package/types/shell/index.d.ts +2 -0
  44. package/utils/auth.js +17 -0
  45. package/utils/object.js +0 -1
  46. package/utils/validators/__tests__/cidr.test.ts +33 -0
  47. 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: () => undefined,
208
- namespaceFilterMode: () => undefined,
209
- key: () => key,
206
+ options: () => [],
207
+ currentProduct: () => undefined,
208
+ key: () => key,
210
209
  },
211
210
  mocks: {
212
211
  $store: {
@@ -224,6 +224,7 @@ export default {
224
224
  :required="required"
225
225
  :placeholder="placeholder"
226
226
  :hide-arrows="hideArrows"
227
+ @change="update($event.target.value)"
227
228
  @blur="update($event.target.value)"
228
229
  >
229
230
  <template #suffix>
@@ -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: 'test',
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 on value change', async() => {
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('blur');
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 :to="url">
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>