From 31b90d12a4ef07c0bdd45187baf31d8931e25c4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karel=20Kr=C3=BDda?= <karel.kryda@gmail.com> Date: Sat, 30 Apr 2022 17:17:22 +0200 Subject: [PATCH] Added the ability to choose on which status pages maintenance information should be displayed --- db/patch-maintenance-table.sql | 9 +++ server/routers/api-router.js | 19 ++--- server/server.js | 58 ++++++++++++++++ src/languages/en.js | 5 ++ src/mixins/socket.js | 8 +++ src/pages/EditMaintenance.vue | 116 +++++++++++++++++++++++++++---- src/pages/MaintenanceDetails.vue | 24 +++++-- src/pages/StatusPage.vue | 6 +- 8 files changed, 216 insertions(+), 29 deletions(-) diff --git a/db/patch-maintenance-table.sql b/db/patch-maintenance-table.sql index ee4a7f88..ce14f766 100644 --- a/db/patch-maintenance-table.sql +++ b/db/patch-maintenance-table.sql @@ -20,6 +20,15 @@ CREATE TABLE monitor_maintenance CONSTRAINT FK_monitor FOREIGN KEY (monitor_id) REFERENCES monitor (id) ON DELETE CASCADE ON UPDATE CASCADE ); +CREATE TABLE maintenance_status_page +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + status_page_id INTEGER NOT NULL, + maintenance_id INTEGER NOT NULL, + CONSTRAINT FK_maintenance FOREIGN KEY (maintenance_id) REFERENCES maintenance (id) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT FK_status_page FOREIGN KEY (status_page_id) REFERENCES status_page (id) ON DELETE CASCADE ON UPDATE CASCADE +); + create index maintenance_user_id on maintenance (user_id); COMMIT; diff --git a/server/routers/api-router.js b/server/routers/api-router.js index a5a87b8c..da036ee2 100644 --- a/server/routers/api-router.js +++ b/server/routers/api-router.js @@ -128,7 +128,7 @@ router.get("/api/status-page/:slug", cache("5 minutes"), async (request, respons incident = incident.toPublicJSON(); } - let maintenance = await getMaintenanceList(); + let maintenance = await getMaintenanceList(statusPage.id); // Public Group List const publicGroupList = []; @@ -158,17 +158,20 @@ router.get("/api/status-page/:slug", cache("5 minutes"), async (request, respons }); // Status Page - Maintenance List -async function getMaintenanceList() { +async function getMaintenanceList(statusPageId) { try { const publicMaintenanceList = []; let maintenanceBeanList = R.convertToBeans("maintenance", await R.getAll(` - SELECT maintenance.* - FROM maintenance - WHERE datetime(maintenance.start_date) <= datetime('now') - AND datetime(maintenance.end_date) >= datetime('now') - ORDER BY maintenance.end_date - `)); + SELECT m.* + FROM maintenance m + JOIN maintenance_status_page msp + ON msp.maintenance_id = m.id + WHERE datetime(m.start_date) <= datetime('now') + AND datetime(m.end_date) >= datetime('now') + AND msp.status_page_id = ? + ORDER BY m.end_date + `, [ statusPageId ])); for (const bean of maintenanceBeanList) { publicMaintenanceList.push(await bean.toPublicJSON()); diff --git a/server/server.js b/server/server.js index b4060351..d45b97a4 100644 --- a/server/server.js +++ b/server/server.js @@ -802,6 +802,40 @@ try { } }); + // Add a new monitor_maintenance + socket.on("addMaintenanceStatusPage", async (maintenanceID, statusPages, callback) => { + try { + checkLogin(socket); + + await R.exec("DELETE FROM maintenance_status_page WHERE maintenance_id = ?", [ + maintenanceID + ]); + + for await (const statusPage of statusPages) { + let bean = R.dispense("maintenance_status_page"); + + bean.import({ + status_page_id: statusPage.id, + maintenance_id: maintenanceID + }); + await R.store(bean); + } + + apicache.clear(); + + callback({ + ok: true, + msg: "Added Successfully.", + }); + + } catch (e) { + callback({ + ok: false, + msg: e.message, + }); + } + }); + socket.on("getMonitorList", async (callback) => { try { checkLogin(socket); @@ -906,6 +940,30 @@ try { } }); + socket.on("getMaintenanceStatusPage", async (maintenanceID, callback) => { + try { + checkLogin(socket); + + console.log(`Get Status Pages for Maintenance: ${maintenanceID} User ID: ${socket.userID}`); + + let statusPages = await R.getAll("SELECT status_page.id, status_page.title FROM maintenance_status_page msp JOIN status_page ON msp.status_page_id = status_page.id WHERE msp.maintenance_id = ? ", [ + maintenanceID, + ]); + + callback({ + ok: true, + statusPages, + }); + + } catch (e) { + console.error(e); + callback({ + ok: false, + msg: e.message, + }); + } + }); + socket.on("getMonitorBeats", async (monitorID, period, callback) => { try { checkLogin(socket); diff --git a/src/languages/en.js b/src/languages/en.js index 8bb1a73c..37f7cb43 100644 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -15,9 +15,14 @@ export default { "Pick Affected Monitors...": "Pick Affected Monitors...", "Start of maintenance": "Start of maintenance", "Expected end of maintenance": "Expected end of maintenance", + "Show on all pages": "Show on all status pages", + "Selected status pages": "Selected status pages", + "Select status pages...": "Select status pages...", End: "End", affectedMonitorsDescription: "Select monitors that are affected by current maintenance", atLeastOneMonitor: "Select at least one affected monitor", + selectedStatusPagesDescription: "Select status pages to display maintenance info on", + atLeastOneStatusPage: "Select at least one status page", maintenanceTitleExample: "Network infrastructure maintenance", maintenanceDescriptionExample: "Example: Network infrastructure maintenance is underway which will affect some of our services.", passwordNotMatchMsg: "The repeat password does not match.", diff --git a/src/mixins/socket.js b/src/mixins/socket.js index 6d4311b1..4fa779bf 100644 --- a/src/mixins/socket.js +++ b/src/mixins/socket.js @@ -388,10 +388,18 @@ export default { socket.emit("addMonitorMaintenance", maintenanceID, monitors, callback); }, + addMaintenanceStatusPage(maintenanceID, statusPages, callback) { + socket.emit("addMaintenanceStatusPage", maintenanceID, statusPages, callback); + }, + getMonitorMaintenance(maintenanceID, callback) { socket.emit("getMonitorMaintenance", maintenanceID, callback); }, + getMaintenanceStatusPage(maintenanceID, callback) { + socket.emit("getMaintenanceStatusPage", maintenanceID, callback); + }, + deleteMonitor(monitorID, callback) { socket.emit("deleteMonitor", monitorID, callback); }, diff --git a/src/pages/EditMaintenance.vue b/src/pages/EditMaintenance.vue index e8cfeb4f..26aa857f 100644 --- a/src/pages/EditMaintenance.vue +++ b/src/pages/EditMaintenance.vue @@ -54,22 +54,63 @@ <!-- Start Date Time --> <div class="my-3"> - <label for="start_date" class="form-label">{{ $t("Start of maintenance") }} ({{ $root.timezone }})</label> + <label for="start_date" class="form-label">{{ $t("Start of maintenance") }} + ({{ $root.timezone }})</label> <input id="start_date" v-model="maintenance.start_date" :type="'datetime-local'" - class="form-control" :class="{'darkCalendar': dark }" required + class="form-control" :class="{'dark-calendar': dark }" required > </div> <!-- End Date Time --> <div class="my-3"> - <label for="end_date" class="form-label">{{ $t("Expected end of maintenance") }} ({{ $root.timezone }})</label> + <label for="end_date" class="form-label">{{ $t("Expected end of maintenance") }} + ({{ $root.timezone }})</label> <input id="end_date" v-model="maintenance.end_date" :type="'datetime-local'" - class="form-control" :class="{'darkCalendar': dark }" required + class="form-control" :class="{'dark-calendar': dark }" required > </div> + <!-- Show on all pages --> + <div class="my-3 form-check"> + <input + id="show-on-all-pages" v-model="showOnAllPages" class="form-check-input" + type="checkbox" + > + <label class="form-check-label" for="show-powered-by">{{ + $t("Show on all pages") + }}</label> + </div> + + <!-- Status pages to display maintenance info on --> + <div v-if="!showOnAllPages" class="my-3"> + <label for="selected_status_pages" class="form-label">{{ + $t("Selected status pages") + }}</label> + + <VueMultiselect + id="selected_status_pages" + v-model="selectedStatusPages" + :options="selectedStatusPagesOptions" + track-by="id" + label="name" + :multiple="true" + :allow-empty="false" + :close-on-select="false" + :clear-on-select="false" + :preserve-search="true" + :placeholder="$t('Select status pages...')" + :preselect-first="false" + :max-height="600" + :taggable="false" + ></VueMultiselect> + + <div class="form-text"> + {{ $t("selectedStatusPagesDescription") }} + </div> + </div> + <div class="mt-5 mb-1"> <button id="monitor-submit-btn" class="btn btn-primary" type="submit" @@ -104,6 +145,9 @@ export default { maintenance: {}, affectedMonitors: [], affectedMonitorsOptions: [], + showOnAllPages: true, + selectedStatusPages: [], + selectedStatusPagesOptions: [], dark: (this.$root.theme === "dark"), }; }, @@ -150,10 +194,18 @@ export default { }); } }); + + Object.values(this.$root.statusPageList).map(statusPage => { + this.selectedStatusPagesOptions.push({ + id: statusPage.id, + name: statusPage.title + }); + }); }, methods: { init() { this.affectedMonitors = []; + this.selectedStatusPages = []; if (this.isAdd) { this.maintenance = { @@ -178,6 +230,21 @@ export default { toast.error(res.msg); } }); + + this.$root.getSocket().emit("getMaintenanceStatusPage", this.$route.params.id, (res) => { + if (res.ok) { + Object.values(res.statusPages).map(statusPage => { + this.selectedStatusPages.push({ + id: statusPage.id, + name: statusPage.title + }); + }); + + this.showOnAllPages = Object.values(res.statusPages).length === this.selectedStatusPagesOptions.length; + } else { + toast.error(res.msg); + } + }); } else { toast.error(res.msg); } @@ -193,17 +260,24 @@ export default { return this.processing = false; } + if (!this.showOnAllPages && this.selectedStatusPages.length === 0) { + toast.error(this.$t("atLeastOneStatusPage")); + return this.processing = false; + } + this.maintenance.start_date = this.$root.toUTC(this.maintenance.start_date); this.maintenance.end_date = this.$root.toUTC(this.maintenance.end_date); if (this.isAdd) { this.$root.addMaintenance(this.maintenance, async (res) => { if (res.ok) { - await this.addMonitorMaintenance(res.maintenanceID, () => { - toast.success(res.msg); - this.processing = false; - this.$root.getMaintenanceList(); - this.$router.push("/dashboard/maintenance/" + res.maintenanceID); + await this.addMonitorMaintenance(res.maintenanceID, async () => { + await this.addMaintenanceStatusPage(res.maintenanceID, () => { + toast.success(res.msg); + this.processing = false; + this.$root.getMaintenanceList(); + this.$router.push("/dashboard/maintenance/" + res.maintenanceID); + }); }); } else { toast.error(res.msg); @@ -214,10 +288,12 @@ export default { } else { this.$root.getSocket().emit("editMaintenance", this.maintenance, async (res) => { if (res.ok) { - await this.addMonitorMaintenance(res.maintenanceID, () => { - this.processing = false; - this.$root.toastRes(res); - this.init(); + await this.addMonitorMaintenance(res.maintenanceID, async () => { + await this.addMaintenanceStatusPage(res.maintenanceID, () => { + this.processing = false; + this.$root.toastRes(res); + this.init(); + }); }); } else { this.processing = false; @@ -238,6 +314,18 @@ export default { callback(); }); }, + + async addMaintenanceStatusPage(maintenanceID, callback) { + await this.$root.addMaintenanceStatusPage(maintenanceID, (this.showOnAllPages) ? this.selectedStatusPagesOptions : this.selectedStatusPages, async (res) => { + if (!res.ok) { + toast.error(res.msg); + } else { + this.$root.getMaintenanceList(); + } + + callback(); + }); + }, }, }; </script> @@ -251,7 +339,7 @@ textarea { min-height: 200px; } -.darkCalendar::-webkit-calendar-picker-indicator { +.dark-calendar::-webkit-calendar-picker-indicator { filter: invert(1); } </style> diff --git a/src/pages/MaintenanceDetails.vue b/src/pages/MaintenanceDetails.vue index 4f9779f6..ae4b5a7b 100644 --- a/src/pages/MaintenanceDetails.vue +++ b/src/pages/MaintenanceDetails.vue @@ -8,7 +8,7 @@ <span>{{ $t("End") }}: {{ $root.datetimeMaintenance(maintenance.end_date) }}</span> </p> - <div class="functions" style="margin-top: 10px"> + <div class="functions" style="margin-top: 10px;"> <router-link :to=" '/editMaintenance/' + maintenance.id " class="btn btn-secondary"> <font-awesome-icon icon="edit" /> {{ $t("Edit") }} </router-link> @@ -17,14 +17,21 @@ </button> </div> - <label for="description" class="form-label" style="margin-top: 20px">{{ $t("Description") }}</label> + <label for="description" class="form-label" style="margin-top: 20px;">{{ $t("Description") }}</label> <textarea id="description" v-model="maintenance.description" class="form-control" disabled></textarea> - <label for="affected_monitors" class="form-label" style="margin-top: 20px">{{ $t("Affected Monitors") }}</label> + <label for="affected_monitors" class="form-label" style="margin-top: 20px;">{{ $t("Affected Monitors") }}</label> <br> - <button v-for="monitor in affectedMonitors" :key="monitor.id" class="btn btn-monitor" style="margin: 5px; cursor: auto; color: white; font-weight: bold"> + <button v-for="monitor in affectedMonitors" :key="monitor.id" class="btn btn-monitor" style="margin: 5px; cursor: auto; color: white; font-weight: bold;"> {{ monitor }} </button> + <br /> + + <label for="selected_status_pages" class="form-label" style="margin-top: 20px;">{{ $t("Selected status pages") }}</label> + <br> + <button v-for="statusPage in selectedStatusPages" :key="statusPage.id" class="btn btn-monitor" style="margin: 5px; cursor: auto; color: white; font-weight: bold;"> + {{ statusPage }} + </button> <Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteMaintenance"> {{ $t("deleteMaintenanceMsg") }} @@ -45,6 +52,7 @@ export default { data() { return { affectedMonitors: [], + selectedStatusPages: [], }; }, computed: { @@ -65,6 +73,14 @@ export default { toast.error(res.msg); } }); + + this.$root.getSocket().emit("getMaintenanceStatusPage", this.$route.params.id, (res) => { + if (res.ok) { + this.selectedStatusPages = Object.values(res.statusPages).map(statusPage => statusPage.title); + } else { + toast.error(res.msg); + } + }); }, deleteDialog() { diff --git a/src/pages/StatusPage.vue b/src/pages/StatusPage.vue index 015a57be..4d021dc4 100644 --- a/src/pages/StatusPage.vue +++ b/src/pages/StatusPage.vue @@ -247,7 +247,7 @@ </div> <div v-else-if="isMaintenance"> - <font-awesome-icon icon="wrench" class="statusMaintenance" /> + <font-awesome-icon icon="wrench" class="status-maintenance" /> {{ $t("Maintenance") }} </div> @@ -595,7 +595,7 @@ export default { } this.incident = res.data.incident; - this.maintenance = res.data.maintenance; + this.maintenance = res.data.maintenance || []; this.$root.publicGroupList = res.data.publicGroupList; }); @@ -956,7 +956,7 @@ footer { background-color: #0d1117; } -.statusMaintenance { +.status-maintenance { color: $maintenance; margin-right: 5px; }