mirror of https://github.com/rancher/dashboard.git
532 lines
13 KiB
Vue
532 lines
13 KiB
Vue
<script>
|
|
import { Checkbox } from '@components/Form/Checkbox';
|
|
import { SOME, NONE } from './selection';
|
|
import { AUTO, CENTER, fitOnScreen } from '@shell/utils/position';
|
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
|
|
|
export default {
|
|
emits: ['update-cols-options', 'on-toggle-all', 'group-value-change', 'on-sort-change', 'col-visibility-change'],
|
|
|
|
components: { Checkbox, LabeledSelect },
|
|
props: {
|
|
columns: {
|
|
type: Array,
|
|
required: true
|
|
},
|
|
sortBy: {
|
|
type: String,
|
|
required: true
|
|
},
|
|
defaultSortBy: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
group: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
groupOptions: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
descending: {
|
|
type: Boolean,
|
|
required: true
|
|
},
|
|
hasAdvancedFiltering: {
|
|
type: Boolean,
|
|
required: false
|
|
},
|
|
tableColsOptions: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
tableActions: {
|
|
type: Boolean,
|
|
required: true,
|
|
},
|
|
rowActions: {
|
|
type: Boolean,
|
|
required: true,
|
|
},
|
|
howMuchSelected: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
checkWidth: {
|
|
type: Number,
|
|
default: 30,
|
|
},
|
|
rowActionsWidth: {
|
|
type: Number,
|
|
required: true
|
|
},
|
|
subExpandColumn: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
expandWidth: {
|
|
type: Number,
|
|
default: 30,
|
|
},
|
|
labelFor: {
|
|
type: Function,
|
|
required: true,
|
|
},
|
|
noRows: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
noResults: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
loading: {
|
|
type: Boolean,
|
|
required: false,
|
|
},
|
|
},
|
|
|
|
data() {
|
|
return {
|
|
tableColsOptionsVisibility: false,
|
|
tableColsMenuPosition: null
|
|
};
|
|
},
|
|
|
|
watch: {
|
|
advancedFilteringValues() {
|
|
// passing different dummy args to make sure update is triggered
|
|
this.watcherUpdateLiveAndDelayed(true, false);
|
|
},
|
|
tableColsOptionsVisibility(neu) {
|
|
if (neu) {
|
|
// check if user clicked outside the table cols options box
|
|
window.addEventListener('click', this.onClickOutside);
|
|
|
|
// update filtering options and toggable cols every time dropdown is open
|
|
this.$emit('update-cols-options');
|
|
} else {
|
|
// unregister click event
|
|
window.removeEventListener('click', this.onClickOutside);
|
|
}
|
|
}
|
|
},
|
|
computed: {
|
|
isAll: {
|
|
get() {
|
|
return this.howMuchSelected !== NONE;
|
|
},
|
|
|
|
set(value) {
|
|
this.$emit('on-toggle-all', value);
|
|
}
|
|
},
|
|
hasAdvGrouping() {
|
|
return this.group?.length && this.groupOptions?.length;
|
|
},
|
|
advGroup: {
|
|
get() {
|
|
return this.group || this.advGroup;
|
|
},
|
|
|
|
set(val) {
|
|
this.$emit('group-value-change', val);
|
|
}
|
|
},
|
|
|
|
isIndeterminate() {
|
|
return this.howMuchSelected === SOME;
|
|
},
|
|
hasColumnWithSubLabel() {
|
|
return this.columns.some((col) => col.subLabel);
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
changeSort(e, col) {
|
|
if ( !col.sort ) {
|
|
return;
|
|
}
|
|
|
|
let desc = false;
|
|
|
|
if ( this.sortBy === col.name ) {
|
|
desc = !this.descending;
|
|
}
|
|
|
|
this.$emit('on-sort-change', col.name, desc);
|
|
},
|
|
|
|
isCurrent(col) {
|
|
return col.name === this.sortBy;
|
|
},
|
|
|
|
ariaSort(col) {
|
|
if (this.isCurrent(col)) {
|
|
return this.descending ? this.t('generic.descending') : this.t('generic.ascending');
|
|
}
|
|
|
|
return this.t('generic.none');
|
|
},
|
|
|
|
tableColsOptionsClick(ev) {
|
|
// set menu position
|
|
const menu = document.querySelector('.table-options-container');
|
|
const elem = document.querySelector('.table-options-btn');
|
|
|
|
this.tableColsMenuPosition = fitOnScreen(menu, ev || elem, {
|
|
overlapX: true,
|
|
fudgeX: 326,
|
|
fudgeY: -22,
|
|
positionX: CENTER,
|
|
positionY: AUTO,
|
|
});
|
|
|
|
// toggle visibility
|
|
this.tableColsOptionsVisibility = !this.tableColsOptionsVisibility;
|
|
},
|
|
|
|
onClickOutside(event) {
|
|
const tableOpts = this.$refs['table-options'];
|
|
|
|
if (!tableOpts || tableOpts.contains(event.target)) {
|
|
return;
|
|
}
|
|
this.tableColsOptionsVisibility = false;
|
|
},
|
|
|
|
tableOptionsCheckbox(value, label) {
|
|
this.$emit('col-visibility-change', {
|
|
label,
|
|
value
|
|
});
|
|
},
|
|
|
|
tooltip(col) {
|
|
if (!col.tooltip) {
|
|
return null;
|
|
}
|
|
|
|
const exists = this.$store.getters['i18n/exists'];
|
|
|
|
return exists(col.tooltip) ? this.t(col.tooltip) : col.tooltip;
|
|
},
|
|
}
|
|
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<thead>
|
|
<tr :class="{'loading': loading, 'top-aligned': hasColumnWithSubLabel}">
|
|
<th
|
|
v-if="tableActions"
|
|
:width="checkWidth"
|
|
>
|
|
<Checkbox
|
|
v-model:value="isAll"
|
|
class="check"
|
|
data-testid="sortable-table_check_select_all"
|
|
:indeterminate="isIndeterminate"
|
|
:disabled="noRows || noResults"
|
|
:alternate-label="t('sortableTable.genericGroupCheckbox')"
|
|
/>
|
|
</th>
|
|
<th
|
|
v-if="subExpandColumn"
|
|
:width="expandWidth"
|
|
/>
|
|
<th
|
|
v-for="(col) in columns"
|
|
v-show="!hasAdvancedFiltering || (hasAdvancedFiltering && col.isColVisible)"
|
|
:key="col.name"
|
|
:align="col.align || 'left'"
|
|
:width="col.width"
|
|
:class="{ sortable: col.sort, [col.breakpoint]: !!col.breakpoint}"
|
|
:tabindex="col.sort ? 0 : -1"
|
|
class="sortable-table-head-element"
|
|
:aria-sort="ariaSort(col)"
|
|
@click.prevent="changeSort($event, col)"
|
|
@keyup.enter="changeSort($event, col)"
|
|
@keyup.space="changeSort($event, col)"
|
|
>
|
|
<div
|
|
class="table-header-container"
|
|
:class="{ 'not-filterable': hasAdvancedFiltering && !col.isFilter }"
|
|
>
|
|
<div
|
|
v-clean-tooltip="tooltip(col)"
|
|
class="content"
|
|
>
|
|
<span
|
|
v-clean-html="labelFor(col)"
|
|
class="text-no-break"
|
|
/>
|
|
<span
|
|
v-if="col.subLabel"
|
|
class="text-muted text-no-break"
|
|
>
|
|
{{ col.subLabel }}
|
|
</span>
|
|
</div>
|
|
<div
|
|
v-if="col.sort"
|
|
class="sort"
|
|
aria-hidden="true"
|
|
>
|
|
<i
|
|
v-show="hasAdvancedFiltering && !col.isFilter"
|
|
v-clean-tooltip="t('sortableTable.tableHeader.noFilter')"
|
|
class="icon icon-info not-filter-icon"
|
|
/>
|
|
<span class="icon-stack">
|
|
<i class="icon icon-sort icon-stack-1x faded" />
|
|
<i
|
|
v-if="isCurrent(col) && !descending"
|
|
class="icon icon-sort-down icon-stack-1x"
|
|
:alt="t('sortableTable.alt.sortingIconDesc')"
|
|
/>
|
|
<i
|
|
v-if="isCurrent(col) && descending"
|
|
class="icon icon-sort-up icon-stack-1x"
|
|
:alt="t('sortableTable.alt.sortingIconAsc')"
|
|
/>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</th>
|
|
<th
|
|
v-if="rowActions && hasAdvancedFiltering && tableColsOptions.length"
|
|
:width="rowActionsWidth"
|
|
>
|
|
<div
|
|
ref="table-options"
|
|
class="table-options-group"
|
|
>
|
|
<button
|
|
aria-haspopup="true"
|
|
aria-expanded="false"
|
|
type="button"
|
|
class="btn btn-sm role-multi-action table-options-btn"
|
|
@click="tableColsOptionsClick"
|
|
>
|
|
<i class="icon icon-actions" />
|
|
</button>
|
|
<div
|
|
v-show="tableColsOptionsVisibility"
|
|
class="table-options-container"
|
|
:style="tableColsMenuPosition"
|
|
>
|
|
<div
|
|
v-if="hasAdvGrouping"
|
|
class="table-options-grouping"
|
|
>
|
|
<span class="table-options-col-subtitle">{{ t('sortableTable.tableHeader.groupBy') }}:</span>
|
|
<LabeledSelect
|
|
v-model:value="advGroup"
|
|
class="table-options-grouping-select"
|
|
:clearable="true"
|
|
:options="groupOptions"
|
|
:disabled="false"
|
|
:searchable="false"
|
|
mode="edit"
|
|
:multiple="false"
|
|
:taggable="false"
|
|
/>
|
|
</div>
|
|
<p class="table-options-col-subtitle mb-20">
|
|
{{ t('sortableTable.tableHeader.show') }}:
|
|
</p>
|
|
<ul>
|
|
<li
|
|
v-for="(col, index) in tableColsOptions"
|
|
v-show="col.isTableOption"
|
|
:key="index"
|
|
:class="{ 'visible': !col.preventColToggle }"
|
|
>
|
|
<Checkbox
|
|
v-show="!col.preventColToggle"
|
|
v-model:value="col.isColVisible"
|
|
class="table-options-checkbox"
|
|
:label="col.label"
|
|
@update:value="tableOptionsCheckbox($event, col.label)"
|
|
/>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</th>
|
|
<th
|
|
v-else-if="rowActions"
|
|
:width="rowActionsWidth"
|
|
/>
|
|
</tr>
|
|
</thead>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
.table-options-group {
|
|
|
|
.table-options-btn.role-multi-action {
|
|
background-color: transparent;
|
|
border: none;
|
|
font-size: 18px;
|
|
&:hover, &:focus {
|
|
background-color: var(--accent-btn);
|
|
box-shadow: none;
|
|
}
|
|
}
|
|
.table-options-container {
|
|
width: 350px;
|
|
border: 1px solid var(--primary);
|
|
background-color: var(--body-bg);
|
|
padding: 20px;
|
|
z-index: 1;
|
|
|
|
.table-options-grouping {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
|
|
span {
|
|
white-space: nowrap;
|
|
margin-right: 10px;
|
|
}
|
|
}
|
|
|
|
ul {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
|
|
li {
|
|
margin: 0;
|
|
padding: 0;
|
|
|
|
&.visible {
|
|
margin: 0 0 10px 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.sortable > SPAN {
|
|
cursor: pointer;
|
|
user-select: none;
|
|
white-space: nowrap;
|
|
&:hover,
|
|
&:active {
|
|
text-decoration: underline;
|
|
color: var(--body-text);
|
|
}
|
|
}
|
|
|
|
.top-aligned th {
|
|
vertical-align: top;
|
|
padding-top: 10px;
|
|
}
|
|
|
|
thead {
|
|
tr {
|
|
background-color: var(--sortable-table-header-bg);
|
|
color: var(--body-text);
|
|
text-align: left;
|
|
border-bottom: 1px solid var(--sortable-table-top-divider);
|
|
}
|
|
}
|
|
|
|
th {
|
|
padding: 8px 5px;
|
|
font-weight: normal;
|
|
border: 0;
|
|
color: var(--body-text);
|
|
|
|
&.sortable-table-head-element:focus-visible {
|
|
@include focus-outline;
|
|
outline-offset: -4px;
|
|
}
|
|
|
|
.table-header-container {
|
|
display: inline-flex;
|
|
|
|
.content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
&.not-filterable {
|
|
margin-top: -2px;
|
|
|
|
.icon-stack {
|
|
margin-top: -2px;
|
|
}
|
|
}
|
|
|
|
.not-filter-icon {
|
|
font-size: 16px;
|
|
color: var(--primary);
|
|
vertical-align: super;
|
|
}
|
|
}
|
|
|
|
&:first-child {
|
|
padding-left: 10px;
|
|
}
|
|
|
|
&:last-child {
|
|
padding-right: 10px;
|
|
}
|
|
|
|
&:not(.sortable) > SPAN {
|
|
display: block;
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
& A {
|
|
color: var(--body-text);
|
|
}
|
|
|
|
// Aligns with COLUMN_BREAKPOINTS
|
|
@media only screen and (max-width: map-get($breakpoints, '--viewport-4')) {
|
|
// HIDE column on sizes below 480px
|
|
&.tablet, &.laptop, &.desktop {
|
|
display: none;
|
|
}
|
|
}
|
|
@media only screen and (max-width: map-get($breakpoints, '--viewport-9')) {
|
|
// HIDE column on sizes below 992px
|
|
&.laptop, &.desktop {
|
|
display: none;
|
|
}
|
|
}
|
|
@media only screen and (max-width: map-get($breakpoints, '--viewport-12')) {
|
|
// HIDE column on sizes below 1281px
|
|
&.desktop {
|
|
display: none;
|
|
}
|
|
}
|
|
}
|
|
|
|
.icon-stack {
|
|
width: 12px;
|
|
}
|
|
|
|
.icon-sort {
|
|
&.faded {
|
|
opacity: .3;
|
|
}
|
|
}
|
|
</style>
|
|
<style lang="scss">
|
|
.table-options-checkbox .checkbox-custom {
|
|
min-width: 14px;
|
|
}
|
|
.table-options-checkbox .checkbox-label {
|
|
color: var(--body-text);
|
|
}
|
|
</style>
|