mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-12-18 02:47:24 +00:00
Feat: Create Group in EditMonitor page (#3379)
* Feat: Create Group in EditMonitor page * Fix: Start group mon. after child is added * Chore: Swap confirm & cancel for ergonomics * Fix rarely issue that group monitor can throw an error if lastBeat is null * Resume the group monitor in the callback --------- Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
This commit is contained in:
parent
d231a05526
commit
a032e11a2e
5 changed files with 277 additions and 41 deletions
|
@ -351,7 +351,10 @@ class Monitor extends BeanModel {
|
||||||
const lastBeat = await Monitor.getPreviousHeartbeat(child.id);
|
const lastBeat = await Monitor.getPreviousHeartbeat(child.id);
|
||||||
|
|
||||||
// Only change state if the monitor is in worse conditions then the ones before
|
// Only change state if the monitor is in worse conditions then the ones before
|
||||||
if (bean.status === UP && (lastBeat.status === PENDING || lastBeat.status === DOWN)) {
|
// lastBeat.status could be null
|
||||||
|
if (!lastBeat) {
|
||||||
|
bean.status = PENDING;
|
||||||
|
} else if (bean.status === UP && (lastBeat.status === PENDING || lastBeat.status === DOWN)) {
|
||||||
bean.status = lastBeat.status;
|
bean.status = lastBeat.status;
|
||||||
} else if (bean.status === PENDING && lastBeat.status === DOWN) {
|
} else if (bean.status === PENDING && lastBeat.status === DOWN) {
|
||||||
bean.status = lastBeat.status;
|
bean.status = lastBeat.status;
|
||||||
|
|
|
@ -657,7 +657,10 @@ let needSetup = false;
|
||||||
await updateMonitorNotification(bean.id, notificationIDList);
|
await updateMonitorNotification(bean.id, notificationIDList);
|
||||||
|
|
||||||
await server.sendMonitorList(socket);
|
await server.sendMonitorList(socket);
|
||||||
await startMonitor(socket.userID, bean.id);
|
|
||||||
|
if (monitor.active !== false) {
|
||||||
|
await startMonitor(socket.userID, bean.id);
|
||||||
|
}
|
||||||
|
|
||||||
log.info("monitor", `Added Monitor: ${monitor.id} User ID: ${socket.userID}`);
|
log.info("monitor", `Added Monitor: ${monitor.id} User ID: ${socket.userID}`);
|
||||||
|
|
||||||
|
|
70
src/components/ActionSelect.vue
Normal file
70
src/components/ActionSelect.vue
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<template>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<select ref="select" v-model="model" class="form-select" :disabled="disabled">
|
||||||
|
<option v-for="option in options" :key="option" :value="option.value">{{ option.label }}</option>
|
||||||
|
</select>
|
||||||
|
<a class="btn btn-outline-primary" @click="action()">
|
||||||
|
<font-awesome-icon :icon="icon" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/**
|
||||||
|
* Generic select field with a customizable action on the right.
|
||||||
|
* Action is passed in as a function.
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
options: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* The value of the select field.
|
||||||
|
*/
|
||||||
|
modelValue: {
|
||||||
|
type: Number,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Whether the select field is enabled / disabled.
|
||||||
|
*/
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* The icon displayed in the right button of the select field.
|
||||||
|
* Accepts a Font Awesome icon string identifier.
|
||||||
|
* @example "plus"
|
||||||
|
*/
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* The action to be performed when the button is clicked.
|
||||||
|
* Action is passed in as a function.
|
||||||
|
*/
|
||||||
|
action: {
|
||||||
|
type: Function,
|
||||||
|
default: () => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: [ "update:modelValue" ],
|
||||||
|
computed: {
|
||||||
|
/**
|
||||||
|
* Send value update to parent on change.
|
||||||
|
*/
|
||||||
|
model: {
|
||||||
|
get() {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit("update:modelValue", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
56
src/components/CreateGroupDialog.vue
Normal file
56
src/components/CreateGroupDialog.vue
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
<template>
|
||||||
|
<div ref="modal" class="modal fade" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">
|
||||||
|
{{ $t("New Group") }}
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form @submit.prevent="confirm">
|
||||||
|
<div>
|
||||||
|
<label for="draftGroupName" class="form-label">{{ $t("Group Name") }}</label>
|
||||||
|
<input id="draftGroupName" v-model="groupName" type="text" class="form-control">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||||
|
{{ $t("Cancel") }}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" :disabled="groupName == '' || groupName == null" @click="confirm">
|
||||||
|
{{ $t("Confirm") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Modal } from "bootstrap";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {},
|
||||||
|
emits: [ "added" ],
|
||||||
|
data: () => ({
|
||||||
|
modal: null,
|
||||||
|
groupName: null,
|
||||||
|
}),
|
||||||
|
mounted() {
|
||||||
|
this.modal = new Modal(this.$refs.modal);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/** Show the confirm dialog */
|
||||||
|
show() {
|
||||||
|
this.modal.show();
|
||||||
|
},
|
||||||
|
confirm() {
|
||||||
|
this.$emit("added", this.groupName);
|
||||||
|
this.modal.hide();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -102,11 +102,13 @@
|
||||||
<!-- Parent Monitor -->
|
<!-- Parent Monitor -->
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<label for="parent" class="form-label">{{ $t("Monitor Group") }}</label>
|
<label for="parent" class="form-label">{{ $t("Monitor Group") }}</label>
|
||||||
<select v-model="monitor.parent" class="form-select" :disabled="sortedMonitorList.length === 0">
|
<ActionSelect
|
||||||
<option v-if="sortedMonitorList.length === 0" :value="null" selected>{{ $t("noGroupMonitorMsg") }}</option>
|
v-model="monitor.parent"
|
||||||
<option v-else :value="null" selected>{{ $t("None") }}</option>
|
:options="parentMonitorOptionsList"
|
||||||
<option v-for="parentMonitor in sortedMonitorList" :key="parentMonitor.id" :value="parentMonitor.id">{{ parentMonitor.pathName }}</option>
|
:disabled="sortedGroupMonitorList.length === 0 && draftGroupName == null"
|
||||||
</select>
|
:icon="'plus'"
|
||||||
|
:action="() => $refs.createGroupDialog.show()"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- URL -->
|
<!-- URL -->
|
||||||
|
@ -807,6 +809,7 @@
|
||||||
<NotificationDialog ref="notificationDialog" @added="addedNotification" />
|
<NotificationDialog ref="notificationDialog" @added="addedNotification" />
|
||||||
<DockerHostDialog ref="dockerHostDialog" @added="addedDockerHost" />
|
<DockerHostDialog ref="dockerHostDialog" @added="addedDockerHost" />
|
||||||
<ProxyDialog ref="proxyDialog" @added="addedProxy" />
|
<ProxyDialog ref="proxyDialog" @added="addedProxy" />
|
||||||
|
<CreateGroupDialog ref="createGroupDialog" @added="addedDraftGroup" />
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
|
@ -814,20 +817,60 @@
|
||||||
<script>
|
<script>
|
||||||
import VueMultiselect from "vue-multiselect";
|
import VueMultiselect from "vue-multiselect";
|
||||||
import { useToast } from "vue-toastification";
|
import { useToast } from "vue-toastification";
|
||||||
|
import ActionSelect from "../components/ActionSelect.vue";
|
||||||
import CopyableInput from "../components/CopyableInput.vue";
|
import CopyableInput from "../components/CopyableInput.vue";
|
||||||
|
import CreateGroupDialog from "../components/CreateGroupDialog.vue";
|
||||||
import NotificationDialog from "../components/NotificationDialog.vue";
|
import NotificationDialog from "../components/NotificationDialog.vue";
|
||||||
import DockerHostDialog from "../components/DockerHostDialog.vue";
|
import DockerHostDialog from "../components/DockerHostDialog.vue";
|
||||||
import ProxyDialog from "../components/ProxyDialog.vue";
|
import ProxyDialog from "../components/ProxyDialog.vue";
|
||||||
import TagsManager from "../components/TagsManager.vue";
|
import TagsManager from "../components/TagsManager.vue";
|
||||||
import { genSecret, isDev, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND } from "../util.ts";
|
import { genSecret, isDev, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND } from "../util.ts";
|
||||||
import { hostNameRegexPattern } from "../util-frontend";
|
import { hostNameRegexPattern } from "../util-frontend";
|
||||||
|
import { sleep } from "../util";
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
|
const monitorDefaults = {
|
||||||
|
type: "http",
|
||||||
|
name: "",
|
||||||
|
parent: null,
|
||||||
|
url: "https://",
|
||||||
|
method: "GET",
|
||||||
|
interval: 60,
|
||||||
|
retryInterval: 60,
|
||||||
|
resendInterval: 0,
|
||||||
|
maxretries: 1,
|
||||||
|
notificationIDList: {},
|
||||||
|
ignoreTls: false,
|
||||||
|
upsideDown: false,
|
||||||
|
packetSize: 56,
|
||||||
|
expiryNotification: false,
|
||||||
|
maxredirects: 10,
|
||||||
|
accepted_statuscodes: [ "200-299" ],
|
||||||
|
dns_resolve_type: "A",
|
||||||
|
dns_resolve_server: "1.1.1.1",
|
||||||
|
docker_container: "",
|
||||||
|
docker_host: null,
|
||||||
|
proxyId: null,
|
||||||
|
mqttUsername: "",
|
||||||
|
mqttPassword: "",
|
||||||
|
mqttTopic: "",
|
||||||
|
mqttSuccessMessage: "",
|
||||||
|
authMethod: null,
|
||||||
|
oauth_auth_method: "client_secret_basic",
|
||||||
|
httpBodyEncoding: "json",
|
||||||
|
kafkaProducerBrokers: [],
|
||||||
|
kafkaProducerSaslOptions: {
|
||||||
|
mechanism: "None",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
ActionSelect,
|
||||||
ProxyDialog,
|
ProxyDialog,
|
||||||
CopyableInput,
|
CopyableInput,
|
||||||
|
CreateGroupDialog,
|
||||||
NotificationDialog,
|
NotificationDialog,
|
||||||
DockerHostDialog,
|
DockerHostDialog,
|
||||||
TagsManager,
|
TagsManager,
|
||||||
|
@ -855,7 +898,8 @@ export default {
|
||||||
"mysql": "mysql://username:password@host:port/database",
|
"mysql": "mysql://username:password@host:port/database",
|
||||||
"redis": "redis://user:password@host:port",
|
"redis": "redis://user:password@host:port",
|
||||||
"mongodb": "mongodb://username:password@host:port/database",
|
"mongodb": "mongodb://username:password@host:port/database",
|
||||||
}
|
},
|
||||||
|
draftGroupName: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -966,7 +1010,7 @@ message HealthCheckResponse {
|
||||||
|
|
||||||
// Filter result by active state, weight and alphabetical
|
// Filter result by active state, weight and alphabetical
|
||||||
// Only return groups which arent't itself and one of its decendants
|
// Only return groups which arent't itself and one of its decendants
|
||||||
sortedMonitorList() {
|
sortedGroupMonitorList() {
|
||||||
let result = Object.values(this.$root.monitorList);
|
let result = Object.values(this.$root.monitorList);
|
||||||
|
|
||||||
// Only groups, not itself, not a decendant
|
// Only groups, not itself, not a decendant
|
||||||
|
@ -1005,6 +1049,45 @@ message HealthCheckResponse {
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the parent monitor options list based on the sorted group monitor list and draft group name.
|
||||||
|
*
|
||||||
|
* @return {Array} The parent monitor options list.
|
||||||
|
*/
|
||||||
|
parentMonitorOptionsList() {
|
||||||
|
let list = [];
|
||||||
|
if (this.sortedGroupMonitorList.length === 0 && this.draftGroupName == null) {
|
||||||
|
list = [
|
||||||
|
{
|
||||||
|
label: this.$t("noGroupMonitorMsg"),
|
||||||
|
value: null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
list = [
|
||||||
|
{
|
||||||
|
label: this.$t("None"),
|
||||||
|
value: null
|
||||||
|
},
|
||||||
|
... this.sortedGroupMonitorList.map(monitor => {
|
||||||
|
return {
|
||||||
|
label: monitor.pathName,
|
||||||
|
value: monitor.id,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.draftGroupName != null) {
|
||||||
|
list = [{
|
||||||
|
label: this.draftGroupName,
|
||||||
|
value: -1,
|
||||||
|
}].concat(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
"$root.proxyList"() {
|
"$root.proxyList"() {
|
||||||
|
@ -1131,38 +1214,7 @@ message HealthCheckResponse {
|
||||||
if (this.isAdd) {
|
if (this.isAdd) {
|
||||||
|
|
||||||
this.monitor = {
|
this.monitor = {
|
||||||
type: "http",
|
...monitorDefaults
|
||||||
name: "",
|
|
||||||
parent: null,
|
|
||||||
url: "https://",
|
|
||||||
method: "GET",
|
|
||||||
interval: 60,
|
|
||||||
retryInterval: this.interval,
|
|
||||||
resendInterval: 0,
|
|
||||||
maxretries: 1,
|
|
||||||
notificationIDList: {},
|
|
||||||
ignoreTls: false,
|
|
||||||
upsideDown: false,
|
|
||||||
packetSize: 56,
|
|
||||||
expiryNotification: false,
|
|
||||||
maxredirects: 10,
|
|
||||||
accepted_statuscodes: [ "200-299" ],
|
|
||||||
dns_resolve_type: "A",
|
|
||||||
dns_resolve_server: "1.1.1.1",
|
|
||||||
docker_container: "",
|
|
||||||
docker_host: null,
|
|
||||||
proxyId: null,
|
|
||||||
mqttUsername: "",
|
|
||||||
mqttPassword: "",
|
|
||||||
mqttTopic: "",
|
|
||||||
mqttSuccessMessage: "",
|
|
||||||
authMethod: null,
|
|
||||||
oauth_auth_method: "client_secret_basic",
|
|
||||||
httpBodyEncoding: "json",
|
|
||||||
kafkaProducerBrokers: [],
|
|
||||||
kafkaProducerSaslOptions: {
|
|
||||||
mechanism: "None",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.$root.proxyList && !this.monitor.proxyId) {
|
if (this.$root.proxyList && !this.monitor.proxyId) {
|
||||||
|
@ -1228,6 +1280,8 @@ message HealthCheckResponse {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.draftGroupName = null;
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
addKafkaProducerBroker(newBroker) {
|
addKafkaProducerBroker(newBroker) {
|
||||||
|
@ -1292,16 +1346,46 @@ message HealthCheckResponse {
|
||||||
this.monitor.url = this.monitor.url.trim();
|
this.monitor.url = this.monitor.url.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let createdNewParent = false;
|
||||||
|
|
||||||
|
if (this.draftGroupName && this.monitor.parent === -1) {
|
||||||
|
// Create Monitor with name of draft group
|
||||||
|
const res = await new Promise((resolve) => {
|
||||||
|
this.$root.add({
|
||||||
|
...monitorDefaults,
|
||||||
|
type: "group",
|
||||||
|
name: this.draftGroupName,
|
||||||
|
interval: this.monitor.interval,
|
||||||
|
active: false,
|
||||||
|
}, resolve);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
createdNewParent = true;
|
||||||
|
this.monitor.parent = res.monitorID;
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
this.processing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isAdd || this.isClone) {
|
if (this.isAdd || this.isClone) {
|
||||||
this.$root.add(this.monitor, async (res) => {
|
this.$root.add(this.monitor, async (res) => {
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
await this.$refs.tagsManager.submit(res.monitorID);
|
await this.$refs.tagsManager.submit(res.monitorID);
|
||||||
|
|
||||||
|
// Start the new parent monitor after edit is done
|
||||||
|
if (createdNewParent) {
|
||||||
|
this.startParentGroupMonitor();
|
||||||
|
}
|
||||||
|
|
||||||
toast.success(res.msg);
|
toast.success(res.msg);
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
this.$root.getMonitorList();
|
this.$root.getMonitorList();
|
||||||
this.$router.push("/dashboard/" + res.monitorID);
|
this.$router.push("/dashboard/" + res.monitorID);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.msg);
|
toast.error(res.msg);
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
|
@ -1315,10 +1399,20 @@ message HealthCheckResponse {
|
||||||
this.processing = false;
|
this.processing = false;
|
||||||
this.$root.toastRes(res);
|
this.$root.toastRes(res);
|
||||||
this.init();
|
this.init();
|
||||||
|
|
||||||
|
// Start the new parent monitor after edit is done
|
||||||
|
if (createdNewParent) {
|
||||||
|
this.startParentGroupMonitor();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async startParentGroupMonitor() {
|
||||||
|
await sleep(2000);
|
||||||
|
await this.$root.getSocket().emit("resumeMonitor", this.monitor.parent, () => {});
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Added a Notification Event
|
* Added a Notification Event
|
||||||
* Enable it if the notification is added in EditMonitor.vue
|
* Enable it if the notification is added in EditMonitor.vue
|
||||||
|
@ -1342,6 +1436,16 @@ message HealthCheckResponse {
|
||||||
addedDockerHost(id) {
|
addedDockerHost(id) {
|
||||||
this.monitor.docker_host = id;
|
this.monitor.docker_host = id;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a draft group.
|
||||||
|
*
|
||||||
|
* @param {string} draftGroupName - The name of the draft group.
|
||||||
|
*/
|
||||||
|
addedDraftGroup(draftGroupName) {
|
||||||
|
this.draftGroupName = draftGroupName;
|
||||||
|
this.monitor.parent = -1;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
Loading…
Reference in a new issue