dashboard-shell-shell 3.0.5-logtest.2 → 3.0.5-order.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/images/arrow.svg +1 -0
- package/assets/images/headerIcon/auth.svg +1 -0
- package/assets/images/headerIcon/settings.svg +1 -0
- package/assets/images/home.svg +6 -0
- package/assets/images/pl/logo.png +0 -0
- package/assets/styles/global/_layout.scss +2 -2
- package/assets/styles/global/_tooltip.scss +3 -1
- package/assets/translations/en-us.yaml +1 -0
- package/assets/translations/zh-hans.yaml +41 -36
- package/components/HoverDropdown.vue +0 -0
- package/components/ResourceList/Masthead.vue +8 -1
- package/components/form/NameNsDescription.vue +15 -0
- package/components/nav/Header copy.vue +1157 -0
- package/components/nav/Header.vue +70 -59
- package/components/nav/TopLevelMenu.vue +0 -1
- package/components/nav/TopLevelMenuNew.vue +607 -0
- package/components/nav/configurationApps.vue +102 -0
- package/components/nav/menuDropdown.vue +132 -0
- package/components/nav/menuPrompt.vue +204 -0
- package/components/templates/omsLayout.vue +109 -0
- package/config/product/order.js +117 -0
- package/config/router/routes.js +39 -2
- package/list/IframePage.vue +3 -0
- package/list/provisioning.cattle.io.cluster.vue +33 -2
- package/package.json +1 -1
- package/pages/auth/login.vue +11 -2
- package/pages/csm/ServiceMarket-iframe.vue +32 -0
- package/pages/csm/order-iframe.vue +32 -0
- package/pages/csm/orderInfo-iframe.vue +32 -0
- package/pages/csm/shelfist-iframe.vue +32 -0
- package/pages/csm/tenant-iframe.vue +32 -0
- package/pages/home.vue +1 -1
- package/rancher-components/Accordion/Accordion.vue +3 -1
- package/rancher-components/RcDropdown/RcDropdown.vue +1 -1
- package/scripts/publish-shell.sh +1 -1
- package/store/index.js +4 -4
- 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}.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>
|