mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-12-26 06:04:13 -08:00
Merge pull request #863 from louislam/restructure-status-page
Restructure status page core implementation
This commit is contained in:
commit
82049a2387
31
db/patch-status-page.sql
Normal file
31
db/patch-status-page.sql
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
CREATE TABLE [status_page](
|
||||||
|
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
[slug] VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
[title] VARCHAR(255) NOT NULL,
|
||||||
|
[description] TEXT,
|
||||||
|
[icon] VARCHAR(255) NOT NULL,
|
||||||
|
[theme] VARCHAR(30) NOT NULL,
|
||||||
|
[published] BOOLEAN NOT NULL DEFAULT 1,
|
||||||
|
[search_engine_index] BOOLEAN NOT NULL DEFAULT 1,
|
||||||
|
[show_tags] BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
[password] VARCHAR,
|
||||||
|
[created_date] DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
[modified_date] DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX [slug] ON [status_page]([slug]);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE [status_page_cname](
|
||||||
|
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
[status_page_id] INTEGER NOT NULL REFERENCES [status_page]([id]) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
[domain] VARCHAR NOT NULL UNIQUE
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE incident ADD status_page_id INTEGER;
|
||||||
|
ALTER TABLE [group] ADD status_page_id INTEGER;
|
||||||
|
|
||||||
|
COMMIT;
|
|
@ -1,5 +1,5 @@
|
||||||
# DON'T UPDATE TO alpine3.13, 1.14, see #41.
|
# DON'T UPDATE TO alpine3.13, 1.14, see #41.
|
||||||
FROM node:14-alpine3.12
|
FROM node:16-alpine3.12
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install apprise, iputils for non-root ping, setpriv
|
# Install apprise, iputils for non-root ping, setpriv
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# DON'T UPDATE TO node:14-bullseye-slim, see #372.
|
# DON'T UPDATE TO node:14-bullseye-slim, see #372.
|
||||||
# If the image changed, the second stage image should be changed too
|
# If the image changed, the second stage image should be changed too
|
||||||
FROM node:14-buster-slim
|
FROM node:16-buster-slim
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv
|
# Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv
|
||||||
|
|
5901
package-lock.json
generated
5901
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -53,6 +53,7 @@ class Database {
|
||||||
"patch-2fa-invalidate-used-token.sql": true,
|
"patch-2fa-invalidate-used-token.sql": true,
|
||||||
"patch-notification_sent_history.sql": true,
|
"patch-notification_sent_history.sql": true,
|
||||||
"patch-monitor-basic-auth.sql": true,
|
"patch-monitor-basic-auth.sql": true,
|
||||||
|
"patch-status-page.sql": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -170,6 +171,7 @@ class Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.patch2();
|
await this.patch2();
|
||||||
|
await this.migrateNewStatusPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -211,6 +213,70 @@ class Database {
|
||||||
await setSetting("databasePatchedFiles", databasePatchedFiles);
|
await setSetting("databasePatchedFiles", databasePatchedFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate status page value in setting to "status_page" table
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static async migrateNewStatusPage() {
|
||||||
|
let title = await setting("title");
|
||||||
|
|
||||||
|
if (title) {
|
||||||
|
console.log("Migrating Status Page");
|
||||||
|
|
||||||
|
let statusPageCheck = await R.findOne("status_page", " slug = 'default' ");
|
||||||
|
|
||||||
|
if (statusPageCheck !== null) {
|
||||||
|
console.log("Migrating Status Page - Skip, default slug record is already existing");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let statusPage = R.dispense("status_page");
|
||||||
|
statusPage.slug = "default";
|
||||||
|
statusPage.title = title;
|
||||||
|
statusPage.description = await setting("description");
|
||||||
|
statusPage.icon = await setting("icon");
|
||||||
|
statusPage.theme = await setting("statusPageTheme");
|
||||||
|
statusPage.published = !!await setting("statusPagePublished");
|
||||||
|
statusPage.search_engine_index = !!await setting("searchEngineIndex");
|
||||||
|
statusPage.show_tags = !!await setting("statusPageTags");
|
||||||
|
statusPage.password = null;
|
||||||
|
|
||||||
|
if (!statusPage.title) {
|
||||||
|
statusPage.title = "My Status Page";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!statusPage.icon) {
|
||||||
|
statusPage.icon = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!statusPage.theme) {
|
||||||
|
statusPage.theme = "light";
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = await R.store(statusPage);
|
||||||
|
|
||||||
|
await R.exec("UPDATE incident SET status_page_id = ? WHERE status_page_id IS NULL", [
|
||||||
|
id
|
||||||
|
]);
|
||||||
|
|
||||||
|
await R.exec("UPDATE [group] SET status_page_id = ? WHERE status_page_id IS NULL", [
|
||||||
|
id
|
||||||
|
]);
|
||||||
|
|
||||||
|
await R.exec("DELETE FROM setting WHERE type = 'statusPage'");
|
||||||
|
|
||||||
|
// Migrate Entry Page if it is status page
|
||||||
|
let entryPage = await setting("entryPage");
|
||||||
|
|
||||||
|
if (entryPage === "statusPage") {
|
||||||
|
await setSetting("entryPage", "statusPage-default", "general");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Migrating Status Page - Done");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used it patch2() only
|
* Used it patch2() only
|
||||||
* @param sqlFilename
|
* @param sqlFilename
|
||||||
|
|
|
@ -3,12 +3,12 @@ const { R } = require("redbean-node");
|
||||||
|
|
||||||
class Group extends BeanModel {
|
class Group extends BeanModel {
|
||||||
|
|
||||||
async toPublicJSON() {
|
async toPublicJSON(showTags = false) {
|
||||||
let monitorBeanList = await this.getMonitorList();
|
let monitorBeanList = await this.getMonitorList();
|
||||||
let monitorList = [];
|
let monitorList = [];
|
||||||
|
|
||||||
for (let bean of monitorBeanList) {
|
for (let bean of monitorBeanList) {
|
||||||
monitorList.push(await bean.toPublicJSON());
|
monitorList.push(await bean.toPublicJSON(showTags));
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -24,18 +24,22 @@ const apicache = require("../modules/apicache");
|
||||||
class Monitor extends BeanModel {
|
class Monitor extends BeanModel {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a object that ready to parse to JSON for public
|
* Return an object that ready to parse to JSON for public
|
||||||
* Only show necessary data to public
|
* Only show necessary data to public
|
||||||
*/
|
*/
|
||||||
async toPublicJSON() {
|
async toPublicJSON(showTags = false) {
|
||||||
return {
|
let obj = {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
};
|
};
|
||||||
|
if (showTags) {
|
||||||
|
obj.tags = await this.getTags();
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a object that ready to parse to JSON
|
* Return an object that ready to parse to JSON
|
||||||
*/
|
*/
|
||||||
async toJSON() {
|
async toJSON() {
|
||||||
|
|
||||||
|
@ -49,7 +53,7 @@ class Monitor extends BeanModel {
|
||||||
notificationIDList[bean.notification_id] = true;
|
notificationIDList[bean.notification_id] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tags = await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [this.id]);
|
const tags = await this.getTags();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
@ -82,6 +86,10 @@ class Monitor extends BeanModel {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getTags() {
|
||||||
|
return await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [this.id]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encode user and password to Base64 encoding
|
* Encode user and password to Base64 encoding
|
||||||
* for HTTP "basic" auth, as per RFC-7617
|
* for HTTP "basic" auth, as per RFC-7617
|
||||||
|
|
60
server/model/status_page.js
Normal file
60
server/model/status_page.js
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||||
|
const { R } = require("redbean-node");
|
||||||
|
|
||||||
|
class StatusPage extends BeanModel {
|
||||||
|
|
||||||
|
static async sendStatusPageList(io, socket) {
|
||||||
|
let result = {};
|
||||||
|
|
||||||
|
let list = await R.findAll("status_page", " ORDER BY title ");
|
||||||
|
|
||||||
|
for (let item of list) {
|
||||||
|
result[item.id] = await item.toJSON();
|
||||||
|
}
|
||||||
|
|
||||||
|
io.to(socket.userID).emit("statusPageList", result);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
async toJSON() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
slug: this.slug,
|
||||||
|
title: this.title,
|
||||||
|
description: this.description,
|
||||||
|
icon: this.getIcon(),
|
||||||
|
theme: this.theme,
|
||||||
|
published: !!this.published,
|
||||||
|
showTags: !!this.show_tags,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async toPublicJSON() {
|
||||||
|
return {
|
||||||
|
slug: this.slug,
|
||||||
|
title: this.title,
|
||||||
|
description: this.description,
|
||||||
|
icon: this.getIcon(),
|
||||||
|
theme: this.theme,
|
||||||
|
published: !!this.published,
|
||||||
|
showTags: !!this.show_tags,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static async slugToID(slug) {
|
||||||
|
return await R.getCell("SELECT id FROM status_page WHERE slug = ? ", [
|
||||||
|
slug
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
getIcon() {
|
||||||
|
if (!this.icon) {
|
||||||
|
return "/icon.svg";
|
||||||
|
} else {
|
||||||
|
return this.icon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = StatusPage;
|
|
@ -6,6 +6,7 @@ const apicache = require("../modules/apicache");
|
||||||
const Monitor = require("../model/monitor");
|
const Monitor = require("../model/monitor");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const { UP, flipStatus, debug } = require("../../src/util");
|
const { UP, flipStatus, debug } = require("../../src/util");
|
||||||
|
const StatusPage = require("../model/status_page");
|
||||||
let router = express.Router();
|
let router = express.Router();
|
||||||
|
|
||||||
let cache = apicache.middleware;
|
let cache = apicache.middleware;
|
||||||
|
@ -82,110 +83,80 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Status Page Config
|
// Status page config, incident, monitor list
|
||||||
router.get("/api/status-page/config", async (_request, response) => {
|
router.get("/api/status-page/:slug", cache("5 minutes"), async (request, response) => {
|
||||||
allowDevAllOrigin(response);
|
allowDevAllOrigin(response);
|
||||||
|
let slug = request.params.slug;
|
||||||
|
|
||||||
let config = await getSettings("statusPage");
|
// Get Status Page
|
||||||
|
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
||||||
|
slug
|
||||||
|
]);
|
||||||
|
|
||||||
if (! config.statusPageTheme) {
|
if (!statusPage) {
|
||||||
config.statusPageTheme = "light";
|
response.statusCode = 404;
|
||||||
|
response.json({
|
||||||
|
msg: "Not Found"
|
||||||
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! config.statusPagePublished) {
|
|
||||||
config.statusPagePublished = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! config.statusPageTags) {
|
|
||||||
config.statusPageTags = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! config.title) {
|
|
||||||
config.title = "Uptime Kuma";
|
|
||||||
}
|
|
||||||
|
|
||||||
response.json(config);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Status Page - Get the current Incident
|
|
||||||
// Can fetch only if published
|
|
||||||
router.get("/api/status-page/incident", async (_, response) => {
|
|
||||||
allowDevAllOrigin(response);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await checkPublished();
|
// Incident
|
||||||
|
let incident = await R.findOne("incident", " pin = 1 AND active = 1 AND status_page_id = ? ", [
|
||||||
let incident = await R.findOne("incident", " pin = 1 AND active = 1");
|
statusPage.id,
|
||||||
|
]);
|
||||||
|
|
||||||
if (incident) {
|
if (incident) {
|
||||||
incident = incident.toPublicJSON();
|
incident = incident.toPublicJSON();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Public Group List
|
||||||
|
const publicGroupList = [];
|
||||||
|
const showTags = !!statusPage.show_tags;
|
||||||
|
debug("Show Tags???" + showTags);
|
||||||
|
const list = await R.find("group", " public = 1 AND status_page_id = ? ORDER BY weight ", [
|
||||||
|
statusPage.id
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (let groupBean of list) {
|
||||||
|
let monitorGroup = await groupBean.toPublicJSON(showTags);
|
||||||
|
publicGroupList.push(monitorGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response
|
||||||
response.json({
|
response.json({
|
||||||
ok: true,
|
config: await statusPage.toPublicJSON(),
|
||||||
incident,
|
incident,
|
||||||
|
publicGroupList
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
send403(response, error.message);
|
send403(response, error.message);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Status Page - Monitor List
|
|
||||||
// Can fetch only if published
|
|
||||||
router.get("/api/status-page/monitor-list", cache("5 minutes"), async (_request, response) => {
|
|
||||||
allowDevAllOrigin(response);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await checkPublished();
|
|
||||||
const publicGroupList = [];
|
|
||||||
const tagsVisible = (await getSettings("statusPage")).statusPageTags;
|
|
||||||
const list = await R.find("group", " public = 1 ORDER BY weight ");
|
|
||||||
for (let groupBean of list) {
|
|
||||||
let monitorGroup = await groupBean.toPublicJSON();
|
|
||||||
if (tagsVisible) {
|
|
||||||
monitorGroup.monitorList = await Promise.all(monitorGroup.monitorList.map(async (monitor) => {
|
|
||||||
// Includes tags as an array in response, allows for tags to be displayed on public status page
|
|
||||||
const tags = await R.getAll(
|
|
||||||
`SELECT monitor_tag.monitor_id, monitor_tag.value, tag.name, tag.color
|
|
||||||
FROM monitor_tag
|
|
||||||
JOIN tag
|
|
||||||
ON monitor_tag.tag_id = tag.id
|
|
||||||
WHERE monitor_tag.monitor_id = ?`, [monitor.id]
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
...monitor,
|
|
||||||
tags: tags
|
|
||||||
};
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
publicGroupList.push(monitorGroup);
|
|
||||||
}
|
|
||||||
|
|
||||||
response.json(publicGroupList);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
send403(response, error.message);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Status Page Polling Data
|
// Status Page Polling Data
|
||||||
// Can fetch only if published
|
// Can fetch only if published
|
||||||
router.get("/api/status-page/heartbeat", cache("5 minutes"), async (_request, response) => {
|
router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (request, response) => {
|
||||||
allowDevAllOrigin(response);
|
allowDevAllOrigin(response);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await checkPublished();
|
|
||||||
|
|
||||||
let heartbeatList = {};
|
let heartbeatList = {};
|
||||||
let uptimeList = {};
|
let uptimeList = {};
|
||||||
|
|
||||||
|
let slug = request.params.slug;
|
||||||
|
let statusPageID = await StatusPage.slugToID(slug);
|
||||||
|
|
||||||
let monitorIDList = await R.getCol(`
|
let monitorIDList = await R.getCol(`
|
||||||
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
|
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
|
||||||
WHERE monitor_group.group_id = \`group\`.id
|
WHERE monitor_group.group_id = \`group\`.id
|
||||||
AND public = 1
|
AND public = 1
|
||||||
`);
|
AND \`group\`.status_page_id = ?
|
||||||
|
`, [
|
||||||
|
statusPageID
|
||||||
|
]);
|
||||||
|
|
||||||
for (let monitorID of monitorIDList) {
|
for (let monitorID of monitorIDList) {
|
||||||
let list = await R.getAll(`
|
let list = await R.getAll(`
|
||||||
|
@ -214,22 +185,12 @@ router.get("/api/status-page/heartbeat", cache("5 minutes"), async (_request, re
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function checkPublished() {
|
|
||||||
if (! await isPublished()) {
|
|
||||||
throw new Error("The status page is not published");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default is published
|
* Default is published
|
||||||
* @returns {Promise<boolean>}
|
* @returns {Promise<boolean>}
|
||||||
*/
|
*/
|
||||||
async function isPublished() {
|
async function isPublished() {
|
||||||
const value = await setting("statusPagePublished");
|
|
||||||
if (value === null) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function send403(res, msg = "") {
|
function send403(res, msg = "") {
|
||||||
|
|
|
@ -132,6 +132,7 @@ const { sendNotificationList, sendHeartbeatList, sendImportantHeartbeatList, sen
|
||||||
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
|
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
|
||||||
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
|
const databaseSocketHandler = require("./socket-handlers/database-socket-handler");
|
||||||
const TwoFA = require("./2fa");
|
const TwoFA = require("./2fa");
|
||||||
|
const StatusPage = require("./model/status_page");
|
||||||
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
|
@ -200,8 +201,8 @@ exports.entryPage = "dashboard";
|
||||||
|
|
||||||
// Entry Page
|
// Entry Page
|
||||||
app.get("/", async (_request, response) => {
|
app.get("/", async (_request, response) => {
|
||||||
if (exports.entryPage === "statusPage") {
|
if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) {
|
||||||
response.redirect("/status");
|
response.redirect("/status/" + exports.entryPage.replace("statusPage-", ""));
|
||||||
} else {
|
} else {
|
||||||
response.redirect("/dashboard");
|
response.redirect("/dashboard");
|
||||||
}
|
}
|
||||||
|
@ -1414,6 +1415,8 @@ async function afterLogin(socket, user) {
|
||||||
for (let monitorID in monitorList) {
|
for (let monitorID in monitorList) {
|
||||||
await Monitor.sendStats(io, monitorID, user.id);
|
await Monitor.sendStats(io, monitorID, user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await StatusPage.sendStatusPageList(io, socket);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getMonitorJSONList(userID) {
|
async function getMonitorJSONList(userID) {
|
||||||
|
|
|
@ -1,25 +1,36 @@
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const { checkLogin, setSettings } = require("../util-server");
|
const { checkLogin, setSettings, setSetting } = require("../util-server");
|
||||||
const dayjs = require("dayjs");
|
const dayjs = require("dayjs");
|
||||||
const { debug } = require("../../src/util");
|
const { debug } = require("../../src/util");
|
||||||
const ImageDataURI = require("../image-data-uri");
|
const ImageDataURI = require("../image-data-uri");
|
||||||
const Database = require("../database");
|
const Database = require("../database");
|
||||||
const apicache = require("../modules/apicache");
|
const apicache = require("../modules/apicache");
|
||||||
|
const StatusPage = require("../model/status_page");
|
||||||
|
const server = require("../server");
|
||||||
|
|
||||||
module.exports.statusPageSocketHandler = (socket) => {
|
module.exports.statusPageSocketHandler = (socket) => {
|
||||||
|
|
||||||
// Post or edit incident
|
// Post or edit incident
|
||||||
socket.on("postIncident", async (incident, callback) => {
|
socket.on("postIncident", async (slug, incident, callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
|
||||||
await R.exec("UPDATE incident SET pin = 0 ");
|
let statusPageID = await StatusPage.slugToID(slug);
|
||||||
|
|
||||||
|
if (!statusPageID) {
|
||||||
|
throw new Error("slug is not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
await R.exec("UPDATE incident SET pin = 0 WHERE status_page_id = ? ", [
|
||||||
|
statusPageID
|
||||||
|
]);
|
||||||
|
|
||||||
let incidentBean;
|
let incidentBean;
|
||||||
|
|
||||||
if (incident.id) {
|
if (incident.id) {
|
||||||
incidentBean = await R.findOne("incident", " id = ?", [
|
incidentBean = await R.findOne("incident", " id = ? AND status_page_id = ? ", [
|
||||||
incident.id
|
incident.id,
|
||||||
|
statusPageID
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,6 +42,7 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||||
incidentBean.content = incident.content;
|
incidentBean.content = incident.content;
|
||||||
incidentBean.style = incident.style;
|
incidentBean.style = incident.style;
|
||||||
incidentBean.pin = true;
|
incidentBean.pin = true;
|
||||||
|
incidentBean.status_page_id = statusPageID;
|
||||||
|
|
||||||
if (incident.id) {
|
if (incident.id) {
|
||||||
incidentBean.lastUpdatedDate = R.isoDateTime(dayjs.utc());
|
incidentBean.lastUpdatedDate = R.isoDateTime(dayjs.utc());
|
||||||
|
@ -52,11 +64,15 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("unpinIncident", async (callback) => {
|
socket.on("unpinIncident", async (slug, callback) => {
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
|
||||||
await R.exec("UPDATE incident SET pin = 0 WHERE pin = 1");
|
let statusPageID = await StatusPage.slugToID(slug);
|
||||||
|
|
||||||
|
await R.exec("UPDATE incident SET pin = 0 WHERE pin = 1 AND status_page_id = ? ", [
|
||||||
|
statusPageID
|
||||||
|
]);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
|
@ -71,13 +87,21 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||||
|
|
||||||
// Save Status Page
|
// Save Status Page
|
||||||
// imgDataUrl Only Accept PNG!
|
// imgDataUrl Only Accept PNG!
|
||||||
socket.on("saveStatusPage", async (config, imgDataUrl, publicGroupList, callback) => {
|
socket.on("saveStatusPage", async (slug, config, imgDataUrl, publicGroupList, callback) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
|
|
||||||
apicache.clear();
|
apicache.clear();
|
||||||
|
|
||||||
|
// Save Config
|
||||||
|
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
||||||
|
slug
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!statusPage) {
|
||||||
|
throw new Error("No slug?");
|
||||||
|
}
|
||||||
|
|
||||||
const header = "data:image/png;base64,";
|
const header = "data:image/png;base64,";
|
||||||
|
|
||||||
// Check logo format
|
// Check logo format
|
||||||
|
@ -88,16 +112,28 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||||
throw new Error("Only allowed PNG logo.");
|
throw new Error("Only allowed PNG logo.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filename = `logo${statusPage.id}.png`;
|
||||||
|
|
||||||
// Convert to file
|
// Convert to file
|
||||||
await ImageDataURI.outputFile(imgDataUrl, Database.uploadDir + "logo.png");
|
await ImageDataURI.outputFile(imgDataUrl, Database.uploadDir + filename);
|
||||||
config.logo = "/upload/logo.png?t=" + Date.now();
|
config.logo = `/upload/${filename}?t=` + Date.now();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
config.icon = imgDataUrl;
|
config.icon = imgDataUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save Config
|
statusPage.slug = config.slug;
|
||||||
await setSettings("statusPage", config);
|
statusPage.title = config.title;
|
||||||
|
statusPage.description = config.description;
|
||||||
|
statusPage.icon = config.logo;
|
||||||
|
statusPage.theme = config.theme;
|
||||||
|
//statusPage.published = ;
|
||||||
|
//statusPage.search_engine_index = ;
|
||||||
|
statusPage.show_tags = config.showTags;
|
||||||
|
//statusPage.password = null;
|
||||||
|
statusPage.modified_date = R.isoDateTime();
|
||||||
|
|
||||||
|
await R.store(statusPage);
|
||||||
|
|
||||||
// Save Public Group List
|
// Save Public Group List
|
||||||
const groupIDList = [];
|
const groupIDList = [];
|
||||||
|
@ -106,13 +142,15 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||||
for (let group of publicGroupList) {
|
for (let group of publicGroupList) {
|
||||||
let groupBean;
|
let groupBean;
|
||||||
if (group.id) {
|
if (group.id) {
|
||||||
groupBean = await R.findOne("group", " id = ? AND public = 1 ", [
|
groupBean = await R.findOne("group", " id = ? AND public = 1 AND status_page_id = ? ", [
|
||||||
group.id
|
group.id,
|
||||||
|
statusPage.id
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
groupBean = R.dispense("group");
|
groupBean = R.dispense("group");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
groupBean.status_page_id = statusPage.id;
|
||||||
groupBean.name = group.name;
|
groupBean.name = group.name;
|
||||||
groupBean.public = true;
|
groupBean.public = true;
|
||||||
groupBean.weight = groupOrder++;
|
groupBean.weight = groupOrder++;
|
||||||
|
@ -124,7 +162,6 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let monitorOrder = 1;
|
let monitorOrder = 1;
|
||||||
console.log(group.monitorList);
|
|
||||||
|
|
||||||
for (let monitor of group.monitorList) {
|
for (let monitor of group.monitorList) {
|
||||||
let relationBean = R.dispense("monitor_group");
|
let relationBean = R.dispense("monitor_group");
|
||||||
|
@ -143,13 +180,19 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||||
const slots = groupIDList.map(() => "?").join(",");
|
const slots = groupIDList.map(() => "?").join(",");
|
||||||
await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots})`, groupIDList);
|
await R.exec(`DELETE FROM \`group\` WHERE id NOT IN (${slots})`, groupIDList);
|
||||||
|
|
||||||
|
// Also change entry page to new slug if it is the default one, and slug is changed.
|
||||||
|
if (server.entryPage === "statusPage-" + slug && statusPage.slug !== slug) {
|
||||||
|
server.entryPage = "statusPage-" + statusPage.slug;
|
||||||
|
await setSetting("entryPage", server.entryPage, "general");
|
||||||
|
}
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
publicGroupList,
|
publicGroupList,
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.error(error);
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: false,
|
ok: false,
|
||||||
|
@ -158,4 +201,99 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add a new status page
|
||||||
|
socket.on("addStatusPage", async (title, slug, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
title = title?.trim();
|
||||||
|
slug = slug?.trim();
|
||||||
|
|
||||||
|
// Check empty
|
||||||
|
if (!title || !slug) {
|
||||||
|
throw new Error("Please input all fields");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure slug is string
|
||||||
|
if (typeof slug !== "string") {
|
||||||
|
throw new Error("Slug -Accept string only");
|
||||||
|
}
|
||||||
|
|
||||||
|
// lower case only
|
||||||
|
slug = slug.toLowerCase();
|
||||||
|
|
||||||
|
// Check slug a-z, 0-9, - only
|
||||||
|
// Regex from: https://stackoverflow.com/questions/22454258/js-regex-string-validation-for-slug
|
||||||
|
if (!slug.match(/^[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*$/)) {
|
||||||
|
throw new Error("Invalid Slug");
|
||||||
|
}
|
||||||
|
|
||||||
|
let statusPage = R.dispense("status_page");
|
||||||
|
statusPage.slug = slug;
|
||||||
|
statusPage.title = title;
|
||||||
|
statusPage.theme = "light";
|
||||||
|
statusPage.icon = "";
|
||||||
|
await R.store(statusPage);
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
msg: "OK!"
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete a status page
|
||||||
|
socket.on("deleteStatusPage", async (slug, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
let statusPageID = await StatusPage.slugToID(slug);
|
||||||
|
|
||||||
|
if (statusPageID) {
|
||||||
|
|
||||||
|
// Reset entry page if it is the default one.
|
||||||
|
if (server.entryPage === "statusPage-" + slug) {
|
||||||
|
server.entryPage = "dashboard";
|
||||||
|
await setSetting("entryPage", server.entryPage, "general");
|
||||||
|
}
|
||||||
|
|
||||||
|
// No need to delete records from `status_page_cname`, because it has cascade foreign key.
|
||||||
|
// But for incident & group, it is hard to add cascade foreign key during migration, so they have to be deleted manually.
|
||||||
|
|
||||||
|
// Delete incident
|
||||||
|
await R.exec("DELETE FROM incident WHERE status_page_id = ? ", [
|
||||||
|
statusPageID
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Delete group
|
||||||
|
await R.exec("DELETE FROM `group` WHERE status_page_id = ? ", [
|
||||||
|
statusPageID
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Delete status_page
|
||||||
|
await R.exec("DELETE FROM status_page WHERE id = ? ", [
|
||||||
|
statusPageID
|
||||||
|
]);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new Error("Status Page is not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -92,6 +92,10 @@ textarea.form-control {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-dark {
|
||||||
|
background-color: #161B22;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 550px) {
|
@media (max-width: 550px) {
|
||||||
.table-shadow-box {
|
.table-shadow-box {
|
||||||
padding: 10px !important;
|
padding: 10px !important;
|
||||||
|
@ -144,6 +148,10 @@ textarea.form-control {
|
||||||
background-color: #090c10;
|
background-color: #090c10;
|
||||||
color: $dark-font-color;
|
color: $dark-font-color;
|
||||||
|
|
||||||
|
mark, .mark {
|
||||||
|
background-color: #b6ad86;
|
||||||
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb, ::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb, ::-webkit-scrollbar-thumb {
|
||||||
background: $dark-border-color;
|
background: $dark-border-color;
|
||||||
}
|
}
|
||||||
|
@ -159,6 +167,12 @@ textarea.form-control {
|
||||||
border-color: $dark-border-color;
|
border-color: $dark-border-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input-group-text {
|
||||||
|
background-color: #282f39;
|
||||||
|
border-color: $dark-border-color;
|
||||||
|
color: $dark-font-color;
|
||||||
|
}
|
||||||
|
|
||||||
.form-check-input:checked {
|
.form-check-input:checked {
|
||||||
border-color: $primary; // Re-apply bootstrap border
|
border-color: $primary; // Re-apply bootstrap border
|
||||||
}
|
}
|
||||||
|
@ -167,7 +181,7 @@ textarea.form-control {
|
||||||
background-color: #232f3b;
|
background-color: #232f3b;
|
||||||
}
|
}
|
||||||
|
|
||||||
a,
|
a:not(.btn),
|
||||||
.table,
|
.table,
|
||||||
.nav-link {
|
.nav-link {
|
||||||
color: $dark-font-color;
|
color: $dark-font-color;
|
||||||
|
@ -438,6 +452,10 @@ textarea.form-control {
|
||||||
border-radius: 10px !important;
|
border-radius: 10px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
color: $primary;
|
||||||
|
}
|
||||||
|
|
||||||
// Localization
|
// Localization
|
||||||
|
|
||||||
@import "localization.scss";
|
@import "localization.scss";
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
<Uptime :monitor="monitor.element" type="24" :pill="true" />
|
<Uptime :monitor="monitor.element" type="24" :pill="true" />
|
||||||
{{ monitor.element.name }}
|
{{ monitor.element.name }}
|
||||||
</div>
|
</div>
|
||||||
<div class="tags">
|
<div v-if="showTags" class="tags">
|
||||||
<Tag v-for="tag in monitor.element.tags" :key="tag" :item="tag" :size="'sm'" />
|
<Tag v-for="tag in monitor.element.tags" :key="tag" :item="tag" :size="'sm'" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -76,6 +76,9 @@ export default {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
showTags: {
|
||||||
|
type: Boolean,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -62,31 +62,31 @@
|
||||||
|
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input
|
<input
|
||||||
id="entryPageYes"
|
id="entryPageDashboard"
|
||||||
v-model="settings.entryPage"
|
v-model="settings.entryPage"
|
||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
type="radio"
|
type="radio"
|
||||||
name="statusPage"
|
name="entryPage"
|
||||||
value="dashboard"
|
value="dashboard"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<label class="form-check-label" for="entryPageYes">
|
<label class="form-check-label" for="entryPageDashboard">
|
||||||
{{ $t("Dashboard") }}
|
{{ $t("Dashboard") }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-check">
|
<div v-for="statusPage in $root.statusPageList" :key="statusPage.id" class="form-check">
|
||||||
<input
|
<input
|
||||||
id="entryPageNo"
|
:id="'status-page-' + statusPage.id"
|
||||||
v-model="settings.entryPage"
|
v-model="settings.entryPage"
|
||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
type="radio"
|
type="radio"
|
||||||
name="statusPage"
|
name="entryPage"
|
||||||
value="statusPage"
|
:value="'statusPage-' + statusPage.slug"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<label class="form-check-label" for="entryPageNo">
|
<label class="form-check-label" :for="'status-page-' + statusPage.id">
|
||||||
{{ $t("Status Page") }}
|
{{ $t("Status Page") }} - {{ statusPage.title }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -34,6 +34,9 @@ import {
|
||||||
faAward,
|
faAward,
|
||||||
faLink,
|
faLink,
|
||||||
faChevronDown,
|
faChevronDown,
|
||||||
|
faPen,
|
||||||
|
faExternalLinkSquareAlt,
|
||||||
|
faSpinner,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
|
@ -67,6 +70,9 @@ library.add(
|
||||||
faAward,
|
faAward,
|
||||||
faLink,
|
faLink,
|
||||||
faChevronDown,
|
faChevronDown,
|
||||||
|
faPen,
|
||||||
|
faExternalLinkSquareAlt,
|
||||||
|
faSpinner,
|
||||||
);
|
);
|
||||||
|
|
||||||
export { FontAwesomeIcon };
|
export { FontAwesomeIcon };
|
||||||
|
|
|
@ -197,6 +197,7 @@ export default {
|
||||||
line: "Line Messenger",
|
line: "Line Messenger",
|
||||||
mattermost: "Mattermost",
|
mattermost: "Mattermost",
|
||||||
"Status Page": "Статус страница",
|
"Status Page": "Статус страница",
|
||||||
|
"Status Pages": "Статус страница",
|
||||||
"Primary Base URL": "Основен базов URL адрес",
|
"Primary Base URL": "Основен базов URL адрес",
|
||||||
"Push URL": "Генериран Push URL адрес",
|
"Push URL": "Генериран Push URL адрес",
|
||||||
needPushEvery: "Необходимо е да извършвате заявка към този URL адрес на всеки {0} секунди",
|
needPushEvery: "Необходимо е да извършвате заявка към този URL адрес на всеки {0} секунди",
|
||||||
|
|
|
@ -183,6 +183,7 @@ export default {
|
||||||
"Edit Status Page": "Upravit stavovou stránku",
|
"Edit Status Page": "Upravit stavovou stránku",
|
||||||
"Go to Dashboard": "Přejít na nástěnku",
|
"Go to Dashboard": "Přejít na nástěnku",
|
||||||
"Status Page": "Stavová stránka",
|
"Status Page": "Stavová stránka",
|
||||||
|
"Status Pages": "Stavová stránka",
|
||||||
defaultNotificationName: "Moje {notification} upozornění ({číslo})",
|
defaultNotificationName: "Moje {notification} upozornění ({číslo})",
|
||||||
here: "sem",
|
here: "sem",
|
||||||
Required: "Vyžadováno",
|
Required: "Vyžadováno",
|
||||||
|
|
|
@ -180,6 +180,7 @@ export default {
|
||||||
"Edit Status Page": "Rediger Statusside",
|
"Edit Status Page": "Rediger Statusside",
|
||||||
"Go to Dashboard": "Gå til Betjeningspanel",
|
"Go to Dashboard": "Gå til Betjeningspanel",
|
||||||
"Status Page": "Statusside",
|
"Status Page": "Statusside",
|
||||||
|
"Status Pages": "Statusside",
|
||||||
telegram: "Telegram",
|
telegram: "Telegram",
|
||||||
webhook: "Webhook",
|
webhook: "Webhook",
|
||||||
smtp: "Email (SMTP)",
|
smtp: "Email (SMTP)",
|
||||||
|
|
|
@ -179,6 +179,7 @@ export default {
|
||||||
"Edit Status Page": "Bearbeite Status-Seite",
|
"Edit Status Page": "Bearbeite Status-Seite",
|
||||||
"Go to Dashboard": "Gehe zum Dashboard",
|
"Go to Dashboard": "Gehe zum Dashboard",
|
||||||
"Status Page": "Status-Seite",
|
"Status Page": "Status-Seite",
|
||||||
|
"Status Pages": "Status-Seite",
|
||||||
telegram: "Telegram",
|
telegram: "Telegram",
|
||||||
webhook: "Webhook",
|
webhook: "Webhook",
|
||||||
smtp: "E-Mail (SMTP)",
|
smtp: "E-Mail (SMTP)",
|
||||||
|
|
|
@ -183,6 +183,7 @@ export default {
|
||||||
"Edit Status Page": "Edit Status Page",
|
"Edit Status Page": "Edit Status Page",
|
||||||
"Go to Dashboard": "Go to Dashboard",
|
"Go to Dashboard": "Go to Dashboard",
|
||||||
"Status Page": "Status Page",
|
"Status Page": "Status Page",
|
||||||
|
"Status Pages": "Status Pages",
|
||||||
defaultNotificationName: "My {notification} Alert ({number})",
|
defaultNotificationName: "My {notification} Alert ({number})",
|
||||||
here: "here",
|
here: "here",
|
||||||
Required: "Required",
|
Required: "Required",
|
||||||
|
@ -330,21 +331,21 @@ export default {
|
||||||
dark: "dark",
|
dark: "dark",
|
||||||
Post: "Post",
|
Post: "Post",
|
||||||
"Please input title and content": "Please input title and content",
|
"Please input title and content": "Please input title and content",
|
||||||
Created: "Created",
|
"Created": "Created",
|
||||||
"Last Updated": "Last Updated",
|
"Last Updated": "Last Updated",
|
||||||
Unpin: "Unpin",
|
"Unpin": "Unpin",
|
||||||
"Switch to Light Theme": "Switch to Light Theme",
|
"Switch to Light Theme": "Switch to Light Theme",
|
||||||
"Switch to Dark Theme": "Switch to Dark Theme",
|
"Switch to Dark Theme": "Switch to Dark Theme",
|
||||||
"Show Tags": "Show Tags",
|
"Show Tags": "Show Tags",
|
||||||
"Hide Tags": "Hide Tags",
|
"Hide Tags": "Hide Tags",
|
||||||
Description: "Description",
|
"Description": "Description",
|
||||||
"No monitors available.": "No monitors available.",
|
"No monitors available.": "No monitors available.",
|
||||||
"Add one": "Add one",
|
"Add one": "Add one",
|
||||||
"No Monitors": "No Monitors",
|
"No Monitors": "No Monitors",
|
||||||
"Untitled Group": "Untitled Group",
|
"Untitled Group": "Untitled Group",
|
||||||
Services: "Services",
|
"Services": "Services",
|
||||||
Discard: "Discard",
|
"Discard": "Discard",
|
||||||
Cancel: "Cancel",
|
"Cancel": "Cancel",
|
||||||
"Powered by": "Powered by",
|
"Powered by": "Powered by",
|
||||||
shrinkDatabaseDescription: "Trigger database VACUUM for SQLite. If your database is created after 1.10.0, AUTO_VACUUM is already enabled and this action is not needed.",
|
shrinkDatabaseDescription: "Trigger database VACUUM for SQLite. If your database is created after 1.10.0, AUTO_VACUUM is already enabled and this action is not needed.",
|
||||||
serwersms: "SerwerSMS.pl",
|
serwersms: "SerwerSMS.pl",
|
||||||
|
@ -352,7 +353,7 @@ export default {
|
||||||
serwersmsAPIPassword: "API Password",
|
serwersmsAPIPassword: "API Password",
|
||||||
serwersmsPhoneNumber: "Phone number",
|
serwersmsPhoneNumber: "Phone number",
|
||||||
serwersmsSenderName: "SMS Sender Name (registered via customer portal)",
|
serwersmsSenderName: "SMS Sender Name (registered via customer portal)",
|
||||||
"stackfield": "Stackfield",
|
stackfield: "Stackfield",
|
||||||
smtpDkimSettings: "DKIM Settings",
|
smtpDkimSettings: "DKIM Settings",
|
||||||
smtpDkimDesc: "Please refer to the Nodemailer DKIM {0} for usage.",
|
smtpDkimDesc: "Please refer to the Nodemailer DKIM {0} for usage.",
|
||||||
documentation: "documentation",
|
documentation: "documentation",
|
||||||
|
@ -363,10 +364,11 @@ export default {
|
||||||
smtpDkimheaderFieldNames: "Header Keys to sign (Optional)",
|
smtpDkimheaderFieldNames: "Header Keys to sign (Optional)",
|
||||||
smtpDkimskipFields: "Header Keys not to sign (Optional)",
|
smtpDkimskipFields: "Header Keys not to sign (Optional)",
|
||||||
gorush: "Gorush",
|
gorush: "Gorush",
|
||||||
alerta: 'Alerta',
|
alerta: "Alerta",
|
||||||
alertaApiEndpoint: 'API Endpoint',
|
alertaApiEndpoint: "API Endpoint",
|
||||||
alertaEnvironment: 'Environment',
|
alertaEnvironment: "Environment",
|
||||||
alertaApiKey: 'API Key',
|
alertaApiKey: "API Key",
|
||||||
alertaAlertState: 'Alert State',
|
alertaAlertState: "Alert State",
|
||||||
alertaRecoverState: 'Recover State',
|
alertaRecoverState: "Recover State",
|
||||||
|
deleteStatusPageMsg: "Are you sure want to delete this status page?",
|
||||||
};
|
};
|
||||||
|
|
|
@ -180,6 +180,7 @@ export default {
|
||||||
"Edit Status Page": "Editar página de estado",
|
"Edit Status Page": "Editar página de estado",
|
||||||
"Go to Dashboard": "Ir al panel de control",
|
"Go to Dashboard": "Ir al panel de control",
|
||||||
"Status Page": "Página de estado",
|
"Status Page": "Página de estado",
|
||||||
|
"Status Pages": "Página de estado",
|
||||||
telegram: "Telegram",
|
telegram: "Telegram",
|
||||||
webhook: "Webhook",
|
webhook: "Webhook",
|
||||||
smtp: "Email (SMTP)",
|
smtp: "Email (SMTP)",
|
||||||
|
|
|
@ -17,6 +17,7 @@ export default {
|
||||||
pauseMonitorMsg: "Kas soovid peatada seire?",
|
pauseMonitorMsg: "Kas soovid peatada seire?",
|
||||||
Settings: "Seaded",
|
Settings: "Seaded",
|
||||||
"Status Page": "Ülevaade",
|
"Status Page": "Ülevaade",
|
||||||
|
"Status Pages": "Ülevaade",
|
||||||
Dashboard: "Töölaud",
|
Dashboard: "Töölaud",
|
||||||
"New Update": "Uuem tarkvara versioon on saadaval.",
|
"New Update": "Uuem tarkvara versioon on saadaval.",
|
||||||
Language: "Keel",
|
Language: "Keel",
|
||||||
|
|
|
@ -178,6 +178,7 @@ export default {
|
||||||
"Add a monitor": "اضافه کردن مانیتور",
|
"Add a monitor": "اضافه کردن مانیتور",
|
||||||
"Edit Status Page": "ویرایش صفحه وضعیت",
|
"Edit Status Page": "ویرایش صفحه وضعیت",
|
||||||
"Status Page": "صفحه وضعیت",
|
"Status Page": "صفحه وضعیت",
|
||||||
|
"Status Pages": "صفحه وضعیت",
|
||||||
"Go to Dashboard": "رفتن به پیشخوان",
|
"Go to Dashboard": "رفتن به پیشخوان",
|
||||||
"Uptime Kuma": "آپتایم کوما",
|
"Uptime Kuma": "آپتایم کوما",
|
||||||
records: "مورد",
|
records: "مورد",
|
||||||
|
|
|
@ -179,6 +179,7 @@ export default {
|
||||||
"Edit Status Page": "Modifier la page de statut",
|
"Edit Status Page": "Modifier la page de statut",
|
||||||
"Go to Dashboard": "Accéder au tableau de bord",
|
"Go to Dashboard": "Accéder au tableau de bord",
|
||||||
"Status Page": "Status Page",
|
"Status Page": "Status Page",
|
||||||
|
"Status Pages": "Status Pages",
|
||||||
defaultNotificationName: "Ma notification {notification} numéro ({number})",
|
defaultNotificationName: "Ma notification {notification} numéro ({number})",
|
||||||
here: "ici",
|
here: "ici",
|
||||||
Required: "Requis",
|
Required: "Requis",
|
||||||
|
@ -304,9 +305,9 @@ export default {
|
||||||
steamApiKeyDescription: "Pour surveiller un serveur Steam, vous avez besoin d'une clé Steam Web-API. Vous pouvez enregistrer votre clé ici : ",
|
steamApiKeyDescription: "Pour surveiller un serveur Steam, vous avez besoin d'une clé Steam Web-API. Vous pouvez enregistrer votre clé ici : ",
|
||||||
"Current User": "Utilisateur actuel",
|
"Current User": "Utilisateur actuel",
|
||||||
recent: "Récent",
|
recent: "Récent",
|
||||||
alertaApiEndpoint: 'API Endpoint',
|
alertaApiEndpoint: "API Endpoint",
|
||||||
alertaEnvironment: 'Environement',
|
alertaEnvironment: "Environement",
|
||||||
alertaApiKey: "Clé de l'API",
|
alertaApiKey: "Clé de l'API",
|
||||||
alertaAlertState: "État de l'Alerte",
|
alertaAlertState: "État de l'Alerte",
|
||||||
alertaRecoverState: 'État de récupération',
|
alertaRecoverState: "État de récupération",
|
||||||
};
|
};
|
||||||
|
|
|
@ -183,6 +183,7 @@ export default {
|
||||||
"Edit Status Page": "Uredi Statusnu stranicu",
|
"Edit Status Page": "Uredi Statusnu stranicu",
|
||||||
"Go to Dashboard": "Na Kontrolnu ploču",
|
"Go to Dashboard": "Na Kontrolnu ploču",
|
||||||
"Status Page": "Statusna stranica",
|
"Status Page": "Statusna stranica",
|
||||||
|
"Status Pages": "Statusna stranica",
|
||||||
defaultNotificationName: "Moja {number}. {notification} obavijest",
|
defaultNotificationName: "Moja {number}. {notification} obavijest",
|
||||||
here: "ovdje",
|
here: "ovdje",
|
||||||
Required: "Potrebno",
|
Required: "Potrebno",
|
||||||
|
|
|
@ -197,6 +197,7 @@ export default {
|
||||||
line: "Line Messenger",
|
line: "Line Messenger",
|
||||||
mattermost: "Mattermost",
|
mattermost: "Mattermost",
|
||||||
"Status Page": "Státusz oldal",
|
"Status Page": "Státusz oldal",
|
||||||
|
"Status Pages": "Státusz oldal",
|
||||||
"Primary Base URL": "Elsődleges URL",
|
"Primary Base URL": "Elsődleges URL",
|
||||||
"Push URL": "Meghívandó URL",
|
"Push URL": "Meghívandó URL",
|
||||||
needPushEvery: "Ezt az URL-t kell meghívni minden {0} másodpercben.",
|
needPushEvery: "Ezt az URL-t kell meghívni minden {0} másodpercben.",
|
||||||
|
|
|
@ -179,6 +179,7 @@ export default {
|
||||||
"Edit Status Page": "Edit Halaman Status",
|
"Edit Status Page": "Edit Halaman Status",
|
||||||
"Go to Dashboard": "Pergi ke Dasbor",
|
"Go to Dashboard": "Pergi ke Dasbor",
|
||||||
"Status Page": "Halaman Status",
|
"Status Page": "Halaman Status",
|
||||||
|
"Status Pages": "Halaman Status",
|
||||||
defaultNotificationName: "{notification} saya Peringatan ({number})",
|
defaultNotificationName: "{notification} saya Peringatan ({number})",
|
||||||
here: "di sini",
|
here: "di sini",
|
||||||
Required: "Dibutuhkan",
|
Required: "Dibutuhkan",
|
||||||
|
|
|
@ -183,6 +183,7 @@ export default {
|
||||||
"Edit Status Page": "Modifica pagina di stato",
|
"Edit Status Page": "Modifica pagina di stato",
|
||||||
"Go to Dashboard": "Vai alla dashboard",
|
"Go to Dashboard": "Vai alla dashboard",
|
||||||
"Status Page": "Pagina di stato",
|
"Status Page": "Pagina di stato",
|
||||||
|
"Status Pages": "Pagina di stato",
|
||||||
defaultNotificationName: "Notifica {notification} ({number})",
|
defaultNotificationName: "Notifica {notification} ({number})",
|
||||||
here: "qui",
|
here: "qui",
|
||||||
Required: "Obbligatorio",
|
Required: "Obbligatorio",
|
||||||
|
|
|
@ -180,6 +180,7 @@ export default {
|
||||||
"Edit Status Page": "ステータスページ編集",
|
"Edit Status Page": "ステータスページ編集",
|
||||||
"Go to Dashboard": "ダッシュボード",
|
"Go to Dashboard": "ダッシュボード",
|
||||||
"Status Page": "ステータスページ",
|
"Status Page": "ステータスページ",
|
||||||
|
"Status Pages": "ステータスページ",
|
||||||
telegram: "Telegram",
|
telegram: "Telegram",
|
||||||
webhook: "Webhook",
|
webhook: "Webhook",
|
||||||
smtp: "Email (SMTP)",
|
smtp: "Email (SMTP)",
|
||||||
|
|
|
@ -179,6 +179,7 @@ export default {
|
||||||
"Edit Status Page": "상태 페이지 수정",
|
"Edit Status Page": "상태 페이지 수정",
|
||||||
"Go to Dashboard": "대시보드로 가기",
|
"Go to Dashboard": "대시보드로 가기",
|
||||||
"Status Page": "상태 페이지",
|
"Status Page": "상태 페이지",
|
||||||
|
"Status Pages": "상태 페이지",
|
||||||
defaultNotificationName: "내 {notification} 알림 ({number})",
|
defaultNotificationName: "내 {notification} 알림 ({number})",
|
||||||
here: "여기",
|
here: "여기",
|
||||||
Required: "필수",
|
Required: "필수",
|
||||||
|
|
|
@ -179,6 +179,7 @@ export default {
|
||||||
"Edit Status Page": "Rediger statusside",
|
"Edit Status Page": "Rediger statusside",
|
||||||
"Go to Dashboard": "Gå til Dashboard",
|
"Go to Dashboard": "Gå til Dashboard",
|
||||||
"Status Page": "Statusside",
|
"Status Page": "Statusside",
|
||||||
|
"Status Pages": "Statusside",
|
||||||
defaultNotificationName: "Min {notification} varsling ({number})",
|
defaultNotificationName: "Min {notification} varsling ({number})",
|
||||||
here: "her",
|
here: "her",
|
||||||
Required: "Obligatorisk",
|
Required: "Obligatorisk",
|
||||||
|
|
|
@ -180,6 +180,7 @@ export default {
|
||||||
"Edit Status Page": "Wijzig status pagina",
|
"Edit Status Page": "Wijzig status pagina",
|
||||||
"Go to Dashboard": "Ga naar Dashboard",
|
"Go to Dashboard": "Ga naar Dashboard",
|
||||||
"Status Page": "Status Pagina",
|
"Status Page": "Status Pagina",
|
||||||
|
"Status Pages": "Status Pagina",
|
||||||
telegram: "Telegram",
|
telegram: "Telegram",
|
||||||
webhook: "Webhook",
|
webhook: "Webhook",
|
||||||
smtp: "Email (SMTP)",
|
smtp: "Email (SMTP)",
|
||||||
|
|
|
@ -179,6 +179,7 @@ export default {
|
||||||
"Edit Status Page": "Edytuj stronę statusu",
|
"Edit Status Page": "Edytuj stronę statusu",
|
||||||
"Go to Dashboard": "Idź do panelu",
|
"Go to Dashboard": "Idź do panelu",
|
||||||
"Status Page": "Strona statusu",
|
"Status Page": "Strona statusu",
|
||||||
|
"Status Pages": "Strona statusu",
|
||||||
defaultNotificationName: "Moje powiadomienie {notification} ({number})",
|
defaultNotificationName: "Moje powiadomienie {notification} ({number})",
|
||||||
here: "tutaj",
|
here: "tutaj",
|
||||||
Required: "Wymagane",
|
Required: "Wymagane",
|
||||||
|
|
|
@ -169,6 +169,7 @@ export default {
|
||||||
"Avg. Ping": "Ping Médio.",
|
"Avg. Ping": "Ping Médio.",
|
||||||
"Avg. Response": "Resposta Média. ",
|
"Avg. Response": "Resposta Média. ",
|
||||||
"Status Page": "Página de Status",
|
"Status Page": "Página de Status",
|
||||||
|
"Status Pages": "Página de Status",
|
||||||
"Entry Page": "Página de entrada",
|
"Entry Page": "Página de entrada",
|
||||||
statusPageNothing: "Nada aqui, por favor, adicione um grupo ou monitor.",
|
statusPageNothing: "Nada aqui, por favor, adicione um grupo ou monitor.",
|
||||||
"No Services": "Nenhum Serviço",
|
"No Services": "Nenhum Serviço",
|
||||||
|
|
|
@ -181,6 +181,7 @@ export default {
|
||||||
"Edit Status Page": "Редактировать",
|
"Edit Status Page": "Редактировать",
|
||||||
"Go to Dashboard": "Панель управления",
|
"Go to Dashboard": "Панель управления",
|
||||||
"Status Page": "Мониторинг",
|
"Status Page": "Мониторинг",
|
||||||
|
"Status Pages": "Página de Status",
|
||||||
Discard: "Отмена",
|
Discard: "Отмена",
|
||||||
"Create Incident": "Создать инцидент",
|
"Create Incident": "Создать инцидент",
|
||||||
"Switch to Dark Theme": "Тёмная тема",
|
"Switch to Dark Theme": "Тёмная тема",
|
||||||
|
|
|
@ -182,7 +182,8 @@ export default {
|
||||||
"Add a monitor": "Dodaj monitor",
|
"Add a monitor": "Dodaj monitor",
|
||||||
"Edit Status Page": "Uredi statusno stran",
|
"Edit Status Page": "Uredi statusno stran",
|
||||||
"Go to Dashboard": "Pojdi na nadzorno ploščo",
|
"Go to Dashboard": "Pojdi na nadzorno ploščo",
|
||||||
"Status Page": "Status",
|
"Status Page": "Página de Status",
|
||||||
|
"Status Pages": "Página de Status",
|
||||||
defaultNotificationName: "Moje {notification} Obvestilo ({number})",
|
defaultNotificationName: "Moje {notification} Obvestilo ({number})",
|
||||||
here: "tukaj",
|
here: "tukaj",
|
||||||
Required: "Obvezno",
|
Required: "Obvezno",
|
||||||
|
|
|
@ -180,6 +180,7 @@ export default {
|
||||||
"Edit Status Page": "Edit Status Page",
|
"Edit Status Page": "Edit Status Page",
|
||||||
"Go to Dashboard": "Go to Dashboard",
|
"Go to Dashboard": "Go to Dashboard",
|
||||||
"Status Page": "Status Page",
|
"Status Page": "Status Page",
|
||||||
|
"Status Pages": "Status Pages",
|
||||||
telegram: "Telegram",
|
telegram: "Telegram",
|
||||||
webhook: "Webhook",
|
webhook: "Webhook",
|
||||||
smtp: "Email (SMTP)",
|
smtp: "Email (SMTP)",
|
||||||
|
|
|
@ -180,6 +180,7 @@ export default {
|
||||||
"Edit Status Page": "Edit Status Page",
|
"Edit Status Page": "Edit Status Page",
|
||||||
"Go to Dashboard": "Go to Dashboard",
|
"Go to Dashboard": "Go to Dashboard",
|
||||||
"Status Page": "Status Page",
|
"Status Page": "Status Page",
|
||||||
|
"Status Pages": "Status Pages",
|
||||||
telegram: "Telegram",
|
telegram: "Telegram",
|
||||||
webhook: "Webhook",
|
webhook: "Webhook",
|
||||||
smtp: "Email (SMTP)",
|
smtp: "Email (SMTP)",
|
||||||
|
|
|
@ -108,94 +108,4 @@ export default {
|
||||||
"Repeat Password": "Upprepa Lösenord",
|
"Repeat Password": "Upprepa Lösenord",
|
||||||
respTime: "Svarstid (ms)",
|
respTime: "Svarstid (ms)",
|
||||||
notAvailableShort: "Ej Tillg.",
|
notAvailableShort: "Ej Tillg.",
|
||||||
Create: "Create",
|
|
||||||
clearEventsMsg: "Are you sure want to delete all events for this monitor?",
|
|
||||||
clearHeartbeatsMsg: "Are you sure want to delete all heartbeats for this monitor?",
|
|
||||||
confirmClearStatisticsMsg: "Are you sure want to delete ALL statistics?",
|
|
||||||
"Clear Data": "Clear Data",
|
|
||||||
Events: "Events",
|
|
||||||
Heartbeats: "Heartbeats",
|
|
||||||
"Auto Get": "Auto Get",
|
|
||||||
enableDefaultNotificationDescription: "For every new monitor this notification will be enabled by default. You can still disable the notification separately for each monitor.",
|
|
||||||
"Default enabled": "Default enabled",
|
|
||||||
"Also apply to existing monitors": "Also apply to existing monitors",
|
|
||||||
Export: "Export",
|
|
||||||
Import: "Import",
|
|
||||||
backupDescription: "You can backup all monitors and all notifications into a JSON file.",
|
|
||||||
backupDescription2: "PS: History and event data is not included.",
|
|
||||||
backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.",
|
|
||||||
alertNoFile: "Please select a file to import.",
|
|
||||||
alertWrongFileType: "Please select a JSON file.",
|
|
||||||
twoFAVerifyLabel: "Please type in your token to verify that 2FA is working",
|
|
||||||
tokenValidSettingsMsg: "Token is valid! You can now save the 2FA settings.",
|
|
||||||
confirmEnableTwoFAMsg: "Are you sure you want to enable 2FA?",
|
|
||||||
confirmDisableTwoFAMsg: "Are you sure you want to disable 2FA?",
|
|
||||||
"Apply on all existing monitors": "Apply on all existing monitors",
|
|
||||||
"Verify Token": "Verify Token",
|
|
||||||
"Setup 2FA": "Setup 2FA",
|
|
||||||
"Enable 2FA": "Enable 2FA",
|
|
||||||
"Disable 2FA": "Disable 2FA",
|
|
||||||
"2FA Settings": "2FA Settings",
|
|
||||||
"Two Factor Authentication": "Two Factor Authentication",
|
|
||||||
Active: "Active",
|
|
||||||
Inactive: "Inactive",
|
|
||||||
Token: "Token",
|
|
||||||
"Show URI": "Show URI",
|
|
||||||
"Clear all statistics": "Clear all Statistics",
|
|
||||||
retryCheckEverySecond: "Retry every {0} seconds.",
|
|
||||||
importHandleDescription: "Choose 'Skip existing' if you want to skip every monitor or notification with the same name. 'Overwrite' will delete every existing monitor and notification.",
|
|
||||||
confirmImportMsg: "Are you sure to import the backup? Please make sure you've selected the right import option.",
|
|
||||||
"Heartbeat Retry Interval": "Heartbeat Retry Interval",
|
|
||||||
"Import Backup": "Import Backup",
|
|
||||||
"Export Backup": "Export Backup",
|
|
||||||
"Skip existing": "Skip existing",
|
|
||||||
Overwrite: "Overwrite",
|
|
||||||
Options: "Options",
|
|
||||||
"Keep both": "Keep both",
|
|
||||||
Tags: "Tags",
|
|
||||||
"Add New below or Select...": "Add New below or Select...",
|
|
||||||
"Tag with this name already exist.": "Tag with this name already exist.",
|
|
||||||
"Tag with this value already exist.": "Tag with this value already exist.",
|
|
||||||
color: "color",
|
|
||||||
"value (optional)": "value (optional)",
|
|
||||||
Gray: "Gray",
|
|
||||||
Red: "Red",
|
|
||||||
Orange: "Orange",
|
|
||||||
Green: "Green",
|
|
||||||
Blue: "Blue",
|
|
||||||
Indigo: "Indigo",
|
|
||||||
Purple: "Purple",
|
|
||||||
Pink: "Pink",
|
|
||||||
"Search...": "Search...",
|
|
||||||
"Avg. Ping": "Avg. Ping",
|
|
||||||
"Avg. Response": "Avg. Response",
|
|
||||||
"Entry Page": "Entry Page",
|
|
||||||
statusPageNothing: "Nothing here, please add a group or a monitor.",
|
|
||||||
"No Services": "No Services",
|
|
||||||
"All Systems Operational": "All Systems Operational",
|
|
||||||
"Partially Degraded Service": "Partially Degraded Service",
|
|
||||||
"Degraded Service": "Degraded Service",
|
|
||||||
"Add Group": "Add Group",
|
|
||||||
"Add a monitor": "Add a monitor",
|
|
||||||
"Edit Status Page": "Edit Status Page",
|
|
||||||
"Go to Dashboard": "Go to Dashboard",
|
|
||||||
"Status Page": "Status Page",
|
|
||||||
telegram: "Telegram",
|
|
||||||
webhook: "Webhook",
|
|
||||||
smtp: "Email (SMTP)",
|
|
||||||
discord: "Discord",
|
|
||||||
teams: "Microsoft Teams",
|
|
||||||
signal: "Signal",
|
|
||||||
gotify: "Gotify",
|
|
||||||
slack: "Slack",
|
|
||||||
"rocket.chat": "Rocket.chat",
|
|
||||||
pushover: "Pushover",
|
|
||||||
pushy: "Pushy",
|
|
||||||
octopush: "Octopush",
|
|
||||||
promosms: "PromoSMS",
|
|
||||||
lunasea: "LunaSea",
|
|
||||||
apprise: "Apprise (Support 50+ Notification services)",
|
|
||||||
pushbullet: "Pushbullet",
|
|
||||||
line: "Line Messenger",
|
|
||||||
mattermost: "Mattermost",
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -149,52 +149,4 @@ export default {
|
||||||
"Two Factor Authentication": "İki Faktörlü Kimlik Doğrulama (2FA)",
|
"Two Factor Authentication": "İki Faktörlü Kimlik Doğrulama (2FA)",
|
||||||
Active: "Aktif",
|
Active: "Aktif",
|
||||||
Inactive: "İnaktif",
|
Inactive: "İnaktif",
|
||||||
Token: "Token",
|
|
||||||
"Show URI": "Show URI",
|
|
||||||
Tags: "Tags",
|
|
||||||
"Add New below or Select...": "Add New below or Select...",
|
|
||||||
"Tag with this name already exist.": "Tag with this name already exist.",
|
|
||||||
"Tag with this value already exist.": "Tag with this value already exist.",
|
|
||||||
color: "color",
|
|
||||||
"value (optional)": "value (optional)",
|
|
||||||
Gray: "Gray",
|
|
||||||
Red: "Red",
|
|
||||||
Orange: "Orange",
|
|
||||||
Green: "Green",
|
|
||||||
Blue: "Blue",
|
|
||||||
Indigo: "Indigo",
|
|
||||||
Purple: "Purple",
|
|
||||||
Pink: "Pink",
|
|
||||||
"Search...": "Search...",
|
|
||||||
"Avg. Ping": "Avg. Ping",
|
|
||||||
"Avg. Response": "Avg. Response",
|
|
||||||
"Entry Page": "Entry Page",
|
|
||||||
statusPageNothing: "Nothing here, please add a group or a monitor.",
|
|
||||||
"No Services": "No Services",
|
|
||||||
"All Systems Operational": "All Systems Operational",
|
|
||||||
"Partially Degraded Service": "Partially Degraded Service",
|
|
||||||
"Degraded Service": "Degraded Service",
|
|
||||||
"Add Group": "Add Group",
|
|
||||||
"Add a monitor": "Add a monitor",
|
|
||||||
"Edit Status Page": "Edit Status Page",
|
|
||||||
"Go to Dashboard": "Go to Dashboard",
|
|
||||||
"Status Page": "Status Page",
|
|
||||||
telegram: "Telegram",
|
|
||||||
webhook: "Webhook",
|
|
||||||
smtp: "Email (SMTP)",
|
|
||||||
discord: "Discord",
|
|
||||||
teams: "Microsoft Teams",
|
|
||||||
signal: "Signal",
|
|
||||||
gotify: "Gotify",
|
|
||||||
slack: "Slack",
|
|
||||||
"rocket.chat": "Rocket.chat",
|
|
||||||
pushover: "Pushover",
|
|
||||||
pushy: "Pushy",
|
|
||||||
octopush: "Octopush",
|
|
||||||
promosms: "PromoSMS",
|
|
||||||
lunasea: "LunaSea",
|
|
||||||
apprise: "Apprise (Support 50+ Notification services)",
|
|
||||||
pushbullet: "Pushbullet",
|
|
||||||
line: "Line Messenger",
|
|
||||||
mattermost: "Mattermost",
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -183,6 +183,7 @@ export default {
|
||||||
"Edit Status Page": "Sửa trang trạng thái",
|
"Edit Status Page": "Sửa trang trạng thái",
|
||||||
"Go to Dashboard": "Đi tới Dashboard",
|
"Go to Dashboard": "Đi tới Dashboard",
|
||||||
"Status Page": "Trang trạng thái",
|
"Status Page": "Trang trạng thái",
|
||||||
|
"Status Pages": "Trang trạng thái",
|
||||||
defaultNotificationName: "My {notification} Alerts ({number})",
|
defaultNotificationName: "My {notification} Alerts ({number})",
|
||||||
here: "tại đây",
|
here: "tại đây",
|
||||||
Required: "Bắt buộc",
|
Required: "Bắt buộc",
|
||||||
|
|
|
@ -185,6 +185,7 @@ export default {
|
||||||
"Edit Status Page": "编辑状态页面",
|
"Edit Status Page": "编辑状态页面",
|
||||||
"Go to Dashboard": "前往仪表盘",
|
"Go to Dashboard": "前往仪表盘",
|
||||||
"Status Page": "状态页面",
|
"Status Page": "状态页面",
|
||||||
|
"Status Pages": "状态页面",
|
||||||
defaultNotificationName: "{notification} 通知({number})",
|
defaultNotificationName: "{notification} 通知({number})",
|
||||||
here: "这里",
|
here: "这里",
|
||||||
Required: "必填",
|
Required: "必填",
|
||||||
|
|
|
@ -96,7 +96,7 @@ export default {
|
||||||
Test: "測試",
|
Test: "測試",
|
||||||
keywordDescription: "搜索 HTML 或 JSON 裡是否有出現關鍵字(注意英文大細階)",
|
keywordDescription: "搜索 HTML 或 JSON 裡是否有出現關鍵字(注意英文大細階)",
|
||||||
"Certificate Info": "憑證詳細資料",
|
"Certificate Info": "憑證詳細資料",
|
||||||
deleteMonitorMsg: "是否確定刪除這個監測器",
|
deleteMonitorMsg: "是否確定刪除這個監測器?",
|
||||||
deleteNotificationMsg: "是否確定刪除這個通知設定?如監測器啟用了這個通知,將會收不到通知。",
|
deleteNotificationMsg: "是否確定刪除這個通知設定?如監測器啟用了這個通知,將會收不到通知。",
|
||||||
"Resolver Server": "DNS 伺服器",
|
"Resolver Server": "DNS 伺服器",
|
||||||
"Resource Record Type": "DNS 記錄類型",
|
"Resource Record Type": "DNS 記錄類型",
|
||||||
|
@ -180,6 +180,7 @@ export default {
|
||||||
"Edit Status Page": "編輯 Status Page",
|
"Edit Status Page": "編輯 Status Page",
|
||||||
"Go to Dashboard": "前往主控台",
|
"Go to Dashboard": "前往主控台",
|
||||||
"Status Page": "Status Page",
|
"Status Page": "Status Page",
|
||||||
|
"Status Pages": "Status Pages",
|
||||||
telegram: "Telegram",
|
telegram: "Telegram",
|
||||||
webhook: "Webhook",
|
webhook: "Webhook",
|
||||||
smtp: "電郵 (SMTP)",
|
smtp: "電郵 (SMTP)",
|
||||||
|
@ -198,4 +199,5 @@ export default {
|
||||||
pushbullet: "Pushbullet",
|
pushbullet: "Pushbullet",
|
||||||
line: "Line Messenger",
|
line: "Line Messenger",
|
||||||
mattermost: "Mattermost",
|
mattermost: "Mattermost",
|
||||||
|
deleteStatusPageMsg: "是否確定刪除這個 Status Page?",
|
||||||
};
|
};
|
||||||
|
|
|
@ -183,6 +183,7 @@ export default {
|
||||||
"Edit Status Page": "編輯狀態頁",
|
"Edit Status Page": "編輯狀態頁",
|
||||||
"Go to Dashboard": "前往儀表板",
|
"Go to Dashboard": "前往儀表板",
|
||||||
"Status Page": "狀態頁",
|
"Status Page": "狀態頁",
|
||||||
|
"Status Pages": "狀態頁",
|
||||||
defaultNotificationName: "我的 {notification} 通知 ({number})",
|
defaultNotificationName: "我的 {notification} 通知 ({number})",
|
||||||
here: "此處",
|
here: "此處",
|
||||||
Required: "必填",
|
Required: "必填",
|
||||||
|
|
|
@ -18,10 +18,10 @@
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<ul class="nav nav-pills">
|
<ul class="nav nav-pills">
|
||||||
<li class="nav-item me-2">
|
<li v-if="$root.loggedIn" class="nav-item me-2">
|
||||||
<a href="/status" class="nav-link status-page">
|
<router-link to="/manage-status-page" class="nav-link">
|
||||||
<font-awesome-icon icon="stream" /> {{ $t("Status Page") }}
|
<font-awesome-icon icon="stream" /> {{ $t("Status Pages") }}
|
||||||
</a>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="$root.loggedIn" class="nav-item me-2">
|
<li v-if="$root.loggedIn" class="nav-item me-2">
|
||||||
<router-link to="/dashboard" class="nav-link">
|
<router-link to="/dashboard" class="nav-link">
|
||||||
|
|
|
@ -7,9 +7,9 @@ const toast = useToast();
|
||||||
let socket;
|
let socket;
|
||||||
|
|
||||||
const noSocketIOPages = [
|
const noSocketIOPages = [
|
||||||
"/status-page",
|
/^\/status-page$/, // /status-page
|
||||||
"/status",
|
/^\/status/, // /status**
|
||||||
"/"
|
/^\/$/ // /
|
||||||
];
|
];
|
||||||
|
|
||||||
const favicon = new Favico({
|
const favicon = new Favico({
|
||||||
|
@ -38,6 +38,8 @@ export default {
|
||||||
uptimeList: { },
|
uptimeList: { },
|
||||||
tlsInfoList: {},
|
tlsInfoList: {},
|
||||||
notificationList: [],
|
notificationList: [],
|
||||||
|
statusPageListLoaded: false,
|
||||||
|
statusPageList: [],
|
||||||
connectionErrorMsg: "Cannot connect to the socket server. Reconnecting...",
|
connectionErrorMsg: "Cannot connect to the socket server. Reconnecting...",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -56,9 +58,13 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
// No need to connect to the socket.io for status page
|
// No need to connect to the socket.io for status page
|
||||||
if (! bypass && noSocketIOPages.includes(location.pathname)) {
|
if (! bypass && location.pathname) {
|
||||||
|
for (let page of noSocketIOPages) {
|
||||||
|
if (location.pathname.match(page)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.socket.initedSocketIO = true;
|
this.socket.initedSocketIO = true;
|
||||||
|
|
||||||
|
@ -108,6 +114,11 @@ export default {
|
||||||
this.notificationList = data;
|
this.notificationList = data;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("statusPageList", (data) => {
|
||||||
|
this.statusPageListLoaded = true;
|
||||||
|
this.statusPageList = data;
|
||||||
|
});
|
||||||
|
|
||||||
socket.on("heartbeat", (data) => {
|
socket.on("heartbeat", (data) => {
|
||||||
if (! (data.monitorID in this.heartbeatList)) {
|
if (! (data.monitorID in this.heartbeatList)) {
|
||||||
this.heartbeatList[data.monitorID] = [];
|
this.heartbeatList[data.monitorID] = [];
|
||||||
|
@ -244,6 +255,14 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toastSuccess(msg) {
|
||||||
|
toast.success(msg);
|
||||||
|
},
|
||||||
|
|
||||||
|
toastError(msg) {
|
||||||
|
toast.error(msg);
|
||||||
|
},
|
||||||
|
|
||||||
login(username, password, token, callback) {
|
login(username, password, token, callback) {
|
||||||
socket.emit("login", {
|
socket.emit("login", {
|
||||||
username,
|
username,
|
||||||
|
@ -437,7 +456,6 @@ export default {
|
||||||
"stats.down"(to, from) {
|
"stats.down"(to, from) {
|
||||||
if (to !== from) {
|
if (to !== from) {
|
||||||
favicon.badge(to);
|
favicon.badge(to);
|
||||||
console.log(to);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -454,9 +472,15 @@ export default {
|
||||||
|
|
||||||
// Reconnect the socket io, if status-page to dashboard
|
// Reconnect the socket io, if status-page to dashboard
|
||||||
"$route.fullPath"(newValue, oldValue) {
|
"$route.fullPath"(newValue, oldValue) {
|
||||||
if (noSocketIOPages.includes(newValue)) {
|
|
||||||
|
if (newValue) {
|
||||||
|
for (let page of noSocketIOPages) {
|
||||||
|
if (newValue.match(page)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.initSocketIO();
|
this.initSocketIO();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ export default {
|
||||||
return "light";
|
return "light";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.path === "/status-page" || this.path === "/status") {
|
if (this.path.startsWith("/status-page") || this.path.startsWith("/status")) {
|
||||||
return this.statusPageTheme;
|
return this.statusPageTheme;
|
||||||
} else {
|
} else {
|
||||||
if (this.userTheme === "auto") {
|
if (this.userTheme === "auto") {
|
||||||
|
|
79
src/pages/AddStatusPage.vue
Normal file
79
src/pages/AddStatusPage.vue
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
<template>
|
||||||
|
<transition name="slide-fade" appear>
|
||||||
|
<div>
|
||||||
|
<h1 class="mb-3">
|
||||||
|
{{ $t("Add New Status Page") }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<form @submit.prevent="submit">
|
||||||
|
<div class="shadow-box">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="name" class="form-label">{{ $t("Name") }}</label>
|
||||||
|
<input id="name" v-model="title" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="slug" class="form-label">{{ $t("Slug") }}</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span id="basic-addon3" class="input-group-text">/status/</span>
|
||||||
|
<input id="slug" v-model="slug" type="text" class="form-control" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">
|
||||||
|
<ul>
|
||||||
|
<li>{{ $t("Accept characters:") }} <mark>a-z</mark> <mark>0-9</mark> <mark>-</mark></li>
|
||||||
|
<li>{{ $t("Start or end with") }} <mark>a-z</mark> <mark>0-9</mark> only</li>
|
||||||
|
<li>{{ $t("No consecutive dashes") }} <mark>--</mark></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 mb-1">
|
||||||
|
<button id="monitor-submit-btn" class="btn btn-primary w-100" type="submit" :disabled="processing">{{ $t("Next") }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
title: "",
|
||||||
|
slug: "",
|
||||||
|
processing: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async submit() {
|
||||||
|
this.processing = true;
|
||||||
|
|
||||||
|
this.$root.getSocket().emit("addStatusPage", this.title, this.slug, (res) => {
|
||||||
|
this.processing = false;
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
location.href = "/status/" + this.slug + "?edit";
|
||||||
|
} else {
|
||||||
|
|
||||||
|
if (res.msg.includes("UNIQUE constraint")) {
|
||||||
|
this.$root.toastError(this.$t("The slug is already taken. Please choose another slug."));
|
||||||
|
} else {
|
||||||
|
this.$root.toastRes(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.shadow-box {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
118
src/pages/ManageStatusPage.vue
Normal file
118
src/pages/ManageStatusPage.vue
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
<template>
|
||||||
|
<transition name="slide-fade" appear>
|
||||||
|
<div>
|
||||||
|
<h1 class="mb-3">
|
||||||
|
{{ $t("Status Pages") }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<router-link to="/add-status-page" class="btn btn-primary mb-3"><font-awesome-icon icon="plus" /> {{ $t("New Status Page") }}</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="shadow-box">
|
||||||
|
<template v-if="$root.statusPageListLoaded">
|
||||||
|
<span v-if="$root.statusPageList.length === 0" class="d-flex align-items-center justify-content-center my-3 spinner">
|
||||||
|
No status pages
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- use <a> instead of <router-link>, because the heartbeat won't load. -->
|
||||||
|
<a v-for="statusPage in $root.statusPageList" :key="statusPage.slug" :href="'/status/' + statusPage.slug" class="item">
|
||||||
|
<img :src="icon(statusPage.icon)" alt class="logo me-2" />
|
||||||
|
<div class="info">
|
||||||
|
<div class="title">{{ statusPage.title }}</div>
|
||||||
|
<div class="slug">/status/{{ statusPage.slug }}</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
<div v-else class="d-flex align-items-center justify-content-center my-3 spinner">
|
||||||
|
<font-awesome-icon icon="spinner" size="2x" spin />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import { getResBaseURL } from "../util-frontend";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
icon(icon) {
|
||||||
|
if (icon === "/icon.svg") {
|
||||||
|
return icon;
|
||||||
|
} else {
|
||||||
|
return getResBaseURL() + icon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: all ease-in-out 0.15s;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $highlight-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: #cdf8f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
$logo-width: 70px;
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: $logo-width;
|
||||||
|
|
||||||
|
// Better when the image is loading
|
||||||
|
min-height: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slug {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
.item {
|
||||||
|
&:hover {
|
||||||
|
background-color: $dark-bg2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: $dark-bg2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -167,6 +167,8 @@ footer {
|
||||||
margin: 0.5em;
|
margin: 0.5em;
|
||||||
padding: 0.7em 1em;
|
padding: 0.7em 1em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
border-left-width: 0;
|
||||||
|
transition: all ease-in-out 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-item:hover {
|
.menu-item:hover {
|
||||||
|
|
|
@ -1,7 +1,70 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="loadedTheme" class="container mt-3">
|
<div v-if="loadedTheme" class="container mt-3">
|
||||||
|
<!-- Sidebar for edit mode -->
|
||||||
|
<div v-if="enableEditMode" class="sidebar">
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="slug" class="form-label">{{ $t("Slug") }}</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span id="basic-addon3" class="input-group-text">/status/</span>
|
||||||
|
<input id="slug" v-model="config.slug" type="text" class="form-control">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="title" class="form-label">{{ $t("Title") }}</label>
|
||||||
|
<input id="title" v-model="config.title" type="text" class="form-control">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-3">
|
||||||
|
<label for="description" class="form-label">{{ $t("Description") }}</label>
|
||||||
|
<textarea id="description" v-model="config.description" class="form-control"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-3 form-check form-switch">
|
||||||
|
<input id="switch-theme" v-model="config.theme" class="form-check-input" type="checkbox" true-value="dark" false-value="light">
|
||||||
|
<label class="form-check-label" for="switch-theme">{{ $t("Switch to Dark Theme") }}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-3 form-check form-switch">
|
||||||
|
<input id="showTags" v-model="config.showTags" class="form-check-input" type="checkbox">
|
||||||
|
<label class="form-check-label" for="showTags">{{ $t("Show Tags") }}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="false" class="my-3">
|
||||||
|
<label for="password" class="form-label">{{ $t("Password") }} <sup>Coming Soon</sup></label>
|
||||||
|
<input id="password" v-model="config.password" disabled type="password" autocomplete="new-password" class="form-control">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="false" class="my-3">
|
||||||
|
<label for="cname" class="form-label">Domain Names <sup>Coming Soon</sup></label>
|
||||||
|
<textarea id="cname" v-model="config.domanNames" rows="3" disabled class="form-control" :placeholder="domainNamesPlaceholder"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="danger-zone">
|
||||||
|
<button class="btn btn-danger me-2" @click="deleteDialog">
|
||||||
|
<font-awesome-icon icon="trash" />
|
||||||
|
{{ $t("Delete") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebar Footer -->
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<button class="btn btn-success me-2" @click="save">
|
||||||
|
<font-awesome-icon icon="save" />
|
||||||
|
{{ $t("Save") }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="btn btn-danger me-2" @click="discard">
|
||||||
|
<font-awesome-icon icon="save" />
|
||||||
|
{{ $t("Discard") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Status Page -->
|
||||||
|
<div :class="{ edit: enableEditMode}" class="main">
|
||||||
<!-- Logo & Title -->
|
<!-- Logo & Title -->
|
||||||
<h1 class="mb-4">
|
<h1 class="mb-4 title-flex">
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
<span class="logo-wrapper" @click="showImageCropUploadMethod">
|
<span class="logo-wrapper" @click="showImageCropUploadMethod">
|
||||||
<img :src="logoURL" alt class="logo me-2" :class="logoClass" />
|
<img :src="logoURL" alt class="logo me-2" :class="logoClass" />
|
||||||
|
@ -33,61 +96,17 @@
|
||||||
{{ $t("Edit Status Page") }}
|
{{ $t("Edit Status Page") }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<a href="/dashboard" class="btn btn-info">
|
<a href="/manage-status-page" class="btn btn-info">
|
||||||
<font-awesome-icon icon="tachometer-alt" />
|
<font-awesome-icon icon="tachometer-alt" />
|
||||||
{{ $t("Go to Dashboard") }}
|
{{ $t("Go to Dashboard") }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<button class="btn btn-success me-2" @click="save">
|
|
||||||
<font-awesome-icon icon="save" />
|
|
||||||
{{ $t("Save") }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="btn btn-danger me-2" @click="discard">
|
|
||||||
<font-awesome-icon icon="save" />
|
|
||||||
{{ $t("Discard") }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="btn btn-primary btn-add-group me-2" @click="createIncident">
|
<button class="btn btn-primary btn-add-group me-2" @click="createIncident">
|
||||||
<font-awesome-icon icon="bullhorn" />
|
<font-awesome-icon icon="bullhorn" />
|
||||||
{{ $t("Create Incident") }}
|
{{ $t("Create Incident") }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!--
|
|
||||||
<button v-if="isPublished" class="btn btn-light me-2" @click="">
|
|
||||||
<font-awesome-icon icon="save" />
|
|
||||||
{{ $t("Unpublish") }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button v-if="!isPublished" class="btn btn-info me-2" @click="">
|
|
||||||
<font-awesome-icon icon="save" />
|
|
||||||
{{ $t("Publish") }}
|
|
||||||
</button>-->
|
|
||||||
|
|
||||||
<!-- Set Default Language -->
|
|
||||||
<!-- Set theme -->
|
|
||||||
<button v-if="theme == 'dark'" class="btn btn-light me-2" @click="changeTheme('light')">
|
|
||||||
<font-awesome-icon icon="save" />
|
|
||||||
{{ $t("Switch to Light Theme") }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button v-if="theme == 'light'" class="btn btn-dark me-2" @click="changeTheme('dark')">
|
|
||||||
<font-awesome-icon icon="save" />
|
|
||||||
{{ $t("Switch to Dark Theme") }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="btn btn-secondary me-2" @click="changeTagsVisibilty(!tagsVisible)">
|
|
||||||
<template v-if="tagsVisible">
|
|
||||||
<font-awesome-icon icon="eye-slash" />
|
|
||||||
{{ $t("Hide Tags") }}
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<font-awesome-icon icon="eye" />
|
|
||||||
{{ $t("Show Tags") }}
|
|
||||||
</template>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -204,13 +223,18 @@
|
||||||
👀 {{ $t("statusPageNothing") }}
|
👀 {{ $t("statusPageNothing") }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PublicGroupList :edit-mode="enableEditMode" />
|
<PublicGroupList :edit-mode="enableEditMode" :show-tags="config.showTags" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="mt-5 mb-4">
|
<footer class="mt-5 mb-4">
|
||||||
{{ $t("Powered by") }} <a target="_blank" href="https://github.com/louislam/uptime-kuma">{{ $t("Uptime Kuma" ) }}</a>
|
{{ $t("Powered by") }} <a target="_blank" href="https://github.com/louislam/uptime-kuma">{{ $t("Uptime Kuma" ) }}</a>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Confirm ref="confirmDelete" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="deleteStatusPage">
|
||||||
|
{{ $t("deleteStatusPageMsg") }}
|
||||||
|
</Confirm>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -221,6 +245,8 @@ import { STATUS_PAGE_ALL_DOWN, STATUS_PAGE_ALL_UP, STATUS_PAGE_PARTIAL_DOWN, UP
|
||||||
import { useToast } from "vue-toastification";
|
import { useToast } from "vue-toastification";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import Favico from "favico.js";
|
import Favico from "favico.js";
|
||||||
|
import { getResBaseURL } from "../util-frontend";
|
||||||
|
import Confirm from "../components/Confirm.vue";
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
|
@ -235,7 +261,8 @@ const favicon = new Favico({
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
PublicGroupList,
|
PublicGroupList,
|
||||||
ImageCropUpload
|
ImageCropUpload,
|
||||||
|
Confirm,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Leave Page for vue route change
|
// Leave Page for vue route change
|
||||||
|
@ -253,6 +280,7 @@ export default {
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
slug: null,
|
||||||
enableEditMode: false,
|
enableEditMode: false,
|
||||||
enableEditIncidentMode: false,
|
enableEditIncidentMode: false,
|
||||||
hasToken: false,
|
hasToken: false,
|
||||||
|
@ -265,6 +293,8 @@ export default {
|
||||||
loadedTheme: false,
|
loadedTheme: false,
|
||||||
loadedData: false,
|
loadedData: false,
|
||||||
baseURL: "",
|
baseURL: "",
|
||||||
|
clickedEditButton: false,
|
||||||
|
domainNamesPlaceholder: "domain1.com\ndomain2.com\n..."
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -302,15 +332,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
isPublished() {
|
isPublished() {
|
||||||
return this.config.statusPagePublished;
|
return this.config.published;
|
||||||
},
|
|
||||||
|
|
||||||
theme() {
|
|
||||||
return this.config.statusPageTheme;
|
|
||||||
},
|
|
||||||
|
|
||||||
tagsVisible() {
|
|
||||||
return this.config.statusPageTags;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
logoClass() {
|
logoClass() {
|
||||||
|
@ -384,13 +406,28 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
// Set Theme
|
// Set Theme
|
||||||
"config.statusPageTheme"() {
|
"config.theme"() {
|
||||||
this.$root.statusPageTheme = this.config.statusPageTheme;
|
this.$root.statusPageTheme = this.config.theme;
|
||||||
this.loadedTheme = true;
|
this.loadedTheme = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
"config.title"(title) {
|
"config.title"(title) {
|
||||||
document.title = title;
|
document.title = title;
|
||||||
|
},
|
||||||
|
|
||||||
|
"$root.monitorList"() {
|
||||||
|
let count = Object.keys(this.$root.monitorList).length;
|
||||||
|
|
||||||
|
// Since publicGroupList is getting from public rest api, monitors' tags may not present if showTags = false
|
||||||
|
if (count > 0) {
|
||||||
|
for (let group of this.$root.publicGroupList) {
|
||||||
|
for (let monitor of group.monitorList) {
|
||||||
|
if (monitor.tags === undefined && this.$root.monitorList[monitor.id]) {
|
||||||
|
monitor.tags = this.$root.monitorList[monitor.id].tags;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
@ -409,28 +446,24 @@ export default {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Special handle for dev
|
// Special handle for dev
|
||||||
const env = process.env.NODE_ENV;
|
this.baseURL = getResBaseURL();
|
||||||
if (env === "development" || localStorage.dev === "dev") {
|
|
||||||
this.baseURL = location.protocol + "//" + location.hostname + ":3001";
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
axios.get("/api/status-page/config").then((res) => {
|
this.slug = this.$route.params.slug;
|
||||||
this.config = res.data;
|
|
||||||
|
|
||||||
if (this.config.logo) {
|
if (!this.slug) {
|
||||||
this.imgDataUrl = this.config.logo;
|
this.slug = "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
axios.get("/api/status-page/" + this.slug).then((res) => {
|
||||||
|
this.config = res.data.config;
|
||||||
|
|
||||||
|
if (this.config.icon) {
|
||||||
|
this.imgDataUrl = this.config.icon;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
axios.get("/api/status-page/incident").then((res) => {
|
|
||||||
if (res.data.ok) {
|
|
||||||
this.incident = res.data.incident;
|
this.incident = res.data.incident;
|
||||||
}
|
this.$root.publicGroupList = res.data.publicGroupList;
|
||||||
});
|
|
||||||
|
|
||||||
axios.get("/api/status-page/monitor-list").then((res) => {
|
|
||||||
this.$root.publicGroupList = res.data;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 5mins a loop
|
// 5mins a loop
|
||||||
|
@ -438,13 +471,19 @@ export default {
|
||||||
feedInterval = setInterval(() => {
|
feedInterval = setInterval(() => {
|
||||||
this.updateHeartbeatList();
|
this.updateHeartbeatList();
|
||||||
}, (300 + 10) * 1000);
|
}, (300 + 10) * 1000);
|
||||||
|
|
||||||
|
// Go to edit page if ?edit present
|
||||||
|
// null means ?edit present, but no value
|
||||||
|
if (this.$route.query.edit || this.$route.query.edit === null) {
|
||||||
|
this.edit();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
updateHeartbeatList() {
|
updateHeartbeatList() {
|
||||||
// If editMode, it will use the data from websocket.
|
// If editMode, it will use the data from websocket.
|
||||||
if (! this.editMode) {
|
if (! this.editMode) {
|
||||||
axios.get("/api/status-page/heartbeat").then((res) => {
|
axios.get("/api/status-page/heartbeat/" + this.slug).then((res) => {
|
||||||
const { heartbeatList, uptimeList } = res.data;
|
const { heartbeatList, uptimeList } = res.data;
|
||||||
|
|
||||||
this.$root.heartbeatList = heartbeatList;
|
this.$root.heartbeatList = heartbeatList;
|
||||||
|
@ -470,16 +509,48 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
edit() {
|
edit() {
|
||||||
|
if (this.hasToken) {
|
||||||
this.$root.initSocketIO(true);
|
this.$root.initSocketIO(true);
|
||||||
this.enableEditMode = true;
|
this.enableEditMode = true;
|
||||||
|
this.clickedEditButton = true;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
this.$root.getSocket().emit("saveStatusPage", this.config, this.imgDataUrl, this.$root.publicGroupList, (res) => {
|
let startTime = new Date();
|
||||||
|
|
||||||
|
this.$root.getSocket().emit("saveStatusPage", this.slug, this.config, this.imgDataUrl, this.$root.publicGroupList, (res) => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
this.enableEditMode = false;
|
this.enableEditMode = false;
|
||||||
this.$root.publicGroupList = res.publicGroupList;
|
this.$root.publicGroupList = res.publicGroupList;
|
||||||
location.reload();
|
|
||||||
|
// Add some delay, so that the side menu animation would be better
|
||||||
|
let endTime = new Date();
|
||||||
|
let time = 100 - (endTime - startTime) / 1000;
|
||||||
|
|
||||||
|
if (time < 0) {
|
||||||
|
time = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
location.href = "/status/" + this.config.slug;
|
||||||
|
}, time);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteDialog() {
|
||||||
|
this.$refs.confirmDelete.show();
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteStatusPage() {
|
||||||
|
this.$root.getSocket().emit("deleteStatusPage", this.slug, (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
this.enableEditMode = false;
|
||||||
|
location.href = "/manage-status-page";
|
||||||
} else {
|
} else {
|
||||||
toast.error(res.msg);
|
toast.error(res.msg);
|
||||||
}
|
}
|
||||||
|
@ -504,30 +575,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
discard() {
|
discard() {
|
||||||
location.reload();
|
location.href = "/status/" + this.slug;
|
||||||
},
|
|
||||||
|
|
||||||
changeTheme(name) {
|
|
||||||
this.config.statusPageTheme = name;
|
|
||||||
},
|
|
||||||
changeTagsVisibilty(newState) {
|
|
||||||
this.config.statusPageTags = newState;
|
|
||||||
|
|
||||||
// On load, the status page will not include tags if it's not enabled for security reasons
|
|
||||||
// Which means if we enable tags, it won't show in the UI until saved
|
|
||||||
// So we have this to enhance UX and load in the tags from the authenticated source instantly
|
|
||||||
this.$root.publicGroupList = this.$root.publicGroupList.map((group) => {
|
|
||||||
return {
|
|
||||||
...group,
|
|
||||||
monitorList: group.monitorList.map((monitor) => {
|
|
||||||
// We only include the tags if visible so we can reuse the logic to hide the tags on disable
|
|
||||||
return {
|
|
||||||
...monitor,
|
|
||||||
tags: newState ? this.$root.monitorList[monitor.id].tags : []
|
|
||||||
};
|
|
||||||
})
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -563,7 +611,7 @@ export default {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$root.getSocket().emit("postIncident", this.incident, (res) => {
|
this.$root.getSocket().emit("postIncident", this.slug, this.incident, (res) => {
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
this.enableEditIncidentMode = false;
|
this.enableEditIncidentMode = false;
|
||||||
|
@ -594,7 +642,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
unpinIncident() {
|
unpinIncident() {
|
||||||
this.$root.getSocket().emit("unpinIncident", () => {
|
this.$root.getSocket().emit("unpinIncident", this.slug, () => {
|
||||||
this.incident = null;
|
this.incident = null;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -637,6 +685,40 @@ h1 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
transition: all ease-in-out 0.1s;
|
||||||
|
|
||||||
|
&.edit {
|
||||||
|
margin-left: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 300px;
|
||||||
|
height: 100vh;
|
||||||
|
padding: 15px 15px 68px 15px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-right: 1px solid #ededed;
|
||||||
|
|
||||||
|
.danger-zone {
|
||||||
|
border-top: 1px solid #ededed;
|
||||||
|
padding-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-footer {
|
||||||
|
width: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
padding: 15px;
|
||||||
|
position: absolute;
|
||||||
|
border-top: 1px solid #ededed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -646,6 +728,12 @@ footer {
|
||||||
min-width: 50px;
|
min-width: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title-flex {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.logo-wrapper {
|
.logo-wrapper {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -704,4 +792,19 @@ footer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
.sidebar {
|
||||||
|
background-color: $dark-header-bg;
|
||||||
|
border-right-color: $dark-border-color;
|
||||||
|
|
||||||
|
.danger-zone {
|
||||||
|
border-top-color: $dark-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-footer {
|
||||||
|
border-top-color: $dark-border-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -18,6 +18,8 @@ import MonitorHistory from "./components/settings/MonitorHistory.vue";
|
||||||
import Security from "./components/settings/Security.vue";
|
import Security from "./components/settings/Security.vue";
|
||||||
import Backup from "./components/settings/Backup.vue";
|
import Backup from "./components/settings/Backup.vue";
|
||||||
import About from "./components/settings/About.vue";
|
import About from "./components/settings/About.vue";
|
||||||
|
import ManageStatusPage from "./pages/ManageStatusPage.vue";
|
||||||
|
import AddStatusPage from "./pages/AddStatusPage.vue";
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
|
@ -98,6 +100,14 @@ const routes = [
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/manage-status-page",
|
||||||
|
component: ManageStatusPage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/add-status-page",
|
||||||
|
component: AddStatusPage,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -114,6 +124,10 @@ const routes = [
|
||||||
path: "/status",
|
path: "/status",
|
||||||
component: StatusPage,
|
component: StatusPage,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/status/:slug",
|
||||||
|
component: StatusPage,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const router = createRouter({
|
export const router = createRouter({
|
||||||
|
|
|
@ -51,7 +51,19 @@ export function timezoneList() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setPageLocale() {
|
export function setPageLocale() {
|
||||||
const html = document.documentElement
|
const html = document.documentElement;
|
||||||
html.setAttribute('lang', currentLocale() )
|
html.setAttribute("lang", currentLocale() );
|
||||||
html.setAttribute('dir', localeDirection() )
|
html.setAttribute("dir", localeDirection() );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mainly used for dev, because the backend and the frontend are in different ports.
|
||||||
|
*/
|
||||||
|
export function getResBaseURL() {
|
||||||
|
const env = process.env.NODE_ENV;
|
||||||
|
if (env === "development" || localStorage.dev === "dev") {
|
||||||
|
return location.protocol + "//" + location.hostname + ":3001";
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue