Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Frank Elsinga <frank@elsinga.de>
394 lines
12 KiB
Vue
394 lines
12 KiB
Vue
<template>
|
|
<div>
|
|
<div
|
|
class="draggable-item"
|
|
:style="depthMargin"
|
|
:class="{ 'drag-over': dragOverCount > 0 }"
|
|
@dragstart="onDragStart"
|
|
@dragenter.prevent="onDragEnter"
|
|
@dragleave.prevent="onDragLeave"
|
|
@dragover.prevent
|
|
@drop.prevent="onDrop"
|
|
>
|
|
<!-- Checkbox -->
|
|
<div v-if="isSelectMode" class="select-input-wrapper">
|
|
<input
|
|
class="form-check-input select-input"
|
|
type="checkbox"
|
|
:aria-label="$t('Check/Uncheck')"
|
|
:checked="isSelected(monitor.id)"
|
|
@click.stop="toggleSelection"
|
|
/>
|
|
</div>
|
|
|
|
<router-link :to="monitorURL(monitor.id)" class="item" :class="{ disabled: !monitor.active }">
|
|
<div class="row">
|
|
<div
|
|
class="col-9 col-xl-6 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>
|
|
{{ monitor.name }}
|
|
</div>
|
|
<div v-if="monitor.tags.length > 0" class="tags gap-1">
|
|
<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-xl-6"
|
|
>
|
|
<HeartbeatBar ref="heartbeatBar" size="small" :monitor-id="monitor.id" />
|
|
</div>
|
|
</div>
|
|
|
|
<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">
|
|
<div v-if="!isCollapsed" class="childs">
|
|
<MonitorListItem
|
|
v-for="(item, index) in sortedChildMonitorList"
|
|
:key="index"
|
|
:monitor="item"
|
|
:isSelectMode="isSelectMode"
|
|
:isSelected="isSelected"
|
|
:select="select"
|
|
:deselect="deselect"
|
|
:depth="depth + 1"
|
|
:filter-func="filterFunc"
|
|
:sort-func="sortFunc"
|
|
/>
|
|
</div>
|
|
</transition>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import HeartbeatBar from "../components/HeartbeatBar.vue";
|
|
import Tag from "../components/Tag.vue";
|
|
import Uptime from "../components/Uptime.vue";
|
|
import { getMonitorRelativeURL } from "../util.ts";
|
|
|
|
export default {
|
|
name: "MonitorListItem",
|
|
components: {
|
|
Uptime,
|
|
HeartbeatBar,
|
|
Tag,
|
|
},
|
|
props: {
|
|
/** Monitor this represents */
|
|
monitor: {
|
|
type: Object,
|
|
default: null,
|
|
},
|
|
/** If the user is in select mode */
|
|
isSelectMode: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
/** How many ancestors are above this monitor */
|
|
depth: {
|
|
type: Number,
|
|
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: () => {},
|
|
},
|
|
/** Function to filter child monitors */
|
|
filterFunc: {
|
|
type: Function,
|
|
default: () => {},
|
|
},
|
|
/** Function to sort child monitors */
|
|
sortFunc: {
|
|
type: Function,
|
|
default: () => {},
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
isCollapsed: true,
|
|
dragOverCount: 0,
|
|
};
|
|
},
|
|
computed: {
|
|
sortedChildMonitorList() {
|
|
let result = Object.values(this.$root.monitorList);
|
|
|
|
// Get children
|
|
result = result.filter((childMonitor) => childMonitor.parent === this.monitor.id);
|
|
|
|
// Run filter on children
|
|
result = result.filter(this.filterFunc);
|
|
|
|
result.sort(this.sortFunc);
|
|
|
|
return result;
|
|
},
|
|
hasChildren() {
|
|
return this.sortedChildMonitorList.length > 0;
|
|
},
|
|
depthMargin() {
|
|
return {
|
|
marginLeft: `${31 * this.depth}px`,
|
|
};
|
|
},
|
|
},
|
|
watch: {
|
|
isSelectMode() {
|
|
// TODO: Resize the heartbeat bar, but too slow
|
|
// this.$refs.heartbeatBar.resize();
|
|
},
|
|
},
|
|
beforeMount() {
|
|
// Always unfold if monitor is accessed directly
|
|
if (this.monitor.childrenIDs.includes(parseInt(this.$route.params.id))) {
|
|
this.isCollapsed = false;
|
|
return;
|
|
}
|
|
|
|
// Set collapsed value based on local storage
|
|
let storage = window.localStorage.getItem("monitorCollapsed");
|
|
if (storage === null) {
|
|
return;
|
|
}
|
|
|
|
let storageObject = JSON.parse(storage);
|
|
if (storageObject[`monitor_${this.monitor.id}`] == null) {
|
|
return;
|
|
}
|
|
|
|
this.isCollapsed = storageObject[`monitor_${this.monitor.id}`];
|
|
},
|
|
methods: {
|
|
/**
|
|
* Changes the collapsed value of the current monitor and saves
|
|
* it to local storage
|
|
* @returns {void}
|
|
*/
|
|
changeCollapsed() {
|
|
this.isCollapsed = !this.isCollapsed;
|
|
|
|
// Save collapsed value into local storage
|
|
let storage = window.localStorage.getItem("monitorCollapsed");
|
|
let storageObject = {};
|
|
if (storage !== null) {
|
|
storageObject = JSON.parse(storage);
|
|
}
|
|
storageObject[`monitor_${this.monitor.id}`] = this.isCollapsed;
|
|
|
|
window.localStorage.setItem("monitorCollapsed", JSON.stringify(storageObject));
|
|
},
|
|
/**
|
|
* Initializes the drag operation if the monitor is draggable.
|
|
* @param {DragEvent} event - The dragstart event triggered by the browser.
|
|
* @returns {void} This method does not return anything.
|
|
*/
|
|
onDragStart(event) {
|
|
try {
|
|
event.dataTransfer.setData("text/monitor-id", String(this.monitor.id));
|
|
event.dataTransfer.effectAllowed = "move";
|
|
} catch (e) {
|
|
// ignore
|
|
}
|
|
},
|
|
|
|
onDragEnter(event) {
|
|
if (this.monitor.type !== "group") {
|
|
return;
|
|
}
|
|
|
|
this.dragOverCount++;
|
|
},
|
|
|
|
onDragLeave(event) {
|
|
if (this.monitor.type !== "group") {
|
|
return;
|
|
}
|
|
|
|
this.dragOverCount = Math.max(0, this.dragOverCount - 1);
|
|
},
|
|
|
|
async onDrop(event) {
|
|
this.dragOverCount = 0;
|
|
|
|
// Only groups accept drops
|
|
if (this.monitor.type !== "group") {
|
|
return;
|
|
}
|
|
|
|
const draggedId = event.dataTransfer.getData("text/monitor-id");
|
|
if (!draggedId) {
|
|
return;
|
|
}
|
|
|
|
const draggedMonitorId = parseInt(draggedId);
|
|
if (isNaN(draggedMonitorId) || draggedMonitorId === this.monitor.id) {
|
|
return;
|
|
}
|
|
|
|
const draggedMonitor = this.$root.monitorList[draggedMonitorId];
|
|
if (!draggedMonitor) {
|
|
return;
|
|
}
|
|
|
|
// Save original parent so we can revert locally if server returns error
|
|
const originalParent = draggedMonitor.parent;
|
|
|
|
// Prepare a full monitor object (clone) and set new parent
|
|
const monitorToSave = JSON.parse(JSON.stringify(draggedMonitor));
|
|
monitorToSave.parent = this.monitor.id;
|
|
|
|
// Optimistically update local state so UI updates immediately
|
|
this.$root.monitorList[draggedMonitorId].parent = this.monitor.id;
|
|
|
|
// Send updated monitor state via socket
|
|
try {
|
|
this.$root.getSocket().emit("editMonitor", monitorToSave, (res) => {
|
|
if (!res || !res.ok) {
|
|
// Revert local change on error
|
|
if (this.$root.monitorList[draggedMonitorId]) {
|
|
this.$root.monitorList[draggedMonitorId].parent = originalParent;
|
|
}
|
|
if (res && res.msg) {
|
|
this.$root.toastError(res.msg);
|
|
}
|
|
} else {
|
|
this.$root.toastRes(res);
|
|
}
|
|
});
|
|
} catch (e) {
|
|
// revert on exception
|
|
if (this.$root.monitorList[draggedMonitorId]) {
|
|
this.$root.monitorList[draggedMonitorId].parent = originalParent;
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
* Get URL of monitor
|
|
* @param {number} id ID of monitor
|
|
* @returns {string} Relative URL of monitor
|
|
*/
|
|
monitorURL(id) {
|
|
return getMonitorRelativeURL(id);
|
|
},
|
|
/**
|
|
* Toggle selection of monitor
|
|
* @returns {void}
|
|
*/
|
|
toggleSelection() {
|
|
if (this.isSelected(this.monitor.id)) {
|
|
this.deselect(this.monitor.id);
|
|
} else {
|
|
this.select(this.monitor.id);
|
|
}
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
@import "../assets/vars.scss";
|
|
|
|
.small-padding {
|
|
padding-left: 5px !important;
|
|
padding-right: 5px !important;
|
|
}
|
|
|
|
.collapse-padding {
|
|
padding-left: 8px !important;
|
|
padding-right: 2px !important;
|
|
}
|
|
|
|
// .monitor-item {
|
|
// width: 100%;
|
|
// }
|
|
|
|
.tags {
|
|
margin-top: 4px;
|
|
padding-left: 67px;
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0;
|
|
}
|
|
|
|
.collapsed {
|
|
transform: rotate(-90deg);
|
|
}
|
|
|
|
.animated {
|
|
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;
|
|
}
|
|
|
|
.drag-over {
|
|
border: 4px dashed $primary;
|
|
border-radius: 0.5rem;
|
|
background-color: $highlight-white;
|
|
}
|
|
|
|
.dark {
|
|
.drag-over {
|
|
background-color: $dark-bg2;
|
|
}
|
|
}
|
|
|
|
/* -4px on all due to border-width */
|
|
.monitor-list .drag-over .item {
|
|
padding: 9px 11px 6px 11px;
|
|
}
|
|
|
|
.draggable-item {
|
|
cursor: grab;
|
|
position: relative;
|
|
|
|
/* We don't want the padding change due to the border animated */
|
|
.item {
|
|
transition: none !important;
|
|
}
|
|
|
|
&.dragging {
|
|
cursor: grabbing;
|
|
}
|
|
}
|
|
</style>
|