mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-12-06 05:24:48 -08:00
Compare commits
2 Commits
4002aee36f
...
082e4b9712
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
082e4b9712 | ||
|
|
23498e4134 |
@@ -109,6 +109,7 @@ const { login } = require("./auth");
|
||||
const passwordHash = require("./password-hash");
|
||||
|
||||
const { Prometheus } = require("./prometheus");
|
||||
const { UptimeCalculator } = require("./uptime-calculator");
|
||||
|
||||
const hostname = config.hostname;
|
||||
|
||||
@@ -1592,9 +1593,11 @@ let needSetup = false;
|
||||
|
||||
log.info("manage", `Clear Heartbeats Monitor: ${monitorID} User ID: ${socket.userID}`);
|
||||
|
||||
await R.exec("DELETE FROM heartbeat WHERE monitor_id = ?", [
|
||||
monitorID
|
||||
]);
|
||||
await UptimeCalculator.clearStatistics(monitorID);
|
||||
|
||||
if (monitorID in server.monitorList) {
|
||||
await restartMonitor(socket.userID, monitorID);
|
||||
}
|
||||
|
||||
await sendHeartbeatList(socket, monitorID, true, true);
|
||||
|
||||
@@ -1616,10 +1619,7 @@ let needSetup = false;
|
||||
|
||||
log.info("manage", `Clear Statistics User ID: ${socket.userID}`);
|
||||
|
||||
await R.exec("DELETE FROM heartbeat");
|
||||
await R.exec("DELETE FROM stat_daily");
|
||||
await R.exec("DELETE FROM stat_hourly");
|
||||
await R.exec("DELETE FROM stat_minutely");
|
||||
await UptimeCalculator.clearAllStatistics();
|
||||
|
||||
// Restart all monitors to reset the stats
|
||||
for (let monitorID in server.monitorList) {
|
||||
|
||||
@@ -90,6 +90,14 @@ class UptimeCalculator {
|
||||
delete UptimeCalculator.list[monitorID];
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all monitors from the list
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async removeAll() {
|
||||
UptimeCalculator.list = {};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@@ -845,6 +853,42 @@ class UptimeCalculator {
|
||||
setMigrationMode(value) {
|
||||
this.migrationMode = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all statistics and heartbeats for a monitor
|
||||
* @param {number} monitorID the id of the monitor
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async clearStatistics(monitorID) {
|
||||
await R.exec("DELETE FROM heartbeat WHERE monitor_id = ?", [
|
||||
monitorID
|
||||
]);
|
||||
|
||||
await R.exec("DELETE FROM stat_minutely WHERE monitor_id = ?", [
|
||||
monitorID
|
||||
]);
|
||||
await R.exec("DELETE FROM stat_hourly WHERE monitor_id = ?", [
|
||||
monitorID
|
||||
]);
|
||||
await R.exec("DELETE FROM stat_daily WHERE monitor_id = ?", [
|
||||
monitorID
|
||||
]);
|
||||
|
||||
await UptimeCalculator.remove(monitorID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all statistics and heartbeats for all monitors
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async clearAllStatistics() {
|
||||
await R.exec("DELETE FROM heartbeat");
|
||||
await R.exec("DELETE FROM stat_minutely");
|
||||
await R.exec("DELETE FROM stat_hourly");
|
||||
await R.exec("DELETE FROM stat_daily");
|
||||
|
||||
await UptimeCalculator.removeAll();
|
||||
}
|
||||
}
|
||||
|
||||
class UptimeDataResult {
|
||||
|
||||
487
src/components/GroupSortDropdown.vue
Normal file
487
src/components/GroupSortDropdown.vue
Normal file
@@ -0,0 +1,487 @@
|
||||
<template>
|
||||
<div v-if="group && group.monitorList && group.monitorList.length > 1" class="sort-dropdown">
|
||||
<div class="dropdown">
|
||||
<button
|
||||
:id="'sortDropdown' + groupIndex"
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-secondary dropdown-toggle sort-button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
:aria-label="$t('Sort options')"
|
||||
:title="$t('Sort options')"
|
||||
>
|
||||
<div class="sort-arrows">
|
||||
<font-awesome-icon
|
||||
icon="arrow-down"
|
||||
:class="{
|
||||
'arrow-inactive': !group.sortKey || group.sortDirection !== 'desc',
|
||||
'arrow-active': group.sortKey && group.sortDirection === 'desc'
|
||||
}"
|
||||
/>
|
||||
<font-awesome-icon
|
||||
icon="arrow-up"
|
||||
:class="{
|
||||
'arrow-inactive': !group.sortKey || group.sortDirection !== 'asc',
|
||||
'arrow-active': group.sortKey && group.sortDirection === 'asc'
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end sort-menu" :aria-labelledby="'sortDropdown' + groupIndex">
|
||||
<li>
|
||||
<button
|
||||
class="dropdown-item sort-item"
|
||||
type="button"
|
||||
:aria-label="$t('Sort by status')"
|
||||
:title="$t('Sort by status')"
|
||||
@click="setSort('status')"
|
||||
>
|
||||
<div class="sort-item-content">
|
||||
<span>{{ $t("Status") }}</span>
|
||||
<span v-if="getSortKey() === 'status'" class="sort-indicators">
|
||||
<font-awesome-icon
|
||||
:icon="group.sortDirection === 'asc' ? 'arrow-up' : 'arrow-down'"
|
||||
class="arrow-active me-1"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
class="dropdown-item sort-item"
|
||||
type="button"
|
||||
:aria-label="$t('Sort by name')"
|
||||
:title="$t('Sort by name')"
|
||||
@click="setSort('name')"
|
||||
>
|
||||
<div class="sort-item-content">
|
||||
<span>{{ $t("Name") }}</span>
|
||||
<span v-if="getSortKey() === 'name'" class="sort-indicators">
|
||||
<font-awesome-icon
|
||||
:icon="group.sortDirection === 'asc' ? 'arrow-up' : 'arrow-down'"
|
||||
class="arrow-active me-1"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
class="dropdown-item sort-item"
|
||||
type="button"
|
||||
:aria-label="$t('Sort by uptime')"
|
||||
:title="$t('Sort by uptime')"
|
||||
@click="setSort('uptime')"
|
||||
>
|
||||
<div class="sort-item-content">
|
||||
<span>{{ $t("Uptime") }}</span>
|
||||
<span v-if="getSortKey() === 'uptime'" class="sort-indicators">
|
||||
<font-awesome-icon
|
||||
:icon="group.sortDirection === 'asc' ? 'arrow-up' : 'arrow-down'"
|
||||
class="arrow-active me-1"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<li v-if="showCertificateExpiry">
|
||||
<button
|
||||
class="dropdown-item sort-item"
|
||||
type="button"
|
||||
:aria-label="$t('Sort by certificate expiry')"
|
||||
:title="$t('Sort by certificate expiry')"
|
||||
@click="setSort('cert')"
|
||||
>
|
||||
<div class="sort-item-content">
|
||||
<span>{{ $t("Cert Exp.") }}</span>
|
||||
<span v-if="getSortKey() === 'cert'" class="sort-indicators">
|
||||
<font-awesome-icon
|
||||
:icon="group.sortDirection === 'asc' ? 'arrow-up' : 'arrow-down'"
|
||||
class="arrow-active me-1"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "GroupSortDropdown",
|
||||
props: {
|
||||
/** Group object containing monitorList and sort settings */
|
||||
group: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
/** Index of the group for unique IDs */
|
||||
groupIndex: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
/** Should certificate expiry options be shown? */
|
||||
showCertificateExpiry: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
emits: [ "update-group" ],
|
||||
computed: {
|
||||
/**
|
||||
* Parse sort settings from URL query parameters
|
||||
* @returns {object} Parsed sort settings for all groups
|
||||
*/
|
||||
sortSettingsFromURL() {
|
||||
const sortSettings = {};
|
||||
if (this.$route && this.$route.query) {
|
||||
for (const [ key, value ] of Object.entries(this.$route.query)) {
|
||||
if (key.startsWith("sort_") && typeof value === "string") {
|
||||
const groupId = key.replace("sort_", "");
|
||||
const [ sortKey, direction ] = value.split("_");
|
||||
if (sortKey && [ "status", "name", "uptime", "cert" ].includes(sortKey) &&
|
||||
direction && [ "asc", "desc" ].includes(direction)) {
|
||||
sortSettings[ groupId ] = {
|
||||
sortKey,
|
||||
direction
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return sortSettings;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// Watch for changes in heartbeat list, reapply sorting
|
||||
"$root.heartbeatList": {
|
||||
handler() {
|
||||
this.applySort();
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
|
||||
// Watch for changes in uptime list, reapply sorting
|
||||
"$root.uptimeList": {
|
||||
handler() {
|
||||
this.applySort();
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
|
||||
// Watch for URL changes and apply sort settings
|
||||
sortSettingsFromURL: {
|
||||
handler(newSortSettings) {
|
||||
if (this.group) {
|
||||
const groupId = this.getGroupIdentifier();
|
||||
const urlSetting = newSortSettings[ groupId ];
|
||||
|
||||
if (urlSetting) {
|
||||
this.updateGroup({
|
||||
sortKey: urlSetting.sortKey,
|
||||
sortDirection: urlSetting.direction
|
||||
});
|
||||
} else {
|
||||
// Set defaults if not in URL
|
||||
if (this.group.sortKey === undefined) {
|
||||
this.updateGroup({ sortKey: "status" });
|
||||
}
|
||||
if (this.group.sortDirection === undefined) {
|
||||
this.updateGroup({ sortDirection: "asc" });
|
||||
}
|
||||
}
|
||||
|
||||
this.applySort();
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Get sort key for the group
|
||||
* @returns {string} sort key
|
||||
*/
|
||||
getSortKey() {
|
||||
return this.group.sortKey || "status";
|
||||
},
|
||||
|
||||
/**
|
||||
* Update group properties by emitting to parent
|
||||
* @param {object} updates - object with properties to update
|
||||
* @returns {void}
|
||||
*/
|
||||
updateGroup(updates) {
|
||||
this.$emit("update-group", this.groupIndex, updates);
|
||||
},
|
||||
|
||||
/**
|
||||
* Set group sort key and direction, then apply sorting
|
||||
* @param {string} key - sort key ('status', 'name', 'uptime', 'cert')
|
||||
* @returns {void}
|
||||
*/
|
||||
setSort(key) {
|
||||
if (this.group.sortKey === key) {
|
||||
this.updateGroup({
|
||||
sortDirection: this.group.sortDirection === "asc" ? "desc" : "asc"
|
||||
});
|
||||
} else {
|
||||
this.updateGroup({
|
||||
sortKey: key,
|
||||
sortDirection: "asc"
|
||||
});
|
||||
}
|
||||
|
||||
this.applySort();
|
||||
this.updateRouterQuery();
|
||||
},
|
||||
|
||||
/**
|
||||
* Update router query parameters with sort settings
|
||||
* @returns {void}
|
||||
*/
|
||||
updateRouterQuery() {
|
||||
if (!this.$router) {
|
||||
return;
|
||||
}
|
||||
|
||||
const query = { ...this.$route.query };
|
||||
const groupId = this.getGroupIdentifier();
|
||||
|
||||
if (this.group.sortKey && this.group.sortDirection) {
|
||||
query[ `sort_${groupId}` ] = `${this.group.sortKey}_${this.group.sortDirection}`;
|
||||
} else {
|
||||
delete query[ `sort_${groupId}` ];
|
||||
}
|
||||
|
||||
this.$router.push({ query }).catch(() => {});
|
||||
},
|
||||
|
||||
/**
|
||||
* Apply sorting logic directly to the group's monitorList (in-place)
|
||||
* @returns {void}
|
||||
*/
|
||||
applySort() {
|
||||
if (!this.group || !this.group.monitorList || !Array.isArray(this.group.monitorList)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sortKey = this.group.sortKey || "status";
|
||||
const sortDirection = this.group.sortDirection || "desc";
|
||||
|
||||
this.updateGroup({
|
||||
monitorList: [ ...this.group.monitorList ].sort((a, b) => {
|
||||
if (!a || !b) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let comparison = 0;
|
||||
let valueA;
|
||||
let valueB;
|
||||
|
||||
if (sortKey === "status") {
|
||||
// Sort by status
|
||||
const getStatusPriority = (monitor) => {
|
||||
if (!monitor || !monitor.id) {
|
||||
return 4;
|
||||
}
|
||||
|
||||
const hbList = this.$root.heartbeatList || {};
|
||||
const hbArr = hbList[ monitor.id ];
|
||||
if (hbArr && hbArr.length > 0) {
|
||||
const lastStatus = hbArr.at(-1).status;
|
||||
if (lastStatus === 0) {
|
||||
return 0;
|
||||
} // Down
|
||||
if (lastStatus === 1) {
|
||||
return 1;
|
||||
} // Up
|
||||
if (lastStatus === 2) {
|
||||
return 2;
|
||||
} // Pending
|
||||
if (lastStatus === 3) {
|
||||
return 3;
|
||||
} // Maintenance
|
||||
}
|
||||
return 4; // Unknown/No data
|
||||
};
|
||||
valueA = getStatusPriority(a);
|
||||
valueB = getStatusPriority(b);
|
||||
} else if (sortKey === "name") {
|
||||
// Sort alphabetically by name
|
||||
valueA = a.name ? a.name.toLowerCase() : "";
|
||||
valueB = b.name ? b.name.toLowerCase() : "";
|
||||
} else if (sortKey === "uptime") {
|
||||
// Sort by uptime
|
||||
const uptimeList = this.$root.uptimeList || {};
|
||||
const uptimeA = a.id ? parseFloat(uptimeList[ `${a.id}_24` ]) || 0 : 0;
|
||||
const uptimeB = b.id ? parseFloat(uptimeList[ `${b.id}_24` ]) || 0 : 0;
|
||||
valueA = uptimeA;
|
||||
valueB = uptimeB;
|
||||
} else if (sortKey === "cert") {
|
||||
// Sort by certificate expiry time
|
||||
valueA = a.validCert && a.certExpiryDaysRemaining ? a.certExpiryDaysRemaining : -1;
|
||||
valueB = b.validCert && b.certExpiryDaysRemaining ? b.certExpiryDaysRemaining : -1;
|
||||
}
|
||||
|
||||
if (valueA < valueB) {
|
||||
comparison = -1;
|
||||
} else if (valueA > valueB) {
|
||||
comparison = 1;
|
||||
}
|
||||
|
||||
// Special handling for status sorting
|
||||
if (sortKey === "status") {
|
||||
return sortDirection === "desc" ? (comparison * -1) : comparison;
|
||||
} else {
|
||||
return sortDirection === "asc" ? comparison : (comparison * -1);
|
||||
}
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get unique identifier for the group
|
||||
* @returns {string} group identifier
|
||||
*/
|
||||
getGroupIdentifier() {
|
||||
// Prefer a stable server-provided id to avoid clashes between groups with the same name
|
||||
if (this.group.id !== undefined && this.group.id !== null) {
|
||||
return this.group.id.toString();
|
||||
}
|
||||
|
||||
// Fallback to the current index for unsaved groups
|
||||
return `group${this.groupIndex}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars";
|
||||
|
||||
.sort-dropdown {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.sort-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.3rem 0.6rem;
|
||||
min-width: 40px;
|
||||
border-radius: 10px;
|
||||
background-color: white;
|
||||
border: none;
|
||||
box-shadow: 0 15px 70px rgba(0, 0, 0, 0.1);
|
||||
transition: all ease-in-out 0.15s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
&:focus, &:active {
|
||||
box-shadow: 0 15px 70px rgba(0, 0, 0, 0.1);
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.dark & {
|
||||
background-color: $dark-bg;
|
||||
color: $dark-font-color;
|
||||
box-shadow: 0 15px 70px rgba(0, 0, 0, 0.3);
|
||||
|
||||
&:hover {
|
||||
background-color: $dark-bg2;
|
||||
}
|
||||
|
||||
&:focus, &:active {
|
||||
box-shadow: 0 15px 70px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sort-arrows {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.arrow-inactive {
|
||||
color: #aaa;
|
||||
font-size: 0.7rem;
|
||||
opacity: 0.5;
|
||||
|
||||
.dark & {
|
||||
color: #6c757d;
|
||||
}
|
||||
}
|
||||
|
||||
.arrow-active {
|
||||
color: #4caf50;
|
||||
font-size: 0.8rem;
|
||||
|
||||
.dark & {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
.sort-menu {
|
||||
min-width: auto;
|
||||
width: auto;
|
||||
padding: 0.2rem 0;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
box-shadow: 0 15px 70px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
|
||||
.dark & {
|
||||
background-color: $dark-bg;
|
||||
color: $dark-font-color;
|
||||
border-color: $dark-border-color;
|
||||
box-shadow: 0 15px 70px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.sort-item {
|
||||
padding: 0.4rem 0.8rem;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.dark & {
|
||||
color: $dark-font-color;
|
||||
|
||||
&:hover {
|
||||
background-color: $dark-bg2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sort-item-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.sort-indicators {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -10,9 +10,18 @@
|
||||
<div class="mb-5" data-testid="group">
|
||||
<!-- Group Title -->
|
||||
<h2 class="group-title">
|
||||
<font-awesome-icon v-if="editMode && showGroupDrag" icon="arrows-alt-v" class="action drag me-3" />
|
||||
<font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeGroup(group.index)" />
|
||||
<Editable v-model="group.element.name" :contenteditable="editMode" tag="span" data-testid="group-name" />
|
||||
<div class="title-section">
|
||||
<font-awesome-icon v-if="editMode && showGroupDrag" icon="arrows-alt-v" class="action drag me-3" />
|
||||
<font-awesome-icon v-if="editMode" icon="times" class="action remove me-3" @click="removeGroup(group.index)" />
|
||||
<Editable v-model="group.element.name" :contenteditable="editMode" tag="span" data-testid="group-name" />
|
||||
</div>
|
||||
|
||||
<GroupSortDropdown
|
||||
:group="group.element"
|
||||
:group-index="group.index"
|
||||
:show-certificate-expiry="showCertificateExpiry"
|
||||
@update-group="updateGroup"
|
||||
/>
|
||||
</h2>
|
||||
|
||||
<div class="shadow-box monitor-list mt-4 position-relative">
|
||||
@@ -87,6 +96,7 @@ import Draggable from "vuedraggable";
|
||||
import HeartbeatBar from "./HeartbeatBar.vue";
|
||||
import Uptime from "./Uptime.vue";
|
||||
import Tag from "./Tag.vue";
|
||||
import GroupSortDropdown from "./GroupSortDropdown.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -95,6 +105,7 @@ export default {
|
||||
HeartbeatBar,
|
||||
Uptime,
|
||||
Tag,
|
||||
GroupSortDropdown,
|
||||
},
|
||||
props: {
|
||||
/** Are we in edit mode? */
|
||||
@@ -113,7 +124,6 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -121,8 +131,11 @@ export default {
|
||||
return (this.$root.publicGroupList.length >= 2);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// No watchers needed - sorting is handled by GroupSortDropdown component
|
||||
},
|
||||
created() {
|
||||
|
||||
// Sorting is now handled by GroupSortDropdown component
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
@@ -136,8 +149,7 @@ export default {
|
||||
|
||||
/**
|
||||
* Remove a monitor from a group
|
||||
* @param {number} groupIndex Index of group to remove monitor
|
||||
* from
|
||||
* @param {number} groupIndex Index of group to remove monitor from
|
||||
* @param {number} index Index of monitor to remove
|
||||
* @returns {void}
|
||||
*/
|
||||
@@ -158,7 +170,9 @@ export default {
|
||||
// We must check if there are any elements in monitorList to
|
||||
// prevent undefined errors if it hasn't been loaded yet
|
||||
if (this.$parent.editMode && ignoreSendUrl && Object.keys(this.$root.monitorList).length) {
|
||||
return this.$root.monitorList[monitor.element.id].type === "http" || this.$root.monitorList[monitor.element.id].type === "keyword" || this.$root.monitorList[monitor.element.id].type === "json-query";
|
||||
return this.$root.monitorList[monitor.element.id].type === "http" ||
|
||||
this.$root.monitorList[monitor.element.id].type === "keyword" ||
|
||||
this.$root.monitorList[monitor.element.id].type === "json-query";
|
||||
}
|
||||
return monitor.element.sendUrl && monitor.element.url && monitor.element.url !== "https://";
|
||||
},
|
||||
@@ -189,6 +203,32 @@ export default {
|
||||
}
|
||||
return "#DC2626";
|
||||
},
|
||||
|
||||
/**
|
||||
* Update group properties
|
||||
* @param {number} groupIndex Index of group to update
|
||||
* @param {object} updates Object with properties to update
|
||||
* @returns {void}
|
||||
*/
|
||||
updateGroup(groupIndex, updates) {
|
||||
Object.assign(this.$root.publicGroupList[groupIndex], updates);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get unique identifier for a group
|
||||
* @param {object} group object
|
||||
* @returns {string} group identifier
|
||||
*/
|
||||
getGroupIdentifier(group) {
|
||||
// Use the name directly if available
|
||||
if (group.name) {
|
||||
// Only remove spaces and use encodeURIComponent for URL safety
|
||||
const cleanName = group.name.replace(/\s+/g, "");
|
||||
return cleanName;
|
||||
}
|
||||
// Fallback to ID or index
|
||||
return group.id ? `group${group.id}` : `group${this.$root.publicGroupList.indexOf(group)}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -250,6 +290,15 @@ export default {
|
||||
}
|
||||
|
||||
.group-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.title-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
min-width: 15px;
|
||||
@@ -260,10 +309,14 @@ export default {
|
||||
.item {
|
||||
padding: 13px 0 10px;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.bg-maintenance {
|
||||
background-color: $maintenance;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -8,6 +8,8 @@ import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
// 2) add the icon name to the library.add() statement below.
|
||||
import {
|
||||
faArrowAltCircleUp,
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faCog,
|
||||
faEdit,
|
||||
faEye,
|
||||
@@ -54,6 +56,8 @@ import {
|
||||
|
||||
library.add(
|
||||
faArrowAltCircleUp,
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faCog,
|
||||
faEdit,
|
||||
faEye,
|
||||
@@ -100,4 +104,3 @@ library.add(
|
||||
);
|
||||
|
||||
export { FontAwesomeIcon };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user