mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-06-14 16:42:35 +02:00
New system for creating and managing incidents
This commit is contained in:
parent
a9df7b4a14
commit
0c75711f99
31 changed files with 4861 additions and 2802 deletions
37
db/patch-incident-system.sql
Normal file
37
db/patch-incident-system.sql
Normal file
|
@ -0,0 +1,37 @@
|
|||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
create table incident_dg_tmp
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
type VARCHAR(30) DEFAULT 'started' NOT NULL,
|
||||
style VARCHAR(30) DEFAULT 'info',
|
||||
title VARCHAR(255),
|
||||
description TEXT NOT NULL,
|
||||
user_id INTEGER references user on update cascade on delete set null DEFAULT 1,
|
||||
override_status BOOLEAN DEFAULT 0 NOT NULL,
|
||||
status VARCHAR(50),
|
||||
created_date DATETIME DEFAULT (DATETIME('now')) NOT NULL,
|
||||
parent_incident INTEGER,
|
||||
resolved BOOLEAN DEFAULT 0 NOT NULL,
|
||||
resolved_date DATETIME
|
||||
);
|
||||
|
||||
insert into incident_dg_tmp(id, title, description, created_date) select id, title, content, created_date from incident;
|
||||
|
||||
drop table incident;
|
||||
|
||||
alter table incident_dg_tmp rename to incident;
|
||||
|
||||
create index incident_user_id on incident (user_id);
|
||||
|
||||
CREATE TABLE monitor_incident
|
||||
(
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
monitor_id INTEGER NOT NULL,
|
||||
incident_id INTEGER NOT NULL,
|
||||
CONSTRAINT FK_incident FOREIGN KEY (incident_id) REFERENCES incident (id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT FK_monitor FOREIGN KEY (monitor_id) REFERENCES monitor (id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
COMMIT;
|
5247
package-lock.json
generated
5247
package-lock.json
generated
File diff suppressed because it is too large
Load diff
18
package.json
18
package.json
|
@ -64,16 +64,16 @@
|
|||
"axios": "~0.21.4",
|
||||
"bcryptjs": "~2.4.3",
|
||||
"bootstrap": "5.1.3",
|
||||
"bree": "~7.1.0",
|
||||
"bree": "~7.1.5",
|
||||
"chardet": "^1.3.0",
|
||||
"chart.js": "~3.6.0",
|
||||
"chart.js": "~3.6.2",
|
||||
"chartjs-adapter-dayjs": "~1.0.0",
|
||||
"check-password-strength": "^2.0.3",
|
||||
"command-exists": "~1.2.9",
|
||||
"compare-versions": "~3.6.0",
|
||||
"dayjs": "~1.10.7",
|
||||
"express": "~4.17.1",
|
||||
"express-basic-auth": "~1.2.0",
|
||||
"express": "~4.17.2",
|
||||
"express-basic-auth": "~1.2.1",
|
||||
"form-data": "~4.0.0",
|
||||
"http-graceful-shutdown": "~3.1.5",
|
||||
"iconv-lite": "^0.6.3",
|
||||
|
@ -84,7 +84,7 @@
|
|||
"notp": "~2.0.3",
|
||||
"password-hash": "~1.2.2",
|
||||
"postcss-rtlcss": "~3.4.1",
|
||||
"postcss-scss": "~4.0.2",
|
||||
"postcss-scss": "~4.0.3",
|
||||
"prom-client": "~13.2.0",
|
||||
"prometheus-api-metrics": "~3.2.0",
|
||||
"qrcode": "~1.5.0",
|
||||
|
@ -112,10 +112,10 @@
|
|||
"@actions/github": "~5.0.0",
|
||||
"@babel/eslint-parser": "~7.15.8",
|
||||
"@babel/preset-env": "^7.15.8",
|
||||
"@types/bootstrap": "~5.1.6",
|
||||
"@vitejs/plugin-legacy": "~1.6.3",
|
||||
"@types/bootstrap": "~5.1.9",
|
||||
"@vitejs/plugin-legacy": "~1.6.4",
|
||||
"@vitejs/plugin-vue": "~1.9.4",
|
||||
"@vue/compiler-sfc": "~3.2.22",
|
||||
"@vue/compiler-sfc": "~3.2.29",
|
||||
"babel-plugin-rewire": "~1.2.0",
|
||||
"core-js": "~3.18.3",
|
||||
"cross-env": "~7.0.3",
|
||||
|
@ -123,7 +123,7 @@
|
|||
"eslint": "~7.32.0",
|
||||
"eslint-plugin-vue": "~7.18.0",
|
||||
"jest": "~27.2.5",
|
||||
"jest-puppeteer": "~6.0.0",
|
||||
"jest-puppeteer": "~6.0.3",
|
||||
"puppeteer": "~10.4.0",
|
||||
"sass": "~1.42.1",
|
||||
"stylelint": "~14.2.0",
|
||||
|
|
|
@ -53,6 +53,7 @@ class Database {
|
|||
"patch-2fa-invalidate-used-token.sql": true,
|
||||
"patch-notification_sent_history.sql": true,
|
||||
"patch-monitor-basic-auth.sql": true,
|
||||
"patch-incident-system.sql": true,
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -7,10 +7,28 @@ class Incident extends BeanModel {
|
|||
id: this.id,
|
||||
style: this.style,
|
||||
title: this.title,
|
||||
content: this.content,
|
||||
pin: this.pin,
|
||||
description: this.description,
|
||||
overrideStatus: this.overrideStatus,
|
||||
status: this.status,
|
||||
createdDate: this.createdDate,
|
||||
lastUpdatedDate: this.lastUpdatedDate,
|
||||
resolved: this.resolved,
|
||||
resolvedDate: this.resolvedDate,
|
||||
};
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
type: this.type,
|
||||
style: this.style,
|
||||
title: this.title,
|
||||
description: this.description,
|
||||
overrideStatus: this.overrideStatus,
|
||||
status: this.status,
|
||||
createdDate: this.createdDate,
|
||||
parentIncident: this.parentIncident,
|
||||
resolved: this.resolved,
|
||||
resolvedDate: this.resolvedDate,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -107,23 +107,53 @@ router.get("/api/status-page/config", async (_request, response) => {
|
|||
response.json(config);
|
||||
});
|
||||
|
||||
// Status Page - Get the current Incident
|
||||
// Status Page - Incidents List
|
||||
// Can fetch only if published
|
||||
router.get("/api/status-page/incident", async (_, response) => {
|
||||
router.get("/api/status-page/incidents", async (_, response) => {
|
||||
allowDevAllOrigin(response);
|
||||
|
||||
try {
|
||||
await checkPublished();
|
||||
|
||||
let incident = await R.findOne("incident", " pin = 1 AND active = 1");
|
||||
let incidentList = [];
|
||||
let incidents = await R.find("incident", " parent_incident IS NULL");
|
||||
|
||||
if (incident) {
|
||||
incident = incident.toPublicJSON();
|
||||
for (let incident of incidents) {
|
||||
incidentList.push(await incident.toPublicJSON());
|
||||
}
|
||||
|
||||
response.json({
|
||||
ok: true,
|
||||
incident,
|
||||
incidents: incidentList
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
send403(response, error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Status Page - Single Incident
|
||||
// Can fetch only if published
|
||||
router.get("/api/status-page/incident/:incident", async (request, response) => {
|
||||
allowDevAllOrigin(response);
|
||||
|
||||
try {
|
||||
await checkPublished();
|
||||
|
||||
let incidentID = request.params.incident;
|
||||
|
||||
let incidents = await R.find("incident", " id = ? OR parent_incident = ?", [
|
||||
incidentID,
|
||||
incidentID
|
||||
]);
|
||||
let monitors = await R.getAll("SELECT monitor.name FROM monitor_incident mi JOIN monitor ON mi.monitor_id = monitor.id WHERE mi.incident_id = ? ", [
|
||||
incidentID
|
||||
]);
|
||||
|
||||
response.json({
|
||||
ok: true,
|
||||
incidents,
|
||||
monitors
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
|
|
344
server/server.js
344
server/server.js
|
@ -132,6 +132,7 @@ const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sen
|
|||
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
|
||||
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
|
||||
const TwoFA = require("./2fa");
|
||||
const apicache = require("./modules/apicache");
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
|
@ -162,6 +163,12 @@ let jwtSecret = null;
|
|||
*/
|
||||
let monitorList = {};
|
||||
|
||||
/**
|
||||
* Main incident list
|
||||
* @type {{}}
|
||||
*/
|
||||
let incidentList = {};
|
||||
|
||||
/**
|
||||
* Show Setup Page
|
||||
* @type {boolean}
|
||||
|
@ -533,7 +540,7 @@ exports.entryPage = "dashboard";
|
|||
// ***************************
|
||||
|
||||
// Add a new monitor
|
||||
socket.on("add", async (monitor, callback) => {
|
||||
socket.on("addMonitor", async (monitor, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
let bean = R.dispense("monitor");
|
||||
|
@ -625,6 +632,215 @@ exports.entryPage = "dashboard";
|
|||
}
|
||||
});
|
||||
|
||||
// Add a new incident
|
||||
socket.on("addIncident", async (incident, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
let bean = R.dispense("incident");
|
||||
|
||||
bean.import(incident);
|
||||
bean.user_id = socket.userID;
|
||||
await R.store(bean);
|
||||
|
||||
await sendIncidentList(socket);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Added Successfully.",
|
||||
incidentID: bean.id,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("editIncident", async (incident, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
let bean = await R.findOne("incident", " id = ? ", [ incident.id ]);
|
||||
|
||||
if (bean.user_id !== socket.userID) {
|
||||
throw new Error("Permission denied.");
|
||||
}
|
||||
|
||||
bean.type = incident.type;
|
||||
bean.style = incident.style;
|
||||
bean.title = incident.title;
|
||||
bean.description = incident.description;
|
||||
bean.overrideStatus = incident.overrideStatus;
|
||||
bean.status = incident.status;
|
||||
|
||||
await R.store(bean);
|
||||
|
||||
await sendIncidentList(socket);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Saved.",
|
||||
incidentID: bean.id,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add a new monitor_incident
|
||||
socket.on("addMonitorIncident", async (incidentID, monitors, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
await R.exec("DELETE FROM monitor_incident WHERE incident_id = ?", [
|
||||
incidentID
|
||||
]);
|
||||
|
||||
for await (const monitor of monitors) {
|
||||
let bean = R.dispense("monitor_incident");
|
||||
|
||||
bean.import({
|
||||
monitor_id: monitor.id,
|
||||
incident_id: incidentID
|
||||
});
|
||||
await R.store(bean);
|
||||
}
|
||||
|
||||
apicache.clear();
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Added Successfully.",
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("resolveIncident", async (incidentID, description, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
await checkIncidentOwner(socket.userID, incidentID);
|
||||
|
||||
console.log(`Resolve Incident: ${incidentID} User ID: ${socket.userID}`);
|
||||
|
||||
await R.exec("UPDATE incident SET resolved = 1, resolved_date = DATETIME('now') WHERE id = ? AND user_id = ? ", [
|
||||
incidentID,
|
||||
socket.userID,
|
||||
]);
|
||||
|
||||
// Add new incident update
|
||||
let bean = R.dispense("incident");
|
||||
|
||||
let incident = {
|
||||
type: "resolved",
|
||||
description,
|
||||
parentIncident: incidentID,
|
||||
};
|
||||
|
||||
bean.import(incident);
|
||||
bean.user_id = socket.userID;
|
||||
await R.store(bean);
|
||||
|
||||
await sendIncidentList(socket);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Resolved Successfully.",
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("postIncidentUpdate", async (incidentID, description, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
await checkIncidentOwner(socket.userID, incidentID);
|
||||
|
||||
console.log(`Post update for Incident: ${incidentID} User ID: ${socket.userID}`);
|
||||
|
||||
// Add new incident update
|
||||
let bean = R.dispense("incident");
|
||||
|
||||
let incident = {
|
||||
type: "update",
|
||||
description,
|
||||
parentIncident: incidentID,
|
||||
};
|
||||
|
||||
bean.import(incident);
|
||||
bean.user_id = socket.userID;
|
||||
await R.store(bean);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Update Posted Successfully.",
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("reopenIncident", async (incidentID, description, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
await checkIncidentOwner(socket.userID, incidentID);
|
||||
|
||||
console.log(`Reopen Incident: ${incidentID} User ID: ${socket.userID}`);
|
||||
|
||||
await R.exec("UPDATE incident SET resolved = 0, resolved_date = NULL WHERE id = ? AND user_id = ? ", [
|
||||
incidentID,
|
||||
socket.userID,
|
||||
]);
|
||||
|
||||
// Add new incident update
|
||||
let bean = R.dispense("incident");
|
||||
|
||||
let incident = {
|
||||
type: "reopened",
|
||||
description,
|
||||
parentIncident: incidentID,
|
||||
};
|
||||
|
||||
bean.import(incident);
|
||||
bean.user_id = socket.userID;
|
||||
await R.store(bean);
|
||||
|
||||
await sendIncidentList(socket);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Reopened Successfully.",
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("getMonitorList", async (callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
@ -665,6 +881,70 @@ exports.entryPage = "dashboard";
|
|||
}
|
||||
});
|
||||
|
||||
socket.on("getIncidentList", async (callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
await sendIncidentList(socket);
|
||||
callback({
|
||||
ok: true,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("getIncident", async (incidentID, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
console.log(`Get Incident: ${incidentID} User ID: ${socket.userID}`);
|
||||
|
||||
let bean = await R.findOne("incident", " id = ? AND user_id = ? ", [
|
||||
incidentID,
|
||||
socket.userID,
|
||||
]);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
incident: await bean.toJSON(),
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("getMonitorIncident", async (incidentID, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
console.log(`Get Monitors for Incident: ${incidentID} User ID: ${socket.userID}`);
|
||||
|
||||
let monitors = await R.getAll("SELECT monitor.id, monitor.name FROM monitor_incident mi JOIN monitor ON mi.monitor_id = monitor.id WHERE mi.incident_id = ? ", [
|
||||
incidentID,
|
||||
]);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
monitors,
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("getMonitorBeats", async (monitorID, period, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
@ -769,6 +1049,36 @@ exports.entryPage = "dashboard";
|
|||
}
|
||||
});
|
||||
|
||||
socket.on("deleteIncident", async (incidentID, callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
console.log(`Delete Incident: ${incidentID} User ID: ${socket.userID}`);
|
||||
|
||||
if (incidentID in incidentList) {
|
||||
delete incidentList[incidentID];
|
||||
}
|
||||
|
||||
await R.exec("DELETE FROM incident WHERE id = ? AND user_id = ? ", [
|
||||
incidentID,
|
||||
socket.userID,
|
||||
]);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
msg: "Deleted Successfully.",
|
||||
});
|
||||
|
||||
await sendIncidentList(socket);
|
||||
|
||||
} catch (e) {
|
||||
callback({
|
||||
ok: false,
|
||||
msg: e.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("getTags", async (callback) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
@ -1388,17 +1698,35 @@ async function checkOwner(userID, monitorID) {
|
|||
}
|
||||
}
|
||||
|
||||
async function checkIncidentOwner(userID, incidentID) {
|
||||
let row = await R.getRow("SELECT id FROM incident WHERE id = ? AND user_id = ? ", [
|
||||
incidentID,
|
||||
userID,
|
||||
]);
|
||||
|
||||
if (! row) {
|
||||
throw new Error("You do not own this incident.");
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMonitorList(socket) {
|
||||
let list = await getMonitorJSONList(socket.userID);
|
||||
io.to(socket.userID).emit("monitorList", list);
|
||||
return list;
|
||||
}
|
||||
|
||||
async function sendIncidentList(socket) {
|
||||
let list = await getIncidentJSONList(socket.userID);
|
||||
io.to(socket.userID).emit("incidentList", list);
|
||||
return list;
|
||||
}
|
||||
|
||||
async function afterLogin(socket, user) {
|
||||
socket.userID = user.id;
|
||||
socket.join(user.id);
|
||||
|
||||
let monitorList = await sendMonitorList(socket);
|
||||
sendIncidentList(socket);
|
||||
sendNotificationList(socket);
|
||||
|
||||
await sleep(500);
|
||||
|
@ -1430,6 +1758,20 @@ async function getMonitorJSONList(userID) {
|
|||
return result;
|
||||
}
|
||||
|
||||
async function getIncidentJSONList(userID) {
|
||||
let result = {};
|
||||
|
||||
let incidentList = await R.find("incident", " parent_incident IS NULL AND user_id = ? ORDER BY title", [
|
||||
userID,
|
||||
]);
|
||||
|
||||
for (let incident of incidentList) {
|
||||
result[incident.id] = await incident.toJSON();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function initDatabase(testMode = false) {
|
||||
if (! fs.existsSync(Database.path)) {
|
||||
console.log("Copying Database");
|
||||
|
|
95
src/assets/_timeline.scss
Normal file
95
src/assets/_timeline.scss
Normal file
|
@ -0,0 +1,95 @@
|
|||
.incident-timeline {
|
||||
/* Variables */
|
||||
|
||||
$outline: #00b277;
|
||||
$background: #00b277;
|
||||
|
||||
$color-primary: rgba(153, 160, 164, 0.59);
|
||||
$color-light: white;
|
||||
$spacing: 50px;
|
||||
$radius: 4px;
|
||||
|
||||
$date: 120px;
|
||||
$dotBorder: 3px;
|
||||
$dot: 15px;
|
||||
$line: 4px;
|
||||
|
||||
/* Base */
|
||||
#timeline-content {
|
||||
margin-top: $spacing;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Timeline */
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.timeline {
|
||||
max-width: 40% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.timeline {
|
||||
border-left: $line solid $color-primary;
|
||||
border-bottom-right-radius: $radius;
|
||||
border-top-right-radius: $radius;
|
||||
background: fade($color-light, 3%);
|
||||
margin: $spacing auto;
|
||||
letter-spacing: 0.5px;
|
||||
position: relative;
|
||||
line-height: 1.4em;
|
||||
font-size: 1.03em;
|
||||
padding: $spacing;
|
||||
list-style: none;
|
||||
text-align: left;
|
||||
max-width: 60%;
|
||||
|
||||
.event {
|
||||
border-bottom: 1px dashed fade($color-light, 10%);
|
||||
padding-bottom: ($spacing * 0.5);
|
||||
margin-bottom: $spacing;
|
||||
position: relative;
|
||||
|
||||
&:last-of-type {
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
&:before, &:after {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&:before {
|
||||
left: ((($date * 0.6) + $spacing + $line + $dot + ($dotBorder * 2)) * 1.5) * -1;
|
||||
color: fade($color-light, 40%);
|
||||
content: attr(data-date);
|
||||
text-align: right;
|
||||
font-weight: bolder;
|
||||
font-size: 1em;
|
||||
min-width: $date;
|
||||
}
|
||||
|
||||
&:after {
|
||||
box-shadow: 0 0 0 $dotBorder fade($color-primary, 100%);
|
||||
left: ($spacing + $line + ($dot * 0.35)) * -1;
|
||||
border: $dotBorder solid lighten($background, 5%);
|
||||
background: $color-light;
|
||||
border-radius: 50%;
|
||||
height: $dot;
|
||||
width: $dot;
|
||||
content: "";
|
||||
top: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.dark .incident-timeline .timeline .event:after {
|
||||
background: #0d1117 !important;
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
<h5 id="exampleModalLabel" class="modal-title">
|
||||
{{ $t("Confirm") }}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" />
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" @click="no" />
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<slot />
|
||||
|
@ -15,7 +15,7 @@
|
|||
<button type="button" class="btn" :class="btnStyle" data-bs-dismiss="modal" @click="yes">
|
||||
{{ yesText }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @click="no">
|
||||
{{ noText }}
|
||||
</button>
|
||||
</div>
|
||||
|
@ -55,6 +55,9 @@ export default {
|
|||
yes() {
|
||||
this.$emit("yes");
|
||||
},
|
||||
no() {
|
||||
this.$emit("no");
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
<template>
|
||||
<div class="shadow-box mb-3">
|
||||
<div class="list-header">
|
||||
<div class="placeholder"></div>
|
||||
<div class="search-wrapper float-start" style="margin-left: 5px;">
|
||||
<font-awesome-icon icon="filter" />
|
||||
<select v-model="selectedList" class="form-control" style="margin-left: 5px">
|
||||
<option value="monitors" selected>{{$t('Monitors')}}</option>
|
||||
<option value="incidents" selected>{{$t('Incidents')}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="search-wrapper">
|
||||
<a v-if="searchText == ''" class="search-icon">
|
||||
<font-awesome-icon icon="search" />
|
||||
|
@ -13,11 +19,14 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="monitor-list" :class="{ scrollbar: scrollbar }">
|
||||
<div v-if="Object.keys($root.monitorList).length === 0" class="text-center mt-3">
|
||||
{{ $t("No Monitors, please") }} <router-link to="/add">{{ $t("add one") }}</router-link>
|
||||
<div v-if="Object.keys($root.monitorList).length === 0 && selectedList === 'monitors'" class="text-center mt-3">
|
||||
{{ $t("No Monitors, please") }} <router-link to="/addMonitor">{{ $t("add one") }}</router-link>
|
||||
</div>
|
||||
<div v-if="Object.keys($root.incidentList).length === 0 && selectedList === 'incidents'" class="text-center mt-3">
|
||||
{{ $t("No Incidents, please") }} <router-link to="/addIncident">{{ $t("add one") }}</router-link>
|
||||
</div>
|
||||
|
||||
<router-link v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }">
|
||||
<router-link v-if="selectedList === 'monitors'" v-for="(item, index) in sortedMonitorList" :key="index" :to="monitorURL(item.id)" class="item" :class="{ 'disabled': ! item.active }">
|
||||
<div class="row">
|
||||
<div class="col-9 col-md-8 small-padding" :class="{ 'monitorItem': $root.userHeartbeatBar == 'bottom' || $root.userHeartbeatBar == 'none' }">
|
||||
<div class="info">
|
||||
|
@ -39,6 +48,24 @@
|
|||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
<router-link v-if="selectedList === 'incidents'" v-for="(item, index) in sortedIncidentList" :key="index" :to="incidentURL(item.id)" class="item" :class="{ 'disabled': item.resolved }">
|
||||
<div class="row">
|
||||
<div class="col-9 col-md-8 small-padding">
|
||||
<div class="info incident-info">
|
||||
<font-awesome-icon v-if="item.resolved" icon="check-circle"
|
||||
class="incident-icon incident-bg-resolved"/>
|
||||
<font-awesome-icon v-else-if="item.style === 'info'" icon="info-circle"
|
||||
class="incident-icon incident-bg-info"/>
|
||||
<font-awesome-icon v-else-if="item.style === 'warning'"
|
||||
icon="exclamation-triangle"
|
||||
class="incident-icon incident-bg-warning"/>
|
||||
<font-awesome-icon v-else-if="item.style === 'critical'" icon="exclamation-circle"
|
||||
class="incident-icon incident-bg-danger"/>
|
||||
{{ item.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -47,7 +74,7 @@
|
|||
import HeartbeatBar from "../components/HeartbeatBar.vue";
|
||||
import Uptime from "../components/Uptime.vue";
|
||||
import Tag from "../components/Tag.vue";
|
||||
import { getMonitorRelativeURL } from "../util.ts";
|
||||
import { getMonitorRelativeURL, getIncidentRelativeURL } from "../util.ts";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -63,6 +90,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
searchText: "",
|
||||
selectedList: "monitors",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -105,6 +133,46 @@ export default {
|
|||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
sortedIncidentList() {
|
||||
let result = Object.values(this.$root.incidentList);
|
||||
|
||||
result.sort((i1, i2) => {
|
||||
|
||||
if (i1.resolved !== i2.resolved) {
|
||||
if (i1.resolved) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (i2.resolved) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
if (Date.parse(i1.createdDate) > Date.parse(i2.createdDate)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (Date.parse(i2.createdDate) < Date.parse(i1.createdDate)) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return i1.title.localeCompare(i2.title);
|
||||
});
|
||||
|
||||
// Simple filter by search text
|
||||
// finds monitor name, tag name or tag value
|
||||
if (this.searchText != "") {
|
||||
const loweredSearchText = this.searchText.toLowerCase();
|
||||
result = result.filter(incident => {
|
||||
return incident.name.toLowerCase().includes(loweredSearchText)
|
||||
|| incident.description.toLowerCase().includes(loweredSearchText)
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
},
|
||||
|
@ -112,6 +180,9 @@ export default {
|
|||
monitorURL(id) {
|
||||
return getMonitorRelativeURL(id);
|
||||
},
|
||||
incidentURL(id) {
|
||||
return getIncidentRelativeURL(id);
|
||||
},
|
||||
clearSearchText() {
|
||||
this.searchText = "";
|
||||
}
|
||||
|
@ -174,4 +245,37 @@ export default {
|
|||
flex-wrap: wrap;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
select {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.incident-info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
column-gap: 10px;
|
||||
}
|
||||
|
||||
.incident-bg-resolved {
|
||||
color: rgba(84, 220, 53, 0.52);
|
||||
}
|
||||
|
||||
.incident-bg-info {
|
||||
color: rgba(53, 162, 220, 0.52);
|
||||
}
|
||||
|
||||
.incident-bg-warning {
|
||||
color: rgba(255, 165, 0, 0.52);
|
||||
}
|
||||
|
||||
.incident-bg-danger {
|
||||
color: #dc354585;
|
||||
}
|
||||
|
||||
.incident-icon {
|
||||
font-size: 20px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
|
|
111
src/components/PublicIncidentsList.vue
Normal file
111
src/components/PublicIncidentsList.vue
Normal file
|
@ -0,0 +1,111 @@
|
|||
<template>
|
||||
<div class="mb-5 ">
|
||||
<h2 class="incident-title">{{ $t("Incident History") }}</h2>
|
||||
|
||||
<div class="shadow-box monitor-list mt-4 position-relative">
|
||||
<div v-if="Object.values($root.publicIncidentsList).filter(incident => incident.resolved).length === 0"
|
||||
class="text-center">
|
||||
{{ $t("No Incidents") }}
|
||||
</div>
|
||||
<!-- Incident List -->
|
||||
<template
|
||||
v-for="incident in Object.values($root.publicIncidentsList).filter(incident => incident.resolved).sort((i1, i2) => Date.parse(i2.resolvedDate) - Date.parse(i1.resolvedDate)).slice(0, 5)">
|
||||
<router-link :to="'/incident/' + incident.id">
|
||||
<div class="item">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-12 small-padding">
|
||||
<div class="info">
|
||||
<p class="title">{{ incident.title }}</p>
|
||||
<p class="description">{{ incident.description }}</p>
|
||||
</div>
|
||||
<div class="sub-info">
|
||||
<span>{{ $t("Resolved") }}</span>
|
||||
<font-awesome-icon icon="circle" class="dot"/>
|
||||
<span>{{ $root.datetime(incident.resolvedDate) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</template>
|
||||
<template
|
||||
v-if="Object.values($root.publicIncidentsList).filter((incident) => incident.resolved).length > 5">
|
||||
<div class="item item-link">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-12 small-padding">
|
||||
<span class="title d-flex justify-content-end">
|
||||
<router-link :to="'/incidents'">
|
||||
<span>{{ $t("Show all incidents") }} <font-awesome-icon icon="chevron-right"
|
||||
class="chevron-right"/></span>
|
||||
</router-link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
props: {},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {},
|
||||
created() {
|
||||
|
||||
},
|
||||
methods: {}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars";
|
||||
|
||||
.monitor-list {
|
||||
min-height: 46px;
|
||||
}
|
||||
|
||||
.mobile {
|
||||
.item {
|
||||
padding: 13px 0 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.info {
|
||||
.title {
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
white-space: normal !important;
|
||||
}
|
||||
|
||||
.sub-info {
|
||||
font-size: .875rem;
|
||||
color: #637381;
|
||||
}
|
||||
|
||||
.dot {
|
||||
font-size: 5px;
|
||||
vertical-align: middle;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.chevron-right {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.item-link:hover {
|
||||
background-color: inherit !important;
|
||||
}
|
||||
|
||||
</style>
|
16
src/icon.js
16
src/icon.js
|
@ -34,6 +34,14 @@ import {
|
|||
faAward,
|
||||
faLink,
|
||||
faChevronDown,
|
||||
faHeartbeat,
|
||||
faFilter,
|
||||
faCircle,
|
||||
faInfoCircle,
|
||||
faExclamationTriangle,
|
||||
faChevronLeft,
|
||||
faChevronRight,
|
||||
faExclamation,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
library.add(
|
||||
|
@ -67,6 +75,14 @@ library.add(
|
|||
faAward,
|
||||
faLink,
|
||||
faChevronDown,
|
||||
faHeartbeat,
|
||||
faFilter,
|
||||
faCircle,
|
||||
faInfoCircle,
|
||||
faExclamationTriangle,
|
||||
faChevronLeft,
|
||||
faChevronRight,
|
||||
faExclamation,
|
||||
);
|
||||
|
||||
export { FontAwesomeIcon };
|
||||
|
|
|
@ -38,6 +38,19 @@ export default {
|
|||
"Check Update On GitHub": "Check Update On GitHub",
|
||||
List: "List",
|
||||
Add: "Add",
|
||||
Monitor: "Monitor",
|
||||
Incident: "Incident",
|
||||
Monitors: "Monitors",
|
||||
Incidents: "Incidents",
|
||||
"Incident History": "Incident History",
|
||||
"No Incidents": "No Incidents",
|
||||
Opened: "Opened",
|
||||
started: "Started",
|
||||
update: "Update",
|
||||
resolved: "Resolved",
|
||||
reopened: "Reopened",
|
||||
backToStatus: "Back to status page",
|
||||
backToIncidents: "Back to incident history",
|
||||
"Add New Monitor": "Add New Monitor",
|
||||
"Quick Stats": "Quick Stats",
|
||||
Up: "Up",
|
||||
|
@ -52,6 +65,30 @@ export default {
|
|||
"No important events": "No important events",
|
||||
Resume: "Resume",
|
||||
Edit: "Edit",
|
||||
"Edit Monitor": "Edit Monitor",
|
||||
"Edit Incident": "Edit Incident",
|
||||
Type: "Type",
|
||||
"Affected Monitors": "Affected Monitors",
|
||||
"Pick Affected Monitors...": "Pick Affected Monitors...",
|
||||
affectedMonitorsIncident: "Select monitors that are affected by current incident",
|
||||
overrideStatus: "Override status",
|
||||
overrideStatusDescription: "Override overall status on status page",
|
||||
"Select status": "Select status",
|
||||
Operational: "Operational",
|
||||
Degraded: "Degraded",
|
||||
"Partial outage": "Partial outage",
|
||||
"Full outage": "Full outage",
|
||||
Informative: "Informative",
|
||||
Resolved: "Resolved",
|
||||
Ongoing: "Ongoing",
|
||||
"Show all incidents": "Show all incidents",
|
||||
Warning: "Warning",
|
||||
Critical: "Critical",
|
||||
"Post update": "Post update",
|
||||
Resolve: "Resolve",
|
||||
Reopen: "Reopen",
|
||||
deleteIncidentMsg: "Are you sure want to delete this incident?",
|
||||
descriptionRequired: "Please enter a description of the current incident situation.",
|
||||
Delete: "Delete",
|
||||
Current: "Current",
|
||||
Uptime: "Uptime",
|
||||
|
@ -112,6 +149,7 @@ export default {
|
|||
"Remember me": "Remember me",
|
||||
Login: "Login",
|
||||
"No Monitors, please": "No Monitors, please",
|
||||
"No Incidents, please": "No Incidents, please",
|
||||
"add one": "add one",
|
||||
"Notification Type": "Notification Type",
|
||||
Email: "Email",
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
{{ $t("List") }}
|
||||
</router-link>
|
||||
|
||||
<router-link to="/add" class="nav-link">
|
||||
<router-link to="/addMonitor" class="nav-link">
|
||||
<div><font-awesome-icon icon="plus" /></div>
|
||||
{{ $t("Add") }}
|
||||
</router-link>
|
||||
|
|
|
@ -2,6 +2,7 @@ import dayjs from "dayjs";
|
|||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
dayjs.extend(relativeTime);
|
||||
|
@ -41,6 +42,30 @@ export default {
|
|||
return dayjs.utc(value).tz(this.timezone).format(format);
|
||||
}
|
||||
return "";
|
||||
},
|
||||
|
||||
groupTimesBy(list, timeParamName = 'createdDate') {
|
||||
let toReturn = {};
|
||||
|
||||
for (let listItem of list) {
|
||||
const year = dayjs.utc(listItem[timeParamName]).tz(this.timezone).format("YYYY");
|
||||
const month = dayjs.utc(listItem[timeParamName]).tz(this.timezone).format("MM");
|
||||
|
||||
if (toReturn[year] == null) {
|
||||
toReturn[year] = {};
|
||||
}
|
||||
if (toReturn[year][month] == null) {
|
||||
toReturn[year][month] = [];
|
||||
}
|
||||
|
||||
toReturn[year][month].push(listItem);
|
||||
}
|
||||
|
||||
return toReturn;
|
||||
},
|
||||
|
||||
getMonthName(month) {
|
||||
return dayjs().month(month - 1).format("MMMM");
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
publicGroupList: [],
|
||||
publicIncidentsList: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
|
@ -27,6 +27,7 @@ export default {
|
|||
allowLoginDialog: false, // Allowed to show login dialog, but "loggedIn" have to be true too. This exists because prevent the login dialog show 0.1s in first before the socket server auth-ed.
|
||||
loggedIn: false,
|
||||
monitorList: { },
|
||||
incidentList: { },
|
||||
heartbeatList: { },
|
||||
importantHeartbeatList: { },
|
||||
avgPingList: { },
|
||||
|
@ -99,6 +100,10 @@ export default {
|
|||
this.monitorList = data;
|
||||
});
|
||||
|
||||
socket.on("incidentList", (data) => {
|
||||
this.incidentList = data;
|
||||
});
|
||||
|
||||
socket.on("notificationList", (data) => {
|
||||
this.notificationList = data;
|
||||
});
|
||||
|
@ -309,14 +314,33 @@ export default {
|
|||
socket.emit("getMonitorList", callback);
|
||||
},
|
||||
|
||||
add(monitor, callback) {
|
||||
socket.emit("add", monitor, callback);
|
||||
addMonitor(monitor, callback) {
|
||||
socket.emit("addMonitor", monitor, callback);
|
||||
},
|
||||
|
||||
deleteMonitor(monitorID, callback) {
|
||||
socket.emit("deleteMonitor", monitorID, callback);
|
||||
},
|
||||
|
||||
getIncidentList(callback) {
|
||||
if (! callback) {
|
||||
callback = () => { };
|
||||
}
|
||||
socket.emit("getIncidentList", callback);
|
||||
},
|
||||
|
||||
addIncident(incident, callback) {
|
||||
socket.emit("addIncident", incident, callback);
|
||||
},
|
||||
|
||||
deleteIncident(incidentID, callback) {
|
||||
socket.emit("deleteIncident", incidentID, callback);
|
||||
},
|
||||
|
||||
addMonitorIncident(incidentID, monitors, callback) {
|
||||
socket.emit("addMonitorIncident", incidentID, monitors, callback);
|
||||
},
|
||||
|
||||
clearData() {
|
||||
console.log("reset heartbeat list");
|
||||
this.heartbeatList = {};
|
||||
|
|
|
@ -33,7 +33,7 @@ export default {
|
|||
return "light";
|
||||
}
|
||||
|
||||
if (this.path === "/status-page" || this.path === "/status") {
|
||||
if (this.path === "/status-page" || this.path === "/status" || this.path === "/incidents" || this.path.split("/")[1] === "incident") {
|
||||
return this.statusPageTheme;
|
||||
} else {
|
||||
if (this.userTheme === "auto") {
|
||||
|
|
|
@ -2,8 +2,22 @@
|
|||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div v-if="! $root.isMobile" class="col-12 col-md-5 col-xl-4">
|
||||
<div>
|
||||
<router-link to="/add" class="btn btn-primary mb-3"><font-awesome-icon icon="plus" /> {{ $t("Add New Monitor") }}</router-link>
|
||||
<div class="dropdown dropdown-create">
|
||||
<button class="btn btn-primary mb-3 dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||
<font-awesome-icon icon="plus" /> {{ $t("Create") }}
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<button type="button" class="dropdown-item" @click="this.$router.push('/addMonitor')">
|
||||
<font-awesome-icon icon="heartbeat" /> {{ $t("Monitor") }}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item" @click="this.$router.push('/addIncident')">
|
||||
<font-awesome-icon icon="exclamation-circle" /> {{ $t("Incident") }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<MonitorList :scrollbar="true" />
|
||||
</div>
|
||||
|
@ -31,7 +45,32 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
.container-fluid {
|
||||
width: 98%;
|
||||
}
|
||||
|
||||
.dropdown-create {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.dark {
|
||||
.dropdown-create {
|
||||
ul {
|
||||
background-color: $dark-bg;
|
||||
border-color: $dark-bg2;
|
||||
border-width: 2px;
|
||||
|
||||
li button {
|
||||
color: $dark-font-color;
|
||||
}
|
||||
|
||||
li button:hover {
|
||||
background-color: $dark-bg2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
</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/monitor/${beat.monitorID}`">{{ beat.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>
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
<button v-if="! monitor.active" class="btn btn-primary" @click="resumeMonitor">
|
||||
<font-awesome-icon icon="play" /> {{ $t("Resume") }}
|
||||
</button>
|
||||
<router-link :to=" '/edit/' + monitor.id " class="btn btn-secondary">
|
||||
<router-link :to=" '/editMonitor/' + monitor.id " class="btn btn-secondary">
|
||||
<font-awesome-icon icon="edit" /> {{ $t("Edit") }}
|
||||
</router-link>
|
||||
<button class="btn btn-danger" @click="deleteDialog">
|
||||
|
|
294
src/pages/EditIncident.vue
Normal file
294
src/pages/EditIncident.vue
Normal file
|
@ -0,0 +1,294 @@
|
|||
<template>
|
||||
<transition name="slide-fade" appear>
|
||||
<div>
|
||||
<h1 class="mb-3">{{ pageName }}</h1>
|
||||
<form @submit.prevent="submit">
|
||||
<div class="shadow-box">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h2 class="mb-2">{{ $t("General") }}</h2>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="type" class="form-label">{{ $t("Style") }}</label>
|
||||
<select id="type" v-model="incident.style" class="form-select">
|
||||
<option value="info">
|
||||
{{ $t("Informative") }}
|
||||
</option>
|
||||
<option value="warning">
|
||||
{{ $t("Warning") }}
|
||||
</option>
|
||||
<option value="critical">
|
||||
{{ $t("Critical") }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Friendly Name -->
|
||||
<div class="my-3">
|
||||
<label for="name" class="form-label">{{ $t("Title") }}</label>
|
||||
<input id="name" v-model="incident.title" type="text" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="my-3">
|
||||
<label for="description" class="form-label">{{ $t("Description") }}</label>
|
||||
<textarea id="description" v-model="incident.description"
|
||||
class="form-control"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Affected Monitors -->
|
||||
<div class="my-3">
|
||||
<label for="affected_monitors" class="form-label">{{ $t("Affected Monitors") }}</label>
|
||||
|
||||
<VueMultiselect
|
||||
id="affected_monitors"
|
||||
v-model="affectedMonitors"
|
||||
:options="affectedMonitorsOptions"
|
||||
track-by="id"
|
||||
label="name"
|
||||
:multiple="true"
|
||||
:close-on-select="false"
|
||||
:clear-on-select="false"
|
||||
:preserve-search="true"
|
||||
:placeholder="$t('Pick Affected Monitors...')"
|
||||
:preselect-first="false"
|
||||
:max-height="600"
|
||||
:taggable="false"
|
||||
></VueMultiselect>
|
||||
|
||||
<div class="form-text">
|
||||
{{ $t("affectedMonitorsIncident") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="mt-5 mb-2">{{ $t("Advanced") }}</h2>
|
||||
|
||||
<div class="my-3 form-check">
|
||||
<input id="override-status" class="form-check-input"
|
||||
type="checkbox" value="" v-model="incident.overrideStatus"
|
||||
:checked="incident.overrideStatus">
|
||||
<label class="form-check-label" for="override-status">
|
||||
{{ $t("overrideStatus") }}
|
||||
</label>
|
||||
<div class="form-text">
|
||||
{{ $t("overrideStatusDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-3" v-if="incident.overrideStatus">
|
||||
<label for="override-status-value" class="form-label">{{ $t("Select status") }}</label>
|
||||
|
||||
<VueMultiselect
|
||||
class="status-selector"
|
||||
id="override-status-value"
|
||||
v-model="incident.status"
|
||||
:options="overrideStatusOptions"
|
||||
track-by="status"
|
||||
label="name"
|
||||
:multiple="false"
|
||||
:close-on-select="true"
|
||||
:clear-on-select="false"
|
||||
:preserve-search="true"
|
||||
:placeholder="$t('Select status') + '...'"
|
||||
:preselect-first="false"
|
||||
:max-height="600"
|
||||
:taggable="true"
|
||||
></VueMultiselect>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 mb-1">
|
||||
<button id="monitor-submit-btn" class="btn btn-primary" type="submit"
|
||||
:disabled="processing">{{ $t("Save") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {useToast} from "vue-toastification";
|
||||
import VueMultiselect from "vue-multiselect";
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
export default {
|
||||
components: {
|
||||
VueMultiselect,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
processing: false,
|
||||
incident: {
|
||||
// Do not add default value here, please check init() method
|
||||
},
|
||||
affectedMonitors: [],
|
||||
affectedMonitorsOptions: [],
|
||||
overrideStatusOptions: [],
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
pageName() {
|
||||
return this.$t((this.isAdd) ? "Create Incident" : "Edit Incident");
|
||||
},
|
||||
|
||||
isAdd() {
|
||||
return this.$route.path === "/addIncident";
|
||||
},
|
||||
|
||||
isEdit() {
|
||||
return this.$route.path.startsWith("/editIncident");
|
||||
},
|
||||
|
||||
},
|
||||
watch: {
|
||||
|
||||
"$route.fullPath"() {
|
||||
this.init();
|
||||
},
|
||||
|
||||
},
|
||||
mounted() {
|
||||
this.init();
|
||||
|
||||
let overrideStatusOptions = [
|
||||
{
|
||||
status: "operational",
|
||||
name: this.$t("Operational")
|
||||
},
|
||||
{
|
||||
status: "partial-outage",
|
||||
name: this.$t("Partial outage")
|
||||
},
|
||||
{
|
||||
status: "full-outage",
|
||||
name: this.$t("Full outage")
|
||||
},
|
||||
];
|
||||
|
||||
this.$root.getMonitorList((res) => {
|
||||
if (res.ok) {
|
||||
Object.values(this.$root.monitorList).map(monitor => {
|
||||
this.affectedMonitorsOptions.push({
|
||||
id: monitor.id,
|
||||
name: monitor.name,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.overrideStatusOptions = overrideStatusOptions;
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
this.affectedMonitors = [];
|
||||
|
||||
if (this.isAdd) {
|
||||
|
||||
this.incident = {
|
||||
style: "info",
|
||||
title: "",
|
||||
description: "",
|
||||
overrideStatus: false,
|
||||
status: "",
|
||||
};
|
||||
} else if (this.isEdit) {
|
||||
this.$root.getSocket().emit("getIncident", this.$route.params.id, (res) => {
|
||||
if (res.ok) {
|
||||
this.incident = res.incident;
|
||||
|
||||
if (this.incident.status) {
|
||||
this.incident.status = this.overrideStatusOptions.filter(status => status.status === this.incident.status);
|
||||
}
|
||||
|
||||
this.$root.getSocket().emit("getMonitorIncident", this.$route.params.id, (res) => {
|
||||
if (res.ok) {
|
||||
Object.values(res.monitors).map(monitor => {
|
||||
this.affectedMonitors.push(monitor);
|
||||
});
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
async submit() {
|
||||
this.processing = true;
|
||||
|
||||
if (this.incident.status) {
|
||||
this.incident.status = this.incident.status.status;
|
||||
}
|
||||
|
||||
if (this.isAdd) {
|
||||
this.$root.addIncident(this.incident, async (res) => {
|
||||
|
||||
if (res.ok) {
|
||||
await this.addMonitorIncident(res.incidentID, () => {
|
||||
toast.success(res.msg);
|
||||
this.processing = false;
|
||||
this.$root.getIncidentList();
|
||||
this.$router.push("/dashboard/incident/" + res.incidentID);
|
||||
});
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
this.processing = false;
|
||||
}
|
||||
|
||||
});
|
||||
} else {
|
||||
this.$root.getSocket().emit("editIncident", this.incident, async (res) => {
|
||||
if (res.ok) {
|
||||
await this.addMonitorIncident(res.incidentID, () => {
|
||||
this.processing = false;
|
||||
this.$root.toastRes(res);
|
||||
this.init();
|
||||
});
|
||||
} else {
|
||||
this.processing = false;
|
||||
toast.error(res.msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async addMonitorIncident(incidentID, callback) {
|
||||
await this.$root.addMonitorIncident(incidentID, this.affectedMonitors, async (res) => {
|
||||
if (!res.ok) {
|
||||
toast.error(res.msg);
|
||||
} else {
|
||||
this.$root.getIncidentList();
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.shadow-box {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 200px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.status-selector span {
|
||||
line-height: initial !important;
|
||||
}
|
||||
</style>
|
|
@ -337,15 +337,15 @@ export default {
|
|||
},
|
||||
|
||||
pageName() {
|
||||
return this.$t((this.isAdd) ? "Add New Monitor" : "Edit");
|
||||
return this.$t((this.isAdd) ? "Add New Monitor" : "Edit Monitor");
|
||||
},
|
||||
|
||||
isAdd() {
|
||||
return this.$route.path === "/add";
|
||||
return this.$route.path === "/addMonitor";
|
||||
},
|
||||
|
||||
isEdit() {
|
||||
return this.$route.path.startsWith("/edit");
|
||||
return this.$route.path.startsWith("/editMonitor");
|
||||
},
|
||||
|
||||
pushURL() {
|
||||
|
@ -501,7 +501,7 @@ export default {
|
|||
}
|
||||
|
||||
if (this.isAdd) {
|
||||
this.$root.add(this.monitor, async (res) => {
|
||||
this.$root.addMonitor(this.monitor, async (res) => {
|
||||
|
||||
if (res.ok) {
|
||||
await this.$refs.tagsManager.submit(res.monitorID);
|
||||
|
@ -509,7 +509,7 @@ export default {
|
|||
toast.success(res.msg);
|
||||
this.processing = false;
|
||||
this.$root.getMonitorList();
|
||||
this.$router.push("/dashboard/" + res.monitorID);
|
||||
this.$router.push("/dashboard/monitor/" + res.monitorID);
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
this.processing = false;
|
||||
|
|
243
src/pages/IncidentDetails.vue
Normal file
243
src/pages/IncidentDetails.vue
Normal file
|
@ -0,0 +1,243 @@
|
|||
<template>
|
||||
<transition name="slide-fade" appear>
|
||||
<div v-if="incident">
|
||||
<h1> {{ incident.title }}</h1>
|
||||
|
||||
<p class="date">
|
||||
<span>{{ $t("Opened") }}: {{ $root.datetime(incident.createdDate) }}</span>
|
||||
</p>
|
||||
<p v-if="incident.resolved" class="date">
|
||||
<span>{{ $t("Resolved") }}: {{ $root.datetime(incident.resolvedDate) }}</span>
|
||||
</p>
|
||||
|
||||
<div class="functions">
|
||||
<button v-if="!incident.resolved" class="btn btn-primary" @click="resolveDialog">
|
||||
<font-awesome-icon icon="check"/>
|
||||
{{ $t("Resolve") }}
|
||||
</button>
|
||||
<button v-if="!incident.resolved" class="btn btn-info" @click="updateDialog">
|
||||
<font-awesome-icon icon="bullhorn"/>
|
||||
{{ $t("Post update") }}
|
||||
</button>
|
||||
<button v-if="incident.resolved" class="btn btn-warning" @click="reopenDialog">
|
||||
<font-awesome-icon icon="exclamation"/>
|
||||
{{ $t("Reopen") }}
|
||||
</button>
|
||||
<router-link :to=" '/editIncident/' + incident.id " class="btn btn-secondary">
|
||||
<font-awesome-icon icon="edit"/>
|
||||
{{ $t("Edit") }}
|
||||
</router-link>
|
||||
<button class="btn btn-danger" @click="deleteDialog">
|
||||
<font-awesome-icon icon="trash"/>
|
||||
{{ $t("Delete") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="this.affectedMonitors.length" class="shadow-box table-shadow-box">
|
||||
<label for="dependent-monitors" class="form-label" style="font-weight: bold">{{
|
||||
$t("Affected Monitors")
|
||||
}}:</label>
|
||||
<br>
|
||||
<button v-for="monitor in this.affectedMonitors" class="btn btn-monitor"
|
||||
style="margin: 5px; cursor: auto; color: white; font-weight: 500">
|
||||
{{ monitor }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Confirm ref="confirmUpdate" :yes-text="$t('Post update')" :no-text="$t('Cancel')" @yes="updateIncident"
|
||||
@no="clear">
|
||||
<span class="textarea-title">{{ $t("Description") }}:</span>
|
||||
<textarea id="update-msg" class="form-control" v-model="messages.update"></textarea>
|
||||
</Confirm>
|
||||
|
||||
<Confirm ref="confirmResolve" :yes-text="$t('Resolve')" :no-text="$t('Cancel')" @yes="resolveIncident"
|
||||
@no="clear">
|
||||
<span class="textarea-title">{{ $t("Description") }}:</span>
|
||||
<textarea id="resolve-msg" class="form-control" v-model="messages.resolve"></textarea>
|
||||
</Confirm>
|
||||
|
||||
<Confirm ref="confirmReopen" :yes-text="$t('Reopen')" :no-text="$t('Cancel')" @yes="reopenIncident"
|
||||
@no="clear">
|
||||
<span class="textarea-title">{{ $t("Description") }}:</span>
|
||||
<textarea id="reopen-msg" class="form-control" v-model="messages.reopen"></textarea>
|
||||
</Confirm>
|
||||
|
||||
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')"
|
||||
@yes="deleteIncident">
|
||||
{{ $t("deleteIncidentMsg") }}
|
||||
</Confirm>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {useToast} from "vue-toastification";
|
||||
|
||||
const toast = useToast();
|
||||
import Confirm from "../components/Confirm.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Confirm,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
messages: {
|
||||
update: "",
|
||||
resolve: "",
|
||||
reopen: "",
|
||||
},
|
||||
affectedMonitors: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
incident() {
|
||||
let id = this.$route.params.id;
|
||||
return this.$root.incidentList[id];
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.init();
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
this.$root.getSocket().emit("getMonitorIncident", this.$route.params.id, (res) => {
|
||||
if (res.ok) {
|
||||
Object.values(res.monitors).map(monitor => {
|
||||
this.affectedMonitors.push(monitor.name);
|
||||
});
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
updateIncident() {
|
||||
if (!this.messages.update.trim().length) {
|
||||
return toast.error(this.$t("descriptionRequired"));
|
||||
}
|
||||
this.$root.getSocket().emit("postIncidentUpdate", this.incident.id, this.messages.update, (res) => {
|
||||
this.$root.toastRes(res);
|
||||
});
|
||||
},
|
||||
|
||||
reopenIncident() {
|
||||
if (!this.messages.reopen.trim().length) {
|
||||
return toast.error(this.$t("descriptionRequired"));
|
||||
}
|
||||
this.$root.getSocket().emit("reopenIncident", this.incident.id, this.messages.reopen, (res) => {
|
||||
this.$root.toastRes(res);
|
||||
});
|
||||
},
|
||||
|
||||
resolveIncident() {
|
||||
if (!this.messages.resolve.trim().length) {
|
||||
return toast.error(this.$t("descriptionRequired"));
|
||||
}
|
||||
this.$root.getSocket().emit("resolveIncident", this.incident.id, this.messages.resolve, (res) => {
|
||||
this.$root.toastRes(res);
|
||||
});
|
||||
},
|
||||
|
||||
updateDialog() {
|
||||
this.$refs.confirmUpdate.show();
|
||||
},
|
||||
|
||||
reopenDialog() {
|
||||
this.$refs.confirmReopen.show();
|
||||
},
|
||||
|
||||
resolveDialog() {
|
||||
this.$refs.confirmResolve.show();
|
||||
},
|
||||
|
||||
deleteDialog() {
|
||||
this.$refs.confirmDelete.show();
|
||||
},
|
||||
|
||||
deleteIncident() {
|
||||
this.$root.deleteIncident(this.incident.id, (res) => {
|
||||
if (res.ok) {
|
||||
toast.success(res.msg);
|
||||
this.$router.push("/dashboard");
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
clear() {
|
||||
this.messages.update = "";
|
||||
this.messages.resolve = "";
|
||||
this.messages.reopen = "";
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
|
||||
@media (max-width: 550px) {
|
||||
.functions {
|
||||
text-align: center;
|
||||
|
||||
button, a {
|
||||
margin-left: 10px !important;
|
||||
margin-right: 10px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
a.btn {
|
||||
padding-left: 25px;
|
||||
padding-right: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.date {
|
||||
color: $primary;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.functions {
|
||||
margin-top: 20px;
|
||||
|
||||
button, a {
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.shadow-box {
|
||||
padding: 20px;
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
.textarea-title {
|
||||
font-size: 17px;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
textarea {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.btn-monitor {
|
||||
color: white;
|
||||
background-color: #5cdd8b;
|
||||
}
|
||||
|
||||
.dark .btn-monitor {
|
||||
color: #0d1117 !important;
|
||||
}
|
||||
|
||||
</style>
|
287
src/pages/IncidentPage.vue
Normal file
287
src/pages/IncidentPage.vue
Normal file
|
@ -0,0 +1,287 @@
|
|||
<template>
|
||||
<div v-if="loadedTheme" class="container mt-3">
|
||||
<!-- Logo & Title -->
|
||||
<h1 class="mb-4">
|
||||
<!-- Logo -->
|
||||
<span class="logo-wrapper">
|
||||
<img :src="logoURL" alt class="logo me-2"/>
|
||||
</span>
|
||||
|
||||
<!-- Title -->
|
||||
<span>{{ config.title }}</span>
|
||||
</h1>
|
||||
|
||||
<router-link v-if="prevRoute === '/status' || !prevRoute" :to="'/status'">
|
||||
<span class="route-back">
|
||||
<font-awesome-icon icon="chevron-left" class="chevron-left"/>
|
||||
{{ $t("backToStatus") }}
|
||||
</span>
|
||||
</router-link>
|
||||
<router-link v-if="prevRoute === '/incidents'" :to="'/incidents'">
|
||||
<span class="route-back">
|
||||
<font-awesome-icon icon="chevron-left" class="chevron-left"/>
|
||||
{{ $t("backToIncidents") }}
|
||||
</span>
|
||||
</router-link>
|
||||
|
||||
<!-- Incident -->
|
||||
<h2 class="incident-title">{{ $t("Incident") }}</h2>
|
||||
<div class="shadow-box alert mb-4 p-4 incident mt-4 position-relative" role="alert">
|
||||
<div class="item">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-12">
|
||||
<div class="div-title d-flex">
|
||||
<font-awesome-icon v-if="incidentDetails.style === 'info'" icon="info-circle"
|
||||
class="incident-icon incident-bg-info"/>
|
||||
<font-awesome-icon v-else-if="incidentDetails.style === 'warning'"
|
||||
icon="exclamation-triangle"
|
||||
class="incident-icon incident-bg-warning"/>
|
||||
<font-awesome-icon v-else-if="incidentDetails.style === 'critical'"
|
||||
icon="exclamation-circle"
|
||||
class="incident-icon incident-bg-danger"/>
|
||||
{{ incidentDetails.title }}
|
||||
<span class="actual-status">
|
||||
<span v-if="incidentDetails.resolved" class="status">{{ $t("Resolved") }}</span>
|
||||
<span v-if="!incidentDetails.resolved" class="status">{{ $t("Ongoing") }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="this.affectedMonitors.length" style="margin-top: 10px">
|
||||
<button v-for="monitor in this.affectedMonitors" class="btn btn-monitor"
|
||||
style="margin: 5px; cursor: auto; font-weight: 500">
|
||||
{{ monitor.name }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="incident-timeline">
|
||||
<div id="timeline-content">
|
||||
<ul class="timeline">
|
||||
<li v-for="incident_line in sortedIncidentHistory()" class="event"
|
||||
:data-date="$t(incident_line.type)">
|
||||
<p class="description">{{ incident_line.description }}</p>
|
||||
<p class="date">{{ $t("Created") }}:
|
||||
{{ $root.datetime(incident_line.createdDate) }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="mt-5 mb-4">
|
||||
{{ $t("Powered by") }} <a target="_blank"
|
||||
href="https://github.com/louislam/uptime-kuma">{{ $t("Uptime Kuma") }}</a>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import {useToast} from "vue-toastification";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
|
||||
data() {
|
||||
return {
|
||||
prevRoute: null,
|
||||
config: {},
|
||||
imgDataUrl: "/icon.svg",
|
||||
loadedTheme: false,
|
||||
loadedData: false,
|
||||
baseURL: "",
|
||||
incident: [],
|
||||
incidentDetails: {},
|
||||
affectedMonitors: [],
|
||||
};
|
||||
},
|
||||
|
||||
async beforeRouteEnter(to, from, next) {
|
||||
//TODO: remember from where I came after refresh
|
||||
next(vm => {
|
||||
if (from.path !== "/") {
|
||||
vm.prevRoute = from.path;
|
||||
}
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
logoURL() {
|
||||
if (this.imgDataUrl.startsWith("data:")) {
|
||||
return this.imgDataUrl;
|
||||
} else {
|
||||
return this.baseURL + this.imgDataUrl;
|
||||
}
|
||||
},
|
||||
|
||||
isPublished() {
|
||||
return this.config.statusPagePublished;
|
||||
},
|
||||
|
||||
theme() {
|
||||
return this.config.statusPageTheme;
|
||||
},
|
||||
|
||||
},
|
||||
watch: {
|
||||
|
||||
// Set Theme
|
||||
"config.statusPageTheme"() {
|
||||
this.$root.statusPageTheme = this.config.statusPageTheme;
|
||||
this.loadedTheme = true;
|
||||
},
|
||||
|
||||
"config.title"(title) {
|
||||
document.title = title;
|
||||
}
|
||||
|
||||
},
|
||||
async created() {
|
||||
// Special handle for dev
|
||||
const env = process.env.NODE_ENV;
|
||||
if (env === "development" || localStorage.dev === "dev") {
|
||||
this.baseURL = location.protocol + "//" + location.hostname + ":3001";
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
axios.get("/api/status-page/config").then((res) => {
|
||||
this.config = res.data;
|
||||
|
||||
if (this.config.logo) {
|
||||
this.imgDataUrl = this.config.logo;
|
||||
}
|
||||
});
|
||||
|
||||
axios.get("/api/status-page/incident/" + this.$route.params.id).then((res) => {
|
||||
if (res.data.ok) {
|
||||
this.incident = res.data.incidents;
|
||||
this.incidentDetails = this.sortedIncidentHistory(res.data.incidents)[0];
|
||||
this.affectedMonitors = res.data.monitors;
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
sortedIncidentHistory() {
|
||||
return Object.values(this.incident).sort((i1, i2) => Date.parse(i1.createdDate) - Date.parse(i2.createdDate));
|
||||
},
|
||||
|
||||
dateFromNow(date) {
|
||||
return dayjs.utc(date).fromNow();
|
||||
},
|
||||
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
@import "../assets/timeline.scss";
|
||||
|
||||
h1 {
|
||||
font-size: 30px;
|
||||
|
||||
img {
|
||||
vertical-align: middle;
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.route-back {
|
||||
color: #637381;
|
||||
|
||||
.chevron-left {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.dark .route-back {
|
||||
color: #b0b7bf;
|
||||
}
|
||||
|
||||
.incident-title {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.description span {
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.logo-wrapper {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.logo {
|
||||
transition: all $easing-in 0.2s;
|
||||
}
|
||||
|
||||
.mobile {
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.overall-status {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.div-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 500;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
column-gap: 10px;
|
||||
}
|
||||
|
||||
.incident-bg-info {
|
||||
color: rgba(53, 162, 220, 0.52);
|
||||
}
|
||||
|
||||
.incident-bg-warning {
|
||||
color: rgba(255, 165, 0, 0.52);
|
||||
}
|
||||
|
||||
.incident-bg-danger {
|
||||
color: #dc354585;
|
||||
}
|
||||
|
||||
.incident-icon {
|
||||
font-size: 30px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.dark .shadow-box {
|
||||
background-color: #0d1117;
|
||||
}
|
||||
|
||||
.btn-monitor {
|
||||
color: white;
|
||||
background-color: #5cdd8b;
|
||||
}
|
||||
|
||||
.dark .btn-monitor {
|
||||
color: #0d1117 !important;
|
||||
}
|
||||
|
||||
.actual-status {
|
||||
margin-top: auto;
|
||||
color: #637381;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
</style>
|
298
src/pages/IncidentsPage.vue
Normal file
298
src/pages/IncidentsPage.vue
Normal file
|
@ -0,0 +1,298 @@
|
|||
<template>
|
||||
<div v-if="loadedTheme" class="container mt-3">
|
||||
<!-- Logo & Title -->
|
||||
<h1 class="mb-4">
|
||||
<!-- Logo -->
|
||||
<span class="logo-wrapper">
|
||||
<img :src="logoURL" alt class="logo me-2"/>
|
||||
</span>
|
||||
|
||||
<!-- Title -->
|
||||
<span>{{ config.title }}</span>
|
||||
</h1>
|
||||
|
||||
<router-link :to="'/status'">
|
||||
<span class="route-back"><font-awesome-icon icon="chevron-left"
|
||||
class="chevron-left"/> {{ $t("backToStatus") }}</span>
|
||||
</router-link>
|
||||
|
||||
<!-- Incidents -->
|
||||
<template v-if="Object.entries(incidents).length">
|
||||
<div v-for="incidents in sortByYear(incidents)"
|
||||
class="shadow-box alert mb-4 p-4 incident mt-4 position-relative d-flex flex-column year-box" role="alert">
|
||||
<h1>{{ incidents[0] }}</h1>
|
||||
<div v-for="incidents in sortByMonth(incidents[1])"
|
||||
class="shadow-box alert mb-4 p-4 incident mt-4 position-relative month-box" role="alert">
|
||||
<h1>{{ $root.getMonthName(incidents[0]) }}</h1>
|
||||
<div v-for="incident in incidents[1]"
|
||||
class="shadow-box alert mb-4 p-4 incident mt-4 position-relative incident-box" role="alert">
|
||||
<div class="item">
|
||||
<div class="row">
|
||||
<div class="col-1 col-md-1 d-flex justify-content-center align-items-center">
|
||||
<font-awesome-icon v-if="incident.style === 'info'" icon="info-circle"
|
||||
class="incident-icon incident-bg-info"/>
|
||||
<font-awesome-icon v-if="incident.style === 'warning'" icon="exclamation-triangle"
|
||||
class="incident-icon incident-bg-warning"/>
|
||||
<font-awesome-icon v-if="incident.style === 'critical'" icon="exclamation-circle"
|
||||
class="incident-icon incident-bg-danger"/>
|
||||
</div>
|
||||
<div class="col-11 col-md-11">
|
||||
<router-link :to="'/incident/' + incident.id">
|
||||
<h4 class="alert-heading">{{ incident.title }}</h4>
|
||||
</router-link>
|
||||
<div class="content">{{ incident.description }}</div>
|
||||
|
||||
<!-- Incident Date -->
|
||||
<div class="date mt-3">
|
||||
{{ $t("Opened") }}: {{ $root.datetime(incident.createdDate) }} ({{
|
||||
dateFromNow(incident.createdDate)
|
||||
}})<br/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<footer class="mt-5 mb-4">
|
||||
{{ $t("Powered by") }} <a target="_blank"
|
||||
href="https://github.com/louislam/uptime-kuma">{{ $t("Uptime Kuma") }}</a>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import {useToast} from "vue-toastification";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
let feedInterval;
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
|
||||
data() {
|
||||
return {
|
||||
config: {},
|
||||
imgDataUrl: "/icon.svg",
|
||||
loadedTheme: false,
|
||||
loadedData: false,
|
||||
baseURL: "",
|
||||
incident: [],
|
||||
incidents: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
logoURL() {
|
||||
if (this.imgDataUrl.startsWith("data:")) {
|
||||
return this.imgDataUrl;
|
||||
} else {
|
||||
return this.baseURL + this.imgDataUrl;
|
||||
}
|
||||
},
|
||||
|
||||
isPublished() {
|
||||
return this.config.statusPagePublished;
|
||||
},
|
||||
|
||||
theme() {
|
||||
return this.config.statusPageTheme;
|
||||
},
|
||||
|
||||
},
|
||||
watch: {
|
||||
|
||||
// Set Theme
|
||||
"config.statusPageTheme"() {
|
||||
this.$root.statusPageTheme = this.config.statusPageTheme;
|
||||
this.loadedTheme = true;
|
||||
},
|
||||
|
||||
"config.title"(title) {
|
||||
document.title = title;
|
||||
}
|
||||
|
||||
},
|
||||
async created() {
|
||||
// Special handle for dev
|
||||
const env = process.env.NODE_ENV;
|
||||
if (env === "development" || localStorage.dev === "dev") {
|
||||
this.baseURL = location.protocol + "//" + location.hostname + ":3001";
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
axios.get("/api/status-page/config").then((res) => {
|
||||
this.config = res.data;
|
||||
|
||||
if (this.config.logo) {
|
||||
this.imgDataUrl = this.config.logo;
|
||||
}
|
||||
});
|
||||
|
||||
axios.get("/api/status-page/incidents/").then((res) => {
|
||||
if (res.data.ok) {
|
||||
this.incidents = this.$root.groupTimesBy(this.sortedIncidentHistory(res.data.incidents));
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
sortByYear(incidents) {
|
||||
let result = Object.entries(incidents);
|
||||
|
||||
result.sort((y1, y2) => y2[0] - y1[0]);
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
sortByMonth(incidents) {
|
||||
let result = Object.entries(incidents);
|
||||
|
||||
result.sort((m1, m2) => m2[0] - m1[0]);
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
sortedIncidentHistory(incidents) {
|
||||
let result = Object.values(incidents);
|
||||
|
||||
result.sort((i1, i2) => {
|
||||
if (Date.parse(i1.createdDate) > Date.parse(i2.createdDate)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (Date.parse(i2.createdDate) < Date.parse(i1.createdDate)) {
|
||||
return 1;
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
dateFromNow(date) {
|
||||
return dayjs.utc(date).fromNow();
|
||||
},
|
||||
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
@import "../assets/timeline.scss";
|
||||
|
||||
h1 {
|
||||
font-size: 30px;
|
||||
|
||||
img {
|
||||
vertical-align: middle;
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.route-back {
|
||||
color: #637381;
|
||||
|
||||
.chevron-left {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.dark .route-back {
|
||||
color: #b0b7bf;
|
||||
}
|
||||
|
||||
.incident-title {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.description span {
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.logo-wrapper {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.logo {
|
||||
transition: all $easing-in 0.2s;
|
||||
}
|
||||
|
||||
.mobile {
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.overall-status {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.div-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 500;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
column-gap: 10px;
|
||||
}
|
||||
|
||||
.incident-bg-info {
|
||||
color: rgba(53, 162, 220, 0.52);
|
||||
}
|
||||
|
||||
.incident-bg-warning {
|
||||
color: rgba(255, 165, 0, 0.52);
|
||||
}
|
||||
|
||||
.incident-bg-danger {
|
||||
color: #dc354585;
|
||||
}
|
||||
|
||||
.incident-icon {
|
||||
font-size: 30px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.dark .shadow-box.year-box {
|
||||
background-color: #0d1117;
|
||||
}
|
||||
|
||||
.dark .shadow-box.month-box {
|
||||
background-color: #090c11;
|
||||
}
|
||||
|
||||
.dark .shadow-box.incident-box {
|
||||
background-color: #0d1117;
|
||||
}
|
||||
|
||||
.btn-monitor {
|
||||
color: white;
|
||||
background-color: #5cdd8b;
|
||||
}
|
||||
|
||||
.dark .btn-monitor {
|
||||
color: #0d1117 !important;
|
||||
}
|
||||
|
||||
.actual-status {
|
||||
margin-top: auto;
|
||||
color: #637381;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
</style>
|
|
@ -50,11 +50,6 @@
|
|||
{{ $t("Discard") }}
|
||||
</button>
|
||||
|
||||
<button class="btn btn-primary btn-add-group me-2" @click="createIncident">
|
||||
<font-awesome-icon icon="bullhorn" />
|
||||
{{ $t("Create Incident") }}
|
||||
</button>
|
||||
|
||||
<!--
|
||||
<button v-if="isPublished" class="btn btn-light me-2" @click="">
|
||||
<font-awesome-icon icon="save" />
|
||||
|
@ -91,58 +86,36 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Incident -->
|
||||
<div v-if="incident !== null" class="shadow-box alert mb-4 p-4 incident" role="alert" :class="incidentClass">
|
||||
<strong v-if="editIncidentMode">{{ $t("Title") }}:</strong>
|
||||
<Editable v-model="incident.title" tag="h4" :contenteditable="editIncidentMode" :noNL="true" class="alert-heading" />
|
||||
<!-- Incidents -->
|
||||
<template v-if="incidents.length">
|
||||
<div v-for="incident in sortedIncidentsList" class="shadow-box alert mb-4 p-4 incident mt-4 position-relative" role="alert">
|
||||
<div class="item">
|
||||
<div class="row">
|
||||
<div class="col-1 col-md-1 d-flex justify-content-center align-items-center">
|
||||
<font-awesome-icon v-if="incident.style === 'info'" icon="info-circle"
|
||||
class="incident-icon incident-bg-info"/>
|
||||
<font-awesome-icon v-if="incident.style === 'warning'" icon="exclamation-triangle"
|
||||
class="incident-icon incident-bg-warning"/>
|
||||
<font-awesome-icon v-if="incident.style === 'critical'" icon="exclamation-circle"
|
||||
class="incident-icon incident-bg-danger"/>
|
||||
</div>
|
||||
<div class="col-11 col-md-11">
|
||||
<router-link :to="'/incident/' + incident.id">
|
||||
<h4 class="alert-heading">{{ incident.title }}</h4>
|
||||
</router-link>
|
||||
<div class="content">{{ incident.description }}</div>
|
||||
|
||||
<strong v-if="editIncidentMode">{{ $t("Content") }}:</strong>
|
||||
<Editable v-model="incident.content" tag="div" :contenteditable="editIncidentMode" class="content" />
|
||||
|
||||
<!-- Incident Date -->
|
||||
<div class="date mt-3">
|
||||
{{ $t("Created") }}: {{ $root.datetime(incident.createdDate) }} ({{ dateFromNow(incident.createdDate) }})<br />
|
||||
<span v-if="incident.lastUpdatedDate">
|
||||
{{ $t("Last Updated") }}: {{ $root.datetime(incident.lastUpdatedDate) }} ({{ dateFromNow(incident.lastUpdatedDate) }})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="editMode" class="mt-3">
|
||||
<button v-if="editIncidentMode" class="btn btn-light me-2" @click="postIncident">
|
||||
<font-awesome-icon icon="bullhorn" />
|
||||
{{ $t("Post") }}
|
||||
</button>
|
||||
|
||||
<button v-if="!editIncidentMode && incident.id" class="btn btn-light me-2" @click="editIncident">
|
||||
<font-awesome-icon icon="edit" />
|
||||
{{ $t("Edit") }}
|
||||
</button>
|
||||
|
||||
<button v-if="editIncidentMode" class="btn btn-light me-2" @click="cancelIncident">
|
||||
<font-awesome-icon icon="times" />
|
||||
{{ $t("Cancel") }}
|
||||
</button>
|
||||
|
||||
<div v-if="editIncidentMode" class="dropdown d-inline-block me-2">
|
||||
<button id="dropdownMenuButton1" class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
{{ $t("Style") }}: {{ $t(incident.style) }}
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1">
|
||||
<li><a class="dropdown-item" href="#" @click="incident.style = 'info'">{{ $t("info") }}</a></li>
|
||||
<li><a class="dropdown-item" href="#" @click="incident.style = 'warning'">{{ $t("warning") }}</a></li>
|
||||
<li><a class="dropdown-item" href="#" @click="incident.style = 'danger'">{{ $t("danger") }}</a></li>
|
||||
<li><a class="dropdown-item" href="#" @click="incident.style = 'primary'">{{ $t("primary") }}</a></li>
|
||||
<li><a class="dropdown-item" href="#" @click="incident.style = 'light'">{{ $t("light") }}</a></li>
|
||||
<li><a class="dropdown-item" href="#" @click="incident.style = 'dark'">{{ $t("dark") }}</a></li>
|
||||
</ul>
|
||||
<!-- Incident Date -->
|
||||
<div class="date mt-3">
|
||||
{{ $t("Opened") }}: {{ $root.datetime(incident.createdDate) }} ({{
|
||||
dateFromNow(incident.createdDate)
|
||||
}})<br/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button v-if="!editIncidentMode && incident.id" class="btn btn-light me-2" @click="unpinIncident">
|
||||
<font-awesome-icon icon="unlink" />
|
||||
{{ $t("Unpin") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Overall Status -->
|
||||
<div class="shadow-box list p-4 overall-status mb-4">
|
||||
|
@ -152,9 +125,9 @@
|
|||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div v-if="allUp">
|
||||
<font-awesome-icon icon="check-circle" class="ok" />
|
||||
{{ $t("All Systems Operational") }}
|
||||
<div v-if="allDown">
|
||||
<font-awesome-icon icon="times-circle" class="danger" />
|
||||
{{ $t("Degraded Service") }}
|
||||
</div>
|
||||
|
||||
<div v-else-if="partialDown">
|
||||
|
@ -162,9 +135,9 @@
|
|||
{{ $t("Partially Degraded Service") }}
|
||||
</div>
|
||||
|
||||
<div v-else-if="allDown">
|
||||
<font-awesome-icon icon="times-circle" class="danger" />
|
||||
{{ $t("Degraded Service") }}
|
||||
<div v-else-if="allUp">
|
||||
<font-awesome-icon icon="check-circle" class="ok" />
|
||||
{{ $t("All Systems Operational") }}
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
|
@ -193,7 +166,7 @@
|
|||
</select>
|
||||
</div>
|
||||
<div v-else class="text-center">
|
||||
{{ $t("No monitors available.") }} <router-link to="/add">{{ $t("Add one") }}</router-link>
|
||||
{{ $t("No monitors available.") }} <router-link to="/addMonitor">{{ $t("Add one") }}</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -207,6 +180,10 @@
|
|||
<PublicGroupList :edit-mode="enableEditMode" />
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<PublicIncidentsList />
|
||||
</div>
|
||||
|
||||
<footer class="mt-5 mb-4">
|
||||
{{ $t("Powered by") }} <a target="_blank" href="https://github.com/louislam/uptime-kuma">{{ $t("Uptime Kuma" ) }}</a>
|
||||
</footer>
|
||||
|
@ -216,6 +193,7 @@
|
|||
<script>
|
||||
import axios from "axios";
|
||||
import PublicGroupList from "../components/PublicGroupList.vue";
|
||||
import PublicIncidentsList from "../components/PublicIncidentsList.vue";
|
||||
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 { useToast } from "vue-toastification";
|
||||
|
@ -229,7 +207,8 @@ let feedInterval;
|
|||
export default {
|
||||
components: {
|
||||
PublicGroupList,
|
||||
ImageCropUpload
|
||||
PublicIncidentsList,
|
||||
ImageCropUpload,
|
||||
},
|
||||
|
||||
// Leave Page for vue route change
|
||||
|
@ -248,20 +227,56 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
enableEditMode: false,
|
||||
enableEditIncidentMode: false,
|
||||
hasToken: false,
|
||||
config: {},
|
||||
selectedMonitor: null,
|
||||
incident: null,
|
||||
previousIncident: null,
|
||||
showImageCropUpload: false,
|
||||
imgDataUrl: "/icon.svg",
|
||||
loadedTheme: false,
|
||||
loadedData: false,
|
||||
baseURL: "",
|
||||
incidents: [],
|
||||
overrideStatus: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
sortedIncidentsList() {
|
||||
let result = Object.values(this.incidents).filter((incident) => !incident.resolved);
|
||||
|
||||
result.sort((i1, i2) => {
|
||||
|
||||
if (i1.style !== i2.style) {
|
||||
if (i1.style === "critical") {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (i2.style === "critical") {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (i1.style === "warning") {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (i2.style === "warning") {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (Date.parse(i1.createdDate) > Date.parse(i2.createdDate)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (Date.parse(i2.createdDate) < Date.parse(i1.createdDate)) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return i1.title.localeCompare(i2.title);
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
logoURL() {
|
||||
if (this.imgDataUrl.startsWith("data:")) {
|
||||
|
@ -291,10 +306,6 @@ export default {
|
|||
return this.enableEditMode && this.$root.socket.connected;
|
||||
},
|
||||
|
||||
editIncidentMode() {
|
||||
return this.enableEditIncidentMode;
|
||||
},
|
||||
|
||||
isPublished() {
|
||||
return this.config.statusPagePublished;
|
||||
},
|
||||
|
@ -347,15 +358,27 @@ export default {
|
|||
},
|
||||
|
||||
allUp() {
|
||||
return this.overallStatus === STATUS_PAGE_ALL_UP;
|
||||
if (this.overrideStatus.override) {
|
||||
return this.overrideStatus.allUp;
|
||||
} else {
|
||||
return this.overallStatus === STATUS_PAGE_ALL_UP;
|
||||
}
|
||||
},
|
||||
|
||||
partialDown() {
|
||||
return this.overallStatus === STATUS_PAGE_PARTIAL_DOWN;
|
||||
if (this.overrideStatus.override) {
|
||||
return this.overrideStatus.partialDown;
|
||||
} else {
|
||||
return this.overallStatus === STATUS_PAGE_PARTIAL_DOWN;
|
||||
}
|
||||
},
|
||||
|
||||
allDown() {
|
||||
return this.overallStatus === STATUS_PAGE_ALL_DOWN;
|
||||
if (this.overrideStatus.override) {
|
||||
return this.overrideStatus.allDown;
|
||||
} else {
|
||||
return this.overallStatus === STATUS_PAGE_ALL_DOWN;
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
|
@ -417,9 +440,17 @@ export default {
|
|||
}
|
||||
});
|
||||
|
||||
axios.get("/api/status-page/incident").then((res) => {
|
||||
axios.get("/api/status-page/incidents").then((res) => {
|
||||
if (res.data.ok) {
|
||||
this.incident = res.data.incident;
|
||||
this.incidents = res.data.incidents;
|
||||
this.$root.publicIncidentsList = res.data.incidents;
|
||||
|
||||
this.overrideStatus = {
|
||||
override: Object.values(this.incidents).filter((incident) => !incident.resolved && incident.overrideStatus).length !== 0,
|
||||
allUp: Object.values(this.incidents).filter((incident) => !incident.resolved && incident.overrideStatus && incident.status === "operational").length !== 0,
|
||||
partialDown: Object.values(this.incidents).filter((incident) => !incident.resolved && incident.overrideStatus && incident.status === "partial-outage").length !== 0,
|
||||
allDown: Object.values(this.incidents).filter((incident) => !incident.resolved && incident.overrideStatus && incident.status === "full-outage").length !== 0,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -520,62 +551,9 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
createIncident() {
|
||||
this.enableEditIncidentMode = true;
|
||||
|
||||
if (this.incident) {
|
||||
this.previousIncident = this.incident;
|
||||
}
|
||||
|
||||
this.incident = {
|
||||
title: "",
|
||||
content: "",
|
||||
style: "primary",
|
||||
};
|
||||
},
|
||||
|
||||
postIncident() {
|
||||
if (this.incident.title == "" || this.incident.content == "") {
|
||||
toast.error(this.$t("Please input title and content"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.$root.getSocket().emit("postIncident", this.incident, (res) => {
|
||||
|
||||
if (res.ok) {
|
||||
this.enableEditIncidentMode = false;
|
||||
this.incident = res.incident;
|
||||
} else {
|
||||
toast.error(res.msg);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Click Edit Button
|
||||
*/
|
||||
editIncident() {
|
||||
this.enableEditIncidentMode = true;
|
||||
this.previousIncident = Object.assign({}, this.incident);
|
||||
},
|
||||
|
||||
cancelIncident() {
|
||||
this.enableEditIncidentMode = false;
|
||||
|
||||
if (this.previousIncident) {
|
||||
this.incident = this.previousIncident;
|
||||
this.previousIncident = null;
|
||||
}
|
||||
},
|
||||
|
||||
unpinIncident() {
|
||||
this.$root.getSocket().emit("unpinIncident", () => {
|
||||
this.incident = null;
|
||||
});
|
||||
},
|
||||
|
||||
dateFromNow(date) {
|
||||
return dayjs.utc(date).fromNow();
|
||||
},
|
||||
|
@ -586,6 +564,7 @@ export default {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
@import "../assets/vars.scss";
|
||||
@import "../assets/timeline.scss";
|
||||
|
||||
.overall-status {
|
||||
font-weight: bold;
|
||||
|
@ -659,18 +638,6 @@ footer {
|
|||
}
|
||||
}
|
||||
|
||||
.incident {
|
||||
.content {
|
||||
&[contenteditable=true] {
|
||||
min-height: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile {
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
|
@ -681,4 +648,33 @@ footer {
|
|||
}
|
||||
}
|
||||
|
||||
.incident.info {
|
||||
background-color: #0c4128;
|
||||
}
|
||||
|
||||
.incident-bg-info {
|
||||
color: rgba(53, 162, 220, 0.52);
|
||||
}
|
||||
|
||||
.incident-bg-warning {
|
||||
color: rgba(255, 165, 0, 0.52);
|
||||
}
|
||||
|
||||
.incident-bg-danger {
|
||||
color: #dc354585;
|
||||
}
|
||||
|
||||
.incident a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.incident-icon {
|
||||
font-size: 30px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.dark .shadow-box {
|
||||
background-color: #0d1117;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
@ -4,11 +4,15 @@ import Layout from "./layouts/Layout.vue";
|
|||
import Dashboard from "./pages/Dashboard.vue";
|
||||
import DashboardHome from "./pages/DashboardHome.vue";
|
||||
import Details from "./pages/Details.vue";
|
||||
import IncidentDetails from "./pages/IncidentDetails.vue";
|
||||
import EditMonitor from "./pages/EditMonitor.vue";
|
||||
import EditIncident from "./pages/EditIncident.vue";
|
||||
import List from "./pages/List.vue";
|
||||
const Settings = () => import("./pages/Settings.vue");
|
||||
import Setup from "./pages/Setup.vue";
|
||||
const StatusPage = () => import("./pages/StatusPage.vue");
|
||||
const IncidentPage = () => import("./pages/IncidentPage.vue");
|
||||
const IncidentsPage = () => import("./pages/IncidentsPage.vue");
|
||||
import Entry from "./pages/Entry.vue";
|
||||
|
||||
import Appearance from "./components/settings/Appearance.vue";
|
||||
|
@ -41,7 +45,7 @@ const routes = [
|
|||
component: DashboardHome,
|
||||
children: [
|
||||
{
|
||||
path: "/dashboard/:id",
|
||||
path: "/dashboard/monitor/:id",
|
||||
component: EmptyLayout,
|
||||
children: [
|
||||
{
|
||||
|
@ -49,15 +53,33 @@ const routes = [
|
|||
component: Details,
|
||||
},
|
||||
{
|
||||
path: "/edit/:id",
|
||||
path: "/editMonitor/:id",
|
||||
component: EditMonitor,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/add",
|
||||
path: "/dashboard/incident/:id",
|
||||
component: EmptyLayout,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: IncidentDetails,
|
||||
},
|
||||
{
|
||||
path: "/editIncident/:id",
|
||||
component: EditIncident,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/addMonitor",
|
||||
component: EditMonitor,
|
||||
},
|
||||
{
|
||||
path: "/addIncident",
|
||||
component: EditIncident,
|
||||
},
|
||||
{
|
||||
path: "/list",
|
||||
component: List,
|
||||
|
@ -113,7 +135,21 @@ const routes = [
|
|||
},
|
||||
{
|
||||
path: "/status",
|
||||
component: StatusPage,
|
||||
component: EmptyLayout,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: StatusPage,
|
||||
},
|
||||
{
|
||||
path: "/incident/:id",
|
||||
component: IncidentPage,
|
||||
},
|
||||
{
|
||||
path: "/incidents",
|
||||
component: IncidentsPage,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
// Backend uses the compiled file util.js
|
||||
// Frontend uses util.ts
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0;
|
||||
exports.getIncidentRelativeURL = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0;
|
||||
const _dayjs = require("dayjs");
|
||||
const dayjs = _dayjs;
|
||||
exports.isDev = process.env.NODE_ENV === "development";
|
||||
|
@ -162,6 +162,10 @@ function genSecret(length = 64) {
|
|||
}
|
||||
exports.genSecret = genSecret;
|
||||
function getMonitorRelativeURL(id) {
|
||||
return "/dashboard/" + id;
|
||||
return "/dashboard/monitor/" + id;
|
||||
}
|
||||
exports.getMonitorRelativeURL = getMonitorRelativeURL;
|
||||
function getIncidentRelativeURL(id) {
|
||||
return "/dashboard/incident/" + id;
|
||||
}
|
||||
exports.getIncidentRelativeURL = getIncidentRelativeURL;
|
||||
|
|
|
@ -185,5 +185,9 @@ export function genSecret(length = 64) {
|
|||
}
|
||||
|
||||
export function getMonitorRelativeURL(id: string) {
|
||||
return "/dashboard/" + id;
|
||||
return "/dashboard/monitor/" + id;
|
||||
}
|
||||
|
||||
export function getIncidentRelativeURL(id: string) {
|
||||
return "/dashboard/incident/" + id;
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ describe("Init", () => {
|
|||
|
||||
it("should create monitor", async () => {
|
||||
// Create monitor
|
||||
await page.goto(baseURL + "/add");
|
||||
await page.goto(baseURL + "/addMonitor");
|
||||
await page.waitForSelector("#name");
|
||||
|
||||
await page.type("#name", "Myself");
|
||||
|
|
Loading…
Add table
Reference in a new issue