Merge branch 'master' into feature/locale_on_status_page

This commit is contained in:
Wampie Driessen 2023-08-04 12:21:33 +02:00 committed by GitHub
commit 1ceb8e98d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 296 additions and 62 deletions

View file

@ -4,8 +4,4 @@ if (process.env.TEST_FRONTEND) {
config.presets = [ "@babel/preset-env" ]; config.presets = [ "@babel/preset-env" ];
} }
if (process.env.TEST_BACKEND) {
config.plugins = [ "babel-plugin-rewire" ];
}
module.exports = config; module.exports = config;

7
package-lock.json generated
View file

@ -90,7 +90,6 @@
"@vue/compiler-sfc": "~3.3.4", "@vue/compiler-sfc": "~3.3.4",
"@vuepic/vue-datepicker": "~3.4.8", "@vuepic/vue-datepicker": "~3.4.8",
"aedes": "^0.46.3", "aedes": "^0.46.3",
"babel-plugin-rewire": "~1.2.0",
"bootstrap": "5.1.3", "bootstrap": "5.1.3",
"chart.js": "~4.2.1", "chart.js": "~4.2.1",
"chartjs-adapter-dayjs-4": "~1.0.4", "chartjs-adapter-dayjs-4": "~1.0.4",
@ -6623,12 +6622,6 @@
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
} }
}, },
"node_modules/babel-plugin-rewire": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/babel-plugin-rewire/-/babel-plugin-rewire-1.2.0.tgz",
"integrity": "sha512-JBZxczHw3tScS+djy6JPLMjblchGhLI89ep15H3SyjujIzlxo5nr6Yjo7AXotdeVczeBmWs0tF8PgJWDdgzAkQ==",
"dev": true
},
"node_modules/babel-preset-current-node-syntax": { "node_modules/babel-preset-current-node-syntax": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz",

View file

@ -153,7 +153,6 @@
"@vue/compiler-sfc": "~3.3.4", "@vue/compiler-sfc": "~3.3.4",
"@vuepic/vue-datepicker": "~3.4.8", "@vuepic/vue-datepicker": "~3.4.8",
"aedes": "^0.46.3", "aedes": "^0.46.3",
"babel-plugin-rewire": "~1.2.0",
"bootstrap": "5.1.3", "bootstrap": "5.1.3",
"chart.js": "~4.2.1", "chart.js": "~4.2.1",
"chartjs-adapter-dayjs-4": "~1.0.4", "chartjs-adapter-dayjs-4": "~1.0.4",

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

@ -1047,3 +1047,13 @@ module.exports.grpcQuery = async (options) => {
}); });
}; };
// For unit test, export functions
if (process.env.TEST_BACKEND) {
module.exports.__test = {
parseCertificateInfo,
};
module.exports.__getPrivateFunction = (functionName) => {
return module.exports.__test[functionName];
};
}

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>

View file

@ -1,5 +1,5 @@
const { genSecret, DOWN, log} = require("../src/util"); const { genSecret, DOWN, log} = require("../src/util");
const utilServerRewire = require("../server/util-server"); const utilServer = require("../server/util-server");
const Discord = require("../server/notification-providers/discord"); const Discord = require("../server/notification-providers/discord");
const axios = require("axios"); const axios = require("axios");
const { UptimeKumaServer } = require("../server/uptime-kuma-server"); const { UptimeKumaServer } = require("../server/uptime-kuma-server");
@ -14,13 +14,13 @@ jest.mock("axios");
describe("Test parseCertificateInfo", () => { describe("Test parseCertificateInfo", () => {
it("should handle undefined", async () => { it("should handle undefined", async () => {
const parseCertificateInfo = utilServerRewire.__get__("parseCertificateInfo"); const parseCertificateInfo = utilServer.__getPrivateFunction("parseCertificateInfo");
const info = parseCertificateInfo(undefined); const info = parseCertificateInfo(undefined);
expect(info).toEqual(undefined); expect(info).toEqual(undefined);
}, 5000); }, 5000);
it("should handle normal cert chain", async () => { it("should handle normal cert chain", async () => {
const parseCertificateInfo = utilServerRewire.__get__("parseCertificateInfo"); const parseCertificateInfo = utilServer.__getPrivateFunction("parseCertificateInfo");
const chain1 = { const chain1 = {
fingerprint: "CF:2C:F3:6A:FE:6B:10:EC:44:77:C8:95:BB:96:2E:06:1F:0E:15:DA", fingerprint: "CF:2C:F3:6A:FE:6B:10:EC:44:77:C8:95:BB:96:2E:06:1F:0E:15:DA",
@ -52,7 +52,7 @@ describe("Test parseCertificateInfo", () => {
}, 5000); }, 5000);
it("should handle cert chain with strange circle", async () => { it("should handle cert chain with strange circle", async () => {
const parseCertificateInfo = utilServerRewire.__get__("parseCertificateInfo"); const parseCertificateInfo = utilServer.__getPrivateFunction("parseCertificateInfo");
const chain1 = { const chain1 = {
fingerprint: "CF:2C:F3:6A:FE:6B:10:EC:44:77:C8:95:BB:96:2E:06:1F:0E:15:DA", fingerprint: "CF:2C:F3:6A:FE:6B:10:EC:44:77:C8:95:BB:96:2E:06:1F:0E:15:DA",
@ -92,7 +92,7 @@ describe("Test parseCertificateInfo", () => {
}, 5000); }, 5000);
it("should handle cert chain with last undefined (should be happen in real, but just in case)", async () => { it("should handle cert chain with last undefined (should be happen in real, but just in case)", async () => {
const parseCertificateInfo = utilServerRewire.__get__("parseCertificateInfo"); const parseCertificateInfo = utilServer.__getPrivateFunction("parseCertificateInfo");
const chain1 = { const chain1 = {
fingerprint: "CF:2C:F3:6A:FE:6B:10:EC:44:77:C8:95:BB:96:2E:06:1F:0E:15:DA", fingerprint: "CF:2C:F3:6A:FE:6B:10:EC:44:77:C8:95:BB:96:2E:06:1F:0E:15:DA",
@ -213,22 +213,22 @@ describe("Test Discord Notification Provider", () => {
describe("The function filterAndJoin", () => { describe("The function filterAndJoin", () => {
it("should join and array of strings to one string", () => { it("should join and array of strings to one string", () => {
const result = utilServerRewire.filterAndJoin([ "one", "two", "three" ]); const result = utilServer.filterAndJoin([ "one", "two", "three" ]);
expect(result).toBe("onetwothree"); expect(result).toBe("onetwothree");
}); });
it("should join strings using a given connector", () => { it("should join strings using a given connector", () => {
const result = utilServerRewire.filterAndJoin([ "one", "two", "three" ], "-"); const result = utilServer.filterAndJoin([ "one", "two", "three" ], "-");
expect(result).toBe("one-two-three"); expect(result).toBe("one-two-three");
}); });
it("should filter null, undefined and empty strings before joining", () => { it("should filter null, undefined and empty strings before joining", () => {
const result = utilServerRewire.filterAndJoin([ undefined, "", "three" ], "--"); const result = utilServer.filterAndJoin([ undefined, "", "three" ], "--");
expect(result).toBe("three"); expect(result).toBe("three");
}); });
it("should return an empty string if all parts are filtered out", () => { it("should return an empty string if all parts are filtered out", () => {
const result = utilServerRewire.filterAndJoin([ undefined, "", "" ], "--"); const result = utilServer.filterAndJoin([ undefined, "", "" ], "--");
expect(result).toBe(""); expect(result).toBe("");
}); });
}); });