[status page] many update and save group list

This commit is contained in:
LouisLam 2021-09-19 19:04:51 +08:00
parent 6681f49a58
commit fd95d41d9f
16 changed files with 288 additions and 127 deletions

View File

@ -17,6 +17,7 @@ module.exports = {
requireConfigFile: false, requireConfigFile: false,
}, },
rules: { rules: {
"linebreak-style": ["error", "unix"],
"camelcase": ["warn", { "camelcase": ["warn", {
"properties": "never", "properties": "never",
"ignoreImports": true "ignoreImports": true

30
db/patch-group-table.sql Normal file
View File

@ -0,0 +1,30 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;
create table `group`
(
id INTEGER not null
constraint group_pk
primary key autoincrement,
name VARCHAR(255) not null,
created_date DATETIME default (DATETIME('now')) not null,
public BOOLEAN default 0 not null,
active BOOLEAN default 1 not null,
weight BOOLEAN NOT NULL DEFAULT 1000
);
CREATE TABLE [monitor_group]
(
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
[monitor_id] INTEGER NOT NULL REFERENCES [group] ([id]) ON DELETE CASCADE ON UPDATE CASCADE REFERENCES [monitor] ([id]) ON DELETE CASCADE ON UPDATE CASCADE,
[group_id] INTEGER NOT NULL,
weight BOOLEAN NOT NULL DEFAULT 1000
);
CREATE INDEX [fk]
ON [monitor_group] (
[monitor_id],
[group_id]);
COMMIT;

View File

@ -10,6 +10,7 @@ create table incident
content TEXT not null, content TEXT not null,
style VARCHAR(30) default 'warning' not null, style VARCHAR(30) default 'warning' not null,
created_date DATETIME default (DATETIME('now')) not null, created_date DATETIME default (DATETIME('now')) not null,
last_updated_date DATETIME,
pin BOOLEAN default 1 not null, pin BOOLEAN default 1 not null,
active BOOLEAN default 1 not null active BOOLEAN default 1 not null
); );

View File

@ -1,6 +0,0 @@
BEGIN TRANSACTION;
alter table monitor
add public_weight BOOLEAN default 1000 not null;
COMMIT;

View File

@ -1,6 +0,0 @@
BEGIN TRANSACTION;
alter table monitor
add public BOOLEAN default 0 not null;
COMMIT;

View File

@ -18,7 +18,7 @@ exports.startInterval = () => {
// For debug // For debug
if (process.env.TEST_CHECK_VERSION === "1") { if (process.env.TEST_CHECK_VERSION === "1") {
res.data.version = "1000.0.0" res.data.version = "1000.0.0";
} }
exports.latestVersion = res.data.version; exports.latestVersion = res.data.version;

View File

@ -30,11 +30,10 @@ class Database {
static patchList = { static patchList = {
"patch-setting-value-type.sql": true, "patch-setting-value-type.sql": true,
"patch-improve-performance.sql": true, "patch-improve-performance.sql": true,
"patch-monitor-public.sql": true,
"patch-2fa.sql": true, "patch-2fa.sql": true,
"patch-add-retry-interval-monitor.sql": true, "patch-add-retry-interval-monitor.sql": true,
"patch-monitor-public-weight.sql": true,
"patch-incident-table.sql": true, "patch-incident-table.sql": true,
"patch-group-table.sql": true,
} }
/** /**
@ -65,7 +64,7 @@ class Database {
} }
// Auto map the model to a bean object // Auto map the model to a bean object
R.freeze(true) R.freeze(true);
await R.autoloadModels("./server/model"); await R.autoloadModels("./server/model");
// Change to WAL // Change to WAL
@ -92,7 +91,7 @@ class Database {
} else if (version > this.latestVersion) { } else if (version > this.latestVersion) {
console.info("Warning: Database version is newer than expected"); console.info("Warning: Database version is newer than expected");
} else { } else {
console.info("Database patch is needed") console.info("Database patch is needed");
this.backup(version); this.backup(version);
@ -107,11 +106,12 @@ class Database {
} }
} catch (ex) { } catch (ex) {
await Database.close(); await Database.close();
this.restore();
console.error(ex) console.error(ex);
console.error("Start Uptime-Kuma failed due to patch db failed") console.error("Start Uptime-Kuma failed due to patch db failed");
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues") console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
this.restore();
process.exit(1); process.exit(1);
} }
} }
@ -136,7 +136,7 @@ class Database {
try { try {
for (let sqlFilename in this.patchList) { for (let sqlFilename in this.patchList) {
await this.patch2Recursion(sqlFilename, databasePatchedFiles) await this.patch2Recursion(sqlFilename, databasePatchedFiles);
} }
if (this.patched) { if (this.patched) {
@ -145,11 +145,13 @@ class Database {
} catch (ex) { } catch (ex) {
await Database.close(); await Database.close();
this.restore();
console.error(ex) console.error(ex);
console.error("Start Uptime-Kuma failed due to patch db failed"); console.error("Start Uptime-Kuma failed due to patch db failed");
console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues"); console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
this.restore();
process.exit(1); process.exit(1);
} }
@ -189,7 +191,7 @@ class Database {
console.log(sqlFilename + " is patched successfully"); console.log(sqlFilename + " is patched successfully");
} else { } else {
console.log(sqlFilename + " is already patched, skip"); debug(sqlFilename + " is already patched, skip");
} }
} }
@ -207,12 +209,12 @@ class Database {
// Remove all comments (--) // Remove all comments (--)
let lines = text.split("\n"); let lines = text.split("\n");
lines = lines.filter((line) => { lines = lines.filter((line) => {
return ! line.startsWith("--") return ! line.startsWith("--");
}); });
// Split statements by semicolon // Split statements by semicolon
// Filter out empty line // Filter out empty line
text = lines.join("\n") text = lines.join("\n");
let statements = text.split(";") let statements = text.split(";")
.map((statement) => { .map((statement) => {
@ -220,7 +222,7 @@ class Database {
}) })
.filter((statement) => { .filter((statement) => {
return statement !== ""; return statement !== "";
}) });
for (let statement of statements) { for (let statement of statements) {
await R.exec(statement); await R.exec(statement);
@ -266,7 +268,7 @@ class Database {
*/ */
static backup(version) { static backup(version) {
if (! this.backupPath) { if (! this.backupPath) {
console.info("Backup the db") console.info("Backup the db");
this.backupPath = this.dataDir + "kuma.db.bak" + version; this.backupPath = this.dataDir + "kuma.db.bak" + version;
fs.copyFileSync(Database.path, this.backupPath); fs.copyFileSync(Database.path, this.backupPath);

33
server/model/group.js Normal file
View File

@ -0,0 +1,33 @@
const { BeanModel } = require("redbean-node/dist/bean-model");
const { R } = require("redbean-node");
class Group extends BeanModel {
async toPublicJSON() {
let monitorBeanList = R.convertToBeans("monitor", await R.getAll(`
SELECT * FROM monitor, monitor_group
WHERE monitor.id = monitor_group.monitor_id
AND group_id = ?
`, [
this.id,
]));
console.log(monitorBeanList);
let monitorList = [];
for (let bean of monitorBeanList) {
monitorList.push(await bean.toPublicJSON());
}
return {
id: this.id,
name: this.name,
weight: this.weight,
monitorList,
};
}
}
module.exports = Group;

View File

@ -10,6 +10,7 @@ class Incident extends BeanModel {
content: this.content, content: this.content,
pin: this.pin, pin: this.pin,
createdDate: this.createdDate, createdDate: this.createdDate,
lastUpdatedDate: this.lastUpdatedDate,
}; };
} }
} }

View File

@ -26,8 +26,10 @@ class Monitor extends BeanModel {
* Only show necessary data to public * Only show necessary data to public
*/ */
async toPublicJSON() { async toPublicJSON() {
// TODO Only show necessary return {
return this.toJSON(); id: this.id,
name: this.name,
};
} }
/** /**

View File

@ -41,7 +41,7 @@ router.get("/api/status-page/incident", async (_, response) => {
response.json({ response.json({
ok: true, ok: true,
incident: (await R.findOne("incident", " pin = 1 AND active = 1")).toPublicJSON(), incident: (await R.findOne("incident", " pin = 1 AND active = 1")).toPublicJSON(),
}) });
} catch (error) { } catch (error) {
send403(response, error.message); send403(response, error.message);
@ -55,15 +55,14 @@ router.get("/api/status-page/monitor-list", async (_request, response) => {
try { try {
await checkPublished(); await checkPublished();
const monitorList = {}; const publicGroupList = [];
let list = await R.find("monitor", " public = 1 ORDER BY weight DESC, name ", [ let list = await R.find("group", " public = 1 ORDER BY weight, name ");
]);
for (let monitor of list) { for (let groupBean of list) {
monitorList[monitor.id] = await monitor.toJSON(); publicGroupList.push(await groupBean.toPublicJSON());
} }
response.json(monitorList); response.json(publicGroupList);
} catch (error) { } catch (error) {
send403(response, error.message); send403(response, error.message);
@ -79,7 +78,7 @@ router.get("/api/status-page/heartbeat", async (_request, response) => {
const monitorList = {}; const monitorList = {};
let list = await R.find("", " ", [ let list = await R.find("", " ", [
]) ]);
for (let monitor of list) { for (let monitor of list) {
monitorList[monitor.id] = await monitor.toJSON(); monitorList[monitor.id] = await monitor.toJSON();
@ -126,7 +125,7 @@ function send403(res, msg = "") {
res.status(403).json({ res.status(403).json({
"status": "fail", "status": "fail",
"msg": msg, "msg": msg,
}) });
} }
module.exports = router; module.exports = router;

View File

@ -1,6 +1,7 @@
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { checkLogin } = require("../util-server"); const { checkLogin } = require("../util-server");
const dayjs = require("dayjs"); const dayjs = require("dayjs");
const { debug } = require("../../src/util");
module.exports.statusPageSocketHandler = (socket) => { module.exports.statusPageSocketHandler = (socket) => {
@ -27,7 +28,13 @@ module.exports.statusPageSocketHandler = (socket) => {
incidentBean.content = incident.content; incidentBean.content = incident.content;
incidentBean.style = incident.style; incidentBean.style = incident.style;
incidentBean.pin = true; incidentBean.pin = true;
if (incident.id) {
incidentBean.lastUpdatedDate = R.isoDateTime(dayjs.utc());
} else {
incidentBean.createdDate = R.isoDateTime(dayjs.utc()); incidentBean.createdDate = R.isoDateTime(dayjs.utc());
}
await R.store(incidentBean); await R.store(incidentBean);
callback({ callback({
@ -58,4 +65,67 @@ module.exports.statusPageSocketHandler = (socket) => {
}); });
} }
}); });
// Save Status Page
socket.on("saveStatusPage", async (publicGroupList, callback) => {
try {
checkLogin(socket);
await R.transaction(async (trx) => {
const groupIDList = [];
let groupOrder = 1;
for (let group of publicGroupList) {
let groupBean;
if (group.id) {
groupBean = await trx.findOne("group", " id = ? AND public = 1 ", [
group.id
]);
} else {
groupBean = R.dispense("group");
}
groupBean.name = group.name;
groupBean.public = true;
groupBean.weight = groupOrder++;
await trx.store(groupBean);
await trx.exec("DELETE FROM monitor_group WHERE group_id = ? ", [
groupBean.id
]);
let monitorOrder = 1;
for (let monitor of group.monitorList) {
let relationBean = R.dispense("monitor_group");
relationBean.weight = monitorOrder++;
relationBean.group_id = groupBean.id;
relationBean.monitor_id = monitor.id;
await trx.store(relationBean);
}
groupIDList.push(groupBean.id);
group.id = groupBean.id;
}
// Delete groups that not in the list
debug("Delete groups that not in the list");
const slots = groupIDList.map(() => "?").join(",");
await trx.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots})`, groupIDList);
callback({
ok: true,
publicGroupList,
});
});
} catch (error) {
callback({
ok: false,
msg: error.message,
});
}
});
}; };

View File

@ -11,12 +11,14 @@ export default {
data() { data() {
return { return {
publicGroupList: [], publicGroupList: [],
} };
}, },
computed: { computed: {
publicMonitorList() { publicMonitorList() {
let result = {}; let result = {};
console.log(this.publicGroupList);
for (let group of this.publicGroupList) { for (let group of this.publicGroupList) {
for (let monitor of group.monitorList) { for (let monitor of group.monitorList) {
result[monitor.id] = monitor; result[monitor.id] = monitor;
@ -26,7 +28,7 @@ export default {
}, },
publicLastHeartbeatList() { publicLastHeartbeatList() {
let result = {} let result = {};
for (let monitorID in this.publicMonitorList) { for (let monitorID in this.publicMonitorList) {
if (this.lastHeartbeatList[monitorID]) { if (this.lastHeartbeatList[monitorID]) {
@ -37,4 +39,4 @@ export default {
return result; return result;
}, },
} }
} };

View File

@ -37,7 +37,7 @@
</div> </div>
<div v-else> <div v-else>
<button class="btn btn-success me-2" @click="leaveEditMode"> <button class="btn btn-success me-2" @click="save">
<font-awesome-icon icon="save" /> <font-awesome-icon icon="save" />
{{ $t("Save") }} {{ $t("Save") }}
</button> </button>
@ -52,6 +52,7 @@
{{ $t("Create Incident") }} {{ $t("Create Incident") }}
</button> </button>
<!--
<button v-if="isPublished" class="btn btn-light me-2" @click=""> <button v-if="isPublished" class="btn btn-light me-2" @click="">
<font-awesome-icon icon="save" /> <font-awesome-icon icon="save" />
{{ $t("Unpublish") }} {{ $t("Unpublish") }}
@ -60,7 +61,7 @@
<button v-if="!isPublished" class="btn btn-info me-2" @click=""> <button v-if="!isPublished" class="btn btn-info me-2" @click="">
<font-awesome-icon icon="save" /> <font-awesome-icon icon="save" />
{{ $t("Publish") }} {{ $t("Publish") }}
</button> </button>-->
<!-- Set Default Language --> <!-- Set Default Language -->
<!-- Set theme --> <!-- Set theme -->
@ -84,6 +85,14 @@
<strong v-if="editIncidentMode">{{ $t("Content") }}:</strong> <strong v-if="editIncidentMode">{{ $t("Content") }}:</strong>
<Editable v-model="incident.content" tag="div" :contenteditable="editIncidentMode" class="content" /> <Editable v-model="incident.content" tag="div" :contenteditable="editIncidentMode" class="content" />
<!-- Incident Date -->
<div class="date mt-3">
Created: {{ incident.createdDate }} ({{ createdDateFromNow }})<br />
<span v-if="incident.lastUpdatedDate">
Last Updated: {{ incident.lastUpdatedDate }} ({{ lastUpdatedDateFromNow }})
</span>
</div>
<div v-if="editMode" class="mt-3"> <div v-if="editMode" class="mt-3">
<button v-if="editIncidentMode" class="btn btn-light me-2" @click="postIncident"> <button v-if="editIncidentMode" class="btn btn-light me-2" @click="postIncident">
<font-awesome-icon icon="bullhorn" /> <font-awesome-icon icon="bullhorn" />
@ -190,6 +199,7 @@ import PublicGroupList from "../components/PublicGroupList.vue";
import ImageCropUpload from "vue-image-crop-upload"; import ImageCropUpload from "vue-image-crop-upload";
import { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_PARTIAL_DOWN, UP } from "../util.ts"; import { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_PARTIAL_DOWN, UP } from "../util.ts";
import { useToast } from "vue-toastification"; import { useToast } from "vue-toastification";
import dayjs from "dayjs";
const toast = useToast(); const toast = useToast();
const leavePageMsg = "Do you really want to leave? you have unsaved changes!"; const leavePageMsg = "Do you really want to leave? you have unsaved changes!";
@ -309,6 +319,14 @@ export default {
return this.overallStatus === STATUS_PAGE_ALL_DOWN; return this.overallStatus === STATUS_PAGE_ALL_DOWN;
}, },
createdDateFromNow() {
return dayjs.utc(this.incident.createdDate).fromNow();
},
lastUpdatedDateFromNow() {
return dayjs.utc(this.incident. lastUpdatedDate).fromNow();
}
}, },
watch: { watch: {
@ -360,7 +378,7 @@ export default {
}); });
axios.get("/api/status-page/monitor-list").then((res) => { axios.get("/api/status-page/monitor-list").then((res) => {
this.monitorList = res.data; this.$root.publicGroupList = res.data;
}); });
// 5mins a loop // 5mins a loop
@ -380,8 +398,16 @@ export default {
this.enableEditMode = true; this.enableEditMode = true;
}, },
leaveEditMode() { save() {
this.$root.getSocket().emit("saveStatusPage", this.$root.publicGroupList, (res) => {
if (res.ok) {
this.enableEditMode = false; this.enableEditMode = false;
console.log(res);
this.$root.publicGroupList = res.publicGroupList;
} else {
toast.error(res.msg);
}
});
}, },
monitorSelectorLabel(monitor) { monitorSelectorLabel(monitor) {
@ -534,8 +560,14 @@ footer {
.incident { .incident {
.content { .content {
&[contenteditable=true] {
min-height: 60px; min-height: 60px;
} }
} }
.date {
font-size: 14px;
}
}
</style> </style>

View File

@ -3,8 +3,8 @@ import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc"; import utc from "dayjs/plugin/utc";
import timezones from "timezones-list"; import timezones from "timezones-list";
dayjs.extend(utc) dayjs.extend(utc);
dayjs.extend(timezone) dayjs.extend(timezone);
function getTimezoneOffset(timeZone) { function getTimezoneOffset(timeZone) {
const now = new Date(); const now = new Date();
@ -28,7 +28,7 @@ export function timezoneList() {
name: `(UTC${display}) ${timezone.tzCode}`, name: `(UTC${display}) ${timezone.tzCode}`,
value: timezone.tzCode, value: timezone.tzCode,
time: getTimezoneOffset(timezone.tzCode), time: getTimezoneOffset(timezone.tzCode),
}) });
} catch (e) { } catch (e) {
console.log("Skip Timezone: " + timezone.tzCode); console.log("Skip Timezone: " + timezone.tzCode);
} }
@ -44,7 +44,7 @@ export function timezoneList() {
} }
return 0; return 0;
}) });
return result; return result;
} }