summaryrefslogtreecommitdiffstats
path: root/src/components/MonitorList.vue
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/MonitorList.vue')
-rw-r--r--src/components/MonitorList.vue485
1 files changed, 485 insertions, 0 deletions
diff --git a/src/components/MonitorList.vue b/src/components/MonitorList.vue
new file mode 100644
index 0000000..a579316
--- /dev/null
+++ b/src/components/MonitorList.vue
@@ -0,0 +1,485 @@
+<template>
+ <div class="shadow-box mb-3" :style="boxStyle">
+ <div class="list-header">
+ <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="search-wrapper">
+ <a v-if="searchText == ''" class="search-icon">
+ <font-awesome-icon icon="search" />
+ </a>
+ <a v-if="searchText != ''" class="search-icon" @click="clearSearchText">
+ <font-awesome-icon icon="times" />
+ </a>
+ <form>
+ <input
+ v-model="searchText"
+ class="form-control search-input"
+ :placeholder="$t('Search...')"
+ :aria-label="$t('Search monitored sites')"
+ autocomplete="off"
+ />
+ </form>
+ </div>
+ </div>
+ <div class="header-filter">
+ <MonitorListFilter :filterState="filterState" @update-filter="updateFilter" />
+ </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 ref="monitorList" class="monitor-list" :class="{ scrollbar: scrollbar }" :style="monitorListStyle" data-testid="monitor-list">
+ <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>
+ </div>
+
+ <MonitorListItem
+ v-for="(item, index) in sortedMonitorList"
+ :key="index"
+ :monitor="item"
+ :isSelectMode="selectMode"
+ :isSelected="isSelected"
+ :select="select"
+ :deselect="deselect"
+ :filter-func="filterFunc"
+ :sort-func="sortFunc"
+ />
+ </div>
+ </div>
+
+ <Confirm ref="confirmPause" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="pauseSelected">
+ {{ $t("pauseMonitorMsg") }}
+ </Confirm>
+</template>
+
+<script>
+import Confirm from "../components/Confirm.vue";
+import MonitorListItem from "../components/MonitorListItem.vue";
+import MonitorListFilter from "./MonitorListFilter.vue";
+import { getMonitorRelativeURL } from "../util.ts";
+
+export default {
+ components: {
+ Confirm,
+ MonitorListItem,
+ MonitorListFilter,
+ },
+ props: {
+ /** Should the scrollbar be shown */
+ scrollbar: {
+ type: Boolean,
+ },
+ },
+ data() {
+ return {
+ searchText: "",
+ selectMode: false,
+ selectAll: false,
+ disableSelectAllWatcher: false,
+ selectedMonitors: {},
+ windowTop: 0,
+ filterState: {
+ status: null,
+ active: null,
+ tags: null,
+ }
+ };
+ },
+ computed: {
+ /**
+ * Improve the sticky appearance of the list by increasing its
+ * height as user scrolls down.
+ * Not used on mobile.
+ * @returns {object} Style for monitor list
+ */
+ boxStyle() {
+ if (window.innerWidth > 550) {
+ return {
+ height: `calc(100vh - 160px + ${this.windowTop}px)`,
+ };
+ } else {
+ return {
+ height: "calc(100vh - 160px)",
+ };
+ }
+
+ },
+
+ /**
+ * Returns a sorted list of monitors based on the applied filters and search text.
+ * @returns {Array} The sorted list of monitors.
+ */
+ sortedMonitorList() {
+ let result = Object.values(this.$root.monitorList);
+
+ result = result.filter(monitor => {
+ // The root list does not show children
+ if (monitor.parent !== null) {
+ return false;
+ }
+ return true;
+ });
+
+ result = result.filter(this.filterFunc);
+
+ result.sort(this.sortFunc);
+
+ 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;
+ },
+
+ /**
+ * Determines if any filters are active.
+ * @returns {boolean} True if any filter is active, false otherwise.
+ */
+ filtersActive() {
+ return this.filterState.status != null || this.filterState.active != null || this.filterState.tags != null || this.searchText !== "";
+ }
+ },
+ 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() {
+ window.addEventListener("scroll", this.onScroll);
+ },
+ beforeUnmount() {
+ window.removeEventListener("scroll", this.onScroll);
+ },
+ methods: {
+ /**
+ * Handle user scroll
+ * @returns {void}
+ */
+ onScroll() {
+ if (window.top.scrollY <= 133) {
+ this.windowTop = window.top.scrollY;
+ } else {
+ this.windowTop = 133;
+ }
+ },
+ /**
+ * Get URL of monitor
+ * @param {number} id ID of monitor
+ * @returns {string} Relative URL of monitor
+ */
+ monitorURL(id) {
+ return getMonitorRelativeURL(id);
+ },
+ /**
+ * Clear the search bar
+ * @returns {void}
+ */
+ clearSearchText() {
+ this.searchText = "";
+ },
+ /**
+ * Update the MonitorList Filter
+ * @param {object} newFilter Object with new filter
+ * @returns {void}
+ */
+ updateFilter(newFilter) {
+ this.filterState = newFilter;
+ },
+ /**
+ * Deselect a monitor
+ * @param {number} id ID of monitor
+ * @returns {void}
+ */
+ deselect(id) {
+ delete this.selectedMonitors[id];
+ },
+ /**
+ * Select a monitor
+ * @param {number} id ID of monitor
+ * @returns {void}
+ */
+ select(id) {
+ this.selectedMonitors[id] = true;
+ },
+ /**
+ * Determine if monitor is selected
+ * @param {number} id ID of monitor
+ * @returns {bool} Is the monitor selected?
+ */
+ isSelected(id) {
+ return id in this.selectedMonitors;
+ },
+ /**
+ * Disable select mode and reset selection
+ * @returns {void}
+ */
+ cancelSelectMode() {
+ this.selectMode = false;
+ this.selectedMonitors = {};
+ },
+ /**
+ * Show dialog to confirm pause
+ * @returns {void}
+ */
+ pauseDialog() {
+ this.$refs.confirmPause.show();
+ },
+ /**
+ * Pause each selected monitor
+ * @returns {void}
+ */
+ 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
+ * @returns {void}
+ */
+ resumeSelected() {
+ Object.keys(this.selectedMonitors)
+ .filter(id => !this.$root.monitorList[id].active)
+ .forEach(id => this.$root.getSocket().emit("resumeMonitor", id, () => {}));
+
+ this.cancelSelectMode();
+ },
+ /**
+ * Whether a monitor should be displayed based on the filters
+ * @param {object} monitor Monitor to check
+ * @returns {boolean} Should the monitor be displayed
+ */
+ filterFunc(monitor) {
+ // Group monitors bypass filter if at least 1 of children matched
+ if (monitor.type === "group") {
+ const children = Object.values(this.$root.monitorList).filter(m => m.parent === monitor.id);
+ if (children.some((child, index, children) => this.filterFunc(child))) {
+ return true;
+ }
+ }
+
+ // filter by search text
+ // finds monitor name, tag name or tag value
+ let searchTextMatch = true;
+ if (this.searchText !== "") {
+ const loweredSearchText = this.searchText.toLowerCase();
+ searchTextMatch =
+ monitor.name.toLowerCase().includes(loweredSearchText)
+ || monitor.tags.find(tag => tag.name.toLowerCase().includes(loweredSearchText)
+ || tag.value?.toLowerCase().includes(loweredSearchText));
+ }
+
+ // filter by status
+ let statusMatch = true;
+ if (this.filterState.status != null && this.filterState.status.length > 0) {
+ if (monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[monitor.id]) {
+ monitor.status = this.$root.lastHeartbeatList[monitor.id].status;
+ }
+ statusMatch = this.filterState.status.includes(monitor.status);
+ }
+
+ // filter by active
+ let activeMatch = true;
+ if (this.filterState.active != null && this.filterState.active.length > 0) {
+ activeMatch = this.filterState.active.includes(monitor.active);
+ }
+
+ // filter by tags
+ let tagsMatch = true;
+ if (this.filterState.tags != null && this.filterState.tags.length > 0) {
+ tagsMatch = monitor.tags.map(tag => tag.tag_id) // convert to array of tag IDs
+ .filter(monitorTagId => this.filterState.tags.includes(monitorTagId)) // perform Array Intersaction between filter and monitor's tags
+ .length > 0;
+ }
+
+ return searchTextMatch && statusMatch && activeMatch && tagsMatch;
+ },
+ /**
+ * Function used in Array.sort to order monitors in a list.
+ * @param {*} m1 monitor 1
+ * @param {*} m2 monitor 2
+ * @returns {number} -1, 0 or 1
+ */
+ sortFunc(m1, m2) {
+ if (m1.active !== m2.active) {
+ if (m1.active === false) {
+ return 1;
+ }
+
+ if (m2.active === false) {
+ return -1;
+ }
+ }
+
+ if (m1.weight !== m2.weight) {
+ if (m1.weight > m2.weight) {
+ return -1;
+ }
+
+ if (m1.weight < m2.weight) {
+ return 1;
+ }
+ }
+
+ return m1.name.localeCompare(m2.name);
+ }
+ },
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../assets/vars.scss";
+
+.shadow-box {
+ height: calc(100vh - 150px);
+ position: sticky;
+ top: 10px;
+}
+
+.small-padding {
+ padding-left: 5px !important;
+ padding-right: 5px !important;
+}
+
+.list-header {
+ border-bottom: 1px solid #dee2e6;
+ border-radius: 10px 10px 0 0;
+ margin: -10px;
+ margin-bottom: 10px;
+ padding: 10px;
+
+ .dark & {
+ background-color: $dark-header-bg;
+ border-bottom: 0;
+ }
+}
+
+.header-top {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.header-filter {
+ display: flex;
+ align-items: center;
+}
+
+@media (max-width: 770px) {
+ .list-header {
+ margin: -20px;
+ margin-bottom: 10px;
+ padding: 5px;
+ }
+}
+
+.search-wrapper {
+ display: flex;
+ align-items: center;
+}
+
+.search-icon {
+ padding: 10px;
+ color: #c0c0c0;
+
+ // Clear filter button (X)
+ svg[data-icon="times"] {
+ cursor: pointer;
+ transition: all ease-in-out 0.1s;
+
+ &:hover {
+ opacity: 0.5;
+ }
+ }
+}
+
+.search-input {
+ max-width: 15em;
+}
+
+.monitor-item {
+ width: 100%;
+}
+
+.tags {
+ margin-top: 4px;
+ padding-left: 67px;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0;
+}
+
+.bottom-style {
+ padding-left: 67px;
+ margin-top: 5px;
+}
+
+.selection-controls {
+ margin-top: 5px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+</style>