Merge pull request #1415 from louislam/status-page-domain
[Status Page] Map domain names to status pages
This commit is contained in:
commit
df5ba02f3f
|
@ -129,6 +129,11 @@ class Database {
|
||||||
await R.exec("PRAGMA cache_size = -12000");
|
await R.exec("PRAGMA cache_size = -12000");
|
||||||
await R.exec("PRAGMA auto_vacuum = FULL");
|
await R.exec("PRAGMA auto_vacuum = FULL");
|
||||||
|
|
||||||
|
// This ensures that an operating system crash or power failure will not corrupt the database.
|
||||||
|
// FULL synchronous is very safe, but it is also slower.
|
||||||
|
// Read more: https://sqlite.org/pragma.html#pragma_synchronous
|
||||||
|
await R.exec("PRAGMA synchronous = FULL");
|
||||||
|
|
||||||
if (!noLog) {
|
if (!noLog) {
|
||||||
console.log("SQLite config:");
|
console.log("SQLite config:");
|
||||||
console.log(await R.getAll("PRAGMA journal_mode"));
|
console.log(await R.getAll("PRAGMA journal_mode"));
|
||||||
|
|
|
@ -3,6 +3,20 @@ const { R } = require("redbean-node");
|
||||||
|
|
||||||
class StatusPage extends BeanModel {
|
class StatusPage extends BeanModel {
|
||||||
|
|
||||||
|
static domainMappingList = { };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return object like this: { "test-uptime.kuma.pet": "default" }
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
static async loadDomainMappingList() {
|
||||||
|
StatusPage.domainMappingList = await R.getAssoc(`
|
||||||
|
SELECT domain, slug
|
||||||
|
FROM status_page, status_page_cname
|
||||||
|
WHERE status_page.id = status_page_cname.status_page_id
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
static async sendStatusPageList(io, socket) {
|
static async sendStatusPageList(io, socket) {
|
||||||
let result = {};
|
let result = {};
|
||||||
|
|
||||||
|
@ -16,6 +30,57 @@ class StatusPage extends BeanModel {
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateDomainNameList(domainNameList) {
|
||||||
|
|
||||||
|
if (!Array.isArray(domainNameList)) {
|
||||||
|
throw new Error("Invalid array");
|
||||||
|
}
|
||||||
|
|
||||||
|
let trx = await R.begin();
|
||||||
|
|
||||||
|
await trx.exec("DELETE FROM status_page_cname WHERE status_page_id = ?", [
|
||||||
|
this.id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (let domain of domainNameList) {
|
||||||
|
if (typeof domain !== "string") {
|
||||||
|
throw new Error("Invalid domain");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domain.trim() === "") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the domain name is used in another status page, delete it
|
||||||
|
await trx.exec("DELETE FROM status_page_cname WHERE domain = ?", [
|
||||||
|
domain,
|
||||||
|
]);
|
||||||
|
|
||||||
|
let mapping = trx.dispense("status_page_cname");
|
||||||
|
mapping.status_page_id = this.id;
|
||||||
|
mapping.domain = domain;
|
||||||
|
await trx.store(mapping);
|
||||||
|
}
|
||||||
|
await trx.commit();
|
||||||
|
} catch (error) {
|
||||||
|
await trx.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getDomainNameList() {
|
||||||
|
let domainList = [];
|
||||||
|
for (let domain in StatusPage.domainMappingList) {
|
||||||
|
let s = StatusPage.domainMappingList[domain];
|
||||||
|
|
||||||
|
if (this.slug === s) {
|
||||||
|
domainList.push(domain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return domainList;
|
||||||
|
}
|
||||||
|
|
||||||
async toJSON() {
|
async toJSON() {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
@ -26,6 +91,7 @@ class StatusPage extends BeanModel {
|
||||||
theme: this.theme,
|
theme: this.theme,
|
||||||
published: !!this.published,
|
published: !!this.published,
|
||||||
showTags: !!this.show_tags,
|
showTags: !!this.show_tags,
|
||||||
|
domainNameList: this.getDomainNameList(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,9 +12,19 @@ let router = express.Router();
|
||||||
let cache = apicache.middleware;
|
let cache = apicache.middleware;
|
||||||
let io = server.io;
|
let io = server.io;
|
||||||
|
|
||||||
router.get("/api/entry-page", async (_, response) => {
|
router.get("/api/entry-page", async (request, response) => {
|
||||||
allowDevAllOrigin(response);
|
allowDevAllOrigin(response);
|
||||||
response.json(server.entryPage);
|
|
||||||
|
let result = { };
|
||||||
|
|
||||||
|
if (request.hostname in StatusPage.domainMappingList) {
|
||||||
|
result.type = "statusPageMatchedDomain";
|
||||||
|
result.statusPageSlug = StatusPage.domainMappingList[request.hostname];
|
||||||
|
} else {
|
||||||
|
result.type = "entryPage";
|
||||||
|
result.entryPage = server.entryPage;
|
||||||
|
}
|
||||||
|
response.json(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/api/push/:pushToken", async (request, response) => {
|
router.get("/api/push/:pushToken", async (request, response) => {
|
||||||
|
|
|
@ -211,6 +211,7 @@ try {
|
||||||
await initDatabase(testMode);
|
await initDatabase(testMode);
|
||||||
|
|
||||||
exports.entryPage = await setting("entryPage");
|
exports.entryPage = await setting("entryPage");
|
||||||
|
await StatusPage.loadDomainMappingList();
|
||||||
|
|
||||||
console.log("Adding route");
|
console.log("Adding route");
|
||||||
|
|
||||||
|
@ -219,8 +220,13 @@ try {
|
||||||
// ***************************
|
// ***************************
|
||||||
|
|
||||||
// Entry Page
|
// Entry Page
|
||||||
app.get("/", async (_request, response) => {
|
app.get("/", async (request, response) => {
|
||||||
if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) {
|
debug(`Request Domain: ${request.hostname}`);
|
||||||
|
|
||||||
|
if (request.hostname in StatusPage.domainMappingList) {
|
||||||
|
debug("This is a status page domain");
|
||||||
|
response.send(indexHTML);
|
||||||
|
} else if (exports.entryPage && exports.entryPage.startsWith("statusPage-")) {
|
||||||
response.redirect("/status/" + exports.entryPage.replace("statusPage-", ""));
|
response.redirect("/status/" + exports.entryPage.replace("statusPage-", ""));
|
||||||
} else {
|
} else {
|
||||||
response.redirect("/dashboard");
|
response.redirect("/dashboard");
|
||||||
|
|
|
@ -85,15 +85,35 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("getStatusPage", async (slug, callback) => {
|
||||||
|
try {
|
||||||
|
checkLogin(socket);
|
||||||
|
|
||||||
|
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
||||||
|
slug
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!statusPage) {
|
||||||
|
throw new Error("No slug?");
|
||||||
|
}
|
||||||
|
|
||||||
|
callback({
|
||||||
|
ok: true,
|
||||||
|
config: await statusPage.toJSON(),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
callback({
|
||||||
|
ok: false,
|
||||||
|
msg: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Save Status Page
|
// Save Status Page
|
||||||
// imgDataUrl Only Accept PNG!
|
// imgDataUrl Only Accept PNG!
|
||||||
socket.on("saveStatusPage", async (slug, config, imgDataUrl, publicGroupList, callback) => {
|
socket.on("saveStatusPage", async (slug, config, imgDataUrl, publicGroupList, callback) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
checkSlug(config.slug);
|
|
||||||
|
|
||||||
checkLogin(socket);
|
checkLogin(socket);
|
||||||
apicache.clear();
|
|
||||||
|
|
||||||
// Save Config
|
// Save Config
|
||||||
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
let statusPage = await R.findOne("status_page", " slug = ? ", [
|
||||||
|
@ -104,6 +124,8 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||||
throw new Error("No slug?");
|
throw new Error("No slug?");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkSlug(config.slug);
|
||||||
|
|
||||||
const header = "data:image/png;base64,";
|
const header = "data:image/png;base64,";
|
||||||
|
|
||||||
// Check logo format
|
// Check logo format
|
||||||
|
@ -137,6 +159,9 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||||
|
|
||||||
await R.store(statusPage);
|
await R.store(statusPage);
|
||||||
|
|
||||||
|
await statusPage.updateDomainNameList(config.domainNameList);
|
||||||
|
await StatusPage.loadDomainMappingList();
|
||||||
|
|
||||||
// Save Public Group List
|
// Save Public Group List
|
||||||
const groupIDList = [];
|
const groupIDList = [];
|
||||||
let groupOrder = 1;
|
let groupOrder = 1;
|
||||||
|
@ -193,6 +218,8 @@ module.exports.statusPageSocketHandler = (socket) => {
|
||||||
await setSetting("entryPage", server.entryPage, "general");
|
await setSetting("entryPage", server.entryPage, "general");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apicache.clear();
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
publicGroupList,
|
publicGroupList,
|
||||||
|
|
|
@ -22,6 +22,18 @@ textarea.form-control {
|
||||||
width: 10px;
|
width: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-group {
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
.list-group-item {
|
||||||
|
background-color: $dark-bg;
|
||||||
|
color: $dark-font-color;
|
||||||
|
border-color: $dark-border-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #ccc;
|
background: #ccc;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
|
@ -412,6 +424,10 @@ textarea.form-control {
|
||||||
background-color: rgba(239, 239, 239, 0.7);
|
background-color: rgba(239, 239, 239, 0.7);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
|
&.no-bg {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: 0 solid #eee;
|
outline: 0 solid #eee;
|
||||||
background-color: rgba(245, 245, 245, 0.9);
|
background-color: rgba(245, 245, 245, 0.9);
|
||||||
|
|
|
@ -37,6 +37,8 @@ import {
|
||||||
faPen,
|
faPen,
|
||||||
faExternalLinkSquareAlt,
|
faExternalLinkSquareAlt,
|
||||||
faSpinner,
|
faSpinner,
|
||||||
|
faUndo,
|
||||||
|
faPlusCircle,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
|
@ -73,6 +75,8 @@ library.add(
|
||||||
faPen,
|
faPen,
|
||||||
faExternalLinkSquareAlt,
|
faExternalLinkSquareAlt,
|
||||||
faSpinner,
|
faSpinner,
|
||||||
|
faUndo,
|
||||||
|
faPlusCircle,
|
||||||
);
|
);
|
||||||
|
|
||||||
export { FontAwesomeIcon };
|
export { FontAwesomeIcon };
|
||||||
|
|
|
@ -339,7 +339,7 @@ export default {
|
||||||
"Switch to Dark Theme": "切換至深色佈景主題",
|
"Switch to Dark Theme": "切換至深色佈景主題",
|
||||||
"Show Tags": "顯示標籤",
|
"Show Tags": "顯示標籤",
|
||||||
"Hide Tags": "隱藏標籤",
|
"Hide Tags": "隱藏標籤",
|
||||||
Description: "說明",
|
Description: "描述",
|
||||||
"No monitors available.": "沒有可用的監測器。",
|
"No monitors available.": "沒有可用的監測器。",
|
||||||
"Add one": "新增一個",
|
"Add one": "新增一個",
|
||||||
"No Monitors": "無監測器",
|
"No Monitors": "無監測器",
|
||||||
|
@ -347,7 +347,6 @@ export default {
|
||||||
Services: "服務",
|
Services: "服務",
|
||||||
Discard: "捨棄",
|
Discard: "捨棄",
|
||||||
Cancel: "取消",
|
Cancel: "取消",
|
||||||
"Powered by": "技術支援",
|
|
||||||
shrinkDatabaseDescription: "觸發 SQLite 的資料庫清理 (VACUUM)。如果您的資料庫是在 1.10.0 版本後建立,AUTO_VACUUM 已自動啟用,則無需此操作。",
|
shrinkDatabaseDescription: "觸發 SQLite 的資料庫清理 (VACUUM)。如果您的資料庫是在 1.10.0 版本後建立,AUTO_VACUUM 已自動啟用,則無需此操作。",
|
||||||
serwersms: "SerwerSMS.pl",
|
serwersms: "SerwerSMS.pl",
|
||||||
serwersmsAPIUser: "API 使用者名稱 (包括 webapi_ 前綴)",
|
serwersmsAPIUser: "API 使用者名稱 (包括 webapi_ 前綴)",
|
||||||
|
|
|
@ -1,19 +1,44 @@
|
||||||
<template>
|
<template>
|
||||||
<div></div>
|
<div>
|
||||||
|
<StatusPage v-if="statusPageSlug" :override-slug="statusPageSlug" />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import StatusPage from "./StatusPage.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
StatusPage,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
statusPageSlug: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
let entryPage = (await axios.get("/api/entry-page")).data;
|
|
||||||
|
|
||||||
if (entryPage === "statusPage") {
|
// There are only 2 cases that could come in here.
|
||||||
this.$router.push("/status");
|
// 1. Matched status Page domain name
|
||||||
|
// 2. Vue Frontend Dev
|
||||||
|
let res = (await axios.get("/api/entry-page")).data;
|
||||||
|
|
||||||
|
if (res.type === "statusPageMatchedDomain") {
|
||||||
|
this.statusPageSlug = res.statusPageSlug;
|
||||||
|
|
||||||
|
} else if (res.type === "entryPage") { // Dev only. For production, the logic is in the server side
|
||||||
|
const entryPage = res.entryPage;
|
||||||
|
|
||||||
|
if (entryPage === "statusPage") {
|
||||||
|
this.$router.push("/status");
|
||||||
|
} else {
|
||||||
|
this.$router.push("/dashboard");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.$router.push("/dashboard");
|
this.$router.push("/dashboard");
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,49 +2,61 @@
|
||||||
<div v-if="loadedTheme" class="container mt-3">
|
<div v-if="loadedTheme" class="container mt-3">
|
||||||
<!-- Sidebar for edit mode -->
|
<!-- Sidebar for edit mode -->
|
||||||
<div v-if="enableEditMode" class="sidebar">
|
<div v-if="enableEditMode" class="sidebar">
|
||||||
<div class="my-3">
|
<div class="sidebar-body">
|
||||||
<label for="slug" class="form-label">{{ $t("Slug") }}</label>
|
<div class="my-3">
|
||||||
<div class="input-group">
|
<label for="slug" class="form-label">{{ $t("Slug") }}</label>
|
||||||
<span id="basic-addon3" class="input-group-text">/status/</span>
|
<div class="input-group">
|
||||||
<input id="slug" v-model="config.slug" type="text" class="form-control">
|
<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>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<label for="title" class="form-label">{{ $t("Title") }}</label>
|
<label for="title" class="form-label">{{ $t("Title") }}</label>
|
||||||
<input id="title" v-model="config.title" type="text" class="form-control">
|
<input id="title" v-model="config.title" type="text" class="form-control">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-3">
|
<div class="my-3">
|
||||||
<label for="description" class="form-label">{{ $t("Description") }}</label>
|
<label for="description" class="form-label">{{ $t("Description") }}</label>
|
||||||
<textarea id="description" v-model="config.description" class="form-control"></textarea>
|
<textarea id="description" v-model="config.description" class="form-control"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-3 form-check form-switch">
|
<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">
|
<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>
|
<label class="form-check-label" for="switch-theme">{{ $t("Switch to Dark Theme") }}</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-3 form-check form-switch">
|
<div class="my-3 form-check form-switch">
|
||||||
<input id="showTags" v-model="config.showTags" class="form-check-input" type="checkbox">
|
<input id="showTags" v-model="config.showTags" class="form-check-input" type="checkbox">
|
||||||
<label class="form-check-label" for="showTags">{{ $t("Show Tags") }}</label>
|
<label class="form-check-label" for="showTags">{{ $t("Show Tags") }}</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="false" class="my-3">
|
<div v-if="false" class="my-3">
|
||||||
<label for="password" class="form-label">{{ $t("Password") }} <sup>Coming Soon</sup></label>
|
<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">
|
<input id="password" v-model="config.password" disabled type="password" autocomplete="new-password" class="form-control">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="false" class="my-3">
|
<!-- Domain Name List -->
|
||||||
<label for="cname" class="form-label">Domain Names <sup>Coming Soon</sup></label>
|
<div class="my-3">
|
||||||
<textarea id="cname" v-model="config.domanNames" rows="3" disabled class="form-control" :placeholder="domainNamesPlaceholder"></textarea>
|
<label class="form-label">
|
||||||
</div>
|
Domain Names
|
||||||
|
<font-awesome-icon icon="plus-circle" class="btn-add-domain action text-primary" @click="addDomainField" />
|
||||||
|
</label>
|
||||||
|
|
||||||
<div class="danger-zone">
|
<ul class="list-group domain-name-list">
|
||||||
<button class="btn btn-danger me-2" @click="deleteDialog">
|
<li v-for="(domain, index) in config.domainNameList" :key="index" class="list-group-item">
|
||||||
<font-awesome-icon icon="trash" />
|
<input v-model="config.domainNameList[index]" type="text" class="no-bg domain-input" placeholder="example.com" />
|
||||||
{{ $t("Delete") }}
|
<font-awesome-icon icon="times" class="action remove ms-2 me-3 text-danger" @click="removeDomain(index)" />
|
||||||
</button>
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="danger-zone">
|
||||||
|
<button class="btn btn-danger me-2" @click="deleteDialog">
|
||||||
|
<font-awesome-icon icon="trash" />
|
||||||
|
{{ $t("Delete") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sidebar Footer -->
|
<!-- Sidebar Footer -->
|
||||||
|
@ -55,7 +67,7 @@
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="btn btn-danger me-2" @click="discard">
|
<button class="btn btn-danger me-2" @click="discard">
|
||||||
<font-awesome-icon icon="save" />
|
<font-awesome-icon icon="undo" />
|
||||||
{{ $t("Discard") }}
|
{{ $t("Discard") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -120,7 +132,7 @@
|
||||||
|
|
||||||
<!-- Incident Date -->
|
<!-- Incident Date -->
|
||||||
<div class="date mt-3">
|
<div class="date mt-3">
|
||||||
{{ $t("Created") }}: {{ $root.datetime(incident.createdDate) }} ({{ dateFromNow(incident.createdDate) }})<br />
|
{{ $t("Date Created") }}: {{ $root.datetime(incident.createdDate) }} ({{ dateFromNow(incident.createdDate) }})<br />
|
||||||
<span v-if="incident.lastUpdatedDate">
|
<span v-if="incident.lastUpdatedDate">
|
||||||
{{ $t("Last Updated") }}: {{ $root.datetime(incident.lastUpdatedDate) }} ({{ dateFromNow(incident.lastUpdatedDate) }})
|
{{ $t("Last Updated") }}: {{ $root.datetime(incident.lastUpdatedDate) }} ({{ dateFromNow(incident.lastUpdatedDate) }})
|
||||||
</span>
|
</span>
|
||||||
|
@ -259,6 +271,7 @@ const favicon = new Favico({
|
||||||
});
|
});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
PublicGroupList,
|
PublicGroupList,
|
||||||
ImageCropUpload,
|
ImageCropUpload,
|
||||||
|
@ -278,6 +291,14 @@ export default {
|
||||||
next();
|
next();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
overrideSlug: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
slug: null,
|
slug: null,
|
||||||
|
@ -294,7 +315,6 @@ export default {
|
||||||
loadedData: false,
|
loadedData: false,
|
||||||
baseURL: "",
|
baseURL: "",
|
||||||
clickedEditButton: false,
|
clickedEditButton: false,
|
||||||
domainNamesPlaceholder: "domain1.com\ndomain2.com\n..."
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -389,6 +409,22 @@ export default {
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If connected to the socket and logged in, request private data of this statusPage
|
||||||
|
* @param connected
|
||||||
|
*/
|
||||||
|
"$root.loggedIn"(loggedIn) {
|
||||||
|
if (loggedIn) {
|
||||||
|
this.$root.getSocket().emit("getStatusPage", this.slug, (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
this.config = res.config;
|
||||||
|
} else {
|
||||||
|
toast.error(res.msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Selected a monitor and add to the list.
|
* Selected a monitor and add to the list.
|
||||||
*/
|
*/
|
||||||
|
@ -449,7 +485,7 @@ export default {
|
||||||
this.baseURL = getResBaseURL();
|
this.baseURL = getResBaseURL();
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
this.slug = this.$route.params.slug;
|
this.slug = this.overrideSlug || this.$route.params.slug;
|
||||||
|
|
||||||
if (!this.slug) {
|
if (!this.slug) {
|
||||||
this.slug = "default";
|
this.slug = "default";
|
||||||
|
@ -458,6 +494,10 @@ export default {
|
||||||
axios.get("/api/status-page/" + this.slug).then((res) => {
|
axios.get("/api/status-page/" + this.slug).then((res) => {
|
||||||
this.config = res.data.config;
|
this.config = res.data.config;
|
||||||
|
|
||||||
|
if (!this.config.domainNameList) {
|
||||||
|
this.config.domainNameList = [];
|
||||||
|
}
|
||||||
|
|
||||||
if (this.config.icon) {
|
if (this.config.icon) {
|
||||||
this.imgDataUrl = this.config.icon;
|
this.imgDataUrl = this.config.icon;
|
||||||
}
|
}
|
||||||
|
@ -575,6 +615,10 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addDomainField() {
|
||||||
|
this.config.domainNameList.push("");
|
||||||
|
},
|
||||||
|
|
||||||
discard() {
|
discard() {
|
||||||
location.href = "/status/" + this.slug;
|
location.href = "/status/" + this.slug;
|
||||||
},
|
},
|
||||||
|
@ -657,6 +701,10 @@ export default {
|
||||||
return dayjs.utc(date).fromNow();
|
return dayjs.utc(date).fromNow();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
removeDomain(index) {
|
||||||
|
this.config.domainNameList.splice(index, 1);
|
||||||
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -705,9 +753,7 @@ h1 {
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
padding: 15px 15px 68px 15px;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: auto;
|
|
||||||
border-right: 1px solid #ededed;
|
border-right: 1px solid #ededed;
|
||||||
|
|
||||||
.danger-zone {
|
.danger-zone {
|
||||||
|
@ -715,13 +761,25 @@ h1 {
|
||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-body {
|
||||||
|
padding: 0 10px 10px 10px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
height: calc(100% - 70px);
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-footer {
|
.sidebar-footer {
|
||||||
width: 100%;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
padding: 15px;
|
|
||||||
position: absolute;
|
|
||||||
border-top: 1px solid #ededed;
|
border-top: 1px solid #ededed;
|
||||||
|
border-right: 1px solid #ededed;
|
||||||
|
padding: 10px;
|
||||||
|
width: 300px;
|
||||||
|
height: 70px;
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -808,7 +866,29 @@ footer {
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-footer {
|
.sidebar-footer {
|
||||||
|
border-right-color: $dark-border-color;
|
||||||
border-top-color: $dark-border-color;
|
border-top-color: $dark-border-color;
|
||||||
|
background-color: $dark-header-bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.domain-name-list {
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 0 10px 10px;
|
||||||
|
|
||||||
|
.domain-input {
|
||||||
|
flex-grow: 1;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
color: $dark-font-color;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #1d2634;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue