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:
Nelson Chan 2023-08-04 14:48:21 +08:00 committed by GitHub
parent d231a05526
commit a032e11a2e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 277 additions and 41 deletions

View File

@ -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;

View File

@ -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}`);

View 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>

View 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>

View File

@ -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>