dashboard/components/nav/ClusterSwitcher.vue

307 lines
7.5 KiB
Vue

<script>
import { createPopper } from '@popperjs/core';
import { MANAGEMENT } from '@/config/types';
import { sortBy } from '@/utils/sort';
import { findBy } from '@/utils/array';
import { mapGetters, mapState } from 'vuex';
import Select from '@/components/form/Select';
import $ from 'jquery';
export default {
components: { Select },
data() {
return { popperInstance: null };
},
computed: {
...mapState(['isMultiCluster']),
...mapGetters(['currentCluster']),
value: {
get() {
const options = this.options;
const existing = findBy(
options,
'id',
this.$store.getters['clusterId']
);
if (existing) {
return existing;
}
return options[0];
},
set(neu) {
this.$router.push({ name: 'c-cluster', params: { cluster: neu.id } });
},
},
options() {
const all = this.$store.getters['management/all'](MANAGEMENT.CLUSTER);
const out = all.map((x) => {
return {
id: x.id,
label: x.nameDisplay,
ready: x.isReady,
osLogo: x.providerOsLogo,
};
});
return sortBy(out, ['ready:desc', 'label']);
},
},
methods: {
handleDelete() {
// because v-select handles the backspace key on the keydown a bunch of weird stuff happens when using META+A -> Delete/Backspace
// but the crux of this issue is that by the time the dd recomputes a new size the dropdown width is the same because the search value is still the same
// by key up we have an empty value and can trigger the update manually
if (this.$refs?.search?.value === '' && this.popperInstance?.update) {
this.popperInstance.update();
}
return true;
},
filterBy(option, label, search) {
return (label || '').toLowerCase().includes(search.toLowerCase());
},
filter(options, search) {
const filtered = options.filter((option) => {
let label = this.$refs.select.getOptionLabel(option);
if (typeof label === 'number') {
label = label.toString();
}
return this.filterBy(option, label, search);
});
if (this.popperInstance?.update) {
this.popperInstance.update();
}
return filtered;
},
focus() {
this.$refs.select.focusSearch();
},
withPopper(dropdownList, component) {
const componentWidth = $(component.$parent.$el).width();
dropdownList.style['min-width'] = `${ componentWidth }px`;
dropdownList.style.width = 'min-content';
dropdownList.className += ' cluster-switcher-container';
const popper = createPopper(component.$refs.toggle, dropdownList, {
placement: 'bottom-end',
modifiers: [
{
name: 'offset',
options: { offset: [0, 2] },
},
{
name: 'toggleClass',
enabled: true,
phase: 'write',
fn({ state }) {
component.$el.setAttribute('x-placement', state.placement);
},
},
],
});
this.popperInstance = popper;
/**
* To prevent memory leaks Popper needs to be destroyed.
* If you return function, it will be called just before dropdown is removed from DOM.
*/
return () => {
this.popperInstance = null;
return popper.destroy();
};
},
},
};
</script>
<template>
<div class="filter">
<Select
ref="select"
key="cluster"
v-model="value"
:append-to-body="true"
:popper-override="withPopper"
placement="bottom-end"
:searchable="true"
:selectable="(option) => option.ready"
:clearable="false"
:options="options"
:filter="filter"
>
<template #search="{ attributes, events }">
<input
class="vs__search"
v-bind="attributes"
v-on="events"
@keyup.delete="handleDelete"
>
</template>
<template #selected-option="opt">
<span class="cluster-label-container">
<img
v-if="currentCluster"
class="cluster-switcher-os-logo"
:src="currentCluster.providerOsLogo"
/>
<span class="cluster-label">
{{ opt.label }}
</span>
</span>
</template>
<template #no-options="{ searching }">
<template v-if="searching">
<t k="clusterSwitcher.noResults" />
</template>
<em v-else class="text-muted"><t k="clusterSwitcher.search" /></em>
</template>
<template #option="opt">
<span class="dropdown-option">
<span class="logo">
<img
v-if="opt.osLogo"
class="cluster-switcher-os-logo"
:src="opt.osLogo"
/>
</span>
<span class="content">
<b v-if="opt === value">{{ opt.label }}</b>
<nuxt-link
v-else-if="opt.ready"
class="cluster"
:to="{ name: 'c-cluster', params: { cluster: opt.id } }"
>
{{ opt.label }}
</nuxt-link>
<span v-else>{{ opt.label }}</span>
</span>
</span>
</template>
</Select>
<button v-shortkey.once="['c']" class="hide" @shortkey="focus()" />
</div>
</template>
<style lang="scss">
// should be outside scoped css, class is dynamic and added by withPopper so scoped + ::v-deep doesn't work
.cluster-switcher-container {
&.vs__dropdown-menu {
.vs__dropdown-option {
// matches the padding of the option (and logo/content) to the selected option so it doesn't look off
padding: 3px 20px 3px 8px;
}
}
}
</style>
<style lang="scss" scoped>
.cluster-switcher-os-logo {
height: 16px;
display: inline-block;
vertical-align: middle;
}
.cluster-switcher-container {
.cluster-label-container {
width: 100%;
display: grid;
grid-template-columns: 15% 85%;
align-items: center;
align-content: center;
}
.dropdown-option {
display: grid;
width: 100%;
grid-template-columns: 25px fit-content(100%);
align-items: center;
align-content: center;
}
}
.filter ::v-deep .unlabeled-select .v-select {
background: rgba(0, 0, 0, 0.05);
border-radius: var(--border-radius);
color: var(--header-btn-text);
display: inline-block;
max-width: 100%;
.vs__selected {
width: 100%;
}
&.vs--disabled .vs__actions {
display: none;
}
.vs__actions:after {
fill: white !important;
color: white !important;
}
.vs__selected-options input {
color: var(--header-btn-text);
}
.vs__dropdown-toggle {
background: rgba(0, 0, 0, 0.05);
border-radius: var(--border-radius);
border: 1px solid var(--header-btn-bg);
color: var(--header-btn-text);
max-width: 100%;
padding-top: 0;
height: 40px;
.vs__actions {
margin-left: -10px;
}
}
.vs__selected {
border: none;
position: absolute;
user-select: none;
cursor: default;
color: white;
line-height: calc(var(--header-height) - 32px);
}
}
.filter ::v-deep .unlabeled-select:not(.view):hover .vs__dropdown-menu {
background: var(--dropdown-bg);
}
.filter ::v-deep .unlabeled-select {
background-color: transparent;
}
.filter ::v-deep .unlabeled-select:not(.focused) {
border: 0;
min-height: 0;
}
.filter ::v-deep .unlabeled-select INPUT[type='search'] {
width: auto;
}
.filter ::v-deep .unlabeled-select INPUT:hover {
background-color: transparent;
}
</style>