mirror of https://github.com/rancher/dashboard.git
1444 lines
37 KiB
Vue
1444 lines
37 KiB
Vue
<script>
|
|
import BrandImage from '@shell/components/BrandImage';
|
|
import ClusterIconMenu from '@shell/components/ClusterIconMenu';
|
|
import IconOrSvg from '../IconOrSvg';
|
|
import { BLANK_CLUSTER } from '@shell/store/store-types.js';
|
|
import { mapGetters } from 'vuex';
|
|
import { CAPI, COUNT, MANAGEMENT } from '@shell/config/types';
|
|
import { MENU_MAX_CLUSTERS, PINNED_CLUSTERS } from '@shell/store/prefs';
|
|
import { sortBy } from '@shell/utils/sort';
|
|
import { ucFirst } from '@shell/utils/string';
|
|
import { KEY } from '@shell/utils/platform';
|
|
import { getVersionInfo } from '@shell/utils/version';
|
|
import { SETTING } from '@shell/config/settings';
|
|
import { getProductFromRoute } from '@shell/utils/router';
|
|
import { isRancherPrime } from '@shell/config/version';
|
|
import Pinned from '@shell/components/nav/Pinned';
|
|
import { getGlobalBannerFontSizes } from '@shell/utils/banners';
|
|
import { TopLevelMenuHelperPagination, TopLevelMenuHelperLegacy } from 'components/nav/TopLevelMenu.helper';
|
|
import { debounce } from 'lodash';
|
|
import { sameContents } from '@shell/utils/array';
|
|
|
|
// TODO: RC (home page/side bar) test search properly
|
|
|
|
export default {
|
|
components: {
|
|
BrandImage,
|
|
ClusterIconMenu,
|
|
IconOrSvg,
|
|
Pinned
|
|
},
|
|
|
|
data() {
|
|
const { displayVersion, fullVersion } = getVersionInfo(this.$store);
|
|
const hasProvCluster = this.$store.getters[`management/schemaFor`](CAPI.RANCHER_CLUSTER);
|
|
|
|
const canPagination = this.$store.getters[`management/paginationEnabled`]({
|
|
id: MANAGEMENT.CLUSTER,
|
|
context: 'side-bar',
|
|
}) && this.$store.getters[`management/paginationEnabled`]({
|
|
id: CAPI.RANCHER_CLUSTER,
|
|
context: 'side-bar',
|
|
});
|
|
const helper = canPagination ? new TopLevelMenuHelperPagination({ $store: this.$store }) : new TopLevelMenuHelperLegacy({ $store: this.$store });
|
|
const provClusters = !canPagination && hasProvCluster ? this.$store.getters[`management/all`](CAPI.RANCHER_CLUSTER) : [];
|
|
const mgmtClusters = !canPagination ? this.$store.getters[`management/all`](MANAGEMENT.CLUSTER) : [];
|
|
|
|
return {
|
|
shown: false,
|
|
displayVersion,
|
|
fullVersion,
|
|
clusterFilter: '',
|
|
hasProvCluster,
|
|
maxClustersToShow: MENU_MAX_CLUSTERS,
|
|
emptyCluster: BLANK_CLUSTER,
|
|
routeCombo: false,
|
|
|
|
canPagination,
|
|
helper,
|
|
debouncedHelperUpdate: debounce((...args) => this.helper.update(...args), 200),
|
|
provClusters,
|
|
mgmtClusters,
|
|
};
|
|
},
|
|
|
|
computed: {
|
|
...mapGetters(['clusterId']),
|
|
...mapGetters(['clusterReady', 'isRancher', 'currentCluster', 'currentProduct', 'isRancherInHarvester']),
|
|
...mapGetters({ features: 'features/get' }),
|
|
|
|
pinnedIds() {
|
|
return this.$store.getters['prefs/get'](PINNED_CLUSTERS);
|
|
},
|
|
|
|
sideMenuStyle() {
|
|
const globalBannerSettings = getGlobalBannerFontSizes(this.$store);
|
|
|
|
return {
|
|
marginBottom: globalBannerSettings?.footerFont,
|
|
marginTop: globalBannerSettings?.headerFont
|
|
};
|
|
},
|
|
|
|
showClusterSearch() {
|
|
return this.allClustersCount > this.maxClustersToShow;
|
|
},
|
|
|
|
allClustersCount() {
|
|
const counts = this.$store.getters[`management/all`](COUNT)?.[0]?.counts || {};
|
|
const count = counts[MANAGEMENT.CLUSTER] || {};
|
|
|
|
return count?.summary.count;
|
|
},
|
|
|
|
// New
|
|
search() {
|
|
return (this.clusterFilter || '').toLowerCase();
|
|
},
|
|
|
|
// New
|
|
showPinClusters() {
|
|
return !this.search;
|
|
},
|
|
|
|
// New
|
|
searchActive() {
|
|
return !!this.search;
|
|
},
|
|
|
|
/**
|
|
* Only Clusters that are pinned
|
|
*
|
|
* (see description of helper.clustersPinned for more details)
|
|
*/
|
|
pinFiltered() {
|
|
return this.hasProvCluster ? this.helper.clustersPinned : [];
|
|
},
|
|
|
|
/**
|
|
* Used to shown unpinned clusters OR results of text search
|
|
*
|
|
* (see description of helper.clustersOthers for more details)
|
|
*/
|
|
clustersFiltered() {
|
|
return this.hasProvCluster ? this.helper.clustersOthers : [];
|
|
},
|
|
|
|
pinnedClustersHeight() {
|
|
const pinCount = this.pinFiltered.length;
|
|
const height = pinCount > 2 ? (pinCount * 43) : 90;
|
|
|
|
return `min-height: ${ height }px`;
|
|
},
|
|
clusterFilterCount() {
|
|
return this.clusterFilter ? this.clustersFiltered.length : this.allClustersCount;
|
|
},
|
|
|
|
multiClusterApps() {
|
|
const options = this.options;
|
|
|
|
return options.filter((opt) => {
|
|
const filterApps = (opt.inStore === 'management' || opt.isMultiClusterApp) && opt.category !== 'configuration' && opt.category !== 'legacy';
|
|
|
|
if (this.isRancherInHarvester) {
|
|
return filterApps && opt.category !== 'hci';
|
|
} else {
|
|
// We expect the location of Virtualization Management to remain the same when rancher-manage-support is not enabled
|
|
return filterApps;
|
|
}
|
|
});
|
|
},
|
|
|
|
configurationApps() {
|
|
const options = this.options;
|
|
|
|
return options.filter((opt) => opt.category === 'configuration');
|
|
},
|
|
|
|
hciApps() {
|
|
const options = this.options;
|
|
|
|
return options.filter((opt) => this.isRancherInHarvester && opt.category === 'hci');
|
|
},
|
|
|
|
options() {
|
|
const cluster = this.clusterId || this.$store.getters['defaultClusterId'];
|
|
|
|
// TODO plugin routes
|
|
const entries = this.$store.getters['type-map/activeProducts']?.map((p) => {
|
|
// Try product-specific index first
|
|
const to = p.to || {
|
|
name: `c-cluster-${ p.name }`,
|
|
params: { cluster }
|
|
};
|
|
|
|
const matched = this.$router.getRoutes().filter((route) => route.name === to.name);
|
|
|
|
if ( !matched.length ) {
|
|
to.name = 'c-cluster-product';
|
|
to.params.product = p.name;
|
|
}
|
|
|
|
return {
|
|
label: this.$store.getters['i18n/withFallback'](`product."${ p.name }"`, null, ucFirst(p.name)),
|
|
icon: `icon-${ p.icon || 'copy' }`,
|
|
svg: p.svg,
|
|
value: p.name,
|
|
removable: p.removable !== false,
|
|
inStore: p.inStore || 'cluster',
|
|
weight: p.weight || 1,
|
|
category: p.category || 'none',
|
|
to,
|
|
isMultiClusterApp: p.isMultiClusterApp,
|
|
};
|
|
});
|
|
|
|
return sortBy(entries, ['weight']);
|
|
},
|
|
|
|
canEditSettings() {
|
|
return (this.$store.getters['management/schemaFor'](MANAGEMENT.SETTING)?.resourceMethods || []).includes('PUT');
|
|
},
|
|
|
|
hasSupport() {
|
|
return isRancherPrime() || this.$store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.SUPPORTED )?.value === 'true';
|
|
},
|
|
|
|
isCurrRouteClusterExplorer() {
|
|
return this.$route?.name?.startsWith('c-cluster');
|
|
},
|
|
|
|
productFromRoute() {
|
|
return getProductFromRoute(this.$route);
|
|
},
|
|
|
|
aboutText() {
|
|
// If a version number (starts with 'v') then use that
|
|
if (this.displayVersion.startsWith('v')) {
|
|
// Don't show the '.0' for a minor release (e.g. 2.8.0, 2.9.0 etc)
|
|
return !this.displayVersion.endsWith('.0') ? this.displayVersion : this.displayVersion.substr(0, this.displayVersion.length - 2);
|
|
}
|
|
|
|
// Default fallback to 'About'
|
|
return this.t('about.title');
|
|
},
|
|
|
|
largeAboutText() {
|
|
return this.aboutText.length > 6;
|
|
},
|
|
|
|
appBar() {
|
|
let activeFound = false;
|
|
|
|
// order is important for the object keys here
|
|
// since we want to check last pinFiltered and clustersFiltered
|
|
const appBar = {
|
|
hciApps: this.hciApps,
|
|
multiClusterApps: this.multiClusterApps,
|
|
configurationApps: this.configurationApps,
|
|
pinFiltered: this.pinFiltered,
|
|
clustersFiltered: this.clustersFiltered,
|
|
};
|
|
|
|
Object.keys(appBar).forEach((menuSection) => {
|
|
const menuSectionItems = appBar[menuSection];
|
|
const isClusterCheck = menuSection === 'pinFiltered' || menuSection === 'clustersFiltered';
|
|
|
|
// need to reset active state on other menu items
|
|
menuSectionItems.forEach((item) => {
|
|
item.isMenuActive = false;
|
|
|
|
if (!activeFound && this.checkActiveRoute(item, isClusterCheck)) {
|
|
activeFound = true;
|
|
item.isMenuActive = true;
|
|
}
|
|
});
|
|
});
|
|
|
|
return appBar;
|
|
}
|
|
},
|
|
|
|
watch: {
|
|
$route() {
|
|
this.shown = false;
|
|
},
|
|
|
|
pinnedIds: {
|
|
immediate: true,
|
|
handler(neu, old) {
|
|
if (sameContents(neu, old)) {
|
|
return;
|
|
}
|
|
|
|
this.updateClusters(neu);
|
|
}
|
|
},
|
|
|
|
search() {
|
|
this.updateClusters(this.pinnedIds);
|
|
},
|
|
|
|
provClusters: {
|
|
handler() {
|
|
// Shouldn't get here if SSP
|
|
this.updateClusters(this.pinnedIds);
|
|
},
|
|
deep: true
|
|
},
|
|
|
|
mgmtClusters: {
|
|
handler() {
|
|
// Shouldn't get here if SSP
|
|
this.updateClusters(this.pinnedIds);
|
|
},
|
|
deep: true
|
|
},
|
|
|
|
},
|
|
|
|
mounted() {
|
|
document.addEventListener('keyup', this.handler);
|
|
},
|
|
|
|
beforeUnmount() {
|
|
document.removeEventListener('keyup', this.handler);
|
|
},
|
|
|
|
methods: {
|
|
checkActiveRoute(obj, isClusterRoute) {
|
|
// 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
|
|
if (isClusterRoute) {
|
|
return this.isCurrRouteClusterExplorer && this.$route?.params?.cluster === obj?.id && this.productFromRoute === this.currentProduct?.name;
|
|
}
|
|
|
|
// for remaining main nav items, check if curr product matches route product is enough
|
|
return this.productFromRoute === obj?.value;
|
|
},
|
|
|
|
handleKeyComboClick() {
|
|
this.routeCombo = !this.routeCombo;
|
|
},
|
|
|
|
clusterMenuClick(ev, cluster) {
|
|
if (this.routeCombo) {
|
|
ev.preventDefault();
|
|
|
|
if (this.isCurrRouteClusterExplorer && this.productFromRoute === this.currentProduct?.name) {
|
|
const clusterRoute = {
|
|
name: this.$route.name,
|
|
params: { ...this.$route.params },
|
|
query: { ...this.$route.query }
|
|
};
|
|
|
|
clusterRoute.params.cluster = cluster.id;
|
|
|
|
return this.$router.push(clusterRoute);
|
|
}
|
|
}
|
|
|
|
return this.$router.push(cluster.clusterRoute);
|
|
},
|
|
|
|
handler(e) {
|
|
if (e.keyCode === KEY.ESCAPE ) {
|
|
this.hide();
|
|
}
|
|
},
|
|
|
|
hide() {
|
|
this.shown = false;
|
|
if (this.clustersFiltered === 0) {
|
|
this.clusterFilter = '';
|
|
}
|
|
},
|
|
|
|
toggle() {
|
|
this.shown = !this.shown;
|
|
},
|
|
|
|
async goToHarvesterCluster() {
|
|
const localCluster = this.$store.getters['management/all'](CAPI.RANCHER_CLUSTER).find((C) => C.id === 'fleet-local/local');
|
|
|
|
try {
|
|
await localCluster.goToHarvesterCluster();
|
|
} catch {
|
|
}
|
|
},
|
|
|
|
getTooltipConfig(item, showWhenClosed = false) {
|
|
if (!item) {
|
|
return;
|
|
}
|
|
|
|
let contentText = '';
|
|
let content;
|
|
let popperClass = '';
|
|
|
|
// this is the normal tooltip scenario where we are just passing a string
|
|
if (typeof item === 'string') {
|
|
contentText = item;
|
|
content = this.shown ? null : contentText;
|
|
|
|
// if key combo is pressed, then we update the tooltip as well
|
|
} else if (this.routeCombo &&
|
|
typeof item === 'object' &&
|
|
!Array.isArray(item) &&
|
|
item !== null &&
|
|
item.ready) {
|
|
contentText = this.t('nav.keyComboTooltip');
|
|
|
|
if (showWhenClosed) {
|
|
content = !this.shown ? contentText : null;
|
|
} else {
|
|
content = this.shown ? contentText : null;
|
|
}
|
|
|
|
// this is scenario where we show a tooltip when we are on the expanded menu to show full description
|
|
} else {
|
|
contentText = item.label;
|
|
// this adds a class to the tooltip container so that we can control the max width
|
|
popperClass = 'menu-description-tooltip';
|
|
|
|
if (item.description) {
|
|
contentText += `<br><br>${ item.description }`;
|
|
}
|
|
|
|
if (showWhenClosed) {
|
|
content = !this.shown ? contentText : null;
|
|
} else {
|
|
content = this.shown ? contentText : null;
|
|
|
|
// this adds a class to adjust tooltip position so it doesn't overlap the cluster pinning action
|
|
popperClass += ' description-tooltip-pos-adjustment';
|
|
}
|
|
}
|
|
|
|
return {
|
|
content,
|
|
placement: 'right',
|
|
popperOptions: { modifiers: { preventOverflow: { enabled: false }, hide: { enabled: false } } },
|
|
popperClass
|
|
};
|
|
},
|
|
|
|
updateClusters(pinnedIds) {
|
|
this.debouncedHelperUpdate({
|
|
pinnedIds,
|
|
searchTerm: this.search,
|
|
unPinnedMax: this.maxClustersToShow
|
|
});
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<!-- Overlay -->
|
|
<div
|
|
v-if="shown"
|
|
class="side-menu-glass"
|
|
@click="hide()"
|
|
/>
|
|
<transition name="fade">
|
|
<!-- Side menu -->
|
|
<div
|
|
data-testid="side-menu"
|
|
class="side-menu"
|
|
:class="{'menu-open': shown, 'menu-close':!shown}"
|
|
:style="sideMenuStyle"
|
|
tabindex="-1"
|
|
>
|
|
<!-- Logo and name -->
|
|
<div class="title">
|
|
<div
|
|
data-testid="top-level-menu"
|
|
class="menu"
|
|
@click="toggle()"
|
|
>
|
|
<svg
|
|
class="menu-icon"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
height="24"
|
|
viewBox="0 0 24 24"
|
|
width="24"
|
|
><path
|
|
d="M0 0h24v24H0z"
|
|
fill="none"
|
|
/><path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z" /></svg>
|
|
</div>
|
|
<div class="side-menu-logo">
|
|
<BrandImage
|
|
data-testid="side-menu__brand-img"
|
|
file-name="rancher-logo.svg"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Menu body -->
|
|
<div class="body">
|
|
<div>
|
|
<!-- Home button -->
|
|
<div @click="hide()">
|
|
<router-link
|
|
class="option cluster selector home"
|
|
:to="{ name: 'home' }"
|
|
>
|
|
<svg
|
|
v-clean-tooltip="getTooltipConfig(t('nav.home'))"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
height="24"
|
|
viewBox="0 0 24 24"
|
|
width="24"
|
|
><path
|
|
d="M0 0h24v24H0z"
|
|
fill="none"
|
|
/><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z" /></svg>
|
|
<div class="home-text">
|
|
{{ t('nav.home') }}
|
|
</div>
|
|
</router-link>
|
|
</div>
|
|
<!-- Search bar -->
|
|
<div
|
|
v-if="showClusterSearch"
|
|
class="clusters-search"
|
|
>
|
|
<div class="clusters-search-count">
|
|
<span>{{ clusterFilterCount }}</span>
|
|
{{ t('nav.search.clusters') }}
|
|
<i
|
|
v-if="clusterFilter"
|
|
class="icon icon-filter_alt"
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
class="search"
|
|
>
|
|
<input
|
|
ref="clusterFilter"
|
|
v-model="clusterFilter"
|
|
:placeholder="t('nav.search.placeholder')"
|
|
>
|
|
<i
|
|
class="magnifier icon icon-search"
|
|
:class="{ active: clusterFilter }"
|
|
/>
|
|
<i
|
|
v-if="clusterFilter"
|
|
class="icon icon-close"
|
|
@click="clusterFilter=''"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Harvester extras -->
|
|
<template v-if="hciApps.length">
|
|
<div class="category" />
|
|
<div>
|
|
<a
|
|
v-if="isRancherInHarvester"
|
|
class="option"
|
|
@click="goToHarvesterCluster()"
|
|
>
|
|
<i
|
|
class="icon icon-dashboard"
|
|
/>
|
|
<div>
|
|
{{ t('nav.harvesterDashboard') }}
|
|
</div>
|
|
</a>
|
|
</div>
|
|
<div
|
|
v-for="(a, i) in appBar.hciApps"
|
|
:key="i"
|
|
@click="hide()"
|
|
>
|
|
<router-link
|
|
class="option"
|
|
:to="a.to"
|
|
:class="{'active-menu-link': a.isMenuActive }"
|
|
>
|
|
<IconOrSvg
|
|
:icon="a.icon"
|
|
:src="a.svg"
|
|
/>
|
|
<div>{{ a.label }}</div>
|
|
</router-link>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Cluster menu -->
|
|
<!-- <template v-if="clusters && !!clusters.length"> -->
|
|
<template v-if="!!allClustersCount">
|
|
<div
|
|
ref="clusterList"
|
|
class="clusters"
|
|
:style="pinnedClustersHeight"
|
|
>
|
|
<!-- Pinned Clusters -->
|
|
<div
|
|
v-if="showPinClusters && pinFiltered.length"
|
|
class="clustersPinned"
|
|
>
|
|
<div
|
|
v-for="(c, index) in appBar.pinFiltered"
|
|
:key="index"
|
|
:data-testid="`pinned-ready-cluster-${index}`"
|
|
@click="hide()"
|
|
>
|
|
<button
|
|
v-if="c.ready"
|
|
v-shortkey.push="{windows: ['alt'], mac: ['option']}"
|
|
:data-testid="`pinned-menu-cluster-${ c.id }`"
|
|
class="cluster selector option"
|
|
:class="{'active-menu-link': c.isMenuActive }"
|
|
:to="c.clusterRoute"
|
|
@click.prevent="clusterMenuClick($event, c)"
|
|
@shortkey="handleKeyComboClick"
|
|
>
|
|
<ClusterIconMenu
|
|
v-clean-tooltip="getTooltipConfig(c, true)"
|
|
:cluster="c"
|
|
:route-combo="routeCombo"
|
|
class="rancher-provider-icon"
|
|
/>
|
|
<div
|
|
v-clean-tooltip="getTooltipConfig(c)"
|
|
class="cluster-name"
|
|
>
|
|
<p>{{ c.label }}</p>
|
|
<p
|
|
v-if="c.description"
|
|
class="description"
|
|
>
|
|
{{ c.description }}
|
|
</p>
|
|
</div>
|
|
<Pinned
|
|
:cluster="c"
|
|
/>
|
|
</button>
|
|
<span
|
|
v-else
|
|
class="option cluster selector disabled"
|
|
:data-testid="`pinned-menu-cluster-disabled-${ c.id }`"
|
|
>
|
|
<ClusterIconMenu
|
|
v-clean-tooltip="getTooltipConfig(c, true)"
|
|
:cluster="c"
|
|
class="rancher-provider-icon"
|
|
/>
|
|
<div
|
|
v-clean-tooltip="getTooltipConfig(c)"
|
|
class="cluster-name"
|
|
>
|
|
<p>{{ c.label }}</p>
|
|
<p
|
|
v-if="c.description"
|
|
class="description"
|
|
>
|
|
{{ c.description }}
|
|
</p>
|
|
</div>
|
|
<Pinned
|
|
:cluster="c"
|
|
/>
|
|
</span>
|
|
</div>
|
|
<div
|
|
v-if="clustersFiltered.length > 0"
|
|
class="category-title"
|
|
>
|
|
<hr>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Clusters Search result -->
|
|
<div class="clustersList">
|
|
<div
|
|
v-for="(c, index) in appBar.clustersFiltered"
|
|
:key="index"
|
|
:data-testid="`top-level-menu-cluster-${index}`"
|
|
@click="hide()"
|
|
>
|
|
<button
|
|
v-if="c.ready"
|
|
v-shortkey.push="{windows: ['alt'], mac: ['option']}"
|
|
:data-testid="`menu-cluster-${ c.id }`"
|
|
class="cluster selector option"
|
|
:class="{'active-menu-link': c.isMenuActive }"
|
|
:to="c.clusterRoute"
|
|
@click="clusterMenuClick($event, c)"
|
|
@shortkey="handleKeyComboClick"
|
|
>
|
|
<ClusterIconMenu
|
|
v-clean-tooltip="getTooltipConfig(c, true)"
|
|
:cluster="c"
|
|
:route-combo="routeCombo"
|
|
class="rancher-provider-icon"
|
|
/>
|
|
<div
|
|
v-clean-tooltip="getTooltipConfig(c)"
|
|
class="cluster-name"
|
|
>
|
|
<!-- HERE LOCAL CLUSTER! -->
|
|
<p>{{ c.label }}</p>
|
|
<p
|
|
v-if="c.description"
|
|
class="description"
|
|
>
|
|
{{ c.description }}
|
|
</p>
|
|
</div>
|
|
<Pinned
|
|
:class="{'showPin': c.pinned}"
|
|
:cluster="c"
|
|
/>
|
|
</button>
|
|
<span
|
|
v-else
|
|
class="option cluster selector disabled"
|
|
:data-testid="`menu-cluster-disabled-${ c.id }`"
|
|
>
|
|
<ClusterIconMenu
|
|
v-clean-tooltip="getTooltipConfig(c, true)"
|
|
:cluster="c"
|
|
class="rancher-provider-icon"
|
|
/>
|
|
<div
|
|
v-clean-tooltip="getTooltipConfig(c)"
|
|
class="cluster-name"
|
|
>
|
|
<p>{{ c.label }}</p>
|
|
<p
|
|
v-if="c.description"
|
|
class="description"
|
|
>
|
|
{{ c.description }}
|
|
</p>
|
|
</div>
|
|
<Pinned
|
|
:class="{'showPin': c.pinned}"
|
|
:cluster="c"
|
|
/>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- No clusters message -->
|
|
<div
|
|
v-if="(clustersFiltered.length === 0 || pinFiltered.length === 0) && searchActive"
|
|
data-testid="top-level-menu-no-results"
|
|
class="none-matching"
|
|
>
|
|
{{ t('nav.search.noResults') }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- See all clusters -->
|
|
<router-link
|
|
v-if="allClustersCount > maxClustersToShow"
|
|
class="clusters-all"
|
|
:to="{name: 'c-cluster-product-resource', params: {
|
|
cluster: emptyCluster,
|
|
product: 'manager',
|
|
resource: 'provisioning.cattle.io.cluster'
|
|
} }"
|
|
>
|
|
<span>
|
|
{{ shown ? t('nav.seeAllClusters') : t('nav.seeAllClustersCollapsed') }}
|
|
<i class="icon icon-chevron-right" />
|
|
</span>
|
|
</router-link>
|
|
</template>
|
|
|
|
<div class="category">
|
|
<template v-if="multiClusterApps.length">
|
|
<div
|
|
class="category-title"
|
|
>
|
|
<hr>
|
|
<span>
|
|
{{ t('nav.categories.multiCluster') }}
|
|
</span>
|
|
</div>
|
|
<div
|
|
v-for="(a, i) in appBar.multiClusterApps"
|
|
:key="i"
|
|
@click="hide()"
|
|
>
|
|
<router-link
|
|
class="option"
|
|
:class="{'active-menu-link': a.isMenuActive }"
|
|
:to="a.to"
|
|
>
|
|
<IconOrSvg
|
|
v-clean-tooltip="getTooltipConfig(a.label)"
|
|
:icon="a.icon"
|
|
:src="a.svg"
|
|
/>
|
|
<span class="option-link">{{ a.label }}</span>
|
|
</router-link>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- App menu -->
|
|
<template v-if="configurationApps.length">
|
|
<div
|
|
class="category-title"
|
|
>
|
|
<hr>
|
|
<span>
|
|
{{ t('nav.categories.configuration') }}
|
|
</span>
|
|
</div>
|
|
<div
|
|
v-for="(a, i) in appBar.configurationApps"
|
|
:key="i"
|
|
@click="hide()"
|
|
>
|
|
<router-link
|
|
class="option"
|
|
:class="{'active-menu-link': a.isMenuActive }"
|
|
:to="a.to"
|
|
>
|
|
<IconOrSvg
|
|
v-clean-tooltip="getTooltipConfig(a.label)"
|
|
:icon="a.icon"
|
|
:src="a.svg"
|
|
/>
|
|
<div>{{ a.label }}</div>
|
|
</router-link>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div
|
|
class="footer"
|
|
>
|
|
<div
|
|
v-if="canEditSettings"
|
|
class="support"
|
|
@click="hide()"
|
|
>
|
|
<router-link
|
|
:to="{name: 'support'}"
|
|
>
|
|
{{ t('nav.support', {hasSupport}) }}
|
|
</router-link>
|
|
</div>
|
|
<div
|
|
class="version"
|
|
:class="{'version-small': largeAboutText}"
|
|
@click="hide()"
|
|
>
|
|
<router-link
|
|
:to="{ name: 'about' }"
|
|
>
|
|
{{ aboutText }}
|
|
</router-link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</transition>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss">
|
|
.menu-description-tooltip {
|
|
max-width: 200px;
|
|
white-space: pre-wrap;
|
|
word-wrap: break-word;
|
|
}
|
|
|
|
.description-tooltip-pos-adjustment {
|
|
// needs !important so that we can
|
|
// offset the tooltip a bit so it doesn't
|
|
// overlap the pin icon and cause bad UX
|
|
left: 48px !important;
|
|
}
|
|
|
|
.localeSelector, .footer-tooltip {
|
|
z-index: 1000;
|
|
}
|
|
|
|
.localeSelector {
|
|
.v-popper__inner {
|
|
padding: 10px 0;
|
|
}
|
|
|
|
.v-popper__arrow-container {
|
|
display: none;
|
|
}
|
|
|
|
.v-popper:focus {
|
|
outline: 0;
|
|
}
|
|
}
|
|
|
|
.theme-dark .cluster-name .description {
|
|
color: var(--input-label) !important;
|
|
}
|
|
.theme-dark .body .option {
|
|
&:hover .cluster-name .description,
|
|
&.router-link-active .cluster-name .description,
|
|
&.active-menu-link .cluster-name .description {
|
|
color: var(--side-menu-desc) !important;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<style lang="scss" scoped>
|
|
$clear-search-size: 20px;
|
|
$icon-size: 25px;
|
|
$option-padding: 9px;
|
|
$option-padding-left: 14px;
|
|
$option-height: $icon-size + $option-padding + $option-padding;
|
|
|
|
.side-menu {
|
|
.menu {
|
|
position: absolute;
|
|
width: $app-bar-collapsed-width;
|
|
height: 54px;
|
|
top: 0;
|
|
grid-area: menu;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
|
|
.menu-icon {
|
|
width: 25px;
|
|
height: 25px;
|
|
fill: var(--header-btn-text);
|
|
}
|
|
}
|
|
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0px;
|
|
bottom: 0;
|
|
width: $app-bar-collapsed-width;
|
|
background-color: var(--topmenu-bg);
|
|
z-index: 100;
|
|
border-right: 1px solid var(--topmost-border);
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: 0;
|
|
overflow: hidden;
|
|
transition: width 250ms;
|
|
|
|
&:focus {
|
|
outline: 0;
|
|
}
|
|
|
|
&.menu-open {
|
|
width: 300px;
|
|
box-shadow: 3px 1px 3px var(--shadow);
|
|
}
|
|
|
|
.title {
|
|
display: flex;
|
|
height: 55px;
|
|
flex: 0 0 55px;
|
|
width: 100%;
|
|
justify-content: flex-start;
|
|
align-items: center;
|
|
|
|
.menu {
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
.menu-icon {
|
|
width: 25px;
|
|
height: 25px;
|
|
}
|
|
}
|
|
.home {
|
|
svg {
|
|
width: 25px;
|
|
height: 25px;
|
|
margin-left: 9px;
|
|
}
|
|
}
|
|
.home-text {
|
|
margin-left: $option-padding-left - 7;
|
|
}
|
|
.body {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
margin: 10px 0;
|
|
width: 300px;
|
|
overflow: auto;
|
|
|
|
.option {
|
|
align-items: center;
|
|
cursor: pointer;
|
|
display: flex;
|
|
color: var(--link);
|
|
font-size: 14px;
|
|
height: $option-height;
|
|
white-space: nowrap;
|
|
background-color: transparent;
|
|
width: 100%;
|
|
border-radius: 0;
|
|
border: none;
|
|
|
|
.cluster-badge-logo-text {
|
|
color: var(--default-active-text);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.pin {
|
|
font-size: 16px;
|
|
margin-left: auto;
|
|
display: none;
|
|
color: var(--body-text);
|
|
&.showPin {
|
|
display: block;
|
|
}
|
|
}
|
|
|
|
.cluster-name {
|
|
line-height: normal;
|
|
|
|
& > p {
|
|
width: 182px;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
text-align: left;
|
|
|
|
&.description {
|
|
font-size: 12px;
|
|
padding-right: 8px;
|
|
color: var(--darker);
|
|
}
|
|
}
|
|
}
|
|
|
|
&:hover {
|
|
text-decoration: none;
|
|
|
|
.pin {
|
|
display: block;
|
|
color: var(--darker-text);
|
|
}
|
|
}
|
|
&.disabled {
|
|
background: transparent;
|
|
cursor: not-allowed;
|
|
|
|
.rancher-provider-icon,
|
|
.cluster-name p {
|
|
filter: grayscale(1);
|
|
color: var(--muted) !important;
|
|
}
|
|
|
|
.pin {
|
|
cursor: pointer;
|
|
}
|
|
}
|
|
|
|
&:focus {
|
|
outline: 0;
|
|
box-shadow: none;
|
|
|
|
> div {
|
|
text-decoration: underline;
|
|
}
|
|
}
|
|
|
|
> i, > img {
|
|
display: block;
|
|
font-size: $icon-size;
|
|
margin-right: 14px;
|
|
&.group-icon {
|
|
width: 42px;
|
|
}
|
|
}
|
|
|
|
.rancher-provider-icon,
|
|
svg {
|
|
margin-right: 16px;
|
|
fill: var(--link);
|
|
}
|
|
|
|
&.router-link-active, &.active-menu-link {
|
|
background: var(--primary-hover-bg);
|
|
color: var(--primary-hover-text);
|
|
|
|
svg {
|
|
fill: var(--primary-hover-text);
|
|
}
|
|
|
|
i {
|
|
color: var(--primary-hover-text);
|
|
}
|
|
|
|
div .description {
|
|
color: var(--default);
|
|
}
|
|
}
|
|
|
|
&:hover {
|
|
color: var(--primary-hover-text);
|
|
background: var(--primary-hover-bg);
|
|
> div {
|
|
color: var(--primary-hover-text);
|
|
|
|
.description {
|
|
color: var(--default);
|
|
}
|
|
}
|
|
svg {
|
|
fill: var(--primary-hover-text);
|
|
}
|
|
div {
|
|
color: var(--primary-hover-text);
|
|
}
|
|
&.disabled {
|
|
background: transparent;
|
|
color: var(--muted);
|
|
|
|
> .pin {
|
|
color:var(--default-text);
|
|
display: block;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.option, .option-disabled {
|
|
padding: $option-padding 0 $option-padding $option-padding-left;
|
|
}
|
|
|
|
.search {
|
|
position: relative;
|
|
> input {
|
|
background-color: transparent;
|
|
padding-right: 35px;
|
|
padding-left: 25px;
|
|
height: 32px;
|
|
}
|
|
> .magnifier {
|
|
position: absolute;
|
|
top: 12px;
|
|
left: 8px;
|
|
width: 12px;
|
|
height: 12px;
|
|
font-size: 12px;
|
|
opacity: 0.4;
|
|
|
|
&.active {
|
|
opacity: 1;
|
|
|
|
&:hover {
|
|
color: var(--body-text);
|
|
}
|
|
}
|
|
}
|
|
> i {
|
|
position: absolute;
|
|
font-size: 12px;
|
|
top: 12px;
|
|
right: 8px;
|
|
opacity: 0.7;
|
|
cursor: pointer;
|
|
&:hover {
|
|
color: var(--disabled-bg);
|
|
}
|
|
}
|
|
}
|
|
|
|
.clusters-all {
|
|
display: flex;
|
|
flex-direction: row-reverse;
|
|
margin-right: 16px;
|
|
margin-top: 10px;
|
|
|
|
span {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
}
|
|
|
|
.clusters {
|
|
overflow-y: auto;
|
|
|
|
a, span {
|
|
margin: 0;
|
|
}
|
|
|
|
&-search {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
margin: 16px 0;
|
|
height: 42px;
|
|
|
|
.search {
|
|
transition: all 0.25s ease-in-out;
|
|
transition-delay: 2s;
|
|
width: 72%;
|
|
height: 36px;
|
|
|
|
input {
|
|
height: 100%;
|
|
}
|
|
}
|
|
|
|
&-count {
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: 42px;
|
|
height: 42px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: var(--default-active-text);
|
|
margin-left: $option-padding-left;
|
|
border-radius: 5px;
|
|
font-size: 10px;
|
|
font-weight: bold;
|
|
|
|
span {
|
|
font-size: 14px;
|
|
}
|
|
|
|
.router-link-active {
|
|
&:hover {
|
|
text-decoration: none;
|
|
}
|
|
}
|
|
|
|
i {
|
|
font-size: 12px;
|
|
position: absolute;
|
|
right: -3px;
|
|
top: 2px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.none-matching {
|
|
width: 100%;
|
|
text-align: center;
|
|
padding: 8px
|
|
}
|
|
|
|
.clustersPinned {
|
|
.category {
|
|
&-title {
|
|
margin: 8px 0;
|
|
margin-left: 16px;
|
|
hr {
|
|
margin: 0;
|
|
width: 94%;
|
|
transition: all 0.25s ease-in-out;
|
|
max-width: 100%;
|
|
}
|
|
}
|
|
}
|
|
.pin {
|
|
display: block;
|
|
}
|
|
}
|
|
|
|
.category {
|
|
display: flex;
|
|
flex-direction: column;
|
|
place-content: flex-end;
|
|
flex: 1;
|
|
|
|
&-title {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: flex-start;
|
|
align-items: center;
|
|
margin: 15px 0;
|
|
margin-left: 16px;
|
|
font-size: 14px;
|
|
text-transform: uppercase;
|
|
|
|
span {
|
|
transition: all 0.25s ease-in-out;
|
|
display: flex;
|
|
max-height: 16px;
|
|
}
|
|
|
|
hr {
|
|
margin: 0;
|
|
max-width: 50px;
|
|
width: 0;
|
|
transition: all 0.25s ease-in-out;
|
|
}
|
|
}
|
|
|
|
i {
|
|
padding-left: $option-padding-left - 5;
|
|
}
|
|
}
|
|
}
|
|
|
|
&.menu-close {
|
|
.side-menu-logo {
|
|
opacity: 0;
|
|
}
|
|
.category {
|
|
&-title {
|
|
span {
|
|
opacity: 0;
|
|
}
|
|
|
|
hr {
|
|
width: 40px;
|
|
}
|
|
}
|
|
}
|
|
.clusters-all {
|
|
flex-direction: row;
|
|
margin-left: $option-padding-left + 2;
|
|
|
|
span {
|
|
i {
|
|
display: none;
|
|
}
|
|
}
|
|
}
|
|
|
|
.clustersPinned {
|
|
.category {
|
|
&-title {
|
|
hr {
|
|
width: 40px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.footer {
|
|
margin: 20px 10px;
|
|
width: 50px;
|
|
|
|
.support {
|
|
display: none;
|
|
}
|
|
|
|
.version{
|
|
text-align: center;
|
|
|
|
&.version-small {
|
|
font-size: 12px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.footer {
|
|
margin: 20px;
|
|
width: 240px;
|
|
display: flex;
|
|
flex: 0;
|
|
flex-direction: row;
|
|
> * {
|
|
flex: 1;
|
|
color: var(--link);
|
|
|
|
&:first-child {
|
|
text-align: left;
|
|
}
|
|
&:last-child {
|
|
text-align: right;
|
|
}
|
|
text-align: center;
|
|
}
|
|
|
|
.version {
|
|
cursor: pointer;
|
|
}
|
|
}
|
|
}
|
|
|
|
.side-menu-glass {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0px;
|
|
bottom: 0;
|
|
width: 100vw;
|
|
z-index: 99;
|
|
opacity: 1;
|
|
}
|
|
|
|
.side-menu-logo {
|
|
align-items: center;
|
|
display: flex;
|
|
transform: translateX($app-bar-collapsed-width);
|
|
opacity: 1;
|
|
max-width: 200px;
|
|
width: 100%;
|
|
justify-content: center;
|
|
transition: all 0.5s;
|
|
overflow: hidden;
|
|
& IMG {
|
|
object-fit: contain;
|
|
height: 21px;
|
|
max-width: 200px;
|
|
}
|
|
}
|
|
|
|
.fade-enter-active, .fade-leave-active {
|
|
transition: all 0.25s;
|
|
transition-timing-function: ease;
|
|
}
|
|
|
|
.fade-leave-active {
|
|
transition: all 0.25s;
|
|
}
|
|
|
|
.fade-leave-to {
|
|
left: -300px;
|
|
}
|
|
|
|
.fade-enter {
|
|
left: -300px;
|
|
}
|
|
|
|
.locale-chooser {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.localeSelector {
|
|
:deep() .v-popper__inner {
|
|
padding: 50px 0;
|
|
}
|
|
|
|
:deep() .v-popper__arrow-container {
|
|
display: none;
|
|
}
|
|
|
|
:deep() .v-popper:focus {
|
|
outline: 0;
|
|
}
|
|
|
|
li {
|
|
padding: 8px 20px;
|
|
|
|
&:hover {
|
|
background-color: var(--primary-hover-bg);
|
|
color: var(--primary-hover-text);
|
|
text-decoration: none;
|
|
}
|
|
}
|
|
}
|
|
</style>
|