mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-11-23 14:54:05 +00:00
Added ability to bulk select, pause & resume (#1886)
Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
This commit is contained in:
parent
59245e624d
commit
db66195f7d
6 changed files with 267 additions and 36 deletions
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in a new issue