dashboard/components/nav/Header.vue

795 lines
19 KiB
Vue

<script>
import { mapGetters } from 'vuex';
import { NORMAN, HCI } from '@/config/types';
import { NAME as VIRTUAL } from '@/config/product/harvester';
import { ucFirst } from '@/utils/string';
import { isMac } from '@/utils/platform';
import Import from '@/components/Import';
import BrandImage from '@/components/BrandImage';
import { getProduct } from '@/config/private-label';
import ClusterProviderIcon from '@/components/ClusterProviderIcon';
import ClusterBadge from '@/components/ClusterBadge';
import { LOGGED_OUT } from '@/config/query-params';
import NamespaceFilter from './NamespaceFilter';
import WorkspaceSwitcher from './WorkspaceSwitcher';
import TopLevelMenu from './TopLevelMenu';
import Jump from './Jump';
const PAGE_HEADER_ACTION = 'page-action';
export default {
components: {
NamespaceFilter,
WorkspaceSwitcher,
Import,
TopLevelMenu,
Jump,
BrandImage,
ClusterBadge,
ClusterProviderIcon
},
props: {
simple: {
type: Boolean,
default: false
}
},
data() {
const searchShortcut = isMac ? '(\u2318-K)' : '(Ctrl+K)';
const shellShortcut = '(Ctrl+`)';
return {
show: false,
showTooltip: false,
searchShortcut,
shellShortcut,
VIRTUAL,
LOGGED_OUT,
};
},
computed: {
...mapGetters(['clusterReady', 'isExplorer', 'isMultiCluster', 'isRancher', 'currentCluster',
'currentProduct', 'backToRancherLink', 'backToRancherGlobalLink', 'pageActions', 'isSingleVirtualCluster']),
...mapGetters('type-map', ['activeProducts']),
appName() {
return getProduct();
},
authEnabled() {
return this.$store.getters['auth/enabled'];
},
principal() {
return this.$store.getters['rancher/byId'](NORMAN.PRINCIPAL, this.$store.getters['auth/principalId']) || {};
},
kubeConfigEnabled() {
return true;
},
shellEnabled() {
return !!this.currentCluster?.links?.shell;
},
showKubeShell() {
return !this.currentProduct?.hideKubeShell;
},
showKubeConfig() {
return !this.currentProduct?.hideKubeConfig;
},
showCopyConfig() {
return !this.currentProduct?.hideCopyConfig;
},
showPageActions() {
return !this.featureRancherDesktop && this.pageActions?.length;
},
showUserMenu() {
return !this.featureRancherDesktop;
},
featureRancherDesktop() {
return this.$config.rancherEnv === 'desktop';
},
importEnabled() {
return !!this.currentCluster?.actions?.apply;
},
prod() {
const name = this.currentProduct.name;
return this.$store.getters['i18n/withFallback'](`product."${ name }"`, null, ucFirst(name));
},
showSearch() {
return this.currentProduct?.inStore === 'cluster';
},
showImportYaml() {
return this.currentProduct?.inStore !== 'harvester';
},
nameTooltip() {
return !this.showTooltip ? {} : {
content: this.currentCluster?.nameDisplay,
delay: 400,
};
},
harvesterDashboard() {
const cluster = this.$store.getters.defaultClusterId;
return {
name: 'c-cluster-product-resource',
params: {
cluster,
product: VIRTUAL,
resource: HCI.DASHBOARD,
}
};
},
},
watch: {
currentCluster(nue, old) {
if (nue && old && nue.id !== old.id) {
this.checkClusterName();
}
}
},
mounted() {
this.checkClusterName();
},
methods: {
showMenu(show) {
if (this.$refs.popover) {
if (show) {
this.$refs.popover.show();
} else {
this.$refs.popover.hide();
}
}
},
openImport() {
this.$modal.show('importModal');
},
closeImport() {
this.$modal.hide('importModal');
},
openSearch() {
this.$modal.show('searchModal');
},
hideSearch() {
this.$modal.hide('searchModal');
},
showPageActionsMenu(show) {
if (this.$refs.pageActions) {
if (show) {
this.$refs.pageActions.show();
} else {
this.$refs.pageActions.hide();
}
}
},
pageAction(action) {
this.$nuxt.$emit(PAGE_HEADER_ACTION, action);
},
checkClusterName() {
this.$nextTick(() => {
const el = this.$refs.clusterName;
this.showTooltip = el && (el.clientWidth < el.scrollWidth);
});
}
}
};
</script>
<template>
<header :class="{'simple': simple}">
<div class="menu-spacer">
<n-link v-if="isSingleVirtualCluster" :to="harvesterDashboard">
<img
class="side-menu-logo"
src="~/assets/images/providers/harvester.svg"
/>
</n-link>
</div>
<div v-if="!simple" class="product">
<div v-if="currentProduct && currentProduct.showClusterSwitcher" v-tooltip="nameTooltip" class="cluster cluster-clipped">
<div v-if="isSingleVirtualCluster" class="product-name">
{{ t('product.harvester') }}
</div>
<template v-else>
<ClusterProviderIcon v-if="currentCluster" :cluster="currentCluster" class="mr-10" />
<div v-if="currentCluster" ref="clusterName" class="cluster-name">
{{ currentCluster.spec.displayName }}
</div>
<ClusterBadge v-if="currentCluster" :cluster="currentCluster" class="ml-10" />
<div v-if="!currentCluster" class="simple-title">
<BrandImage class="side-menu-logo-img" file-name="rancher-logo.svg" />
</div>
</template>
</div>
<div v-if="currentProduct && !currentProduct.showClusterSwitcher" class="cluster">
<div class="product-name">
{{ prod }}
</div>
</div>
</div>
<div v-else class="simple-title">
<div v-if="isSingleVirtualCluster" class="product-name">
{{ t('product.harvester') }}
</div>
<div v-else class="side-menu-logo">
<BrandImage class="side-menu-logo-img" file-name="rancher-logo.svg" />
</div>
</div>
<div>
<TopLevelMenu v-if="isMultiCluster || !isSingleVirtualCluster"></TopLevelMenu>
</div>
<div class="rd-header-right">
<div
v-if="currentCluster && !simple && (currentProduct.showNamespaceFilter || currentProduct.showWorkspaceSwitcher)"
class="top"
>
<NamespaceFilter v-if="clusterReady && currentProduct && (currentProduct.showNamespaceFilter || isExplorer)" />
<WorkspaceSwitcher v-else-if="clusterReady && currentProduct && currentProduct.showWorkspaceSwitcher" />
</div>
<div v-if="currentCluster && !simple" class="header-buttons">
<template v-if="currentProduct && currentProduct.showClusterSwitcher">
<button
v-if="showImportYaml"
v-tooltip="t('nav.import')"
:disabled="!importEnabled"
type="button"
class="btn header-btn role-tertiary"
@click="openImport()"
>
<i class="icon icon-upload icon-lg" />
</button>
<modal
class="import-modal"
name="importModal"
width="75%"
height="auto"
styles="max-height: 90vh;"
>
<Import :cluster="currentCluster" @close="closeImport" />
</modal>
<button
v-if="showKubeShell"
v-tooltip="t('nav.shellShortcut', {key: shellShortcut})"
v-shortkey="{windows: ['ctrl', '`'], mac: ['meta', '`']}"
:disabled="!shellEnabled"
type="button"
class="btn header-btn role-tertiary"
@shortkey="currentCluster.openShell()"
@click="currentCluster.openShell()"
>
<i class="icon icon-terminal icon-lg" />
</button>
<button
v-if="showKubeConfig"
v-tooltip="t('nav.kubeconfig.download')"
:disabled="!kubeConfigEnabled"
type="button"
class="btn header-btn role-tertiary"
@click="currentCluster.downloadKubeConfig()"
>
<i class="icon icon-file icon-lg" />
</button>
<button
v-if="showCopyConfig"
v-tooltip="t('nav.kubeconfig.copy')"
:disabled="!kubeConfigEnabled"
type="button"
class="btn header-btn role-tertiary"
@click="currentCluster.copyKubeConfig()"
>
<i class="icon icon-copy icon-lg" />
</button>
</template>
<button
v-if="showSearch"
v-tooltip="t('nav.resourceSearch.toolTip', {key: searchShortcut})"
v-shortkey="{windows: ['ctrl', 'k'], mac: ['meta', 'k']}"
type="button"
class="btn header-btn role-tertiary"
@shortkey="openSearch()"
@click="openSearch()"
>
<i class="icon icon-search icon-lg" />
</button>
<modal
v-if="showSearch"
class="search-modal"
name="searchModal"
width="50%"
height="auto"
>
<Jump @closeSearch="hideSearch()" />
</modal>
</div>
<div
v-if="showPageActions"
class="actions"
>
<i
class="icon icon-actions"
@blur="showPageActionsMenu(false)"
@click="showPageActionsMenu(true)"
@focus.capture="showPageActionsMenu(true)"
/>
<v-popover
ref="pageActions"
placement="bottom-end"
offset="0"
trigger="manual"
:delay="{show: 0, hide: 0}"
:popper-options="{modifiers: { flip: { enabled: false } } }"
:container="false"
>
<template slot="popover" class="user-menu">
<ul class="list-unstyled dropdown" @click.stop="showPageActionsMenu(false)">
<li v-for="a in pageActions" :key="a.label" class="user-menu-item">
<a v-if="!a.seperator" @click="pageAction(a)">{{ a.labelKey ? t(a.labelKey) : a.label }}</a>
<div v-else class="menu-seperator">
<div class="menu-seperator-line" />
</div>
</li>
</ul>
</template>
</v-popover>
</div>
<div class="header-spacer"></div>
<div
v-if="showUserMenu"
class="user user-menu"
tabindex="0"
@blur="showMenu(false)"
@click="showMenu(true)"
@focus.capture="showMenu(true)"
>
<v-popover
ref="popover"
placement="bottom-end"
offset="-10"
trigger="manual"
:delay="{show: 0, hide: 0}"
:popper-options="{modifiers: { flip: { enabled: false } } }"
:container="false"
>
<div class="user-image text-right hand">
<img v-if="principal && principal.avatarSrc" :src="principal.avatarSrc" :class="{'avatar-round': principal.roundAvatar}" width="36" height="36" />
<i v-else class="icon icon-user icon-3x avatar" />
</div>
<template slot="popover" class="user-menu">
<ul class="list-unstyled dropdown" @click.stop="showMenu(false)">
<li v-if="authEnabled" class="user-info">
<div class="user-name">
<i class="icon icon-lg icon-user" /> {{ principal.loginName }}
</div>
<div class="text-small pt-5 pb-5">
{{ principal.name }}
</div>
</li>
<nuxt-link tag="li" :to="{name: 'prefs'}" class="user-menu-item">
<a>{{ t('nav.userMenu.preferences') }} <i class="icon icon-fw icon-gear" /></a>
</nuxt-link>
<nuxt-link v-if="isRancher || isSingleVirtualCluster" tag="li" :to="{name: 'account'}" class="user-menu-item">
<a>{{ t('nav.userMenu.accountAndKeys', {}, true) }} <i class="icon icon-fw icon-user" /></a>
</nuxt-link>
<nuxt-link v-if="authEnabled" tag="li" :to="{name: 'auth-logout', query: { [LOGGED_OUT]: true }}" class="user-menu-item">
<a @blur="showMenu(false)">{{ t('nav.userMenu.logOut') }} <i class="icon icon-fw icon-close" /></a>
</nuxt-link>
</ul>
</template>
</v-popover>
</div>
</div>
</header>
</template>
<style lang="scss" scoped>
HEADER {
display: grid;
.title {
border-left: 1px solid var(--header-border);
padding-left: 10px;
opacity: 0.7;
text-transform: uppercase;
}
.filter {
::v-deep .labeled-select,
::v-deep .unlabeled-select {
.vs__search::placeholder {
color: var(--body-text) !important;
}
.vs__dropdown-toggle .vs__actions:after {
color: var(--body-text) !important;
font-size: 1.5rem;
padding-right: 4px;
}
.vs__dropdown-toggle {
background: transparent;
border: 1px solid var(--header-border);
}
}
}
> * {
padding: 0 5px;
}
.back {
padding-top: 6px;
> *:first-child {
height: 40px;
}
}
.simple-title {
align-items: center;
display: flex;
.title {
height: 24px;
line-height: 24px;
}
}
grid-template-areas: "menu product top a header-right"; // TODO what's a good name for a here
grid-template-columns: var(--header-height) calc(var(--nav-width) - var(--header-height)) 1fr min-content min-content;
grid-template-rows: var(--header-height);
&.simple {
grid-template-columns: var(--header-height) min-content 1fr min-content min-content;
}
> .menu-spacer {
width: 65px;
grid-area: menu;
}
.cluster {
align-items: center;
display: flex;
height: 32px;
white-space: nowrap;
.cluster-name {
font-size: 16px;
text-overflow: ellipsis;
overflow: hidden;
}
&.cluster-clipped {
overflow: hidden;
}
}
> .product {
grid-area: product;
align-items: center;
position: relative;
display: flex;
.logo {
height: 30px;
position: absolute;
top: 9px;
left: 0;
z-index: 2;
img {
height: 30px;
}
}
}
.product-name {
font-size: 16px;
}
.side-menu-logo {
align-items: center;
display: flex;
margin-right: 8px;
height: 55px;
margin-left: 5px;
max-width: 200px;
padding: 12px 0;
}
.side-menu-logo-img {
object-fit: contain;
height: 21px;
max-width: 200px;
}
> * {
background-color: var(--header-bg);
border-bottom: var(--header-border-size) solid var(--header-border);
}
.menu-spacer {
grid-area: menu;
}
.rd-header-right {
display: flex;
flex-direction: row;
padding: 0;
grid-area: header-right;
> * {
padding: 0 5px;
}
> .top {
padding-top: 6px;
INPUT[type='search']::placeholder,
.vs__open-indicator,
.vs__selected {
color: var(--header-btn-bg) !important;
background: var(--header-btn-bg);
border-radius: var(--border-radius);
border: none;
margin: 0 35px 0 25px!important;
}
.vs__selected {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.25);
}
.vs__deselect {
fill: var(--header-btn-bg);
}
.filter .vs__dropdown-toggle {
background: var(--header-btn-bg);
border-radius: var(--border-radius);
border: none;
margin: 0 35px 0 25px!important;
}
}
.header-buttons {
align-items: center;
display: flex;
margin-top: 1px;
// Spacing between header buttons
.btn:not(:last-of-type) {
margin-right: 10px;
}
.btn:focus {
box-shadow: none;
}
}
.header-btn {
width: 40px;
}
::v-deep > div > .btn.role-tertiary {
border: 1px solid var(--header-btn-bg);
border: none;
background: var(--header-btn-bg);
color: var(--header-btn-text);
padding: 0 10px;
line-height: 32px;
min-height: 32px;
i {
// Ideally same height as the parent button, but this means tooltip needs adjusting (which is it's own can of worms)
line-height: 20px;
}
&:hover {
background: var(--primary);
color: #fff;
}
&[disabled=disabled] {
background-color: rgba(0,0,0,0.25) !important;
color: var(--header-btn-text) !important;
opacity: 0.7;
}
}
.actions {
align-items: center;
cursor: pointer;
display: flex;
> I {
font-size: 18px;
padding: 6px;
&:hover {
color: var(--link);
}
}
}
.header-spacer {
background-color: var(--header-bg);
position: relative;
}
.user-menu {
padding-top: 9.5px;
}
> .user {
outline: none;
width: var(--header-height);
.v-popover {
display: flex;
::v-deep .trigger{
.user-image {
display: flex;
}
}
}
.user-image {
display: flex;
align-items: center;
}
&:focus {
.v-popover {
::v-deep .trigger {
line-height: 0;
.user-image {
max-height: 40px;
}
.user-image > * {
@include form-focus
}
}
}
}
background-color: var(--header-bg);
.avatar-round {
border: 0;
border-radius: 50%;
}
}
}
}
.list-unstyled {
li {
a {
display: flex;
justify-content: space-between;
padding: 10px;
}
&.user-info {
display: block;
margin-bottom: 10px;
padding: 10px 20px;
border-bottom: solid 1px var(--border);
min-width: 200px;
}
}
}
.config-actions {
li {
a {
justify-content: start;
align-items: center;
& .icon {
margin: 0 4px;
}
&:hover {
cursor: pointer;
}
}
}
}
.popover .popover-inner {
padding: 0;
border-radius: 0;
}
.user-name {
color: var(--secondary);
}
.user-menu {
// Remove the default padding on the popup so that the hover on menu items goes full width of the menu
::v-deep .popover-inner {
padding: 10px 0;
}
::v-deep .v-popover {
display: flex;
}
}
.actions {
::v-deep .popover:focus {
outline: 0;
}
.dropdown {
margin: 0 -10px;
}
}
.user-menu-item {
a {
cursor: hand;
padding: 0px 10px;
&:hover {
background-color: var(--dropdown-hover-bg);
color: var(--dropdown-hover-text);
text-decoration: none;
}
// When the menu item is focused, pop the margin and compensate the padding, so that
// the focus border appears within the menu
&:focus {
margin: 0 2px;
padding: 10px 8px;
}
}
div.menu-seperator {
cursor: default;
padding: 4px 0;
.menu-seperator-line {
background-color: var(--border);
height: 1px;
}
}
}
</style>