Merge pull request #863 from louislam/restructure-status-page
Restructure status page core implementationpull/1385/head
commit
82049a2387
@ -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;
|
File diff suppressed because it is too large
Load Diff
@ -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;
|
@ -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>
|
@ -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>
|
Loading…
Reference in new issue