mirror of https://github.com/rancher/dashboard.git
782 lines
18 KiB
Vue
782 lines
18 KiB
Vue
<script>
|
|
import BrandImage from '@shell/components/BrandImage';
|
|
import ClusterProviderIcon from '@shell/components/ClusterProviderIcon';
|
|
import IconOrSvg from '../IconOrSvg';
|
|
import { mapGetters } from 'vuex';
|
|
import $ from 'jquery';
|
|
import { CAPI, MANAGEMENT } from '@shell/config/types';
|
|
import { mapPref, MENU_MAX_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 { LEGACY } from '@shell/store/features';
|
|
import { SETTING } from '@shell/config/settings';
|
|
import { filterOnlyKubernetesClusters, filterHiddenLocalCluster } from '@shell/utils/cluster';
|
|
import { isRancherPrime } from '@shell/config/version';
|
|
|
|
export default {
|
|
|
|
components: {
|
|
BrandImage,
|
|
ClusterProviderIcon,
|
|
IconOrSvg
|
|
},
|
|
|
|
data() {
|
|
const { displayVersion, fullVersion } = getVersionInfo(this.$store);
|
|
const hasProvCluster = this.$store.getters[`management/schemaFor`](CAPI.RANCHER_CLUSTER);
|
|
|
|
return {
|
|
shown: false,
|
|
displayVersion,
|
|
fullVersion,
|
|
clusterFilter: '',
|
|
hasProvCluster,
|
|
};
|
|
},
|
|
|
|
fetch() {
|
|
if (this.hasProvCluster) {
|
|
this.$store.dispatch('management/findAll', { type: CAPI.RANCHER_CLUSTER });
|
|
}
|
|
},
|
|
|
|
computed: {
|
|
...mapGetters(['clusterId']),
|
|
...mapGetters(['clusterReady', 'isRancher', 'currentCluster', 'currentProduct']),
|
|
...mapGetters('type-map', ['activeProducts']),
|
|
...mapGetters({ features: 'features/get' }),
|
|
|
|
value: {
|
|
get() {
|
|
return this.$store.getters['productId'];
|
|
},
|
|
},
|
|
|
|
legacyEnabled() {
|
|
return this.features(LEGACY);
|
|
},
|
|
|
|
showClusterSearch() {
|
|
return this.clusters.length > this.maxClustersToShow;
|
|
},
|
|
|
|
clusters() {
|
|
const all = this.$store.getters['management/all'](MANAGEMENT.CLUSTER);
|
|
let kubeClusters = filterHiddenLocalCluster(filterOnlyKubernetesClusters(all), this.$store);
|
|
let pClusters = null;
|
|
|
|
if (this.hasProvCluster) {
|
|
pClusters = this.$store.getters['management/all'](CAPI.RANCHER_CLUSTER);
|
|
const available = pClusters.reduce((p, c) => {
|
|
p[c.mgmt] = true;
|
|
|
|
return p;
|
|
}, {});
|
|
|
|
// Filter to only show mgmt clusters that exist for the available provisionning clusters
|
|
// Addresses issue where a mgmt cluster can take some time to get cleaned up after the corresponding
|
|
// provisionning cluster has been deleted
|
|
kubeClusters = kubeClusters.filter(c => !!available[c]);
|
|
}
|
|
|
|
return kubeClusters.map((x) => {
|
|
const pCluster = pClusters?.find(c => c.mgmt.id === x.id);
|
|
|
|
return {
|
|
id: x.id,
|
|
label: x.nameDisplay,
|
|
ready: x.isReady && !pCluster?.hasError,
|
|
osLogo: x.providerOsLogo,
|
|
providerNavLogo: x.providerMenuLogo,
|
|
badge: x.badge,
|
|
isLocal: x.isLocal
|
|
};
|
|
});
|
|
},
|
|
|
|
clustersFiltered() {
|
|
const search = (this.clusterFilter || '').toLowerCase();
|
|
|
|
const out = search ? this.clusters.filter(item => item.label.toLowerCase().includes(search)) : this.clusters;
|
|
|
|
const sorted = sortBy(out, ['name:desc', 'label']);
|
|
|
|
return sorted;
|
|
},
|
|
|
|
maxClustersToShow: mapPref(MENU_MAX_CLUSTERS),
|
|
|
|
multiClusterApps() {
|
|
const options = this.options;
|
|
|
|
return options.filter(opt => (opt.inStore === 'management' || opt.isMultiClusterApp) && opt.category !== 'configuration' && opt.category !== 'legacy');
|
|
},
|
|
|
|
legacyApps() {
|
|
const options = this.options;
|
|
|
|
return options.filter(opt => opt.inStore === 'management' && opt.category === 'legacy');
|
|
},
|
|
|
|
configurationApps() {
|
|
const options = this.options;
|
|
|
|
return options.filter(opt => opt.category === 'configuration');
|
|
},
|
|
|
|
options() {
|
|
const cluster = this.clusterId || this.$store.getters['defaultClusterId'];
|
|
// TODO plugin routes
|
|
const entries = this.activeProducts.map((p) => {
|
|
// Try product-specific index first
|
|
const to = p.to || {
|
|
name: `c-cluster-${ p.name }`,
|
|
params: { cluster }
|
|
};
|
|
|
|
if ( !this.$router.getMatchedComponents(to).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';
|
|
},
|
|
},
|
|
|
|
watch: {
|
|
$route() {
|
|
this.shown = false;
|
|
}
|
|
},
|
|
|
|
mounted() {
|
|
document.addEventListener('keyup', this.handler);
|
|
},
|
|
|
|
beforeDestroy() {
|
|
document.removeEventListener('keyup', this.handler);
|
|
},
|
|
|
|
methods: {
|
|
// Cluster list number of items shown is configurable via user preference
|
|
setClusterListHeight(maxToShow) {
|
|
const el = this.$refs.clusterList;
|
|
const max = Math.min(maxToShow, this.clusters.length);
|
|
|
|
if (el) {
|
|
const $el = $(el);
|
|
const h = 33 * max;
|
|
|
|
$el.css('min-height', `${ h }px`);
|
|
$el.css('max-height', `${ h }px`);
|
|
}
|
|
},
|
|
handler(e) {
|
|
if (e.keyCode === KEY.ESCAPE ) {
|
|
this.hide();
|
|
}
|
|
},
|
|
|
|
hide() {
|
|
this.shown = false;
|
|
},
|
|
|
|
toggle() {
|
|
this.shown = !this.shown;
|
|
this.$nextTick(() => {
|
|
this.setClusterListHeight(this.maxClustersToShow);
|
|
});
|
|
},
|
|
}
|
|
};
|
|
</script>
|
|
<template>
|
|
<div>
|
|
<div
|
|
data-testid="top-level-menu"
|
|
class="menu"
|
|
:class="{'raised': shown, 'unraised':!shown}"
|
|
@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
|
|
v-if="shown"
|
|
class="side-menu-glass"
|
|
@click="hide()"
|
|
/>
|
|
<transition name="fade">
|
|
<div
|
|
v-if="shown"
|
|
data-testid="side-menu"
|
|
class="side-menu"
|
|
tabindex="-1"
|
|
>
|
|
<div class="title">
|
|
<div class="menu-spacer" />
|
|
<div class="side-menu-logo">
|
|
<BrandImage file-name="rancher-logo.svg" />
|
|
</div>
|
|
</div>
|
|
<div class="body">
|
|
<div @click="hide()">
|
|
<nuxt-link
|
|
class="option cluster selector home"
|
|
:to="{ name: 'home' }"
|
|
>
|
|
<svg
|
|
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>
|
|
{{ t('nav.home') }}
|
|
</div>
|
|
</nuxt-link>
|
|
</div>
|
|
<template v-if="clusters && !!clusters.length">
|
|
<div class="category">
|
|
{{ t('nav.categories.explore') }}
|
|
</div>
|
|
<div
|
|
v-if="showClusterSearch"
|
|
class="search"
|
|
>
|
|
<input
|
|
ref="clusterFilter"
|
|
v-model="clusterFilter"
|
|
:placeholder="t('nav.search.placeholder')"
|
|
>
|
|
<i
|
|
v-if="clusterFilter"
|
|
class="icon icon-close"
|
|
@click="clusterFilter=''"
|
|
/>
|
|
</div>
|
|
<div
|
|
ref="clusterList"
|
|
class="clusters"
|
|
>
|
|
<div
|
|
v-for="c in clustersFiltered"
|
|
:key="c.id"
|
|
@click="hide()"
|
|
>
|
|
<nuxt-link
|
|
v-if="c.ready"
|
|
class="cluster selector option"
|
|
:to="{ name: 'c-cluster', params: { cluster: c.id } }"
|
|
>
|
|
<ClusterProviderIcon
|
|
:small="true"
|
|
:cluster="c"
|
|
class="rancher-provider-icon mr-10"
|
|
/>
|
|
<div class="cluster-name">
|
|
{{ c.label }}
|
|
</div>
|
|
</nuxt-link>
|
|
<span
|
|
v-else
|
|
class="option-disabled cluster selector disabled"
|
|
>
|
|
<ClusterProviderIcon
|
|
:small="true"
|
|
:cluster="c"
|
|
class="rancher-provider-icon mr-10"
|
|
/>
|
|
<div class="cluster-name">{{ c.label }}</div>
|
|
</span>
|
|
</div>
|
|
<div
|
|
v-if="clustersFiltered.length === 0"
|
|
class="none-matching"
|
|
>
|
|
{{ t('nav.search.noResults') }}
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template v-if="multiClusterApps.length">
|
|
<div class="category">
|
|
{{ t('nav.categories.multiCluster') }}
|
|
</div>
|
|
<div
|
|
v-for="a in multiClusterApps"
|
|
:key="a.label"
|
|
@click="hide()"
|
|
>
|
|
<nuxt-link
|
|
class="option"
|
|
:to="a.to"
|
|
>
|
|
<IconOrSvg
|
|
:icon="a.icon"
|
|
:src="a.svg"
|
|
/>
|
|
<div>{{ a.label }}</div>
|
|
</nuxt-link>
|
|
</div>
|
|
</template>
|
|
<template v-if="legacyEnabled">
|
|
<div class="category">
|
|
{{ t('nav.categories.legacy') }}
|
|
</div>
|
|
<div
|
|
v-for="a in legacyApps"
|
|
:key="a.label"
|
|
@click="hide()"
|
|
>
|
|
<nuxt-link
|
|
class="option"
|
|
:to="a.to"
|
|
>
|
|
<IconOrSvg
|
|
:icon="a.icon"
|
|
:src="a.svg"
|
|
/>
|
|
<div>{{ a.label }}</div>
|
|
</nuxt-link>
|
|
</div>
|
|
</template>
|
|
<template v-if="configurationApps.length">
|
|
<div class="category">
|
|
{{ t('nav.categories.configuration') }}
|
|
</div>
|
|
<div
|
|
v-for="a in configurationApps"
|
|
:key="a.label"
|
|
@click="hide()"
|
|
>
|
|
<nuxt-link
|
|
class="option"
|
|
:to="a.to"
|
|
>
|
|
<IconOrSvg
|
|
:icon="a.icon"
|
|
:src="a.svg"
|
|
/>
|
|
<div>{{ a.label }}</div>
|
|
</nuxt-link>
|
|
</div>
|
|
</template>
|
|
<div class="pad" />
|
|
</div>
|
|
<div class="footer">
|
|
<div
|
|
v-if="canEditSettings"
|
|
@click="hide()"
|
|
>
|
|
<nuxt-link :to="{name: 'support'}">
|
|
{{ t('nav.support', {hasSupport}) }}
|
|
</nuxt-link>
|
|
</div>
|
|
<div @click="hide()">
|
|
<nuxt-link
|
|
:to="{ name: 'about' }"
|
|
class="version"
|
|
>
|
|
{{ t('about.title') }}
|
|
</nuxt-link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</transition>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.cluster.disabled > * {
|
|
cursor: not-allowed;
|
|
filter: grayscale(1);
|
|
color: var(--muted);
|
|
}
|
|
</style>
|
|
|
|
<style lang="scss">
|
|
.localeSelector, .footer-tooltip {
|
|
z-index: 1000;
|
|
}
|
|
|
|
.cluster {
|
|
&.selector:not(.disabled):hover {
|
|
color: var(--primary-hover-text);
|
|
background: var(--primary-hover-bg);
|
|
border-radius: 5px;
|
|
text-decoration: none;
|
|
|
|
.rancher-provider-icon {
|
|
.rancher-icon-fill {
|
|
fill: var(--primary-hover-text);;
|
|
}
|
|
}
|
|
}
|
|
|
|
.rancher-provider-icon {
|
|
.rancher-icon-fill {
|
|
// Should match .option color
|
|
fill: var(--link);
|
|
}
|
|
}
|
|
}
|
|
|
|
.localeSelector {
|
|
.popover-inner {
|
|
padding: 10px 0;
|
|
}
|
|
|
|
.popover-arrow {
|
|
display: none;
|
|
}
|
|
|
|
.popover:focus {
|
|
outline: 0;
|
|
}
|
|
}
|
|
|
|
</style>
|
|
|
|
<style lang="scss" scoped>
|
|
$clear-search-size: 20px;
|
|
$icon-size: 25px;
|
|
$option-padding: 4px;
|
|
$option-height: $icon-size + $option-padding + $option-padding;
|
|
|
|
.option {
|
|
align-items: center;
|
|
cursor: pointer;
|
|
display: flex;
|
|
color: var(--link);
|
|
|
|
&:hover {
|
|
text-decoration: none;
|
|
}
|
|
|
|
&:focus {
|
|
outline: 0;
|
|
> div {
|
|
text-decoration: underline;
|
|
}
|
|
}
|
|
|
|
> i {
|
|
width: $icon-size;
|
|
font-size: $icon-size;
|
|
margin-right: 8px;
|
|
}
|
|
svg {
|
|
margin-right: 8px;
|
|
fill: var(--link);
|
|
}
|
|
img {
|
|
margin-right: 8px;
|
|
}
|
|
|
|
> div {
|
|
color: var(--link);
|
|
}
|
|
|
|
&:hover {
|
|
color: var(--primary-hover-text);
|
|
background: var(--primary-hover-bg);
|
|
border-radius: 5px;
|
|
> div {
|
|
color: var(--primary-hover-text);
|
|
}
|
|
svg {
|
|
fill: var(--primary-hover-text);
|
|
}
|
|
div {
|
|
color: var(--primary-hover-text);
|
|
}
|
|
}
|
|
}
|
|
|
|
.option, .option-disabled {
|
|
padding: $option-padding 0 $option-padding 10px;
|
|
}
|
|
|
|
.menu {
|
|
position: absolute;
|
|
left: 0;
|
|
width: 55px;
|
|
height: 54px;
|
|
top: 0;
|
|
grid-area: menu;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
&:hover {
|
|
background-color: var(--topmost-light-hover);
|
|
}
|
|
.menu-icon {
|
|
width: 24px;
|
|
height: 24px;
|
|
fill: var(--header-btn-text);
|
|
}
|
|
&.raised {
|
|
z-index: 200;
|
|
}
|
|
}
|
|
|
|
.side-menu {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0px;
|
|
bottom: 0;
|
|
width: 280px;
|
|
background-color: var(--topmenu-bg);
|
|
z-index: 100;
|
|
border-right: 1px solid var(--topmost-border);
|
|
box-shadow: 0 0 15px 4px var(--topmost-shadow);
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: 0;
|
|
|
|
&:focus {
|
|
outline: 0;
|
|
}
|
|
|
|
.title {
|
|
display: flex;
|
|
height: 55px;
|
|
flex: 0 0 55px;
|
|
width: 100%;
|
|
border-bottom: 1px solid var(--nav-border);
|
|
justify-content: flex-start;
|
|
align-items: center;
|
|
.menu {
|
|
display: flex;
|
|
justify-content: center;
|
|
width: 55px;
|
|
margin-right: 10px;
|
|
}
|
|
.menu-icon {
|
|
width: 24px;
|
|
height: 24px;
|
|
}
|
|
.menu-spacer {
|
|
width: 55px;
|
|
}
|
|
}
|
|
.body {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
margin: 10px 20px;
|
|
overflow-y: auto;
|
|
|
|
.category {
|
|
padding: 10px 0;
|
|
text-transform: uppercase;
|
|
opacity: 0.8;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.home {
|
|
color: var(--link);
|
|
}
|
|
|
|
.home:focus {
|
|
outline: 0;
|
|
}
|
|
|
|
.cluster {
|
|
align-items: center;
|
|
display: flex;
|
|
height: $option-height;
|
|
|
|
white-space: nowrap;
|
|
&:focus {
|
|
outline: 0;
|
|
}
|
|
.cluster-name {
|
|
text-overflow: ellipsis;
|
|
overflow: hidden;
|
|
}
|
|
> img {
|
|
max-height: $icon-size;
|
|
max-width: $icon-size;
|
|
margin-right: 8px;
|
|
}
|
|
}
|
|
|
|
.pad {
|
|
flex: 1;
|
|
}
|
|
|
|
.search {
|
|
position: relative;
|
|
> input {
|
|
background-color: transparent;
|
|
margin-bottom: 8px;
|
|
padding-right: 34px;
|
|
}
|
|
> i {
|
|
position: absolute;
|
|
font-size: $clear-search-size;
|
|
top: 9px;
|
|
right: 8px;
|
|
opacity: 0.7;
|
|
cursor: pointer;
|
|
&:hover {
|
|
color: var(--disabled-bg);
|
|
}
|
|
}
|
|
}
|
|
|
|
.clusters {
|
|
overflow-y: auto;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
.none-matching {
|
|
padding: 8px
|
|
}
|
|
}
|
|
.footer {
|
|
margin: 20px;
|
|
|
|
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 {
|
|
background-color: transparent;
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0px;
|
|
bottom: 0;
|
|
width: 100vw;
|
|
z-index: 99;
|
|
opacity: 1;
|
|
}
|
|
|
|
.side-menu-logo {
|
|
align-items: center;
|
|
display: flex;
|
|
margin-left: 10px;
|
|
opacity: 1;
|
|
transition: opacity 1.2s;
|
|
transition-delay: 0s;
|
|
height: 55px;
|
|
max-width: 200px;
|
|
& IMG {
|
|
object-fit: contain;
|
|
height: 21px;
|
|
max-width: 200px;
|
|
}
|
|
}
|
|
|
|
.fade-enter-active {
|
|
.side-menu-logo {
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
.fade-enter-active, .fade-leave-active {
|
|
transition: all 0.2s;
|
|
transition-timing-function: ease;
|
|
}
|
|
|
|
.fade-leave-active {
|
|
transition: all 0.4s;
|
|
}
|
|
|
|
.fade-leave-to {
|
|
left: -300px;
|
|
}
|
|
|
|
.fade-enter {
|
|
left: -300px;
|
|
|
|
.side-menu-logo {
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
.locale-chooser {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.localeSelector {
|
|
::v-deep .popover-inner {
|
|
padding: 50px 0;
|
|
}
|
|
|
|
::v-deep .popover-arrow {
|
|
display: none;
|
|
}
|
|
|
|
::v-deep .popover:focus {
|
|
outline: 0;
|
|
}
|
|
|
|
li {
|
|
padding: 8px 20px;
|
|
|
|
&:hover {
|
|
background-color: var(--primary-hover-bg);
|
|
color: var(--primary-hover-text);
|
|
text-decoration: none;
|
|
}
|
|
}
|
|
}
|
|
</style>
|