Merge branch 'master' into 1.23.X-merge-to-2.X.X

# Conflicts:
#	docker/debian-base.dockerfile
#	package-lock.json
#	server/database.js
#	server/model/monitor.js
#	server/uptime-kuma-server.js
#	server/util-server.js
This commit is contained in:
Louis Lam
2023-11-13 21:15:51 +08:00
316 changed files with 9487 additions and 4997 deletions

View File

@@ -51,7 +51,10 @@ export default {
};
},
methods: {
/** Submit form data to add new status page */
/**
* Submit form data to add new status page
* @returns {Promise<void>}
*/
async submit() {
this.processing = true;
@@ -63,7 +66,7 @@ export default {
} else {
if (res.msg.includes("UNIQUE constraint")) {
this.$root.toastError(this.$t("The slug is already taken. Please choose another slug."));
this.$root.toastError("The slug is already taken. Please choose another slug.");
} else {
this.$root.toastRes(res);
}

View File

@@ -42,13 +42,13 @@
</thead>
<tbody>
<tr v-for="(beat, index) in displayedRecords" :key="index" :class="{ 'shadow-box': $root.windowWidth <= 550}">
<td><router-link :to="`/dashboard/${beat.monitorID}`">{{ beat.name }}</router-link></td>
<td><router-link :to="`/dashboard/${beat.monitorID}`">{{ $root.monitorList[beat.monitorID]?.name }}</router-link></td>
<td><Status :status="beat.status" /></td>
<td :class="{ 'border-0':! beat.msg}"><Datetime :value="beat.time" /></td>
<td class="border-0">{{ beat.msg }}</td>
</tr>
<tr v-if="importantHeartBeatList.length === 0">
<tr v-if="importantHeartBeatListLength === 0">
<td colspan="4">
{{ $t("No important events") }}
</td>
@@ -59,7 +59,7 @@
<div class="d-flex justify-content-center kuma_pagination">
<pagination
v-model="page"
:records="importantHeartBeatList.length"
:records="importantHeartBeatListLength"
:per-page="perPage"
:options="paginationConfig"
/>
@@ -92,72 +92,89 @@ export default {
page: 1,
perPage: 25,
initialPerPage: 25,
heartBeatList: [],
paginationConfig: {
hideCount: true,
chunksNavigation: "scroll",
},
importantHeartBeatListLength: 0,
displayedRecords: [],
};
},
computed: {
importantHeartBeatList() {
let result = [];
for (let monitorID in this.$root.importantHeartbeatList) {
let list = this.$root.importantHeartbeatList[monitorID];
result = result.concat(list);
}
for (let beat of result) {
let monitor = this.$root.monitorList[beat.monitorID];
if (monitor) {
beat.name = monitor.name;
}
}
result.sort((a, b) => {
if (a.time > b.time) {
return -1;
}
if (a.time < b.time) {
return 1;
}
return 0;
});
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
this.heartBeatList = result;
return result;
},
displayedRecords() {
const startIndex = this.perPage * (this.page - 1);
const endIndex = startIndex + this.perPage;
return this.heartBeatList.slice(startIndex, endIndex);
},
},
watch: {
importantHeartBeatList() {
perPage() {
this.$nextTick(() => {
this.updatePerPage();
this.getImportantHeartbeatListPaged();
});
},
page() {
this.getImportantHeartbeatListPaged();
},
},
mounted() {
this.getImportantHeartbeatListLength();
this.$root.emitter.on("newImportantHeartbeat", this.onNewImportantHeartbeat);
this.initialPerPage = this.perPage;
window.addEventListener("resize", this.updatePerPage);
this.updatePerPage();
},
beforeUnmount() {
this.$root.emitter.off("newImportantHeartbeat", this.onNewImportantHeartbeat);
window.removeEventListener("resize", this.updatePerPage);
},
methods: {
/**
* Updates the displayed records when a new important heartbeat arrives.
* @param {object} heartbeat - The heartbeat object received.
* @returns {void}
*/
onNewImportantHeartbeat(heartbeat) {
if (this.page === 1) {
this.displayedRecords.unshift(heartbeat);
if (this.displayedRecords.length > this.perPage) {
this.displayedRecords.pop();
}
this.importantHeartBeatListLength += 1;
}
},
/**
* Retrieves the length of the important heartbeat list for all monitors.
* @returns {void}
*/
getImportantHeartbeatListLength() {
this.$root.getSocket().emit("monitorImportantHeartbeatListCount", null, (res) => {
if (res.ok) {
this.importantHeartBeatListLength = res.count;
this.getImportantHeartbeatListPaged();
}
});
},
/**
* Retrieves the important heartbeat list for the current page.
* @returns {void}
*/
getImportantHeartbeatListPaged() {
const offset = (this.page - 1) * this.perPage;
this.$root.getSocket().emit("monitorImportantHeartbeatListPaged", null, offset, this.perPage, (res) => {
if (res.ok) {
this.displayedRecords = res.data;
}
});
},
/**
* Updates the number of items shown per page based on the available height.
* @returns {void}
*/
updatePerPage() {
const tableContainer = this.$refs.tableContainer;
const tableContainerHeight = tableContainer.offsetHeight;

View File

@@ -76,6 +76,34 @@
</div>
</div>
<!-- Push Examples -->
<div v-if="monitor.type === 'push'" class="shadow-box big-padding">
<a href="#" @click="pushMonitor.showPushExamples = !pushMonitor.showPushExamples">{{ $t("pushViewCode") }}</a>
<transition name="slide-fade" appear>
<div v-if="pushMonitor.showPushExamples" class="mt-3">
<select id="push-current-example" v-model="pushMonitor.currentExample" class="form-select">
<optgroup :label="$t('programmingLanguages')">
<option value="csharp">C#</option>
<option value="go">Go</option>
<option value="java">Java</option>
<option value="javascript-fetch">JavaScript (fetch)</option>
<option value="php">PHP</option>
<option value="python">Python</option>
<option value="typescript-fetch">TypeScript (fetch)</option>
</optgroup>
<optgroup :label="$t('pushOthers')">
<option value="bash-curl">Bash (curl)</option>
<option value="powershell">PowerShell</option>
<option value="docker">Docker</option>
</optgroup>
</select>
<prism-editor v-model="pushMonitor.code" class="css-editor mt-3" :highlight="pushExampleHighlighter" line-numbers readonly></prism-editor>
</div>
</transition>
</div>
<!-- Stats -->
<div class="shadow-box big-padding text-center stats">
<div class="row">
@@ -95,6 +123,8 @@
<CountUp :value="avgPing" />
</span>
</div>
<!-- Uptime (24-hour) -->
<div class="col-12 col-sm col row d-flex align-items-center d-sm-block">
<h4 class="col-4 col-sm-12">{{ $t("Uptime") }}</h4>
<p class="col-4 col-sm-12 mb-0 mb-sm-2">(24{{ $t("-hour") }})</p>
@@ -102,6 +132,8 @@
<Uptime :monitor="monitor" type="24" />
</span>
</div>
<!-- Uptime (30-day) -->
<div class="col-12 col-sm col row d-flex align-items-center d-sm-block">
<h4 class="col-4 col-sm-12">{{ $t("Uptime") }}</h4>
<p class="col-4 col-sm-12 mb-0 mb-sm-2">(30{{ $t("-day") }})</p>
@@ -110,6 +142,15 @@
</span>
</div>
<!-- Uptime (1-year) -->
<div class="col-12 col-sm col row d-flex align-items-center d-sm-block">
<h4 class="col-4 col-sm-12">{{ $t("Uptime") }}</h4>
<p class="col-4 col-sm-12 mb-0 mb-sm-2">(1{{ $t("-year") }})</p>
<span class="col-4 col-sm-12 num">
<Uptime :monitor="monitor" type="1y" />
</span>
</div>
<div v-if="tlsInfo" class="col-12 col-sm col row d-flex align-items-center d-sm-block">
<h4 class="col-4 col-sm-12">{{ $t("Cert Exp.") }}</h4>
<p class="col-4 col-sm-12 mb-0 mb-sm-2">(<Datetime :value="tlsInfo.certInfo.validTo" date-only />)</p>
@@ -182,7 +223,7 @@
<td class="border-0">{{ beat.msg }}</td>
</tr>
<tr v-if="importantHeartBeatList.length === 0">
<tr v-if="importantHeartBeatListLength === 0">
<td colspan="3">
{{ $t("No important events") }}
</td>
@@ -193,7 +234,7 @@
<div class="d-flex justify-content-center kuma_pagination">
<pagination
v-model="page"
:records="importantHeartBeatList.length"
:records="importantHeartBeatListLength"
:per-page="perPage"
:options="paginationConfig"
/>
@@ -236,6 +277,12 @@ import CertificateInfo from "../components/CertificateInfo.vue";
import { getMonitorRelativeURL } from "../util.ts";
import { URL } from "whatwg-url";
import { getResBaseURL } from "../util-frontend";
import { highlight, languages } from "prismjs/components/prism-core";
import "prismjs/components/prism-clike";
import "prismjs/components/prism-javascript";
import "prismjs/components/prism-css";
import { PrismEditor } from "vue-prism-editor";
import "vue-prism-editor/dist/prismeditor.min.css";
export default {
components: {
@@ -249,6 +296,7 @@ export default {
PingChart,
Tag,
CertificateInfo,
PrismEditor,
},
data() {
return {
@@ -262,6 +310,13 @@ export default {
chunksNavigation: "scroll",
},
cacheTime: Date.now(),
importantHeartBeatListLength: 0,
displayedRecords: [],
pushMonitor: {
showPushExamples: false,
currentExample: "javascript-fetch",
code: "",
},
};
},
computed: {
@@ -300,16 +355,6 @@ export default {
return this.$t("notAvailableShort");
},
importantHeartBeatList() {
if (this.$root.importantHeartbeatList[this.monitor.id]) {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
this.heartBeatList = this.$root.importantHeartbeatList[this.monitor.id];
return this.$root.importantHeartbeatList[this.monitor.id];
}
return [];
},
status() {
if (this.$root.statusList[this.monitor.id]) {
return this.$root.statusList[this.monitor.id];
@@ -333,12 +378,6 @@ export default {
return this.tlsInfo != null && this.toggleCertInfoBox;
},
displayedRecords() {
const startIndex = this.perPage * (this.page - 1);
const endIndex = startIndex + this.perPage;
return this.heartBeatList.slice(startIndex, endIndex);
},
group() {
if (!this.monitor.pathName.includes("/")) {
return "";
@@ -354,73 +393,136 @@ export default {
return getResBaseURL() + this.monitor.screenshot + "?time=" + this.cacheTime;
}
},
mounted() {
},
methods: {
getResBaseURL,
/** Request a test notification be sent for this monitor */
testNotification() {
this.$root.getSocket().emit("testNotification", this.monitor.id);
toast.success("Test notification is requested.");
watch: {
page(to) {
this.getImportantHeartbeatListPaged();
},
/** Show dialog to confirm pause */
monitor(to) {
this.getImportantHeartbeatListLength();
},
"monitor.type"() {
if (this.monitor && this.monitor.type === "push") {
this.loadPushExample();
}
},
"pushMonitor.currentExample"() {
this.loadPushExample();
},
},
mounted() {
this.getImportantHeartbeatListLength();
this.$root.emitter.on("newImportantHeartbeat", this.onNewImportantHeartbeat);
if (this.monitor && this.monitor.type === "push") {
if (this.lastHeartBeat.status === -1) {
this.pushMonitor.showPushExamples = true;
}
this.loadPushExample();
}
},
beforeUnmount() {
this.$root.emitter.off("newImportantHeartbeat", this.onNewImportantHeartbeat);
},
methods: {
getResBaseURL,
/**
* Request a test notification be sent for this monitor
* @returns {void}
*/
testNotification() {
this.$root.getSocket().emit("testNotification", this.monitor.id);
this.$root.toastSuccess("Test notification is requested.");
},
/**
* Show dialog to confirm pause
* @returns {void}
*/
pauseDialog() {
this.$refs.confirmPause.show();
},
/** Resume this monitor */
/**
* Resume this monitor
* @returns {void}
*/
resumeMonitor() {
this.$root.getSocket().emit("resumeMonitor", this.monitor.id, (res) => {
this.$root.toastRes(res);
});
},
/** Request that this monitor is paused */
/**
* Request that this monitor is paused
* @returns {void}
*/
pauseMonitor() {
this.$root.getSocket().emit("pauseMonitor", this.monitor.id, (res) => {
this.$root.toastRes(res);
});
},
/** Show dialog to confirm deletion */
/**
* Show dialog to confirm deletion
* @returns {void}
*/
deleteDialog() {
this.$refs.confirmDelete.show();
},
/** Show dialog to confirm clearing events */
/**
* Show dialog to confirm clearing events
* @returns {void}
*/
clearEventsDialog() {
this.$refs.confirmClearEvents.show();
},
/** Show dialog to confirm clearing heartbeats */
/**
* Show dialog to confirm clearing heartbeats
* @returns {void}
*/
clearHeartbeatsDialog() {
this.$refs.confirmClearHeartbeats.show();
},
/** Request that this monitor is deleted */
/**
* Request that this monitor is deleted
* @returns {void}
*/
deleteMonitor() {
this.$root.deleteMonitor(this.monitor.id, (res) => {
this.$root.toastRes(res);
if (res.ok) {
toast.success(res.msg);
this.$router.push("/dashboard");
}
});
},
/**
* Request that this monitors events are cleared
* @returns {void}
*/
clearEvents() {
this.$root.clearEvents(this.monitor.id, (res) => {
if (res.ok) {
this.getImportantHeartbeatListLength();
} else {
toast.error(res.msg);
}
});
},
/** Request that this monitors events are cleared */
clearEvents() {
this.$root.clearEvents(this.monitor.id, (res) => {
if (! res.ok) {
toast.error(res.msg);
}
});
},
/** Request that this monitors heartbeats are cleared */
/**
* Request that this monitors heartbeats are cleared
* @returns {void}
*/
clearHeartbeats() {
this.$root.clearHeartbeats(this.monitor.id, (res) => {
if (! res.ok) {
@@ -431,8 +533,8 @@ export default {
/**
* Return the correct title for the ping stat
* @param {boolean} [average=false] Is the statistic an average?
* @returns {string} Title formated dependant on monitor type
* @param {boolean} average Is the statistic an average?
* @returns {string} Title formatted dependant on monitor type
*/
pingTitle(average = false) {
let translationPrefix = "";
@@ -456,7 +558,11 @@ export default {
return getMonitorRelativeURL(id);
},
/** Filter and hide password in URL for display */
/**
* Filter and hide password in URL for display
* @param {string} urlString URL to censor
* @returns {string} Censored URL
*/
filterPassword(urlString) {
try {
let parsedUrl = new URL(urlString);
@@ -468,6 +574,72 @@ export default {
// Handle SQL Server
return urlString.replaceAll(/Password=(.+);/ig, "Password=******;");
}
},
/**
* Retrieves the length of the important heartbeat list for this monitor.
* @returns {void}
*/
getImportantHeartbeatListLength() {
if (this.monitor) {
this.$root.getSocket().emit("monitorImportantHeartbeatListCount", this.monitor.id, (res) => {
if (res.ok) {
this.importantHeartBeatListLength = res.count;
this.getImportantHeartbeatListPaged();
}
});
}
},
/**
* Retrieves the important heartbeat list for the current page.
* @returns {void}
*/
getImportantHeartbeatListPaged() {
if (this.monitor) {
const offset = (this.page - 1) * this.perPage;
this.$root.getSocket().emit("monitorImportantHeartbeatListPaged", this.monitor.id, offset, this.perPage, (res) => {
if (res.ok) {
this.displayedRecords = res.data;
}
});
}
},
/**
* Updates the displayed records when a new important heartbeat arrives.
* @param {object} heartbeat - The heartbeat object received.
* @returns {void}
*/
onNewImportantHeartbeat(heartbeat) {
if (heartbeat.monitorID === this.monitor?.id) {
if (this.page === 1) {
this.displayedRecords.unshift(heartbeat);
if (this.displayedRecords.length > this.perPage) {
this.displayedRecords.pop();
}
this.importantHeartBeatListLength += 1;
}
}
},
/**
* Highlight the example code
* @param {string} code Code
* @returns {string} Highlighted code
*/
pushExampleHighlighter(code) {
return highlight(code, languages.js);
},
loadPushExample() {
this.pushMonitor.code = "";
this.$root.getSocket().emit("getPushExample", this.pushMonitor.currentExample, (res) => {
let code = res.code
.replace("60", this.monitor.interval)
.replace("https://example.com/api/push/key?status=up&msg=OK&ping=", this.pushURL);
this.pushMonitor.code = code;
});
}
},
};

View File

@@ -246,14 +246,11 @@
</template>
<script>
import { useToast } from "vue-toastification";
import VueMultiselect from "vue-multiselect";
import Datepicker from "@vuepic/vue-datepicker";
import { timezoneList } from "../util-frontend";
import cronstrue from "cronstrue/i18n";
const toast = useToast();
export default {
components: {
VueMultiselect,
@@ -417,7 +414,10 @@ export default {
});
},
methods: {
/** Initialise page */
/**
* Initialise page
* @returns {void}
*/
init() {
this.affectedMonitors = [];
this.selectedStatusPages = [];
@@ -454,7 +454,7 @@ export default {
this.affectedMonitors.push(this.affectedMonitorsOptions.find(item => item.id === monitor.id));
});
} else {
toast.error(res.msg);
this.$root.toastError(res.msg);
}
});
@@ -469,22 +469,25 @@ export default {
this.showOnAllPages = Object.values(res.statusPages).length === this.selectedStatusPagesOptions.length;
} else {
toast.error(res.msg);
this.$root.toastError(res.msg);
}
});
} else {
toast.error(res.msg);
this.$root.toastError(res.msg);
}
});
}
},
/** Create new maintenance */
/**
* Create new maintenance
* @returns {Promise<void>}
*/
async submit() {
this.processing = true;
if (this.affectedMonitors.length === 0) {
toast.error(this.$t("atLeastOneMonitor"));
this.$root.toastError(this.$t("atLeastOneMonitor"));
return this.processing = false;
}
@@ -493,14 +496,14 @@ export default {
if (res.ok) {
await this.addMonitorMaintenance(res.maintenanceID, async () => {
await this.addMaintenanceStatusPage(res.maintenanceID, () => {
toast.success(res.msg);
this.$root.toastRes(res);
this.processing = false;
this.$root.getMaintenanceList();
this.$router.push("/maintenance");
});
});
} else {
toast.error(res.msg);
this.$root.toastRes(res);
this.processing = false;
}
@@ -518,7 +521,7 @@ export default {
});
} else {
this.processing = false;
toast.error(res.msg);
this.$root.toastError(res.msg);
}
});
}
@@ -526,13 +529,14 @@ export default {
/**
* Add monitor to maintenance
* @param {number} maintenanceID
* @param {socketCB} callback
* @param {number} maintenanceID ID of maintenance to modify
* @param {socketCB} callback Callback for socket response
* @returns {Promise<void>}
*/
async addMonitorMaintenance(maintenanceID, callback) {
await this.$root.addMonitorMaintenance(maintenanceID, this.affectedMonitors, async (res) => {
if (!res.ok) {
toast.error(res.msg);
this.$root.toastError(res.msg);
} else {
this.$root.getMonitorList();
}
@@ -543,13 +547,14 @@ export default {
/**
* Add status page to maintenance
* @param {number} maintenanceID
* @param {socketCB} callback
* @param {number} maintenanceID ID of maintenance to modify
* @param {socketCB} callback Callback for socket response
* @returns {void}
*/
async addMaintenanceStatusPage(maintenanceID, callback) {
await this.$root.addMaintenanceStatusPage(maintenanceID, (this.showOnAllPages) ? this.selectedStatusPagesOptions : this.selectedStatusPages, async (res) => {
if (!res.ok) {
toast.error(res.msg);
this.$root.toastError(res.msg);
} else {
this.$root.getMaintenanceList();
}

View File

@@ -119,6 +119,9 @@
{{ $t("needPushEvery", [monitor.interval]) }}<br />
{{ $t("pushOptionalParams", ["status, msg, ping"]) }}
</div>
<button class="btn btn-primary" type="button" @click="resetToken">
{{ $t("Reset Token") }}
</button>
</div>
<!-- Keyword -->
@@ -658,6 +661,7 @@
<label for="httpBodyEncoding" class="form-label">{{ $t("Body Encoding") }}</label>
<select id="httpBodyEncoding" v-model="monitor.httpBodyEncoding" class="form-select">
<option value="json">JSON</option>
<option value="form">x-www-form-urlencoded</option>
<option value="xml">XML</option>
</select>
</div>
@@ -849,7 +853,9 @@ import { hostNameRegexPattern } from "../util-frontend";
import { sleep } from "../util";
import HiddenInput from "../components/HiddenInput.vue";
const toast = useToast();
const toast = useToast;
const pushTokenLength = 32;
const monitorDefaults = {
type: "http",
@@ -1010,6 +1016,9 @@ message HealthCheckResponse {
</soap:Body>
</soap:Envelope>` ]);
}
if (this.monitor && this.monitor.httpBodyEncoding === "form") {
return this.$t("Example:", [ "key1=value1&key2=value2" ]);
}
return this.$t("Example:", [ `
{
"key": "value"
@@ -1077,8 +1086,7 @@ message HealthCheckResponse {
/**
* Generates the parent monitor options list based on the sorted group monitor list and draft group name.
*
* @return {Array} The parent monitor options list.
* @returns {Array} The parent monitor options list.
*/
parentMonitorOptionsList() {
let list = [];
@@ -1164,7 +1172,9 @@ message HealthCheckResponse {
"monitor.type"() {
if (this.monitor.type === "push") {
if (! this.monitor.pushToken) {
this.monitor.pushToken = genSecret(10);
// ideally this would require checking if the generated token is already used
// it's very unlikely to get a collision though (62^32 ~ 2.27265788 * 10^57 unique tokens)
this.monitor.pushToken = genSecret(pushTokenLength);
}
}
@@ -1185,7 +1195,7 @@ message HealthCheckResponse {
if (res.ok) {
this.gameList = res.gameList;
} else {
toast.error(res.msg);
this.$root.toastError(res.msg);
}
});
}
@@ -1257,7 +1267,10 @@ message HealthCheckResponse {
this.kafkaSaslMechanismOptions = kafkaSaslMechanismOptions;
},
methods: {
/** Initialize the edit monitor form */
/**
* Initialize the edit monitor form
* @returns {void}
*/
init() {
if (this.isAdd) {
@@ -1327,7 +1340,7 @@ message HealthCheckResponse {
this.monitor.timeout = ~~(this.monitor.interval * 8) / 10;
}
} else {
toast.error(res.msg);
this.$root.toastError(res.msg);
}
});
}
@@ -1370,6 +1383,10 @@ message HealthCheckResponse {
return true;
},
resetToken() {
this.monitor.pushToken = genSecret(pushTokenLength);
},
/**
* Submit the form data for processing
* @returns {void}
@@ -1423,7 +1440,7 @@ message HealthCheckResponse {
createdNewParent = true;
this.monitor.parent = res.monitorID;
} else {
toast.error(res.msg);
this.$root.toastError(res.msg);
this.processing = false;
return;
}
@@ -1439,17 +1456,14 @@ message HealthCheckResponse {
if (createdNewParent) {
this.startParentGroupMonitor();
}
toast.success(res.msg);
this.processing = false;
this.$root.getMonitorList();
this.$router.push("/dashboard/" + res.monitorID);
} else {
toast.error(res.msg);
this.processing = false;
}
this.$root.toastRes(res);
});
} else {
await this.$refs.tagsManager.submit(this.monitor.id);
@@ -1476,6 +1490,7 @@ message HealthCheckResponse {
* Added a Notification Event
* Enable it if the notification is added in EditMonitor.vue
* @param {number} id ID of notification to add
* @returns {void}
*/
addedNotification(id) {
this.monitor.notificationIDList[id] = true;
@@ -1485,21 +1500,26 @@ message HealthCheckResponse {
* Added a Proxy Event
* Enable it if the proxy is added in EditMonitor.vue
* @param {number} id ID of proxy to add
* @returns {void}
*/
addedProxy(id) {
this.monitor.proxyId = id;
},
// Added a Docker Host Event
// Enable it if the Docker Host is added in EditMonitor.vue
/**
* Added a Docker Host Event
* Enable it if the Docker Host is added in EditMonitor.vue
* @param {number} id ID of docker host
* @returns {void}
*/
addedDockerHost(id) {
this.monitor.docker_host = id;
},
/**
* Adds a draft group.
*
* @param {string} draftGroupName - The name of the draft group.
* @param {string} draftGroupName The name of the draft group.
* @returns {void}
*/
addedDraftGroup(draftGroupName) {
this.draftGroupName = draftGroupName;

View File

@@ -19,25 +19,33 @@ export default {
},
async mounted() {
// There are only 2 cases that could come in here.
// There are only 3 cases that could come in here.
// 1. Matched status Page domain name
// 2. Vue Frontend Dev
let res = (await axios.get("/api/entry-page")).data;
// 3. Vue Frontend Dev (not setup database yet)
let res;
try {
res = (await axios.get("/api/entry-page")).data;
if (res.type === "statusPageMatchedDomain") {
this.statusPageSlug = res.statusPageSlug;
this.$root.forceStatusPageTheme = true;
if (res.type === "statusPageMatchedDomain") {
this.statusPageSlug = res.statusPageSlug;
this.$root.forceStatusPageTheme = true;
} else if (res.type === "entryPage") { // Dev only. For production, the logic is in the server side
const entryPage = res.entryPage;
} else if (res.type === "entryPage") { // Dev only. For production, the logic is in the server side
const entryPage = res.entryPage;
if (entryPage === "statusPage") {
this.$router.push("/status");
if (entryPage === "statusPage") {
this.$router.push("/status");
} else {
this.$router.push("/dashboard");
}
} else if (res.type === "setup-database") {
this.$router.push("/setup-database");
} else {
this.$router.push("/dashboard");
}
} else {
this.$router.push("/dashboard");
} catch (e) {
alert("Cannot connect to the backend server. Did you start the backend server? (npm run start-server-dev)");
}
},

View File

@@ -65,7 +65,10 @@ export default {
this.init();
},
methods: {
/** Initialise page */
/**
* Initialise page
* @returns {void}
*/
init() {
this.$root.getSocket().emit("getMonitorMaintenance", this.$route.params.id, (res) => {
if (res.ok) {
@@ -84,20 +87,22 @@ export default {
});
},
/** Confirm deletion */
/**
* Confirm deletion
* @returns {void}
*/
deleteDialog() {
this.$refs.confirmDelete.show();
},
/** Delete maintenance after showing confirmation */
/**
* Delete maintenance after showing confirmation
* @returns {void}
*/
deleteMaintenance() {
this.$root.deleteMaintenance(this.maintenance.id, (res) => {
if (res.ok) {
toast.success(res.msg);
this.$router.push("/maintenance");
} else {
toast.error(res.msg);
}
this.$root.toastRes(res);
this.$router.push("/maintenance");
});
},
},

View File

@@ -81,8 +81,6 @@ import { getResBaseURL } from "../util-frontend";
import { getMaintenanceRelativeURL } from "../util.ts";
import Confirm from "../components/Confirm.vue";
import MaintenanceTime from "../components/MaintenanceTime.vue";
import { useToast } from "vue-toastification";
const toast = useToast();
export default {
components: {
@@ -135,7 +133,7 @@ export default {
/**
* Get maintenance URL
* @param {number} id
* @param {number} id ID of maintenance to read
* @returns {string} Relative URL
*/
maintenanceURL(id) {
@@ -144,27 +142,33 @@ export default {
/**
* Show delete confirmation
* @param {number} maintenanceID
* @param {number} maintenanceID ID of maintenance to show delete
* confirmation for.
* @returns {void}
*/
deleteDialog(maintenanceID) {
this.selectedMaintenanceID = maintenanceID;
this.$refs.confirmDelete.show();
},
/** Delete maintenance after showing confirmation dialog */
/**
* Delete maintenance after showing confirmation dialog
* @returns {void}
*/
deleteMaintenance() {
this.$root.deleteMaintenance(this.selectedMaintenanceID, (res) => {
this.$root.toastRes(res);
if (res.ok) {
toast.success(res.msg);
this.$router.push("/maintenance");
} else {
toast.error(res.msg);
}
});
},
/**
* Show dialog to confirm pause
* @param {number} maintenanceID ID of maintenance to confirm
* pause.
* @returns {void}
*/
pauseDialog(maintenanceID) {
this.selectedMaintenanceID = maintenanceID;
@@ -173,6 +177,7 @@ export default {
/**
* Pause maintenance
* @returns {void}
*/
pauseMaintenance() {
this.$root.getSocket().emit("pauseMaintenance", this.selectedMaintenanceID, (res) => {
@@ -182,6 +187,8 @@ export default {
/**
* Resume maintenance
* @param {number} id ID of maintenance to resume
* @returns {void}
*/
resumeMaintenance(id) {
this.$root.getSocket().emit("resumeMaintenance", id, (res) => {

View File

@@ -45,7 +45,10 @@ export default {
},
methods: {
/** Go back 1 in browser history */
/**
* Go back 1 in browser history
* @returns {void}
*/
goBack() {
history.back();
}

View File

@@ -113,9 +113,6 @@ export default {
proxies: {
title: this.$t("Proxies"),
},
backup: {
title: this.$t("Backup"),
},
about: {
title: this.$t("About"),
},
@@ -139,6 +136,7 @@ export default {
/**
* Load the general settings page
* For desktop only, on mobile do nothing
* @returns {void}
*/
loadGeneralPage() {
if (!this.currentPage && !this.$root.isMobile) {
@@ -146,7 +144,10 @@ export default {
}
},
/** Load settings from server */
/**
* Load settings from server
* @returns {void}
*/
loadSettings() {
this.$root.getSocket().emit("getSettings", (res) => {
this.settings = res.data;
@@ -190,13 +191,15 @@ export default {
/**
* Callback for saving settings
* @callback saveSettingsCB
* @param {Object} res Result of operation
* @param {object} res Result of operation
* @returns {void}
*/
/**
* Save Settings
* @param {saveSettingsCB} [callback]
* @param {string} [currentPassword] Only need for disableAuth to true
* @param {saveSettingsCB} callback Callback for socket response
* @param {string} currentPassword Only need for disableAuth to true
* @returns {void}
*/
saveSettings(callback, currentPassword) {
let valid = this.validateSettings();
@@ -216,7 +219,7 @@ export default {
/**
* Ensure settings are valid
* @returns {Object} Contains success state and error msg
* @returns {object} Contains success state and error msg
*/
validateSettings() {
if (this.settings.keepDataPeriodDays < 0) {

View File

@@ -46,9 +46,6 @@
</template>
<script>
import { useToast } from "vue-toastification";
const toast = useToast();
export default {
data() {
return {
@@ -62,6 +59,8 @@ export default {
},
mounted() {
// TODO: Check if it is a database setup
this.$root.getSocket().emit("needSetup", (needSetup) => {
if (! needSetup) {
this.$router.push("/");
@@ -77,7 +76,7 @@ export default {
this.processing = true;
if (this.password !== this.repeatPassword) {
toast.error(this.$t("PasswordsDoNotMatch"));
this.$root.toastError("PasswordsDoNotMatch");
this.processing = false;
return;
}

238
src/pages/SetupDatabase.vue Normal file
View File

@@ -0,0 +1,238 @@
<template>
<div v-if="show" class="form-container">
<form @submit.prevent="submit">
<div>
<object width="64" height="64" data="/icon.svg" />
<div style="font-size: 28px; font-weight: bold; margin-top: 5px;">
Uptime Kuma
</div>
</div>
<div v-if="info.runningSetup" class="mt-5">
<div class="alert alert-success mx-3 px-4" role="alert">
<div class="d-flex align-items-center">
<strong>Setting up the database. It may take a while, please be patient.</strong>
<div class="ms-3 pt-1">
<div class="spinner-border" role="status" aria-hidden="true"></div>
</div>
</div>
</div>
</div>
<template v-if="!info.runningSetup">
<div class="form-floating short mt-3">
<select id="language" v-model="$root.language" class="form-select">
<option v-for="(lang, i) in $i18n.availableLocales" :key="`Lang${i}`" :value="lang">
{{ $i18n.messages[lang].languageName }}
</option>
</select>
<label for="language" class="form-label">{{ $t("Language") }}</label>
</div>
<p class="mt-5 short">
{{ $t("setupDatabaseChooseDatabase") }}
</p>
<div class="btn-group" role="group" aria-label="Basic radio toggle button group">
<template v-if="info.isEnabledEmbeddedMariaDB">
<input id="btnradio3" v-model="dbConfig.type" type="radio" class="btn-check" autocomplete="off" value="embedded-mariadb">
<label class="btn btn-outline-primary" for="btnradio3">
Embedded MariaDB
</label>
</template>
<input id="btnradio2" v-model="dbConfig.type" type="radio" class="btn-check" autocomplete="off" value="mariadb">
<label class="btn btn-outline-primary" for="btnradio2">
MariaDB/MySQL
</label>
<input id="btnradio1" v-model="dbConfig.type" type="radio" class="btn-check" autocomplete="off" value="sqlite">
<label class="btn btn-outline-primary" for="btnradio1">
SQLite
</label>
</div>
<div v-if="dbConfig.type === 'embedded-mariadb'" class="mt-3 short">
{{ $t("setupDatabaseEmbeddedMariaDB") }}
</div>
<div v-if="dbConfig.type === 'mariadb'" class="mt-3 short">
{{ $t("setupDatabaseMariaDB") }}
</div>
<div v-if="dbConfig.type === 'sqlite'" class="mt-3 short">
{{ $t("setupDatabaseSQLite") }}
</div>
<template v-if="dbConfig.type === 'mariadb'">
<div class="form-floating mt-3 short">
<input id="floatingInput" v-model="dbConfig.hostname" type="text" class="form-control" required>
<label for="floatingInput">{{ $t("Hostname") }}</label>
</div>
<div class="form-floating mt-3 short">
<input id="floatingInput" v-model="dbConfig.port" type="text" class="form-control" required>
<label for="floatingInput">{{ $t("Port") }}</label>
</div>
<div class="form-floating mt-3 short">
<input id="floatingInput" v-model="dbConfig.username" type="text" class="form-control" required>
<label for="floatingInput">{{ $t("Username") }}</label>
</div>
<div class="form-floating mt-3 short">
<input id="floatingInput" v-model="dbConfig.password" type="password" class="form-control" required>
<label for="floatingInput">{{ $t("Password") }}</label>
</div>
<div class="form-floating mt-3 short">
<input id="floatingInput" v-model="dbConfig.dbName" type="text" class="form-control" required>
<label for="floatingInput">{{ $t("dbName") }}</label>
</div>
</template>
<button class="btn btn-primary mt-4 short" type="submit" :disabled="disabledButton">
{{ $t("Next") }}
</button>
</template>
</form>
</div>
</template>
<script>
import axios from "axios";
import { useToast } from "vue-toastification";
import { sleep } from "../util.ts";
const toast = useToast();
export default {
data() {
return {
show: false,
dbConfig: {
type: undefined,
port: 3306,
hostname: "",
username: "",
password: "",
dbName: "kuma",
},
info: {
needSetup: false,
runningSetup: false,
isEnabledEmbeddedMariaDB: false,
},
};
},
computed: {
disabledButton() {
return this.dbConfig.type === undefined || this.info.runningSetup;
},
},
async mounted() {
let res = await axios.get("/setup-database-info");
this.info = res.data;
if (this.info && this.info.needSetup === false) {
location.href = "/setup";
} else {
this.show = true;
}
},
methods: {
async submit() {
this.info.runningSetup = true;
try {
await axios.post("/setup-database", {
dbConfig: this.dbConfig,
});
await sleep(2000);
await this.goToMainServerWhenReady();
} catch (e) {
toast.error(e.response.data);
} finally {
this.info.runningSetup = false;
}
},
async goToMainServerWhenReady() {
try {
console.log("Trying...");
let res = await axios.get("/setup-database-info");
if (res.data && res.data.needSetup === false) {
this.show = false;
location.href = "/setup";
} else {
if (res.data) {
this.info = res.data;
}
throw new Error("not ready");
}
} catch (e) {
console.log("Not ready yet");
await sleep(2000);
await this.goToMainServerWhenReady();
}
},
test() {
this.$root.toastError("not implemented");
}
},
};
</script>
<style lang="scss" scoped>
.form-container {
display: flex;
align-items: center;
justify-content: center;
padding-top: 40px;
padding-bottom: 40px;
}
.btn-group {
label {
width: 200px;
line-height: 55px;
font-size: 16px;
font-weight: bold;
}
}
.form-floating {
> .form-select {
padding-left: 1.3rem;
padding-top: 1.525rem;
line-height: 1.35;
~ label {
padding-left: 1.3rem;
}
}
> label {
padding-left: 1.3rem;
}
> .form-control {
padding-left: 1.3rem;
}
}
.short {
width: 300px;
}
form {
max-width: 800px;
text-align: center;
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
}
</style>

View File

@@ -453,6 +453,7 @@ export default {
/**
* If the monitor is added to public list, which will not be in this list.
* @returns {object[]} List of monitors
*/
sortedMonitorList() {
let result = [];
@@ -597,7 +598,8 @@ export default {
/**
* If connected to the socket and logged in, request private data of this statusPage
* @param connected
* @param {boolean} loggedIn Is the client logged in?
* @returns {void}
*/
"$root.loggedIn"(loggedIn) {
if (loggedIn) {
@@ -612,7 +614,7 @@ export default {
}
} else {
toast.error(res.msg);
this.$root.toastError(res.msg);
}
});
}
@@ -620,6 +622,8 @@ export default {
/**
* Selected a monitor and add to the list.
* @param {object} monitor Monitor to add
* @returns {void}
*/
selectedMonitor(monitor) {
if (monitor) {
@@ -726,7 +730,7 @@ export default {
/**
* Get status page data
* It should be preloaded in window.preloadData
* @returns {Promise<any>}
* @returns {Promise<any>} Status page data
*/
getData: function () {
if (window.preloadData) {
@@ -741,13 +745,16 @@ export default {
/**
* Provide syntax highlighting for CSS
* @param {string} code Text to highlight
* @returns {string}
* @returns {string} Highlighted CSS
*/
highlighter(code) {
return highlight(code, languages.css);
},
/** Update the heartbeat list and update favicon if neccessary */
/**
* Update the heartbeat list and update favicon if necessary
* @returns {void}
*/
updateHeartbeatList() {
// If editMode, it will use the data from websocket.
if (! this.editMode) {
@@ -795,7 +802,10 @@ export default {
}, 1000);
},
/** Enable editing mode */
/**
* Enable editing mode
* @returns {void}
*/
edit() {
if (this.hasToken) {
this.$root.initSocketIO(true);
@@ -807,7 +817,10 @@ export default {
}
},
/** Save the status page */
/**
* Save the status page
* @returns {void}
*/
save() {
this.loading = true;
let startTime = new Date();
@@ -838,33 +851,42 @@ export default {
});
},
/** Show dialog confirming deletion */
/**
* Show dialog confirming deletion
* @returns {void}
*/
deleteDialog() {
this.$refs.confirmDelete.show();
},
/** Request deletion of this status page */
/**
* Request deletion of this status page
* @returns {void}
*/
deleteStatusPage() {
this.$root.getSocket().emit("deleteStatusPage", this.slug, (res) => {
if (res.ok) {
this.enableEditMode = false;
location.href = "/manage-status-page";
} else {
toast.error(res.msg);
this.$root.toastError(res.msg);
}
});
},
/**
* Returns label for a specifed monitor
* @param {Object} monitor Object representing monitor
* @returns {string}
* Returns label for a specified monitor
* @param {object} monitor Object representing monitor
* @returns {string} Monitor label
*/
monitorSelectorLabel(monitor) {
return `${monitor.name}`;
},
/** Add a group to the status page */
/**
* Add a group to the status page
* @returns {void}
*/
addGroup() {
let groupName = this.$t("Untitled Group");
@@ -878,12 +900,18 @@ export default {
});
},
/** Add a domain to the status page */
/**
* Add a domain to the status page
* @returns {void}
*/
addDomainField() {
this.config.domainNameList.push("");
},
/** Discard changes to status page */
/**
* Discard changes to status page
* @returns {void}
*/
discard() {
location.href = "/status/" + this.slug;
},
@@ -891,19 +919,26 @@ export default {
/**
* Set URL of new image after successful crop operation
* @param {string} imgDataUrl URL of image in data:// format
* @returns {void}
*/
cropSuccess(imgDataUrl) {
this.imgDataUrl = imgDataUrl;
},
/** Show image crop dialog if in edit mode */
/**
* Show image crop dialog if in edit mode
* @returns {void}
*/
showImageCropUploadMethod() {
if (this.editMode) {
this.showImageCropUpload = true;
}
},
/** Create an incident for this status page */
/**
* Create an incident for this status page
* @returns {void}
*/
createIncident() {
this.enableEditIncidentMode = true;
@@ -918,10 +953,13 @@ export default {
};
},
/** Post the incident to the status page */
/**
* Post the incident to the status page
* @returns {void}
*/
postIncident() {
if (this.incident.title === "" || this.incident.content === "") {
toast.error(this.$t("Please input title and content"));
this.$root.toastError("Please input title and content");
return;
}
@@ -931,20 +969,26 @@ export default {
this.enableEditIncidentMode = false;
this.incident = res.incident;
} else {
toast.error(res.msg);
this.$root.toastError(res.msg);
}
});
},
/** Click Edit Button */
/**
* Click Edit Button
* @returns {void}
*/
editIncident() {
this.enableEditIncidentMode = true;
this.previousIncident = Object.assign({}, this.incident);
},
/** Cancel creation or editing of incident */
/**
* Cancel creation or editing of incident
* @returns {void}
*/
cancelIncident() {
this.enableEditIncidentMode = false;
@@ -954,7 +998,10 @@ export default {
}
},
/** Unpin the incident */
/**
* Unpin the incident
* @returns {void}
*/
unpinIncident() {
this.$root.getSocket().emit("unpinIncident", this.slug, () => {
this.incident = null;
@@ -963,7 +1010,8 @@ export default {
/**
* Get the relative time difference of a date from now
* @returns {string}
* @param {any} date Date to get time difference
* @returns {string} Time difference
*/
dateFromNow(date) {
return dayjs.utc(date).fromNow();
@@ -972,6 +1020,7 @@ export default {
/**
* Remove a domain from the status page
* @param {number} index Index of domain to remove
* @returns {void}
*/
removeDomain(index) {
this.config.domainNameList.splice(index, 1);
@@ -979,7 +1028,7 @@ export default {
/**
* Generate sanitized HTML from maintenance description
* @param {string} description
* @param {string} description Text to sanitize
* @returns {string} Sanitized HTML
*/
maintenanceHTML(description) {
@@ -1196,20 +1245,6 @@ footer {
}
}
/* required class */
.css-editor {
/* we dont use `language-` classes anymore so thats why we need to add background and text color manually */
border-radius: 1rem;
padding: 10px 5px;
border: 1px solid #ced4da;
.dark & {
background: $dark-bg;
border: 1px solid $dark-border-color;
}
}
.bg-maintenance {
.alert-heading {
font-weight: bold;