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,1157 @@
1
+ <script>
2
+ import { mapGetters } from 'vuex';
3
+ import debounce from 'lodash/debounce';
4
+ import { MANAGEMENT, NORMAN, STEVE } from '@shell/config/types';
5
+ import { HARVESTER_NAME as HARVESTER } from '@shell/config/features';
6
+ import { ucFirst } from '@shell/utils/string';
7
+ import { isAlternate, isMac } from '@shell/utils/platform';
8
+ import BrandImage from '@shell/components/BrandImage';
9
+ import { getProduct, getVendor } from '@shell/config/private-label';
10
+ import ClusterProviderIcon from '@shell/components/ClusterProviderIcon';
11
+ import ClusterBadge from '@shell/components/ClusterBadge';
12
+ import AppModal from '@shell/components/AppModal';
13
+ import { LOGGED_OUT, IS_SSO } from '@shell/config/query-params';
14
+ import NamespaceFilter from './NamespaceFilter';
15
+ import WorkspaceSwitcher from './WorkspaceSwitcher';
16
+ import TopLevelMenu from './TopLevelMenu';
17
+ import TopLevelMenuNew from './TopLevelMenuNew';
18
+
19
+ import { allHash } from '@shell/utils/promise';
20
+ import { ActionLocation, ExtensionPoint } from '@shell/core/types';
21
+ import { getApplicableExtensionEnhancements } from '@shell/core/plugin-helpers';
22
+ import IconOrSvg from '@shell/components/IconOrSvg';
23
+ import { wait } from '@shell/utils/async';
24
+ import { configType } from '@shell/models/management.cattle.io.authconfig';
25
+ import HeaderPageActionMenu from './HeaderPageActionMenu.vue';
26
+ import NotificationCenter from './NotificationCenter';
27
+ import {
28
+ RcDropdown,
29
+ RcDropdownItem,
30
+ RcDropdownSeparator,
31
+ RcDropdownTrigger
32
+ } from '@components/RcDropdown';
33
+ import configurationApps from './configurationApps'
34
+
35
+ export default {
36
+
37
+ components: {
38
+ configurationApps,
39
+ NamespaceFilter,
40
+ WorkspaceSwitcher,
41
+ TopLevelMenuNew,
42
+ TopLevelMenu,
43
+ BrandImage,
44
+ ClusterBadge,
45
+ ClusterProviderIcon,
46
+ IconOrSvg,
47
+ AppModal,
48
+ NotificationCenter,
49
+ HeaderPageActionMenu,
50
+ RcDropdown,
51
+ RcDropdownItem,
52
+ RcDropdownSeparator,
53
+ RcDropdownTrigger,
54
+ },
55
+
56
+ props: {
57
+ simple: {
58
+ type: Boolean,
59
+ default: false
60
+ }
61
+ },
62
+
63
+ fetch() {
64
+ // fetch needed data to check if any auth provider is enabled
65
+ this.$store.dispatch('auth/getAuthProviders');
66
+ },
67
+
68
+ data() {
69
+ const searchShortcut = isMac ? '(\u2318-K)' : '(Ctrl+K)';
70
+ const shellShortcut = '(Ctrl+`)';
71
+
72
+ return {
73
+ authInfo: {},
74
+ show: false,
75
+ showTooltip: false,
76
+ isUserMenuOpen: false,
77
+ isPageActionMenuOpen: false,
78
+ kubeConfigCopying: false,
79
+ searchShortcut,
80
+ shellShortcut,
81
+ LOGGED_OUT,
82
+ navHeaderRight: null,
83
+ extensionHeaderActions: getApplicableExtensionEnhancements(this, ExtensionPoint.ACTION, ActionLocation.HEADER, this.$route),
84
+ ctx: this,
85
+ showImportModal: false,
86
+ showSearchModal: false,
87
+ userIcon: require('@shell/assets/images/logo.png'),
88
+ homeIcon: require('@shell/assets/images/home.svg'),
89
+ };
90
+ },
91
+
92
+ computed: {
93
+ ...mapGetters([
94
+ 'clusterReady',
95
+ 'isExplorer',
96
+ 'isRancher',
97
+ 'currentCluster',
98
+ 'currentProduct',
99
+ 'rootProduct',
100
+ 'backToRancherLink',
101
+ 'backToRancherGlobalLink',
102
+ 'pageActions',
103
+ 'isSingleProduct',
104
+ 'isRancherInHarvester',
105
+ 'showTopLevelMenu',
106
+ 'isMultiCluster',
107
+ 'showWorkspaceSwitcher'
108
+ ]),
109
+
110
+ samlAuthProviderEnabled() {
111
+ const publicAuthProviders = this.$store.getters['rancher/all']('authProvider');
112
+
113
+ return publicAuthProviders.find((authProvider) => configType[authProvider.id] === 'saml') || {};
114
+ },
115
+
116
+ shouldShowSloLogoutModal() {
117
+ if (this.isAuthLocalProvider) {
118
+ // If the user logged in as a local user... they cannot log out as if they were an auth config user
119
+ return false;
120
+ }
121
+
122
+ const { logoutAllSupported, logoutAllEnabled, logoutAllForced } = this.samlAuthProviderEnabled;
123
+
124
+ return logoutAllSupported && logoutAllEnabled && !logoutAllForced;
125
+ },
126
+
127
+ appName() {
128
+ return getProduct();
129
+ },
130
+
131
+ vendor() {
132
+ this.$store.getters['management/all'](MANAGEMENT.SETTING)?.find((setting) => setting.id === 'ui-pl');
133
+
134
+ return getVendor();
135
+ },
136
+
137
+ authEnabled() {
138
+ return this.$store.getters['auth/enabled'];
139
+ },
140
+
141
+ isAuthLocalProvider() {
142
+ return this.principal && this.principal.provider === 'local';
143
+ },
144
+
145
+ generateLogoutRoute() {
146
+ return this.isAuthLocalProvider ? { name: 'auth-logout', query: { [LOGGED_OUT]: true } } : { name: 'auth-logout', query: { [LOGGED_OUT]: true, [IS_SSO]: true } };
147
+ },
148
+
149
+ principal() {
150
+ return this.$store.getters['rancher/byId'](NORMAN.PRINCIPAL, this.$store.getters['auth/principalId']) || {};
151
+ },
152
+
153
+ kubeConfigEnabled() {
154
+ return true;
155
+ },
156
+
157
+ shellEnabled() {
158
+ return !!this.currentCluster?.links?.shell;
159
+ },
160
+
161
+ showKubeShell() {
162
+ return !this.rootProduct?.hideKubeShell;
163
+ },
164
+
165
+ showKubeConfig() {
166
+ return !this.rootProduct?.hideKubeConfig;
167
+ },
168
+
169
+ showCopyConfig() {
170
+ return !this.rootProduct?.hideCopyConfig;
171
+ },
172
+
173
+ showPreferencesLink() {
174
+ return (this.$store.getters['management/schemaFor'](STEVE.PREFERENCE, false, false)?.resourceMethods || []).includes('PUT');
175
+ },
176
+
177
+ showAccountAndApiKeyLink() {
178
+ // Keep this simple for the moment and only check if the user can see tokens... plus the usual isRancher/isSingleProduct
179
+ const canSeeTokens = this.$store.getters['rancher/schemaFor'](NORMAN.TOKEN, false, false);
180
+
181
+ return canSeeTokens && (this.isRancher || this.isSingleProduct);
182
+ },
183
+
184
+ showPageActions() {
185
+ return !this.featureRancherDesktop && this.pageActions && this.pageActions.length;
186
+ },
187
+
188
+ showUserMenu() {
189
+ return !this.featureRancherDesktop;
190
+ },
191
+
192
+ showFilter() {
193
+ // Some products won't have a current cluster
194
+ const validClusterOrProduct = this.currentCluster ||
195
+ (this.currentProduct && this.currentProduct.customNamespaceFilter) ||
196
+ (this.currentProduct && this.currentProduct.showWorkspaceSwitcher);
197
+ // Don't show if the header is in 'simple' mode
198
+ const notSimple = !this.simple;
199
+ // One of these must be enabled, otherwise t here's no component to show
200
+ const validFilterSettings = this.currentProduct?.showNamespaceFilter || this.currentProduct?.showWorkspaceSwitcher;
201
+
202
+ return validClusterOrProduct && notSimple && validFilterSettings;
203
+ },
204
+
205
+ featureRancherDesktop() {
206
+ return this.$config.rancherEnv === 'desktop';
207
+ },
208
+
209
+ importEnabled() {
210
+ return !!this.currentCluster?.actions?.apply;
211
+ },
212
+
213
+ prod() {
214
+ const name = this.rootProduct.name;
215
+
216
+ return this.$store.getters['i18n/withFallback'](`product."${ name }"`, null, ucFirst(name));
217
+ },
218
+
219
+ showSearch() {
220
+ return this.rootProduct?.inStore === 'cluster';
221
+ },
222
+
223
+ showImportYaml() {
224
+ return this.rootProduct?.inStore !== 'harvester';
225
+ },
226
+
227
+ nameTooltip() {
228
+ return !this.showTooltip ? {} : {
229
+ content: this.currentCluster?.nameDisplay,
230
+ delay: 400,
231
+ };
232
+ },
233
+
234
+ singleProductLogoRoute() {
235
+ const cluster = this.$store.getters.defaultClusterId;
236
+ let objs = {
237
+ params: {
238
+ cluster
239
+ }
240
+ }
241
+ if (this.isSingleProduct?.logoRoute?.params) {
242
+ objs = {
243
+ ...this.isSingleProduct.logoRoute,
244
+ params: {
245
+ cluster,
246
+ ...this.isSingleProduct.logoRoute.params,
247
+ }
248
+ };
249
+ }
250
+ return objs
251
+ },
252
+
253
+ isHarvester() {
254
+ return this.$store.getters['currentProduct'].inStore === HARVESTER;
255
+ },
256
+ },
257
+
258
+ watch: {
259
+ currentCluster(neu, old) {
260
+ if (neu && old && neu.id !== old.id) {
261
+ this.checkClusterName();
262
+ }
263
+ },
264
+ // since the Header is a "persistent component" we need to update it at every route change...
265
+ $route: {
266
+ handler(neu) {
267
+ if (neu) {
268
+ this.extensionHeaderActions = getApplicableExtensionEnhancements(this, ExtensionPoint.ACTION, ActionLocation.HEADER, neu);
269
+
270
+ this.navHeaderRight = this.$plugin?.getDynamic('component', 'NavHeaderRight');
271
+ }
272
+ },
273
+ immediate: true,
274
+ deep: true,
275
+ }
276
+ },
277
+
278
+ mounted() {
279
+ this.checkClusterName();
280
+ this.debouncedLayoutHeader = debounce(this.layoutHeader, 400);
281
+ window.addEventListener('resize', this.debouncedLayoutHeader);
282
+
283
+ this.$nextTick(() => this.layoutHeader(null, true));
284
+ },
285
+
286
+ beforeUnmount() {
287
+ window.removeEventListener('resize', this.debouncedLayoutHeader);
288
+ },
289
+
290
+ methods: {
291
+
292
+ LogOutfn (type) {
293
+ sessionStorage.removeItem('TOPLEVELPERMISSIONS')
294
+ if (type === '1') {
295
+ this.$router.push({ name: 'account'})
296
+ } else if (type === '2') {
297
+ this.showSloModal()
298
+ } else if (type === '3') {
299
+ this.$router.push(this.generateLogoutRoute)
300
+ }
301
+ },
302
+
303
+ showSloModal() {
304
+ this.$store.dispatch('management/promptModal', {
305
+ component: 'SloDialog',
306
+ componentProps: { authProvider: this.samlAuthProviderEnabled },
307
+ modalWidth: '500px'
308
+ });
309
+ },
310
+ // Sizes the product area of the header such that it shrinks to ensure the whole header bar can be shown
311
+ // where possible - we use a minimum width of 32px which is enough to just show the product icon
312
+ layoutHeader() {
313
+ const header = this.$refs.header;
314
+ const product = this.$refs.product;
315
+
316
+ if (!header || !product) {
317
+ return;
318
+ }
319
+
320
+ // If the product element has an exact size, remove it and then recalculate
321
+ if (product.style.width) {
322
+ product.style.width = '';
323
+
324
+ this.$nextTick(() => this.layoutHeader());
325
+
326
+ return;
327
+ }
328
+
329
+ const overflow = header.scrollWidth - window.innerWidth;
330
+
331
+ if (overflow > 0) {
332
+ const w = Math.max(32, product.offsetWidth - overflow);
333
+
334
+ // Set exact width on the product div so that the content in it fits that available space
335
+ product.style.width = `${ w }px`;
336
+ }
337
+ },
338
+ showMenu(show) {
339
+ this.isUserMenuOpen = show;
340
+ },
341
+
342
+ openImport() {
343
+ this.$store.dispatch('cluster/promptModal', {
344
+ component: 'ImportDialog',
345
+ modalWidth: '75%',
346
+ height: 'auto',
347
+ styles: 'max-height: 90vh;',
348
+ componentProps: { cluster: this.currentCluster }
349
+ });
350
+ },
351
+
352
+ openSearch() {
353
+ this.$store.dispatch('cluster/promptModal', {
354
+ component: 'SearchDialog',
355
+ testId: 'search-modal',
356
+ modalWidth: '50%',
357
+ height: 'auto',
358
+ styles: 'max-height: 90vh;',
359
+ returnFocusSelector: '#header-btn-search'
360
+ });
361
+ },
362
+
363
+ checkClusterName() {
364
+ this.$nextTick(() => {
365
+ const el = this.$refs.clusterName;
366
+
367
+ this.showTooltip = el && (el.clientWidth < el.scrollWidth);
368
+ });
369
+ },
370
+
371
+ copyKubeConfig(event) {
372
+ const button = event.target?.parentElement;
373
+
374
+ if (this.kubeConfigCopying) {
375
+ return;
376
+ }
377
+
378
+ this.kubeConfigCopying = true;
379
+
380
+ if (button) {
381
+ button.classList.add('header-btn-active');
382
+ }
383
+
384
+ // Make sure we wait at least 1 second so that the user can see the visual indication that the config has been copied
385
+ allHash({
386
+ copy: this.currentCluster.copyKubeConfig(),
387
+ minDelay: wait(1000),
388
+ }).finally(() => {
389
+ this.kubeConfigCopying = false;
390
+
391
+ if (button) {
392
+ button.classList.remove('header-btn-active');
393
+ }
394
+ });
395
+ },
396
+
397
+ handleExtensionAction(action, event) {
398
+ const fn = action.invoke;
399
+ const opts = {
400
+ event,
401
+ action,
402
+ isAlt: isAlternate(event),
403
+ product: this.currentProduct.name,
404
+ cluster: this.currentCluster,
405
+ };
406
+ const enabled = action.enabled ? action.enabled.apply(this, [this.ctx]) : true;
407
+
408
+ if (fn && enabled) {
409
+ fn.apply(this, [opts, [], { $route: this.$route }]);
410
+ }
411
+ },
412
+
413
+ handleExtensionTooltip(action) {
414
+ if (action.tooltipKey || action.tooltip) {
415
+ const tooltip = action.tooltipKey ? this.t(action.tooltipKey) : action.tooltip;
416
+ const shortcut = action.shortcutLabel ? action.shortcutLabel() : '';
417
+
418
+ return `${ tooltip } ${ shortcut }`;
419
+ }
420
+
421
+ return null;
422
+ }
423
+ }
424
+ };
425
+ </script>
426
+
427
+ <template>
428
+
429
+ <!-- 整个页面的顶部 header -->
430
+ <header
431
+ ref="header"
432
+ data-testid="header"
433
+ >
434
+ <div>
435
+ <!-- 顶部一级菜单(根据不同产品/集群条件决定是否显示) -->
436
+ <!-- <TopLevelMenu v-if="isRancherInHarvester || isMultiCluster || !isSingleProduct" /> -->
437
+ </div>
438
+
439
+ <!-- ===== 左侧 logo 区域 ===== -->
440
+ <div>
441
+ <div v-if="isRancherInHarvester || isMultiCluster || !isSingleProduct" class="menu-spacer">
442
+ <div class="menu_home_img">
443
+ <router-link
444
+ class="option cluster selector home"
445
+ :to="{ name: 'home' }"
446
+ role="link"
447
+ :aria-label="t('nav.ariaLabel.homePage')"
448
+ >
449
+ <img :src="homeIcon" >
450
+ </router-link>
451
+ </div>
452
+ <!-- 显示用户头像或 logo 图片 -->
453
+ <img
454
+ :src="userIcon"
455
+ >
456
+ </div>
457
+
458
+ <div v-else>
459
+ <!-- 如果是单产品模式且不是 RancherInHarvester,就显示 logo 路由跳转 -->
460
+ <router-link
461
+ :to="singleProductLogoRoute"
462
+ >
463
+ <!-- 显示用户头像或 logo 图片 -->
464
+ <img
465
+ :src="userIcon"
466
+ >
467
+ </router-link>
468
+ </div>
469
+
470
+ <TopLevelMenuNew v-if="isRancherInHarvester || isMultiCluster || !isSingleProduct"/>
471
+
472
+ </div>
473
+
474
+ <!-- ===== 中间占位符(把左右内容分开) ===== -->
475
+ <div class="spacer" />
476
+
477
+ <!-- ===== 右侧功能区域 ===== -->
478
+ <div class="rd-header-right">
479
+
480
+
481
+ <!-- 动态插入右侧自定义组件(例如插件扩展的 header) -->
482
+ <component :is="navHeaderRight" />
483
+
484
+ <!-- ===== 集群/命名空间过滤器 ===== -->
485
+ <div
486
+ v-if="showFilter"
487
+ class="top"
488
+ >
489
+
490
+ <!-- 如果集群就绪 + 当前产品支持命名空间过滤器 或 Explorer 模式 -->
491
+ <NamespaceFilter v-if="clusterReady && currentProduct && (currentProduct.showNamespaceFilter || isExplorer)" />
492
+
493
+ <!-- 否则显示工作空间切换器 -->
494
+ <WorkspaceSwitcher v-else-if="clusterReady && currentProduct && currentProduct.showWorkspaceSwitcher" />
495
+ </div>
496
+
497
+ <!-- ===== 集群相关按钮(仅在有集群且不是 simple 模式下显示) ===== -->
498
+ <div
499
+ v-if="currentCluster && !simple"
500
+ >
501
+
502
+ <!-- 如果当前产品支持集群切换器 -->
503
+ <template v-if="currentProduct && currentProduct.showClusterSwitcher">
504
+
505
+ <!-- 导入 YAML 按钮 -->
506
+ <!-- <button
507
+ v-if="showImportYaml"
508
+ v-clean-tooltip="t('nav.import')"
509
+ :disabled="!importEnabled"
510
+ type="button"
511
+ class="btn header-btn role-tertiary"
512
+ data-testid="header-action-import-yaml"
513
+ role="button"
514
+ tabindex="0"
515
+ :aria-label="t('nav.import')"
516
+ @click="openImport()"
517
+ >
518
+ <i class="icon icon-upload icon-lg" />
519
+ </button> -->
520
+
521
+ <!-- <button
522
+ v-if="showKubeShell"
523
+ id="btn-kubectl"
524
+ v-clean-tooltip="t('nav.shellShortcut', {key: shellShortcut})"
525
+ v-shortkey="{windows: ['ctrl', '`'], mac: ['meta', '`']}"
526
+ :disabled="!shellEnabled"
527
+ type="button"
528
+ class="btn header-btn role-tertiary"
529
+ role="button"
530
+ tabindex="0"
531
+ :aria-label="t('nav.shellShortcut', {key:''})"
532
+ @shortkey="currentCluster.openShell()"
533
+ @click="currentCluster.openShell()"
534
+ >
535
+ <i class="icon icon-terminal icon-lg" />
536
+ </button> -->
537
+
538
+ <!-- 下载 kubeconfig 按钮 -->
539
+ <!-- <button
540
+ v-if="showKubeConfig"
541
+ v-clean-tooltip="t('nav.kubeconfig.download')"
542
+ :disabled="!kubeConfigEnabled"
543
+ type="button"
544
+ class="btn header-btn role-tertiary"
545
+ data-testid="btn-download-kubeconfig"
546
+ role="button"
547
+ tabindex="0"
548
+ :aria-label="t('nav.kubeconfig.download')"
549
+ @click="currentCluster.downloadKubeConfig()"
550
+ >
551
+ <i class="icon icon-file icon-lg" />
552
+ </button> -->
553
+
554
+ <!-- 复制 kubeconfig 按钮 -->
555
+ <!-- <button
556
+ v-if="showCopyConfig"
557
+ v-clean-tooltip="t('nav.kubeconfig.copy')"
558
+ :disabled="!kubeConfigEnabled"
559
+ type="button"
560
+ class="btn header-btn role-tertiary"
561
+ data-testid="btn-copy-kubeconfig"
562
+ role="button"
563
+ tabindex="0"
564
+ :aria-label="t('nav.kubeconfig.copy')"
565
+ @click="copyKubeConfig($event)"
566
+ >
567
+
568
+ <i
569
+ v-if="kubeConfigCopying"
570
+ class="icon icon-checkmark icon-lg"
571
+ />
572
+
573
+ <i
574
+ v-else
575
+ class="icon icon-copy icon-lg"
576
+ />
577
+ </button> -->
578
+ </template>
579
+
580
+ <!-- 资源搜索按钮 -->
581
+ <!-- 2025/9/30 隐藏 -->
582
+ <!-- <button
583
+ v-if="showSearch"
584
+ id="header-btn-search"
585
+ v-clean-tooltip="t('nav.resourceSearch.toolTip', {key: searchShortcut})"
586
+ v-shortkey="{windows: ['ctrl', 'k'], mac: ['meta', 'k']}"
587
+ type="button"
588
+ class="btn header-btn role-tertiary"
589
+ data-testid="header-resource-search"
590
+ role="button"
591
+ tabindex="0"
592
+ :aria-label="t('nav.resourceSearch.toolTip', {key: ''})"
593
+ @shortkey="openSearch()"
594
+ @click="openSearch()"
595
+ >
596
+ <i class="icon icon-search icon-lg" />
597
+ </button> -->
598
+
599
+ <!-- 搜索弹窗 -->
600
+ <app-modal
601
+ v-if="showSearch && showSearchModal"
602
+ class="search-modal"
603
+ name="searchModal"
604
+ width="50%"
605
+ height="auto"
606
+ :trigger-focus-trap="true"
607
+ return-focus-selector="#header-btn-search"
608
+ @close="hideSearch()"
609
+ >
610
+ </app-modal>
611
+ </div>
612
+
613
+ <!-- ===== 插件扩展按钮区 ===== -->
614
+ <div
615
+ v-if="extensionHeaderActions.length"
616
+ class="header-buttons"
617
+ >
618
+ <template v-for="action, i in extensionHeaderActions" :key="`${action.label}${i}`">
619
+ <!-- kubectl-explain.action | 2025/9/30隐藏 -->
620
+ <button
621
+ v-clean-tooltip="handleExtensionTooltip(action)"
622
+ v-shortkey="action.shortcutKey"
623
+ :disabled="action.enabled ? !action.enabled(ctx) : false"
624
+ type="button"
625
+ class="btn header-btn role-tertiary"
626
+ :data-testid="`extension-header-action-${ action.labelKey || action.label }`"
627
+ role="button"
628
+ tabindex="0"
629
+ :aria-label="action.label"
630
+ @shortkey="handleExtensionAction(action, $event)"
631
+ @click="handleExtensionAction(action, $event)"
632
+ >
633
+ <IconOrSvg
634
+ class="icon icon-lg"
635
+ :icon="action.icon"
636
+ :src="action.svg"
637
+ color="header"
638
+ />
639
+ </button>
640
+ </template>
641
+ </div>
642
+
643
+ <!-- ===== 用户菜单(右上角头像 + 下拉) ===== -->
644
+ <div class="center-self">
645
+
646
+ <configurationApps v-if="isRancherInHarvester || isMultiCluster || !isSingleProduct"/>
647
+
648
+ <!-- <header-page-action-menu v-if="showPageActions" /> -->
649
+
650
+ <rc-dropdown
651
+ v-if="showUserMenu"
652
+ :aria-label="t('nav.userMenu.label')"
653
+ >
654
+
655
+ <!-- 头像触发按钮 -->
656
+ <rc-dropdown-trigger
657
+ ghost
658
+ small
659
+ data-testid="nav_header_showUserMenu"
660
+ :aria-label="t('nav.userMenu.button.label')"
661
+ >
662
+ <!-- <img
663
+ v-if="principal && principal.avatarSrc"
664
+ :src="principal.avatarSrc"
665
+ :class="{'avatar-round': principal.roundAvatar}"
666
+ width="36"
667
+ height="36"
668
+ :alt="t('nav.alt.userAvatar')"
669
+ >
670
+ <i
671
+ v-else
672
+ class="icon icon-user icon-3x avatar"
673
+ /> -->
674
+ <i class="icon icon-usericon" style="padding-bottom: 5px;" />
675
+ <span class="login-name">{{ principal.loginName }}</span>
676
+ </rc-dropdown-trigger>
677
+
678
+ <!-- 下拉菜单内容 -->
679
+ <template #dropdownCollection>
680
+ <!-- <template v-if="authEnabled">
681
+ <div class="user-info">
682
+ <div class="user-name">
683
+ <i class="icon icon-lg icon-user" /> {{ principal.loginName }}
684
+ </div>
685
+ <div class="text-small">
686
+ <template v-if="principal.loginName !== principal.name">
687
+ {{ principal.name }}
688
+ </template>
689
+ </div>
690
+ </div>
691
+ <rc-dropdown-separator />
692
+ </template> -->
693
+ <!-- <rc-dropdown-item
694
+ v-if="showPreferencesLink"
695
+ @click="$router.push({ name: 'prefs'})"
696
+ >
697
+ {{ t('nav.userMenu.preferences') }}
698
+ </rc-dropdown-item> -->
699
+
700
+ <!-- 退出登录(支持 SLO 弹窗) -->
701
+ <rc-dropdown-item
702
+ v-if="showAccountAndApiKeyLink"
703
+ @click="LogOutfn('1')"
704
+ >
705
+ {{ t('nav.userMenu.accountAndKeys', {}, true) }}
706
+ </rc-dropdown-item>
707
+
708
+ <!-- 普通退出登录 -->
709
+ <rc-dropdown-item
710
+ v-if="authEnabled && shouldShowSloLogoutModal"
711
+ @click="LogOutfn('2')"
712
+ >
713
+ {{ t('nav.userMenu.logOut') }}
714
+ </rc-dropdown-item>
715
+
716
+ <!-- 普通退出登录 -->
717
+ <rc-dropdown-item
718
+ v-else-if="authEnabled"
719
+ @click="LogOutfn('3')"
720
+ >
721
+ {{ t('nav.userMenu.logOut') }}
722
+ </rc-dropdown-item>
723
+ </template>
724
+ </rc-dropdown>
725
+ </div>
726
+ </div>
727
+ </header>
728
+ </template>
729
+
730
+ <style lang="scss" scoped>
731
+ // It would be nice to grab this from `Group.vue`, but there's margin, padding and border, which is overkill to var
732
+ $side-menu-group-padding-left: 16px;
733
+
734
+ .menu_home_img {
735
+ width: 51px;
736
+ display: flex;
737
+ align-items: center;
738
+ padding: 0 15px;
739
+ border-right: 1px solid rgb(215, 215, 215);
740
+ box-sizing: border-box;
741
+ height: 100%;
742
+ cursor: pointer;
743
+ }
744
+
745
+ HEADER {
746
+ display: flex;
747
+ z-index: z-index('mainHeader');
748
+ box-shadow: 0px 3px 3px 1px rgba(0,0,0,0.12);
749
+
750
+ > .spacer {
751
+ flex: 1;
752
+ }
753
+
754
+ > .menu-spacer {
755
+ display: flex;
756
+ flex: 0 0 15px;
757
+
758
+ &.isSingleProduct {
759
+ display: flex;
760
+ justify-content: center;
761
+
762
+ // Align the icon with the side nav menu items ($side-menu-group-padding-left)
763
+ .side-menu-logo {
764
+ margin-left: $side-menu-group-padding-left;
765
+ }
766
+ }
767
+ }
768
+
769
+ .title {
770
+ border-left: 1px solid var(--header-border);
771
+ padding-left: 10px;
772
+ opacity: 0.7;
773
+ text-transform: uppercase;
774
+ }
775
+
776
+ .filter {
777
+ :deep() .labeled-select,
778
+ :deep() .unlabeled-select {
779
+ .vs__search::placeholder {
780
+ color: var(--body-text) !important;
781
+ }
782
+
783
+ .vs__dropdown-toggle .vs__actions:after {
784
+ color: var(--body-text) !important;
785
+ }
786
+
787
+ .vs__dropdown-toggle {
788
+ background: transparent;
789
+ border: 1px solid var(--header-border);
790
+ }
791
+ }
792
+ }
793
+
794
+ .back {
795
+ padding-top: 6px;
796
+
797
+ > *:first-child {
798
+ height: 40px;
799
+ }
800
+ }
801
+
802
+ .simple-title {
803
+ align-items: center;
804
+ display: flex;
805
+
806
+ .title {
807
+ height: 24px;
808
+ line-height: 24px;
809
+ }
810
+ }
811
+
812
+ .cluster {
813
+ align-items: center;
814
+ display: flex;
815
+ height: 32px;
816
+ white-space: nowrap;
817
+ .cluster-name {
818
+ font-size: 16px;
819
+ text-overflow: ellipsis;
820
+ overflow: hidden;
821
+ }
822
+ &.cluster-clipped {
823
+ overflow: hidden;
824
+ }
825
+ }
826
+
827
+ > .product {
828
+ align-items: center;
829
+ position: relative;
830
+ display: flex;
831
+
832
+ .logo {
833
+ height: 30px;
834
+ position: absolute;
835
+ top: 9px;
836
+ left: 0;
837
+ z-index: 2;
838
+
839
+ img {
840
+ height: 30px;
841
+ }
842
+ }
843
+ }
844
+
845
+ .product-name {
846
+ font-size: 16px;
847
+ }
848
+
849
+ .side-menu-logo {
850
+ align-items: center;
851
+ display: flex;
852
+ margin-right: 8px;
853
+ height: 55px;
854
+ margin-left: 5px;
855
+ max-width: 200px;
856
+ padding: 12px 0;
857
+ }
858
+
859
+ .side-menu-logo-img {
860
+ object-fit: contain;
861
+ height: 21px;
862
+ max-width: 200px;
863
+ }
864
+
865
+ > * {
866
+ background-color: var(--header-bg);
867
+ /* border-bottom: var(--header-border-size) solid var(--header-border); */
868
+ }
869
+
870
+ .rd-header-right {
871
+ display: flex;
872
+ flex-direction: row;
873
+ padding: 0;
874
+
875
+ > * {
876
+ padding: 0 5px;
877
+ }
878
+
879
+ > .top {
880
+ padding-top: 10px;
881
+
882
+ INPUT[type='search']::placeholder,
883
+ .vs__open-indicator,
884
+ .vs__selected {
885
+ color: var(--header-btn-bg) !important;
886
+ background: var(--header-btn-bg);
887
+ border-radius: var(--border-radius);
888
+ border: none;
889
+ margin: 0 35px 0 25px!important;
890
+ }
891
+
892
+ .vs__selected {
893
+ background: rgba(255, 255, 255, 0.15);
894
+ border-color: rgba(255, 255, 255, 0.25);
895
+ }
896
+
897
+ .vs__deselect {
898
+ fill: var(--header-btn-bg);
899
+ }
900
+
901
+ .filter .vs__dropdown-toggle {
902
+ background: var(--header-btn-bg);
903
+ border-radius: var(--border-radius);
904
+ border: none;
905
+ margin: 0 35px 0 25px!important;
906
+ }
907
+ }
908
+
909
+ .header-buttons {
910
+ align-items: center;
911
+ display: flex;
912
+ margin-top: 1px;
913
+
914
+ // Spacing between header buttons
915
+ .btn:not(:last-of-type) {
916
+ margin-right: 10px;
917
+ width: 30px;
918
+ min-width: 30px;
919
+ }
920
+
921
+ .btn:focus {
922
+ box-shadow: none;
923
+ }
924
+
925
+ > .header-btn {
926
+ &.header-btn-active, &.header-btn-active:hover {
927
+ background-color: var(--success);
928
+ color: var(--success-text);
929
+ }
930
+
931
+ img {
932
+ height: 20px;
933
+ width: 20px;
934
+ }
935
+ }
936
+ }
937
+
938
+ .header-btn {
939
+ width: 40px;
940
+ display: flex;
941
+ align-items: center;
942
+ }
943
+
944
+ :deep() div .btn.role-tertiary {
945
+ border: 1px solid var(--header-btn-bg);
946
+ border: none;
947
+ background: var(--header-btn-bg);
948
+ color: var(--header-btn-text);
949
+ padding: 0 10px 0 10px;
950
+ line-height: 32px;
951
+ min-height: 32px;
952
+ width: 30px;
953
+ min-width: 30px;
954
+
955
+ i {
956
+ // Ideally same height as the parent button, but this means tooltip needs adjusting (which is it's own can of worms)
957
+ line-height: 20px;
958
+ }
959
+
960
+ // &:hover {
961
+ // background: var(--primary);
962
+ // color: #fff;
963
+ // }
964
+
965
+ &[disabled=disabled] {
966
+ background-color: rgba(0,0,0,0.25) !important;
967
+ color: var(--header-btn-text) !important;
968
+ opacity: 0.7;
969
+ }
970
+ }
971
+
972
+ :deep(.actions) {
973
+ align-items: center;
974
+ cursor: pointer;
975
+ display: flex;
976
+
977
+ > I {
978
+ font-size: 18px;
979
+ padding: 6px;
980
+ &:hover {
981
+ color: var(--link);
982
+ }
983
+ }
984
+
985
+ :deep(.v-popper:focus) {
986
+ outline: 0;
987
+ }
988
+
989
+ .dropdown {
990
+ margin: 0 -10px;
991
+ }
992
+ }
993
+
994
+ .header-spacer {
995
+ background-color: var(--header-bg);
996
+ position: relative;
997
+ }
998
+
999
+ .avatar-round {
1000
+ border: 0;
1001
+ border-radius: 50%;
1002
+ }
1003
+
1004
+ > .user {
1005
+ outline: none;
1006
+ /* width: var(--header-height); */
1007
+
1008
+ .v-popper {
1009
+ display: flex;
1010
+ :deep() .trigger{
1011
+ .user-image {
1012
+ display: flex;
1013
+ }
1014
+ }
1015
+ }
1016
+
1017
+ .user-image {
1018
+ display: flex;
1019
+ align-items: center;
1020
+ &:hover{
1021
+ color: var(--primary);
1022
+ }
1023
+
1024
+ }
1025
+
1026
+ &:focus {
1027
+ .v-popper {
1028
+ :deep() .trigger {
1029
+ line-height: 0;
1030
+ .user-image {
1031
+ max-height: 40px;
1032
+ }
1033
+ .user-image > * {
1034
+ @include form-focus
1035
+ }
1036
+ }
1037
+ }
1038
+ }
1039
+
1040
+ background-color: var(--header-bg);
1041
+ }
1042
+
1043
+ > .center-self {
1044
+ align-self: center;
1045
+ display: flex;
1046
+ align-items: center;
1047
+ padding-right: 1rem;
1048
+ }
1049
+ }
1050
+ }
1051
+
1052
+ .list-unstyled {
1053
+ li {
1054
+ a {
1055
+ display: flex;
1056
+ justify-content: space-between;
1057
+ padding: 10px;
1058
+ }
1059
+ }
1060
+ }
1061
+
1062
+ div {
1063
+ &.user-info {
1064
+ padding: 0 8px;
1065
+ margin: 0 9px;
1066
+ min-width: 200px;
1067
+ display: flex;
1068
+ gap: 5px;
1069
+ flex-direction: column;
1070
+ }
1071
+ }
1072
+
1073
+ .config-actions {
1074
+ li {
1075
+ a {
1076
+ justify-content: start;
1077
+ align-items: center;
1078
+
1079
+ & .icon {
1080
+ margin: 0 4px;
1081
+ }
1082
+
1083
+ &:hover {
1084
+ cursor: pointer;
1085
+ }
1086
+ }
1087
+ }
1088
+ }
1089
+
1090
+ .v-popper__popper .v-popper__inner {
1091
+ padding: 0;
1092
+ border-radius: 0;
1093
+ }
1094
+
1095
+ .user-name {
1096
+ display: flex;
1097
+ align-items: center;
1098
+ color: var(--secondary);
1099
+ }
1100
+
1101
+ .user-menu {
1102
+ :deep(.v-popper__arrow-container) {
1103
+ display: none;
1104
+ }
1105
+
1106
+ :deep(.v-popper__inner) {
1107
+ padding: 10px 0 10px 0;
1108
+ }
1109
+
1110
+ :deep(.v-popper) {
1111
+ display: flex;
1112
+ }
1113
+ }
1114
+
1115
+ .user-menu-item {
1116
+ a, &.no-link > span {
1117
+ cursor: pointer;
1118
+ padding: 0px 10px;
1119
+
1120
+ &:hover {
1121
+ background-color: var(--dropdown-hover-bg);
1122
+ color: var(--dropdown-hover-text);
1123
+ text-decoration: none;
1124
+ }
1125
+
1126
+ // When the menu item is focused, pop the margin and compensate the padding, so that
1127
+ // the focus border appears within the menu
1128
+ &:focus {
1129
+ margin: 0 2px;
1130
+ padding: 10px 8px;
1131
+ }
1132
+ }
1133
+
1134
+ &.no-link > span {
1135
+ display: flex;
1136
+ justify-content: space-between;
1137
+ padding: 10px;
1138
+ color: var(--popover-text);
1139
+ }
1140
+
1141
+ div.menu-separator {
1142
+ cursor: default;
1143
+ padding: 4px 0;
1144
+
1145
+ .menu-separator-line {
1146
+ background-color: var(--border);
1147
+ height: 1px;
1148
+ }
1149
+ }
1150
+ }
1151
+ .login-name{
1152
+ font-size: 14px;
1153
+ margin-left: 5px;
1154
+ // margin-top: 5px;
1155
+ line-height: 20px;
1156
+ }
1157
+ </style>