From 1033ca5cf4a8141cbe7c57d95638143ab8d39a8e Mon Sep 17 00:00:00 2001 From: Louis Lam Date: Wed, 16 Mar 2022 15:38:10 +0800 Subject: [PATCH] [Status Page] wip, combine api, add status_page_id into group and incident tables --- db/patch-status-page.sql | 3 + server/database.js | 12 ++- server/model/status_page.js | 6 ++ server/routers/api-router.js | 74 ++++++++----------- .../status-page-socket-handler.js | 34 ++++++--- src/pages/StatusPage.vue | 66 ++++++++--------- 6 files changed, 105 insertions(+), 90 deletions(-) diff --git a/db/patch-status-page.sql b/db/patch-status-page.sql index be4667f2..d23b75bc 100644 --- a/db/patch-status-page.sql +++ b/db/patch-status-page.sql @@ -25,4 +25,7 @@ CREATE TABLE [status_page_cname]( [domain] VARCHAR NOT NULL UNIQUE ); +ALTER TABLE incident ADD status_page_id INTEGER; +ALTER TABLE [group] ADD status_page_id INTEGER; + COMMIT; diff --git a/server/database.js b/server/database.js index 6fb99b0d..0a5500ad 100644 --- a/server/database.js +++ b/server/database.js @@ -240,8 +240,18 @@ class Database { statusPage.search_engine_index = await setting("searchEngineIndex"); statusPage.show_tags = await setting("statusPageTags"); statusPage.password = null; - await R.store(statusPage); + let id = await R.store(statusPage); + + await R.exec("UPDATE incident SET status_page_id = ? WHERE status_page_id IS NULL", [ + id + ]); + + await R.exec("UPDATE [group] SET status_page_id = ? WHERE status_page_id IS NULL", [ + id + ]); + await R.exec("DELETE FROM setting WHERE type = 'statusPage'"); + console.log("Migrating Status Page - Done"); } diff --git a/server/model/status_page.js b/server/model/status_page.js index 6ab7a5f4..e2155c4c 100644 --- a/server/model/status_page.js +++ b/server/model/status_page.js @@ -41,6 +41,12 @@ class StatusPage extends BeanModel { }; } + static async slugToID(slug) { + return await R.getCell("SELECT id FROM status_page WHERE slug = ? ", [ + slug + ]); + } + } module.exports = StatusPage; diff --git a/server/routers/api-router.js b/server/routers/api-router.js index a9ca4710..e9076e99 100644 --- a/server/routers/api-router.js +++ b/server/routers/api-router.js @@ -6,6 +6,7 @@ const apicache = require("../modules/apicache"); const Monitor = require("../model/monitor"); const dayjs = require("dayjs"); const { UP, flipStatus, debug } = require("../../src/util"); +const StatusPage = require("../model/status_page"); let router = express.Router(); let cache = apicache.middleware; @@ -82,11 +83,12 @@ router.get("/api/push/:pushToken", async (request, response) => { } }); -// Status Page Config -router.get("/api/status-page/config/:slug", async (request, response) => { +// Status page config, incident, monitor list +router.get("/api/status-page/:slug", cache("5 minutes"), async (request, response) => { allowDevAllOrigin(response); let slug = request.params.slug; + // Get Status Page let statusPage = await R.findOne("status_page", " slug = ? ", [ slug ]); @@ -99,50 +101,30 @@ router.get("/api/status-page/config/:slug", async (request, response) => { return; } - response.json(await statusPage.toPublicJSON()); -}); - -// Status Page - Get the current Incident -// Can fetch only if published -router.get("/api/status-page/incident/:slug", async (_, response) => { - allowDevAllOrigin(response); - try { - await checkPublished(); - - let incident = await R.findOne("incident", " pin = 1 AND active = 1"); + // Incident + let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [ + statusPage.id, + ]); if (incident) { incident = incident.toPublicJSON(); } - response.json({ - ok: true, - incident, - }); - - } catch (error) { - send403(response, error.message); - } -}); - -// Status Page - Monitor List -// Can fetch only if published -router.get("/api/status-page/monitor-list/:slug", cache("5 minutes"), async (_request, response) => { - allowDevAllOrigin(response); - - try { - await checkPublished(); + // Public Group List const publicGroupList = []; - const tagsVisible = (await getSettings("statusPage")).statusPageTags; - const list = await R.find("group", " public = 1 ORDER BY weight "); + const tagsVisible = !!statusPage.show_tags; + const list = await R.find("group", " public = 1 AND status_page_id = ? ORDER BY weight ", [ + statusPage.id + ]); + for (let groupBean of list) { let monitorGroup = await groupBean.toPublicJSON(); if (tagsVisible) { monitorGroup.monitorList = await Promise.all(monitorGroup.monitorList.map(async (monitor) => { // Includes tags as an array in response, allows for tags to be displayed on public status page const tags = await R.getAll( - `SELECT monitor_tag.monitor_id, monitor_tag.value, tag.name, tag.color + `SELECT monitor_tag.monitor_id, monitor_tag.value, tag.name, tag.color FROM monitor_tag JOIN tag ON monitor_tag.tag_id = tag.id @@ -158,29 +140,39 @@ router.get("/api/status-page/monitor-list/:slug", cache("5 minutes"), async (_re publicGroupList.push(monitorGroup); } - response.json(publicGroupList); + // Response + response.json({ + config: await statusPage.toPublicJSON(), + incident, + publicGroupList + }); } catch (error) { send403(response, error.message); } + }); // Status Page Polling Data // Can fetch only if published -router.get("/api/status-page/heartbeat/:slug", cache("5 minutes"), async (_request, response) => { +router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (request, response) => { allowDevAllOrigin(response); try { - await checkPublished(); - let heartbeatList = {}; let uptimeList = {}; + let slug = request.params.slug; + let statusPageID = await StatusPage.slugToID(slug); + let monitorIDList = await R.getCol(` SELECT monitor_group.monitor_id FROM monitor_group, \`group\` WHERE monitor_group.group_id = \`group\`.id AND public = 1 - `); + AND \`group\`.status_page_id = ? + `, [ + statusPageID + ]); for (let monitorID of monitorIDList) { let list = await R.getAll(` @@ -209,12 +201,6 @@ router.get("/api/status-page/heartbeat/:slug", cache("5 minutes"), async (_reque } }); -async function checkPublished() { - if (! await isPublished()) { - throw new Error("The status page is not published"); - } -} - /** * Default is published * @returns {Promise} diff --git a/server/socket-handlers/status-page-socket-handler.js b/server/socket-handlers/status-page-socket-handler.js index 42e0b9a0..060ccf8d 100644 --- a/server/socket-handlers/status-page-socket-handler.js +++ b/server/socket-handlers/status-page-socket-handler.js @@ -5,21 +5,31 @@ const { debug } = require("../../src/util"); const ImageDataURI = require("../image-data-uri"); const Database = require("../database"); const apicache = require("../modules/apicache"); +const StatusPage = require("../model/status_page"); module.exports.statusPageSocketHandler = (socket) => { // Post or edit incident - socket.on("postIncident", async (incident, callback) => { + socket.on("postIncident", async (slug, incident, callback) => { try { checkLogin(socket); - await R.exec("UPDATE incident SET pin = 0 "); + let statusPageID = await StatusPage.slugToID(slug); + + if (!statusPageID) { + throw new Error("slug is not found"); + } + + await R.exec("UPDATE incident SET pin = 0 WHERE status_page_id = ? ", [ + statusPageID + ]); let incidentBean; if (incident.id) { - incidentBean = await R.findOne("incident", " id = ?", [ - incident.id + incidentBean = await R.findOne("incident", " id = ? AND status_page_id = ? ", [ + incident.id, + statusPageID ]); } @@ -31,6 +41,7 @@ module.exports.statusPageSocketHandler = (socket) => { incidentBean.content = incident.content; incidentBean.style = incident.style; incidentBean.pin = true; + incidentBean.status_page_id = statusPageID; if (incident.id) { incidentBean.lastUpdatedDate = R.isoDateTime(dayjs.utc()); @@ -52,11 +63,15 @@ module.exports.statusPageSocketHandler = (socket) => { } }); - socket.on("unpinIncident", async (callback) => { + socket.on("unpinIncident", async (slug, callback) => { try { checkLogin(socket); - await R.exec("UPDATE incident SET pin = 0 WHERE pin = 1"); + let statusPageID = await StatusPage.slugToID(slug); + + await R.exec("UPDATE incident SET pin = 0 WHERE pin = 1 AND status_page_id = ? ", [ + statusPageID + ]); callback({ ok: true, @@ -125,13 +140,15 @@ module.exports.statusPageSocketHandler = (socket) => { for (let group of publicGroupList) { let groupBean; if (group.id) { - groupBean = await R.findOne("group", " id = ? AND public = 1 ", [ - group.id + groupBean = await R.findOne("group", " id = ? AND public = 1 AND status_page_id = ? ", [ + group.id, + statusPage.id ]); } else { groupBean = R.dispense("group"); } + groupBean.status_page_id = statusPage.id; groupBean.name = group.name; groupBean.public = true; groupBean.weight = groupOrder++; @@ -143,7 +160,6 @@ module.exports.statusPageSocketHandler = (socket) => { ]); let monitorOrder = 1; - console.log(group.monitorList); for (let monitor of group.monitorList) { let relationBean = R.dispense("monitor_group"); diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue index 09c3dc57..f2850e5d 100644 --- a/src/pages/StatusPage.vue +++ b/src/pages/StatusPage.vue @@ -404,9 +404,12 @@ export default { }, "config.showTags"(value) { - console.log("here???"); this.changeTagsVisibility(value); - } + }, + + "$root.monitorList"() { + this.changeTagsVisibility(this.config.showTags); + }, }, async created() { @@ -437,29 +440,14 @@ export default { } axios.get("/api/status-page/" + this.slug).then((res) => { - this.config = res.data; + this.config = res.data.config; if (this.config.logo) { this.imgDataUrl = this.config.logo; } - }); - - axios.get("/api/status-page/config/" + this.slug).then((res) => { - this.config = res.data; - - if (this.config.logo) { - this.imgDataUrl = this.config.logo; - } - }); - - axios.get("/api/status-page/incident/" + this.slug).then((res) => { - if (res.data.ok) { - this.incident = res.data.incident; - } - }); - axios.get("/api/status-page/monitor-list/" + this.slug).then((res) => { - this.$root.publicGroupList = res.data; + this.incident = res.data.incident; + this.$root.publicGroupList = res.data.publicGroupList; }); // 5mins a loop @@ -560,21 +548,27 @@ export default { changeTagsVisibility(show) { - // On load, the status page will not include tags if it's not enabled for security reasons - // Which means if we enable tags, it won't show in the UI until saved - // So we have this to enhance UX and load in the tags from the authenticated source instantly - this.$root.publicGroupList = this.$root.publicGroupList.map((group) => { - return { - ...group, - monitorList: group.monitorList.map((monitor) => { - // We only include the tags if visible so we can reuse the logic to hide the tags on disable - return { - ...monitor, - tags: show ? this.$root.monitorList[monitor.id].tags : [] - }; - }) - }; - }); + // If Edit Mode + if (Object.keys(this.$root.monitorList).length > 0) { + // On load, the status page will not include tags if it's not enabled for security reasons + // Which means if we enable tags, it won't show in the UI until saved + // So we have this to enhance UX and load in the tags from the authenticated source instantly + this.$root.publicGroupList = this.$root.publicGroupList.map((group) => { + return { + ...group, + monitorList: group.monitorList.map((monitor) => { + // We only include the tags if visible so we can reuse the logic to hide the tags on disable + return { + ...monitor, + tags: show ? this.$root.monitorList[monitor.id].tags : [] + }; + }) + }; + }); + } else { + + } + }, /** @@ -610,7 +604,7 @@ export default { return; } - this.$root.getSocket().emit("postIncident", this.incident, (res) => { + this.$root.getSocket().emit("postIncident", this.slug, this.incident, (res) => { if (res.ok) { this.enableEditIncidentMode = false;