Added ability to bulk select, pause & resume (#1886)

Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
This commit is contained in:
Shaun 2023-07-31 04:24:00 -04:00 committed by GitHub
parent 59245e624d
commit db66195f7d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 267 additions and 36 deletions

View file

@ -10,6 +10,7 @@
"color-function-notation": "legacy", "color-function-notation": "legacy",
"shorthand-property-no-redundant-values": null, "shorthand-property-no-redundant-values": null,
"color-hex-length": null, "color-hex-length": null,
"declaration-block-no-redundant-longhand-properties": null "declaration-block-no-redundant-longhand-properties": null,
"at-rule-no-unknown": null
} }
} }

View file

@ -111,6 +111,10 @@ optgroup {
padding-right: 20px; padding-right: 20px;
} }
.btn-sm {
border-radius: 25px;
}
.btn-primary { .btn-primary {
color: white; color: white;
@ -158,6 +162,26 @@ optgroup {
background-color: #161B22; background-color: #161B22;
} }
.btn-outline-normal {
padding: 4px 10px;
border: 1px solid #ced4da;
border-radius: 25px;
background-color: transparent;
.dark & {
color: $dark-font-color;
border: 1px solid $dark-font-color2;
}
&.active {
background-color: $highlight-white;
.dark & {
background-color: $dark-font-color2;
}
}
}
@media (max-width: 550px) { @media (max-width: 550px) {
.table-shadow-box { .table-shadow-box {
padding: 10px !important; padding: 10px !important;
@ -436,7 +460,6 @@ optgroup {
.monitor-list { .monitor-list {
&.scrollbar { &.scrollbar {
overflow-y: auto; overflow-y: auto;
height: calc(100% - 107px);
} }
@media (max-width: 770px) { @media (max-width: 770px) {

View file

@ -2,6 +2,10 @@
<div class="shadow-box mb-3" :style="boxStyle"> <div class="shadow-box mb-3" :style="boxStyle">
<div class="list-header"> <div class="list-header">
<div class="header-top"> <div class="header-top">
<button class="btn btn-outline-normal ms-2" :class="{ 'active': selectMode }" type="button" @click="selectMode = !selectMode">
{{ $t("Select") }}
</button>
<div class="placeholder"></div> <div class="placeholder"></div>
<div class="search-wrapper"> <div class="search-wrapper">
<a v-if="searchText == ''" class="search-icon"> <a v-if="searchText == ''" class="search-icon">
@ -21,27 +25,55 @@
<div class="header-filter"> <div class="header-filter">
<MonitorListFilter :filterState="filterState" @update-filter="updateFilter" /> <MonitorListFilter :filterState="filterState" @update-filter="updateFilter" />
</div> </div>
<!-- Selection Controls -->
<div v-if="selectMode" class="selection-controls px-2 pt-2">
<input
v-model="selectAll"
class="form-check-input select-input"
type="checkbox"
/>
<button class="btn-outline-normal" @click="pauseDialog"><font-awesome-icon icon="pause" size="sm" /> {{ $t("Pause") }}</button>
<button class="btn-outline-normal" @click="resumeSelected"><font-awesome-icon icon="play" size="sm" /> {{ $t("Resume") }}</button>
<span v-if="selectedMonitorCount > 0">
{{ $t("selectedMonitorCount", [ selectedMonitorCount ]) }}
</span>
</div>
</div> </div>
<div class="monitor-list" :class="{ scrollbar: scrollbar }"> <div ref="monitorList" class="monitor-list" :class="{ scrollbar: scrollbar }" :style="monitorListStyle">
<div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3"> <div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3">
{{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link> {{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link>
</div> </div>
<MonitorListItem <MonitorListItem
v-for="(item, index) in sortedMonitorList" :key="index" :monitor="item" v-for="(item, index) in sortedMonitorList"
:key="index"
:monitor="item"
:isSearch="searchText !== ''" :isSearch="searchText !== ''"
:isSelectMode="selectMode"
:isSelected="isSelected"
:select="select"
:deselect="deselect"
/> />
</div> </div>
</div> </div>
<Confirm ref="confirmPause" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="pauseSelected">
{{ $t("pauseMonitorMsg") }}
</Confirm>
</template> </template>
<script> <script>
import Confirm from "../components/Confirm.vue";
import MonitorListItem from "../components/MonitorListItem.vue"; import MonitorListItem from "../components/MonitorListItem.vue";
import MonitorListFilter from "./MonitorListFilter.vue"; import MonitorListFilter from "./MonitorListFilter.vue";
import { getMonitorRelativeURL } from "../util.ts"; import { getMonitorRelativeURL } from "../util.ts";
export default { export default {
components: { components: {
Confirm,
MonitorListItem, MonitorListItem,
MonitorListFilter, MonitorListFilter,
}, },
@ -54,6 +86,10 @@ export default {
data() { data() {
return { return {
searchText: "", searchText: "",
selectMode: false,
selectAll: false,
disableSelectAllWatcher: false,
selectedMonitors: {},
windowTop: 0, windowTop: 0,
filterState: { filterState: {
status: null, status: null,
@ -146,6 +182,58 @@ export default {
return result; return result;
}, },
isDarkTheme() {
return document.body.classList.contains("dark");
},
monitorListStyle() {
let listHeaderHeight = 107;
if (this.selectMode) {
listHeaderHeight += 42;
}
return {
"height": `calc(100% - ${listHeaderHeight}px)`
};
},
selectedMonitorCount() {
return Object.keys(this.selectedMonitors).length;
},
},
watch: {
searchText() {
for (let monitor of this.sortedMonitorList) {
if (!this.selectedMonitors[monitor.id]) {
if (this.selectAll) {
this.disableSelectAllWatcher = true;
this.selectAll = false;
}
break;
}
}
},
selectAll() {
if (!this.disableSelectAllWatcher) {
this.selectedMonitors = {};
if (this.selectAll) {
this.sortedMonitorList.forEach((item) => {
this.selectedMonitors[item.id] = true;
});
}
} else {
this.disableSelectAllWatcher = false;
}
},
selectMode() {
if (!this.selectMode) {
this.selectAll = false;
this.selectedMonitors = {};
}
}
}, },
mounted() { mounted() {
window.addEventListener("scroll", this.onScroll); window.addEventListener("scroll", this.onScroll);
@ -181,6 +269,53 @@ export default {
updateFilter(newFilter) { updateFilter(newFilter) {
this.filterState = newFilter; this.filterState = newFilter;
}, },
/**
* Deselect a monitor
* @param {number} id ID of monitor
*/
deselect(id) {
delete this.selectedMonitors[id];
},
/**
* Select a monitor
* @param {number} id ID of monitor
*/
select(id) {
this.selectedMonitors[id] = true;
},
/**
* Determine if monitor is selected
* @param {number} id ID of monitor
* @returns {bool}
*/
isSelected(id) {
return id in this.selectedMonitors;
},
/** Disable select mode and reset selection */
cancelSelectMode() {
this.selectMode = false;
this.selectedMonitors = {};
},
/** Show dialog to confirm pause */
pauseDialog() {
this.$refs.confirmPause.show();
},
/** Pause each selected monitor */
pauseSelected() {
Object.keys(this.selectedMonitors)
.filter(id => this.$root.monitorList[id].active)
.forEach(id => this.$root.getSocket().emit("pauseMonitor", id));
this.cancelSelectMode();
},
/** Resume each selected monitor */
resumeSelected() {
Object.keys(this.selectedMonitors)
.filter(id => !this.$root.monitorList[id].active)
.forEach(id => this.$root.getSocket().emit("resumeMonitor", id));
this.cancelSelectMode();
},
}, },
}; };
</script> </script>
@ -271,4 +406,12 @@ export default {
padding-left: 67px; padding-left: 67px;
margin-top: 5px; margin-top: 5px;
} }
.selection-controls {
margin-top: 5px;
display: flex;
align-items: center;
gap: 10px;
}
</style> </style>

View file

@ -44,6 +44,7 @@ export default {
<style lang="scss"> <style lang="scss">
@import "../assets/vars.scss"; @import "../assets/vars.scss";
@import "../assets/app.scss";
.filter-dropdown-menu { .filter-dropdown-menu {
z-index: 100; z-index: 100;
@ -102,18 +103,10 @@ export default {
} }
.filter-dropdown-status { .filter-dropdown-status {
@extend .btn-outline-normal;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 4px 10px;
margin-left: 5px; margin-left: 5px;
border: 1px solid #ced4da;
border-radius: 25px;
background-color: transparent;
.dark & {
color: $dark-font-color;
border: 1px solid $dark-font-color2;
}
&.active { &.active {
border: 1px solid $highlight; border: 1px solid $highlight;

View file

@ -1,34 +1,56 @@
<template> <template>
<div> <div>
<router-link :to="monitorURL(monitor.id)" class="item" :class="{ 'disabled': ! monitor.active }"> <div :style="depthMargin">
<div class="row"> <!-- Checkbox -->
<div class="col-9 col-md-8 small-padding" :class="{ 'monitor-item': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }"> <div v-if="isSelectMode" class="select-input-wrapper">
<div class="info" :style="depthMargin"> <input
<Uptime :monitor="monitor" type="24" :pill="true" /> class="form-check-input select-input"
<span v-if="hasChildren" class="collapse-padding" @click.prevent="changeCollapsed"> type="checkbox"
<font-awesome-icon icon="chevron-down" class="animated" :class="{ collapsed: isCollapsed}" /> :aria-label="$t('Check/Uncheck')"
</span> :checked="isSelected(monitor.id)"
{{ monitorName }} @click.stop="toggleSelection"
</div> />
<div class="tags">
<Tag v-for="tag in monitor.tags" :key="tag" :item="tag" :size="'sm'" />
</div>
</div>
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-3 col-md-4">
<HeartbeatBar size="small" :monitor-id="monitor.id" />
</div>
</div> </div>
<div v-if="$root.userHeartbeatBar == 'bottom'" class="row"> <router-link :to="monitorURL(monitor.id)" class="item" :class="{ 'disabled': ! monitor.active }">
<div class="col-12 bottom-style"> <div class="row">
<HeartbeatBar size="small" :monitor-id="monitor.id" /> <div class="col-9 col-md-8 small-padding" :class="{ 'monitor-item': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
<div class="info">
<Uptime :monitor="monitor" type="24" :pill="true" />
<span v-if="hasChildren" class="collapse-padding" @click.prevent="changeCollapsed">
<font-awesome-icon icon="chevron-down" class="animated" :class="{ collapsed: isCollapsed}" />
</span>
{{ monitorName }}
</div>
<div v-if="monitor.tags.length > 0" class="tags">
<Tag v-for="tag in monitor.tags" :key="tag" :item="tag" :size="'sm'" />
</div>
</div>
<div v-show="$root.userHeartbeatBar == 'normal'" :key="$root.userHeartbeatBar" class="col-3 col-md-4">
<HeartbeatBar ref="heartbeatBar" size="small" :monitor-id="monitor.id" />
</div>
</div> </div>
</div>
</router-link> <div v-if="$root.userHeartbeatBar == 'bottom'" class="row">
<div class="col-12 bottom-style">
<HeartbeatBar ref="heartbeatBar" size="small" :monitor-id="monitor.id" />
</div>
</div>
</router-link>
</div>
<transition name="slide-fade-up"> <transition name="slide-fade-up">
<div v-if="!isCollapsed" class="childs"> <div v-if="!isCollapsed" class="childs">
<MonitorListItem v-for="(item, index) in sortedChildMonitorList" :key="index" :monitor="item" :isSearch="isSearch" :depth="depth + 1" /> <MonitorListItem
v-for="(item, index) in sortedChildMonitorList"
:key="index" :monitor="item"
:isSearch="isSearch"
:isSelectMode="isSelectMode"
:isSelected="isSelected"
:select="select"
:deselect="deselect"
:depth="depth + 1"
/>
</div> </div>
</transition> </transition>
</div> </div>
@ -58,11 +80,31 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
/** If the user is in select mode */
isSelectMode: {
type: Boolean,
default: false,
},
/** How many ancestors are above this monitor */ /** How many ancestors are above this monitor */
depth: { depth: {
type: Number, type: Number,
default: 0, default: 0,
}, },
/** Callback to determine if monitor is selected */
isSelected: {
type: Function,
default: () => {}
},
/** Callback fired when monitor is selected */
select: {
type: Function,
default: () => {}
},
/** Callback fired when monitor is deselected */
deselect: {
type: Function,
default: () => {}
},
}, },
data() { data() {
return { return {
@ -118,6 +160,12 @@ export default {
} }
} }
}, },
watch: {
isSelectMode() {
// TODO: Resize the heartbeat bar, but too slow
// this.$refs.heartbeatBar.resize();
}
},
beforeMount() { beforeMount() {
// Always unfold if monitor is accessed directly // Always unfold if monitor is accessed directly
@ -164,6 +212,16 @@ export default {
monitorURL(id) { monitorURL(id) {
return getMonitorRelativeURL(id); return getMonitorRelativeURL(id);
}, },
/**
* Toggle selection of monitor
*/
toggleSelection() {
if (this.isSelected(this.monitor.id)) {
this.deselect(this.monitor.id);
} else {
this.select(this.monitor.id);
}
},
}, },
}; };
</script> </script>
@ -201,4 +259,14 @@ export default {
transition: all 0.2s $easing-in; transition: all 0.2s $easing-in;
} }
.select-input-wrapper {
float: left;
margin-top: 15px;
margin-left: 3px;
margin-right: 10px;
padding-left: 4px;
position: relative;
z-index: 15;
}
</style> </style>

View file

@ -269,6 +269,9 @@
"Services": "Services", "Services": "Services",
"Discard": "Discard", "Discard": "Discard",
"Cancel": "Cancel", "Cancel": "Cancel",
"Select": "Select",
"selectedMonitorCount": "Selected: {0}",
"Check/Uncheck": "Check/Uncheck",
"Powered by": "Powered by", "Powered by": "Powered by",
"shrinkDatabaseDescription": "Trigger database VACUUM for SQLite. If your database is created after 1.10.0, AUTO_VACUUM is already enabled and this action is not needed.", "shrinkDatabaseDescription": "Trigger database VACUUM for SQLite. If your database is created after 1.10.0, AUTO_VACUUM is already enabled and this action is not needed.",
"Customize": "Customize", "Customize": "Customize",