mirror of https://github.com/rancher/dashboard.git
455 lines
11 KiB
Vue
455 lines
11 KiB
Vue
<script>
|
|
import { mapGetters } from 'vuex';
|
|
import { NAMESPACE_FILTERS } from '@/store/prefs';
|
|
import { NAMESPACE, MANAGEMENT } from '@/config/types';
|
|
import { sortBy } from '@/utils/sort';
|
|
import { isArray, addObjects, findBy, filterBy } from '@/utils/array';
|
|
import Select from '@/components/form/Select';
|
|
import { NAME as HARVESTER } from '@/config/product/harvester';
|
|
|
|
export default {
|
|
components: { Select },
|
|
|
|
data() {
|
|
return {
|
|
isHovered: false,
|
|
hoveredTimeout: null,
|
|
maskedWidth: null,
|
|
};
|
|
},
|
|
|
|
computed: {
|
|
...mapGetters(['isVirtualCluster', 'isSingleVirtualCluster', 'isMultiVirtualCluster']),
|
|
filterIsHovered() {
|
|
return this.isHovered;
|
|
},
|
|
|
|
maskedDropdownWidth() {
|
|
const refs = this.$refs;
|
|
const select = refs.select;
|
|
|
|
if (select) {
|
|
const selectWidth = select.$el.offsetWidth;
|
|
|
|
return selectWidth;
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
options() {
|
|
const t = this.$store.getters['i18n/t'];
|
|
let out = [];
|
|
|
|
if (!this.isVirtualCluster) {
|
|
out = [
|
|
{
|
|
id: 'all',
|
|
kind: 'special',
|
|
label: t('nav.ns.all'),
|
|
},
|
|
{
|
|
id: 'all://user',
|
|
kind: 'special',
|
|
label: t('nav.ns.user'),
|
|
},
|
|
{
|
|
id: 'all://system',
|
|
kind: 'special',
|
|
label: t('nav.ns.system'),
|
|
},
|
|
{
|
|
id: 'namespaced://true',
|
|
kind: 'special',
|
|
label: t('nav.ns.namespaced'),
|
|
},
|
|
{
|
|
id: 'namespaced://false',
|
|
kind: 'special',
|
|
label: t('nav.ns.clusterLevel'),
|
|
},
|
|
];
|
|
|
|
divider();
|
|
}
|
|
|
|
const inStore = this.$store.getters['currentStore'](NAMESPACE);
|
|
const namespaces = sortBy(
|
|
this.$store.getters[`${ inStore }/all`](NAMESPACE),
|
|
['nameDisplay']
|
|
).filter( (N) => {
|
|
const needFilter = !N.isSystem && !N.isFleetManaged;
|
|
const isVirtualProduct = this.$store.getters['currentProduct'].name === HARVESTER;
|
|
|
|
return this.isVirtualCluster && isVirtualProduct ? needFilter : true;
|
|
});
|
|
|
|
if (this.$store.getters['isRancher'] || this.isMultiVirtualCluster) {
|
|
const cluster = this.$store.getters['currentCluster'];
|
|
let projects = this.$store.getters['management/all'](
|
|
MANAGEMENT.PROJECT
|
|
);
|
|
|
|
projects = sortBy(filterBy(projects, 'spec.clusterName', cluster.id), [
|
|
'nameDisplay',
|
|
]);
|
|
const projectsById = {};
|
|
const namespacesByProject = {};
|
|
let firstProject = true;
|
|
|
|
namespacesByProject[null] = []; // For namespaces not in a project
|
|
for (const project of projects) {
|
|
projectsById[project.metadata.name] = project;
|
|
}
|
|
|
|
for (const namespace of namespaces) {
|
|
let projectId = namespace.projectId;
|
|
|
|
if (!projectId || !projectsById[projectId]) {
|
|
// If there's a projectId but that project doesn't exist, treat it like no project
|
|
projectId = null;
|
|
}
|
|
|
|
let entry = namespacesByProject[projectId];
|
|
|
|
if (!entry) {
|
|
entry = [];
|
|
namespacesByProject[namespace.projectId] = entry;
|
|
}
|
|
entry.push(namespace);
|
|
}
|
|
|
|
for (const project of projects) {
|
|
const id = project.metadata.name;
|
|
|
|
if (firstProject) {
|
|
firstProject = false;
|
|
} else {
|
|
divider();
|
|
}
|
|
|
|
out.push({
|
|
id: `project://${ id }`,
|
|
kind: 'project',
|
|
label: t('nav.ns.project', { name: project.nameDisplay }),
|
|
});
|
|
|
|
const forThisProject = namespacesByProject[id] || [];
|
|
|
|
addNamespace(forThisProject);
|
|
}
|
|
|
|
const orphans = namespacesByProject[null];
|
|
|
|
if (orphans.length) {
|
|
if (!firstProject) {
|
|
divider();
|
|
}
|
|
|
|
out.push({
|
|
id: 'all://orphans',
|
|
kind: 'project',
|
|
label: t('nav.ns.orphan'),
|
|
disabled: true,
|
|
});
|
|
|
|
addNamespace(orphans);
|
|
}
|
|
} else {
|
|
addNamespace(namespaces);
|
|
}
|
|
|
|
return out;
|
|
|
|
function addNamespace(namespaces) {
|
|
if (!isArray(namespaces)) {
|
|
namespaces = [namespaces];
|
|
}
|
|
|
|
addObjects(
|
|
out,
|
|
namespaces.map((namespace) => {
|
|
return {
|
|
id: `ns://${ namespace.id }`,
|
|
kind: 'namespace',
|
|
label: t('nav.ns.namespace', { name: namespace.nameDisplay }),
|
|
};
|
|
})
|
|
);
|
|
}
|
|
|
|
function divider() {
|
|
out.push({
|
|
kind: 'divider',
|
|
label: `Divider ${ out.length }`,
|
|
disabled: true,
|
|
});
|
|
}
|
|
},
|
|
|
|
value: {
|
|
get() {
|
|
const prefs = this.$store.getters['prefs/get'](NAMESPACE_FILTERS);
|
|
const clusterId = this.$store.getters['clusterId'];
|
|
const values = prefs[clusterId] || ['all://user'];
|
|
const options = this.options;
|
|
|
|
// Remove values that are not valid options
|
|
const out = values
|
|
.map((value) => {
|
|
return findBy(options, 'id', value);
|
|
})
|
|
.filter(x => !!x);
|
|
|
|
return out;
|
|
},
|
|
|
|
set(neu) {
|
|
const old = (this.value || []).slice();
|
|
|
|
neu = neu.filter(x => !!x.id);
|
|
|
|
const last = neu[neu.length - 1];
|
|
const lastIsSpecial = last?.kind === 'special';
|
|
const hadUser = !!old.find(x => x.id === 'all://user');
|
|
const hadAll = !!old.find(x => x.id === 'all');
|
|
|
|
if (lastIsSpecial) {
|
|
neu = [last];
|
|
}
|
|
|
|
if (neu.length > 1) {
|
|
neu = neu.filter(x => x.kind !== 'special');
|
|
}
|
|
|
|
if (neu.find(x => x.id === 'all')) {
|
|
neu = [];
|
|
}
|
|
|
|
let ids;
|
|
|
|
// If there as something selected and you remove it, go back to user by default
|
|
// Unless it was user or all
|
|
if (neu.length === 0 && !hadUser && !hadAll) {
|
|
ids = ['all://user'];
|
|
} else {
|
|
ids = neu.map(x => x.id);
|
|
}
|
|
|
|
this.$nextTick(() => {
|
|
this.$store.dispatch('switchNamespaces', ids);
|
|
});
|
|
},
|
|
},
|
|
|
|
showGroupedOptions() {
|
|
if (this.value && this.value.length >= 2) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
},
|
|
|
|
mounted() {
|
|
this.$nextTick(() => {
|
|
this.maskedWidth = this.maskedDropdownWidth;
|
|
});
|
|
},
|
|
|
|
methods: {
|
|
focus() {
|
|
const el = this.$refs.select.$refs['select-input'].searchEl;
|
|
|
|
if ( el ) {
|
|
el.focus();
|
|
}
|
|
},
|
|
focusHandler(event) {
|
|
if (event === 'selectBlurred') {
|
|
this.maskedWidth = this.maskedDropdownWidth;
|
|
this.isHovered = false;
|
|
|
|
return;
|
|
}
|
|
|
|
// we dont handle blur here because the select specifically handles its blur events and emits when it does.
|
|
// we should listen to this blur event because the swapping of the masked select with real select.
|
|
if (event.type === 'focus') {
|
|
this.isHovered = true;
|
|
this.$nextTick(() => {
|
|
this.focus();
|
|
});
|
|
}
|
|
},
|
|
|
|
isHoveredHandler(event) {
|
|
clearTimeout(this.hoveredTimeout);
|
|
|
|
if (event.type === 'mouseenter') {
|
|
this.isHovered = true;
|
|
} else if (event.type === 'mouseleave') {
|
|
this.hoveredTimeout = setTimeout(() => {
|
|
this.maskedWidth = this.maskedDropdownWidth;
|
|
this.isHovered = false;
|
|
}, 200);
|
|
}
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
class="filter"
|
|
:class="{'show-masked': showGroupedOptions && !filterIsHovered}"
|
|
@mouseenter="isHoveredHandler"
|
|
@mouseleave="isHoveredHandler"
|
|
>
|
|
<div tabindex="0" class="unlabeled-select masked-dropdown" @focus="focusHandler">
|
|
<div class="v-select inline vs--searchable">
|
|
<div class="vs__dropdown-toggle">
|
|
<div class="vs__selected-options">
|
|
<div class="vs__selected">
|
|
{{ t('namespaceFilter.selected.label', { total: value.length }) }}
|
|
</div>
|
|
</div>
|
|
<div class="vs__actions"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<Select
|
|
ref="select"
|
|
v-model="value"
|
|
:append-to-body="false"
|
|
:class="{
|
|
'has-more': showGroupedOptions,
|
|
}"
|
|
multiple
|
|
:placeholder="t('nav.ns.all')"
|
|
:selectable="(option) => !option.disabled && option.id"
|
|
:options="options"
|
|
:close-on-select="false"
|
|
@on-blur="focusHandler('selectBlurred')"
|
|
>
|
|
<template v-slot:option="opt">
|
|
<template v-if="opt.kind === 'namespace'">
|
|
<i class="mr-5 icon icon-fw icon-folder" /> {{ opt.label }}
|
|
</template>
|
|
<template v-else-if="opt.kind === 'project'">
|
|
<b>{{ opt.label }}</b>
|
|
</template>
|
|
<template v-else-if="opt.kind === 'divider'">
|
|
<hr />
|
|
</template>
|
|
<template v-else>
|
|
{{ opt.label }}
|
|
</template>
|
|
</template>
|
|
</Select>
|
|
<button v-shortkey.once="['n']" class="hide" @shortkey="focus()" />
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
.filter {
|
|
min-width: 220px;
|
|
max-width: 100%;
|
|
display: inline-block;
|
|
}
|
|
|
|
.filter {
|
|
::v-deep .vs__dropdown-menu {
|
|
li.vs__dropdown-option {
|
|
padding: 4px 5px;
|
|
}
|
|
}
|
|
}
|
|
|
|
.filter.show-masked ::v-deep .unlabeled-select:not(.masked-dropdown) {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
height: 0;
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
}
|
|
|
|
.filter:not(.show-masked) ::v-deep .unlabeled-select.masked-dropdown {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
height: 0;
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
}
|
|
|
|
.filter ::v-deep .unlabeled-select.has-more .v-select:not(.vs--open) .vs__dropdown-toggle {
|
|
overflow: hidden;
|
|
}
|
|
|
|
.filter ::v-deep .unlabeled-select.has-more .v-select.vs--open .vs__dropdown-toggle {
|
|
height: max-content;
|
|
background-color: var(--header-bg);
|
|
}
|
|
|
|
.filter ::v-deep .unlabeled-select {
|
|
background-color: transparent;
|
|
border: 0;
|
|
}
|
|
|
|
.filter ::v-deep .unlabeled-select:not(.focused) {
|
|
min-height: 0;
|
|
}
|
|
|
|
.filter ::v-deep .unlabeled-select:not(.view):hover .vs__dropdown-menu {
|
|
background: var(--dropdown-bg);
|
|
}
|
|
|
|
.filter ::v-deep .unlabeled-select .v-select.inline {
|
|
margin: 0;
|
|
}
|
|
|
|
.filter ::v-deep .unlabeled-select .v-select .vs__selected {
|
|
margin: 4px;
|
|
user-select: none;
|
|
cursor: default;
|
|
background: rgba(0, 0, 0, 0.05);
|
|
border: 1px solid var(--header-border);
|
|
color: var(--header-btn-text);
|
|
height: calc(var(--header-height) - 26px);
|
|
width: initial;
|
|
}
|
|
|
|
.filter ::v-deep .unlabeled-select .vs__search::placeholder {
|
|
color: var(--header-btn-text);
|
|
}
|
|
|
|
.filter ::v-deep .unlabeled-select INPUT:hover {
|
|
background-color: transparent;
|
|
}
|
|
|
|
.filter ::v-deep .unlabeled-select .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);
|
|
height: 40px;
|
|
max-width: 100%;
|
|
padding-top: 0;
|
|
}
|
|
|
|
.filter ::v-deep .unlabeled-select .vs__deselect:after {
|
|
color: var(--header-btn-text);
|
|
}
|
|
|
|
.filter ::v-deep .unlabeled-select .v-select .vs__actions:after {
|
|
fill: var(--header-btn-text) !important;
|
|
color: var(--header-btn-text) !important;
|
|
}
|
|
|
|
.filter ::v-deep .unlabeled-select INPUT[type='search'] {
|
|
padding: 7px;
|
|
}
|
|
</style>
|