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