dashboard-shell-shell 3.0.5-logtest.3 → 3.0.5-order.2

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 (37) hide show
  1. package/assets/images/arrow.svg +1 -0
  2. package/assets/images/headerIcon/auth.svg +1 -0
  3. package/assets/images/headerIcon/settings.svg +1 -0
  4. package/assets/images/home.svg +6 -0
  5. package/assets/images/pl/logo.png +0 -0
  6. package/assets/styles/global/_layout.scss +2 -2
  7. package/assets/styles/global/_tooltip.scss +3 -1
  8. package/assets/translations/en-us.yaml +1 -0
  9. package/assets/translations/zh-hans.yaml +41 -36
  10. package/components/HoverDropdown.vue +0 -0
  11. package/components/ResourceList/Masthead.vue +8 -1
  12. package/components/form/NameNsDescription.vue +15 -0
  13. package/components/nav/Header copy.vue +1157 -0
  14. package/components/nav/Header.vue +70 -59
  15. package/components/nav/TopLevelMenu.vue +0 -1
  16. package/components/nav/TopLevelMenuNew.vue +607 -0
  17. package/components/nav/configurationApps.vue +102 -0
  18. package/components/nav/menuDropdown.vue +132 -0
  19. package/components/nav/menuPrompt.vue +204 -0
  20. package/components/templates/omsLayout.vue +109 -0
  21. package/config/product/order.js +117 -0
  22. package/config/router/routes.js +39 -2
  23. package/list/IframePage.vue +3 -0
  24. package/list/provisioning.cattle.io.cluster.vue +33 -2
  25. package/package.json +1 -1
  26. package/pages/auth/login.vue +11 -2
  27. package/pages/csm/ServiceMarket-iframe.vue +32 -0
  28. package/pages/csm/order-iframe.vue +32 -0
  29. package/pages/csm/orderInfo-iframe.vue +32 -0
  30. package/pages/csm/shelfist-iframe.vue +32 -0
  31. package/pages/csm/tenant-iframe.vue +32 -0
  32. package/pages/home.vue +1 -1
  33. package/rancher-components/Accordion/Accordion.vue +3 -1
  34. package/rancher-components/RcDropdown/RcDropdown.vue +1 -1
  35. package/scripts/publish-shell.sh +1 -1
  36. package/store/index.js +4 -4
  37. package/store/type-map.js +4 -0
@@ -0,0 +1,607 @@
1
+ <script>
2
+ import BrandImage from '@shell/components/BrandImage';
3
+ import ClusterIconMenu from '@shell/components/ClusterIconMenu';
4
+ import IconOrSvg from '../IconOrSvg';
5
+ import { BLANK_CLUSTER } from '@shell/store/store-types.js';
6
+ import { mapGetters } from 'vuex';
7
+ import { CAPI, COUNT, MANAGEMENT } from '@shell/config/types';
8
+ import { MENU_MAX_CLUSTERS, PINNED_CLUSTERS } from '@shell/store/prefs';
9
+ import { sortBy } from '@shell/utils/sort';
10
+ import { ucFirst } from '@shell/utils/string';
11
+ import { KEY } from '@shell/utils/platform';
12
+ import { getVersionInfo } from '@shell/utils/version';
13
+ import { SETTING } from '@shell/config/settings';
14
+ import { getProductFromRoute } from '@shell/utils/router';
15
+ import { isRancherPrime } from '@shell/config/version';
16
+ import Pinned from '@shell/components/nav/Pinned';
17
+ import { TopLevelMenuHelperPagination, TopLevelMenuHelperLegacy } from '@shell/components/nav/TopLevelMenu.helper';
18
+ import { debounce } from 'lodash';
19
+ import { sameContents } from '@shell/utils/array';
20
+ import Group from '@shell/components/nav/Group';
21
+ import {
22
+ RcDropdown,
23
+ RcDropdownItem,
24
+ RcDropdownSeparator,
25
+ RcDropdownTrigger
26
+ } from '@components/RcDropdown';
27
+ import { isAdminUser } from '@shell/store/type-map.js'
28
+ import menuDropdown from './menuDropdown'
29
+
30
+ export default {
31
+ components: {
32
+ menuDropdown,
33
+ RcDropdown,
34
+ RcDropdownItem,
35
+ RcDropdownTrigger,
36
+ RcDropdownSeparator,
37
+ BrandImage,
38
+ ClusterIconMenu,
39
+ IconOrSvg,
40
+ Pinned,
41
+ Group
42
+ },
43
+
44
+ data() {
45
+ const { displayVersion, fullVersion } = getVersionInfo(this.$store);
46
+ const hasProvCluster = this.$store.getters[`management/schemaFor`](CAPI.RANCHER_CLUSTER);
47
+
48
+ const canPagination = this.$store.getters[`management/paginationEnabled`]({
49
+ id: MANAGEMENT.CLUSTER,
50
+ context: 'side-bar',
51
+ }) && this.$store.getters[`management/paginationEnabled`]({
52
+ id: CAPI.RANCHER_CLUSTER,
53
+ context: 'side-bar',
54
+ });
55
+ const helper = canPagination ? new TopLevelMenuHelperPagination({ $store: this.$store }) : new TopLevelMenuHelperLegacy({ $store: this.$store });
56
+ const provClusters = !canPagination && hasProvCluster ? this.$store.getters[`management/all`](CAPI.RANCHER_CLUSTER) : [];
57
+ const mgmtClusters = !canPagination ? this.$store.getters[`management/all`](MANAGEMENT.CLUSTER) : [];
58
+
59
+ if (!canPagination) {
60
+ // Reduce the impact of the initial load, but only if we're not making a request
61
+ const args = {
62
+ pinnedIds: this.pinnedIds,
63
+ searchTerm: this.search,
64
+ unPinnedMax: this.maxClustersToShow
65
+ };
66
+
67
+ helper.update(args);
68
+ }
69
+
70
+ return {
71
+ shown: false,
72
+ displayVersion,
73
+ fullVersion,
74
+ clusterFilter: '',
75
+ hasProvCluster,
76
+ maxClustersToShow: MENU_MAX_CLUSTERS,
77
+ emptyCluster: BLANK_CLUSTER,
78
+ routeCombo: false,
79
+
80
+ canPagination,
81
+ helper,
82
+ debouncedHelperUpdateSlow: debounce((...args) => this.helper.update(...args), 1000),
83
+ debouncedHelperUpdateMedium: debounce((...args) => this.helper.update(...args), 750),
84
+ debouncedHelperUpdateQuick: debounce((...args) => this.helper.update(...args), 200),
85
+ provClusters,
86
+ mgmtClusters,
87
+ };
88
+ },
89
+
90
+ computed: {
91
+ ...mapGetters(['clusterId']),
92
+ ...mapGetters(['clusterReady', 'isRancher', 'currentCluster', 'currentProduct', 'isRancherInHarvester']),
93
+ ...mapGetters({ features: 'features/get' }),
94
+
95
+ pinnedIds() {
96
+ return this.$store.getters['prefs/get'](PINNED_CLUSTERS);
97
+ },
98
+
99
+ showClusterSearch() {
100
+ return this.allClustersCount > this.maxClustersToShow;
101
+ },
102
+
103
+ allClustersCount() {
104
+ const counts = this.$store.getters[`management/all`](COUNT)?.[0]?.counts || {};
105
+ const count = counts[MANAGEMENT.CLUSTER] || {};
106
+
107
+ return count?.summary.count;
108
+ },
109
+
110
+ // New
111
+ search() {
112
+ return (this.clusterFilter || '').toLowerCase();
113
+ },
114
+
115
+ // New
116
+ showPinClusters() {
117
+ return !this.clusterFilter;
118
+ },
119
+
120
+ // New
121
+ searchActive() {
122
+ return !!this.search;
123
+ },
124
+
125
+ /**
126
+ * Only Clusters that are pinned
127
+ *
128
+ * (see description of helper.clustersPinned for more details)
129
+ */
130
+ pinFiltered() {
131
+ return this.hasProvCluster ? this.helper.clustersPinned : [];
132
+ },
133
+
134
+ /**
135
+ * Used to shown unpinned clusters OR results of text search
136
+ *
137
+ * (see description of helper.clustersOthers for more details)
138
+ */
139
+ clustersFiltered() {
140
+ return this.hasProvCluster ? this.helper.clustersOthers : [];
141
+ },
142
+
143
+ pinnedClustersHeight() {
144
+ const pinCount = this.pinFiltered.length;
145
+ const height = pinCount > 2 ? (pinCount * 43) : 90;
146
+
147
+ return `min-height: ${ height }px`;
148
+ },
149
+ clusterFilterCount() {
150
+ return this.clusterFilter ? this.clustersFiltered.length : this.allClustersCount;
151
+ },
152
+
153
+ multiClusterApps() {
154
+ const options = this.options;
155
+
156
+ return options.filter((opt) => {
157
+ const filterApps = (opt.inStore === 'management' || opt.isMultiClusterApp) && opt.category !== 'configuration' && opt.category !== 'legacy';
158
+
159
+ if (this.isRancherInHarvester) {
160
+ return filterApps && opt.category !== 'hci';
161
+ } else {
162
+ // We expect the location of Virtualization Management to remain the same when rancher-manage-support is not enabled
163
+ return filterApps;
164
+ }
165
+ });
166
+ },
167
+
168
+ configurationApps() {
169
+ const options = this.options;
170
+
171
+ return options.filter((opt) => opt.category === 'configuration');
172
+ },
173
+
174
+ hciApps() {
175
+ const options = this.options;
176
+
177
+ return options.filter((opt) => this.isRancherInHarvester && opt.category === 'hci');
178
+ },
179
+
180
+ options() {
181
+ const cluster = this.clusterId || this.$store.getters['defaultClusterId'];
182
+
183
+ // TODO plugin routes
184
+ const entries = this.$store.getters['type-map/activeProducts']?.map((p) => {
185
+ // Try product-specific index first
186
+ const to = p.to || {
187
+ name: `c-cluster-${ p.name }`,
188
+ params: { cluster }
189
+ };
190
+
191
+ const matched = this.$router.getRoutes().filter((route) => route.name === to.name);
192
+
193
+ if ( !matched.length ) {
194
+ to.name = 'c-cluster-product';
195
+ to.params.product = p.name;
196
+ }
197
+
198
+ return {
199
+ label: this.$store.getters['i18n/withFallback'](`product."${ p.name }"`, null, ucFirst(p.name)),
200
+ icon: `icon-${ p.icon || 'copy' }`,
201
+ svg: p.svg,
202
+ value: p.name,
203
+ removable: p.removable !== false,
204
+ inStore: p.inStore || 'cluster',
205
+ weight: p.weight || 1,
206
+ category: p.category || 'none',
207
+ to,
208
+ isMultiClusterApp: p.isMultiClusterApp,
209
+ };
210
+ });
211
+
212
+ return sortBy(entries, ['weight']);
213
+ },
214
+
215
+ canEditSettings() {
216
+ return (this.$store.getters['management/schemaFor'](MANAGEMENT.SETTING)?.resourceMethods || []).includes('PUT');
217
+ },
218
+
219
+ hasSupport() {
220
+ return isRancherPrime() || this.$store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.SUPPORTED )?.value === 'true';
221
+ },
222
+
223
+ isCurrRouteClusterExplorer() {
224
+ return this.$route?.name?.startsWith('c-cluster');
225
+ },
226
+
227
+ productFromRoute() {
228
+ return getProductFromRoute(this.$route);
229
+ },
230
+
231
+ aboutText() {
232
+ // If a version number (starts with 'v') then use that
233
+ if (this.displayVersion.startsWith('v')) {
234
+ // Don't show the '.0' for a minor release (e.g. 2.8.0, 2.9.0 etc)
235
+ return !this.displayVersion.endsWith('.0') ? this.displayVersion : this.displayVersion.substr(0, this.displayVersion.length - 2);
236
+ }
237
+
238
+ // Default fallback to 'About'
239
+ return this.t('about.title');
240
+ },
241
+
242
+ largeAboutText() {
243
+ return this.aboutText.length > 6;
244
+ },
245
+
246
+ appBar() {
247
+ let activeFound = false;
248
+
249
+ // order is important for the object keys here
250
+ // since we want to check last pinFiltered and clustersFiltered
251
+ const appBar = {
252
+ hciApps: this.hciApps,
253
+ multiClusterApps: this.multiClusterApps,
254
+ configurationApps: this.configurationApps,
255
+ pinFiltered: this.pinFiltered,
256
+ clustersFiltered: this.clustersFiltered,
257
+ };
258
+
259
+ Object.keys(appBar).forEach((menuSection) => {
260
+ const menuSectionItems = appBar[menuSection];
261
+ const isClusterCheck = menuSection === 'pinFiltered' || menuSection === 'clustersFiltered';
262
+
263
+ // need to reset active state on other menu items
264
+ menuSectionItems.forEach((item) => {
265
+ item.isMenuActive = false;
266
+
267
+ if (!activeFound && this.checkActiveRoute(item, isClusterCheck)) {
268
+ activeFound = true;
269
+ item.isMenuActive = true;
270
+ }
271
+ });
272
+ });
273
+
274
+ return appBar;
275
+ },
276
+
277
+ hideLocalCluster() {
278
+ const hideLocalSetting = this.$store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.HIDE_LOCAL_CLUSTER) || {};
279
+ const value = hideLocalSetting.value || hideLocalSetting.default || 'false';
280
+
281
+ return value === 'true';
282
+ }
283
+ },
284
+
285
+ // See https://github.com/rancher/dashboard/issues/12831 for outstanding performance related work
286
+ watch: {
287
+ $route() {
288
+ this.shown = false;
289
+ },
290
+
291
+ // Before SSP world all of these changes were kicked off given Vue change detection to properties in a computed method.
292
+ // Changes could come from two scenarios
293
+ // 1. Changes made by the user (pin / search). Could be tens per second
294
+ // 2. Changes made by rancher to clusters (state, label, etc change). Could be hundreds a second
295
+ // They can be restricted to help the churn caused from above
296
+ // 1. When SSP enabled reduce http spam
297
+ // 2. When SSP is disabled (legacy) reduce fn churn (this was a known performance customer issue)
298
+
299
+ pinnedIds: {
300
+ immediate: true,
301
+ handler(neu, old) {
302
+ if (sameContents(neu, old)) {
303
+ return;
304
+ }
305
+
306
+ // Low throughput (user click). Changes should be shown quickly
307
+ this.updateClusters(neu, 'quick');
308
+ }
309
+ },
310
+
311
+ search() {
312
+ // Medium throughput. Changes should be shown quickly, unless we want to reduce http spam in SSP world
313
+ this.updateClusters(this.pinnedIds, this.canPagination ? 'medium' : 'quick');
314
+ },
315
+
316
+ provClusters: {
317
+ handler(neu, old) {
318
+ // Potentially incredibly high throughput. Changes should be at least limited (slow if state change, quick if added/removed). Shouldn't get here if SSP
319
+ this.updateClusters(this.pinnedIds, neu?.length === old?.length ? 'slow' : 'quick');
320
+ },
321
+ deep: true,
322
+ immediate: true,
323
+ },
324
+
325
+ mgmtClusters: {
326
+ handler(neu, old) {
327
+ // Potentially incredibly high throughput. Changes should be at least limited (slow if state change, quick if added/removed). Shouldn't get here if SSP
328
+ this.updateClusters(this.pinnedIds, neu?.length === old?.length ? 'slow' : 'quick');
329
+ },
330
+ deep: true,
331
+ immediate: true,
332
+ },
333
+
334
+ hideLocalCluster() {
335
+ this.updateClusters(this.pinnedIds, 'slow');
336
+ }
337
+ },
338
+
339
+ mounted() {
340
+ document.addEventListener('keyup', this.handler);
341
+ },
342
+
343
+ beforeUnmount() {
344
+ document.removeEventListener('keyup', this.handler);
345
+ this.helper?.destroy();
346
+ },
347
+
348
+ methods: {
349
+ isAdminUser,
350
+ checkActiveRoute(obj, isClusterRoute) {
351
+ // for Cluster links in main nav: check if route is a cluster explorer one + check if route cluster matches cluster obj id + check if curr product matches route product
352
+ if (isClusterRoute) {
353
+ return this.isCurrRouteClusterExplorer && this.$route?.params?.cluster === obj?.id && this.productFromRoute === this.currentProduct?.name;
354
+ }
355
+
356
+ // for remaining main nav items, check if curr product matches route product is enough
357
+ return this.productFromRoute === obj?.value;
358
+ },
359
+
360
+ handleKeyComboClick() {
361
+ this.routeCombo = !this.routeCombo;
362
+ },
363
+
364
+ clusterMenuClick(ev, cluster) {
365
+ if (this.routeCombo) {
366
+ ev.preventDefault();
367
+
368
+ if (this.isCurrRouteClusterExplorer && this.productFromRoute === this.currentProduct?.name) {
369
+ const clusterRoute = {
370
+ name: this.$route.name,
371
+ params: { ...this.$route.params },
372
+ query: { ...this.$route.query }
373
+ };
374
+
375
+ clusterRoute.params.cluster = cluster.id;
376
+
377
+ return this.$router.push(clusterRoute);
378
+ }
379
+ }
380
+
381
+ return this.$router.push(cluster.clusterRoute);
382
+ },
383
+
384
+ handler(e) {
385
+ if (e.keyCode === KEY.ESCAPE ) {
386
+ this.hide();
387
+ }
388
+ },
389
+
390
+ hide() {
391
+ this.shown = false;
392
+ if (this.clustersFiltered === 0) {
393
+ this.clusterFilter = '';
394
+ }
395
+ },
396
+
397
+ toggle() {
398
+ this.shown = !this.shown;
399
+ },
400
+
401
+ async goToHarvesterCluster() {
402
+ const localCluster = this.$store.getters['management/all'](CAPI.RANCHER_CLUSTER).find((C) => C.id === 'fleet-local/local');
403
+
404
+ try {
405
+ await localCluster.goToHarvesterCluster();
406
+ } catch {
407
+ }
408
+ },
409
+
410
+ getTooltipConfig(item, showWhenClosed = false) {
411
+ if (!item) {
412
+ return;
413
+ }
414
+
415
+ let contentText = '';
416
+ let content;
417
+ let popperClass = '';
418
+
419
+ // this is the normal tooltip scenario where we are just passing a string
420
+ if (typeof item === 'string') {
421
+ contentText = item;
422
+ content = this.shown ? null : contentText;
423
+
424
+ // if key combo is pressed, then we update the tooltip as well
425
+ } else if (this.routeCombo &&
426
+ typeof item === 'object' &&
427
+ !Array.isArray(item) &&
428
+ item !== null &&
429
+ item.ready) {
430
+ contentText = this.t('nav.keyComboTooltip');
431
+
432
+ if (showWhenClosed) {
433
+ content = !this.shown ? contentText : null;
434
+ } else {
435
+ content = this.shown ? contentText : null;
436
+ }
437
+
438
+ // this is scenario where we show a tooltip when we are on the expanded menu to show full description
439
+ } else {
440
+ contentText = item.label;
441
+ // this adds a class to the tooltip container so that we can control the max width
442
+ popperClass = 'menu-description-tooltip';
443
+
444
+ if (item.description) {
445
+ contentText += `<br><br>${ item.description }`;
446
+ }
447
+
448
+ if (showWhenClosed) {
449
+ content = !this.shown ? contentText : null;
450
+ } else {
451
+ content = this.shown ? contentText : null;
452
+
453
+ // this adds a class to adjust tooltip position so it doesn't overlap the cluster pinning action
454
+ popperClass += ' description-tooltip-pos-adjustment';
455
+ }
456
+ }
457
+
458
+ return {
459
+ content,
460
+ placement: 'right',
461
+ popperClass
462
+ };
463
+ },
464
+
465
+ updateClusters(pinnedIds, speed = 'slow' | 'medium' | 'quick') {
466
+ const args = {
467
+ pinnedIds,
468
+ searchTerm: this.search,
469
+ unPinnedMax: this.maxClustersToShow
470
+ };
471
+
472
+ switch (speed) {
473
+ case 'slow':
474
+ this.debouncedHelperUpdateSlow(args);
475
+ break;
476
+ case 'medium':
477
+ this.debouncedHelperUpdateMedium(args);
478
+ break;
479
+ case 'quick':
480
+ this.debouncedHelperUpdateQuick(args);
481
+ break;
482
+ }
483
+ }
484
+ }
485
+ };
486
+ </script>
487
+
488
+ <template>
489
+ <div class="topMenuList">
490
+ <div v-if="multiClusterApps.length" class="topMenu_GlobalApp">
491
+ <div class="topMenu_GlobalApp_item">
492
+ <div v-if="isAdminUser($store.getters)" class="topLevelMenu">
493
+ <span class="option-link">运营</span>
494
+ </div>
495
+ <div v-else class="topLevelMenu">
496
+ <span class="option-link">云安全中心</span>
497
+ </div>
498
+ </div>
499
+ <div v-for="(a, i) in appBar.multiClusterApps" @click="hide()" class="topMenu_GlobalApp_item">
500
+ <div v-if="a.value !== 'fleet'" :class="{'active-menu-link': a.isMenuActive }" class="topLevelMenu">
501
+ <router-link
502
+ class="option"
503
+ :to="a.to"
504
+ role="link"
505
+ :aria-label="`${t('nav.ariaLabel.multiClusterApps')} ${ a.label }`"
506
+ >
507
+ <span class="option-link">{{ a.label }}</span>
508
+ </router-link>
509
+ </div>
510
+ </div>
511
+ </div>
512
+
513
+ <!-- <menuDropdown>
514
+ <template #title>
515
+ <div @click="hide()" :class="{'active-menu-link': appBar.clustersFiltered.some(s => { return s.isMenuActive }) }" class="topLevelMenu">
516
+ <div class="option-link">
517
+ 容器管理
518
+ </div>
519
+ </div>
520
+ </template>
521
+ <template #dropdownCollection>
522
+ <div class="dropdown_box">
523
+ <div class="topLevelMenu_title" @click="hide()" v-for="(c, index) in appBar.clustersFiltered" :key="index">
524
+ <div
525
+ v-if="c.ready"
526
+ v-shortkey.push="{windows: ['alt'], mac: ['option']}"
527
+ :data-testid="`menu-cluster-${ c.id }`"
528
+ :class="{'active-menu-link': c.isMenuActive }"
529
+ :to="c.clusterRoute"
530
+ role="button"
531
+ :aria-label="`${t('nav.ariaLabel.cluster')} ${ c.label }`"
532
+ @click="clusterMenuClick($event, c)"
533
+ @shortkey="handleKeyComboClick"
534
+ >
535
+ <div class="option-link">{{ c.label }}</div>
536
+ </div>
537
+ <div
538
+ v-else
539
+ class="option cluster selector disabled"
540
+ :data-testid="`pinned-menu-cluster-disabled-${ c.id }`"
541
+ >
542
+ <div style="color: #eee;">{{ c.label }}</div>
543
+ </div>
544
+ </div>
545
+ </div>
546
+ </template>
547
+ </menuDropdown> -->
548
+
549
+ </div>
550
+ </template>
551
+ <style lang="scss" scoped>
552
+ .dropdown_box {
553
+ min-width: 140px;
554
+ }
555
+ .option-link {
556
+ display: flex;
557
+ align-items: center;
558
+ justify-content: center;
559
+ font-size: 14px;
560
+ color: #333;
561
+ }
562
+ .active-menu-link.topLevelMenu {
563
+ border-bottom: 1px solid var(--link);
564
+ }
565
+ .active-menu-link {
566
+ .option-link {
567
+ color: var(--link);
568
+ }
569
+ }
570
+ .topMenuList {
571
+ display: flex;
572
+ }
573
+ .topMenu_GlobalApp {
574
+ display: flex;
575
+ }
576
+ .topLevelMenu {
577
+ height: 50px;
578
+ font-size: 14px;
579
+ min-width: 120px;
580
+ line-height: 50px;
581
+ padding: 0 20px;
582
+ display: flex;
583
+ text-align: center;
584
+ align-items: center;
585
+ justify-content: center;
586
+ }
587
+ .topLevelMenu:hover {
588
+ cursor: pointer;
589
+ .option-link {
590
+ color: var(--link);
591
+ }
592
+ }
593
+ .topLevelMenu_title {
594
+ height: 38px;
595
+ display: flex;
596
+ line-height: 38px;
597
+ align-items: center;
598
+ justify-content: center;
599
+ }
600
+ .topLevelMenu_title:hover {
601
+ cursor: pointer;
602
+ background-color: var(--dropdown-hover-bg);
603
+ .option-link {
604
+ color: var(--link);
605
+ }
606
+ }
607
+ </style>
@@ -0,0 +1,102 @@
1
+ <script>
2
+ import { mapGetters } from 'vuex';
3
+ import { ucFirst } from '@shell/utils/string';
4
+ import menuPrompt from './menuPrompt'
5
+
6
+ export default {
7
+ components: {
8
+ menuPrompt
9
+ },
10
+ computed: {
11
+ ...mapGetters(['clusterId', 'currentProduct']),
12
+
13
+ options() {
14
+ const cluster = this.clusterId || this.$store.getters['defaultClusterId'];
15
+
16
+ return this.$store.getters['type-map/activeProducts']?.map((p) => {
17
+ const to = p.to || { name: `c-cluster-${p.name}`, params: { cluster } };
18
+
19
+ const matched = this.$router.getRoutes().filter((route) => route.name === to.name);
20
+ if (!matched.length) {
21
+ to.name = 'c-cluster-product';
22
+ to.params.product = p.name;
23
+ }
24
+
25
+ return {
26
+ label: this.$store.getters['i18n/withFallback'](`product."${p.name}"`, null, ucFirst(p.name)),
27
+ to,
28
+ category: p.category || 'none',
29
+ value: p.name,
30
+ isMenuActive: false
31
+ };
32
+ }) || [];
33
+ },
34
+
35
+ configurationApps() {
36
+ return this.options.filter(opt => opt.category === 'configuration');
37
+ },
38
+
39
+ appBar() {
40
+ let activeFound = false;
41
+ const appBar = { configurationApps: this.configurationApps };
42
+
43
+ appBar.configurationApps.forEach(item => {
44
+ item.isMenuActive = false;
45
+ if (!activeFound && this.currentProduct?.name === item.value) {
46
+ item.isMenuActive = true;
47
+ activeFound = true;
48
+ }
49
+ });
50
+
51
+ return appBar;
52
+ }
53
+ },
54
+
55
+ methods: {
56
+ hide() {
57
+ // 可保留清理逻辑,如果不需要可以为空
58
+ }
59
+ }
60
+ };
61
+ </script>
62
+
63
+ <template>
64
+ <div v-for="(a, i) in appBar.configurationApps" :key="i">
65
+ <div v-if="a.value !== 'uiplugins'" class="headerIcon_box" @click="hide()">
66
+ <menuPrompt :align="'center'">
67
+ <template #title>
68
+ <router-link
69
+ class="option"
70
+ :to="a.to"
71
+ role="link"
72
+ :aria-label="`${t('nav.ariaLabel.configurationApps')} ${ a.label }`"
73
+ >
74
+ <img class="headerIcon_img" :src="require(`@/shell/assets/images/headerIcon/${a.value ? a.value : 'auth'}.svg`)" alt="" srcset="">
75
+ </router-link>
76
+ </template>
77
+ <template #dropdownCollection>
78
+ <div class="description">{{ a.label }}</div>
79
+ </template>
80
+ </menuPrompt>
81
+ </div>
82
+ </div>
83
+ </template>
84
+ <style lang="scss">
85
+ .headerIcon_box {
86
+ width: 45px;
87
+ height: 50px;
88
+ display: flex;
89
+ align-items: center;
90
+ justify-content: center;
91
+ .headerIcon_img {
92
+ width: 18px;
93
+ height: 18px;
94
+ margin-top: 4px;
95
+ }
96
+ .description {
97
+ padding: 5px;
98
+ min-width: 100px;
99
+ text-align: center;
100
+ }
101
+ }
102
+ </style>