many update

This commit is contained in:
LouisLam 2021-06-27 16:10:55 +08:00
parent 6974f0f3ad
commit d02401b1c2
12 changed files with 344 additions and 88 deletions

View file

@ -3,6 +3,7 @@
"version": "0.0.0", "version": "0.0.0",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"dev-server": "node server/server.js",
"build": "vite build", "build": "vite build",
"serve": "vite preview" "serve": "vite preview"
}, },

View file

@ -1,6 +1,18 @@
const dayjs = require("dayjs"); const dayjs = require("dayjs");
const utc = require('dayjs/plugin/utc')
var timezone = require('dayjs/plugin/timezone')
dayjs.extend(utc)
dayjs.extend(timezone)
const axios = require("axios");
const {R} = require("redbean-node");
const {BeanModel} = require("redbean-node/dist/bean-model"); const {BeanModel} = require("redbean-node/dist/bean-model");
/**
* status:
* 0 = DOWN
* 1 = UP
*/
class Monitor extends BeanModel { class Monitor extends BeanModel {
toJSON() { toJSON() {
@ -16,9 +28,36 @@ class Monitor extends BeanModel {
} }
start(io) { start(io) {
const beat = () => { const beat = async () => {
console.log(`Monitor ${this.id}: Heartbeat`) console.log(`Monitor ${this.id}: Heartbeat`)
io.to(this.user_id).emit("heartbeat", dayjs().unix());
let bean = R.dispense("heartbeat")
bean.monitor_id = this.id;
bean.time = R.isoDateTime(dayjs.utc());
bean.status = 0;
try {
if (this.type === "http") {
let startTime = dayjs().valueOf();
let res = await axios.get(this.url)
bean.msg = `${res.status} - ${res.statusText}`
bean.ping = dayjs().valueOf() - startTime;
bean.status = 1;
}
} catch (error) {
bean.msg = error.message;
}
io.to(this.user_id).emit("heartbeat", {
monitorID: this.id,
status: bean.status,
time: bean.time,
msg: bean.msg,
ping: bean.ping,
});
await R.store(bean)
} }
beat(); beat();

View file

@ -4,7 +4,6 @@ const http = require('http');
const server = http.createServer(app); const server = http.createServer(app);
const { Server } = require("socket.io"); const { Server } = require("socket.io");
const io = new Server(server); const io = new Server(server);
const axios = require('axios');
const dayjs = require("dayjs"); const dayjs = require("dayjs");
const {R} = require("redbean-node"); const {R} = require("redbean-node");
const passwordHash = require('password-hash'); const passwordHash = require('password-hash');
@ -12,7 +11,6 @@ const jwt = require('jsonwebtoken');
const Monitor = require("./model/monitor"); const Monitor = require("./model/monitor");
const {sleep} = require("./util"); const {sleep} = require("./util");
let stop = false; let stop = false;
let interval = 6000; let interval = 6000;
let totalClient = 0; let totalClient = 0;
@ -21,12 +19,11 @@ let loadFromDatabase = true;
let monitorList = {}; let monitorList = {};
(async () => { (async () => {
R.setup('sqlite', { R.setup('sqlite', {
filename: '../data/kuma.db' filename: './data/kuma.db'
}); });
R.freeze(true) R.freeze(true)
await R.autoloadModels("./model"); await R.autoloadModels("./server/model");
await initDatabase(); await initDatabase();
@ -118,15 +115,54 @@ let monitorList = {};
bean.user_id = socket.userID bean.user_id = socket.userID
await R.store(bean) await R.store(bean)
await startMonitor(socket.userID, bean.id);
await sendMonitorList(socket);
callback({ callback({
ok: true, ok: true,
msg: "Added Successfully.", msg: "Added Successfully.",
monitorID: bean.id monitorID: bean.id
}); });
} catch (e) {
callback({
ok: false,
msg: e.message
});
}
});
socket.on("editMonitor", async (monitor, callback) => {
try {
checkLogin(socket)
let bean = await R.findOne("monitor", " id = ? ", [ monitor.id ])
if (bean.user_id !== socket.userID) {
throw new Error("Permission denied.")
}
bean.name = monitor.name
bean.type = monitor.type
bean.url = monitor.url
bean.interval = monitor.interval
await R.store(bean)
if (bean.active) {
await restartMonitor(socket.userID, bean.id)
}
await sendMonitorList(socket); await sendMonitorList(socket);
callback({
ok: true,
msg: "Saved.",
monitorID: bean.id
});
} catch (e) { } catch (e) {
console.log(e)
callback({ callback({
ok: false, ok: false,
msg: e.message msg: e.message
@ -294,14 +330,14 @@ async function afterLogin(socket, user) {
} }
async function getMonitorJSONList(userID) { async function getMonitorJSONList(userID) {
let result = []; let result = {};
let monitorList = await R.find("monitor", " user_id = ? ORDER BY weight DESC ", [ let monitorList = await R.find("monitor", " user_id = ? ORDER BY active DESC, name ASC ", [
userID userID
]) ])
for (let monitor of monitorList) { for (let monitor of monitorList) {
result.push(monitor.toJSON()) result[monitor.id] = monitor.toJSON();
} }
return result; return result;
@ -346,10 +382,18 @@ async function startMonitor(userID, monitorID) {
monitorID monitorID
]) ])
if (monitor.id in monitorList) {
monitorList[monitor.id].stop();
}
monitorList[monitor.id] = monitor; monitorList[monitor.id] = monitor;
monitor.start(io) monitor.start(io)
} }
async function restartMonitor(userID, monitorID) {
return await startMonitor(userID, monitorID)
}
async function pauseMonitor(userID, monitorID) { async function pauseMonitor(userID, monitorID) {
await checkOwner(userID, monitorID) await checkOwner(userID, monitorID)

View file

@ -1,4 +1,5 @@
$primary: #5CDD8B; $primary: #5CDD8B;
$danger: #DC3545;
$link-color: #111; $link-color: #111;
$border-radius: 50rem; $border-radius: 50rem;

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="wrap" :style="wrapStyle" ref="wrap"> <div class="wrap" :style="wrapStyle" ref="wrap">
<div class="hp-bar-big" :style="barStyle"> <div class="hp-bar-big" :style="barStyle">
<div class="beat" :style="beatStyle" v-for="(beat, index) in shortBeatList" :key="index"> <div class="beat" :class="{ 'empty' : (beat === 0), 'down' : (beat.status === 0) }" :style="beatStyle" v-for="(beat, index) in shortBeatList" :key="index">
</div> </div>
</div> </div>
</div> </div>
@ -11,12 +11,15 @@
export default { export default {
props: {
size: {
type: String,
default: "big"
},
monitorId: Number
},
data() { data() {
return { return {
i: 1,
beatList: [
],
beatWidth: 10, beatWidth: 10,
beatHeight: 30, beatHeight: 30,
hoverScale: 1.5, hoverScale: 1.5,
@ -25,26 +28,38 @@ export default {
maxBeat: -1, maxBeat: -1,
} }
}, },
destroyed() { unmounted() {
window.removeEventListener("resize", this.resize); window.removeEventListener("resize", this.resize);
}, },
mounted() { mounted() {
if (this.size === "small") {
this.beatWidth = 5.6;
this.beatMargin = 2.4;
this.beatHeight = 16
}
window.addEventListener("resize", this.resize); window.addEventListener("resize", this.resize);
this.resize(); this.resize();
setInterval(() => {
this.beatList.push(this.i++)
}, 3000)
}, },
methods: { methods: {
resize() { resize() {
if (this.$refs.wrap) {
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2)) this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2))
} }
}
}, },
computed: { computed: {
beatList() {
if (! (this.monitorId in this.$root.heartbeatList)) {
this.$root.heartbeatList[this.monitorId] = [];
}
return this.$root.heartbeatList[this.monitorId]
},
shortBeatList() { shortBeatList() {
let placeholders = []
let start = this.beatList.length - this.maxBeat; let start = this.beatList.length - this.maxBeat;
if (this.move) { if (this.move) {
@ -52,10 +67,16 @@ export default {
} }
if (start < 0) { if (start < 0) {
// Add empty placeholder
for (let i = start; i < 0; i++) {
placeholders.push(0)
}
start = 0; start = 0;
} }
return this.beatList.slice(start)
return placeholders.concat(this.beatList.slice(start))
}, },
wrapStyle() { wrapStyle() {
@ -104,7 +125,6 @@ export default {
watch: { watch: {
beatList: { beatList: {
handler(val, oldVal) { handler(val, oldVal) {
console.log("add beat2")
this.move = true; this.move = true;
setTimeout(() => { setTimeout(() => {
@ -131,14 +151,17 @@ export default {
display: inline-block; display: inline-block;
background-color: $primary; background-color: $primary;
border-radius: 50rem; border-radius: 50rem;
transition: all ease-in-out 0.15s;
&.new-beat { &.empty {
background-color: aliceblue; background-color: aliceblue;
} }
&.down {
background-color: $danger;
}
&:hover { &:not(.empty):hover {
transition: all ease-in-out 0.15s;
opacity: 0.8; opacity: 0.8;
transform: scale(var(--hover-scale)); transform: scale(var(--hover-scale));
} }

View file

@ -0,0 +1,13 @@
<template>
<router-view />
</template>
<script>
export default {
}
</script>
<style scoped>
</style>

View file

@ -3,6 +3,7 @@ import {createRouter, createWebHistory} from 'vue-router'
import App from './App.vue' import App from './App.vue'
import Layout from './layouts/Layout.vue' import Layout from './layouts/Layout.vue'
import EmptyLayout from './layouts/EmptyLayout.vue'
import Settings from "./pages/Settings.vue"; import Settings from "./pages/Settings.vue";
import Dashboard from "./pages/Dashboard.vue"; import Dashboard from "./pages/Dashboard.vue";
import DashboardHome from "./pages/DashboardHome.vue"; import DashboardHome from "./pages/DashboardHome.vue";
@ -30,15 +31,21 @@ const routes = [
component: DashboardHome, component: DashboardHome,
children: [ children: [
{ {
path: ':id', path: '/dashboard/:id',
component: EmptyLayout,
children: [
{
path: '',
component: Details, component: Details,
}, },
{ {
path: '/add', path: '/edit/:id',
component: EditMonitor, component: EditMonitor,
}, },
]
},
{ {
path: '/edit/:id', path: '/add',
component: EditMonitor, component: EditMonitor,
}, },
] ]

View file

@ -21,7 +21,10 @@ export default {
], ],
importantHeartbeatList: [ importantHeartbeatList: [
] ],
heartbeatList: {
},
} }
}, },
@ -34,6 +37,16 @@ export default {
this.monitorList = data; this.monitorList = data;
}); });
socket.on('heartbeat', (data) => {
if (! (data.monitorID in this.heartbeatList)) {
this.heartbeatList[data.monitorID] = [];
}
this.heartbeatList[data.monitorID].push(data)
});
socket.on('disconnect', () => { socket.on('disconnect', () => {
this.socket.connected = false; this.socket.connected = false;
}); });
@ -53,9 +66,11 @@ export default {
}, },
methods: { methods: {
getSocket() { getSocket() {
return socket; return socket;
}, },
toastRes(res) { toastRes(res) {
if (res.ok) { if (res.ok) {
toast.success(res.msg); toast.success(res.msg);
@ -63,6 +78,7 @@ export default {
toast.error(res.msg); toast.error(res.msg);
} }
}, },
login(username, password, callback) { login(username, password, callback) {
socket.emit("login", { socket.emit("login", {
username, username,
@ -81,18 +97,19 @@ export default {
callback(res) callback(res)
}) })
}, },
loginByToken(token) { loginByToken(token) {
socket.emit("loginByToken", token, (res) => { socket.emit("loginByToken", token, (res) => {
this.allowLoginDialog = true; this.allowLoginDialog = true;
if (! res.ok) { if (! res.ok) {
this.logout() this.logout()
console.log(res.msg)
} else { } else {
this.loggedIn = true; this.loggedIn = true;
} }
}) })
}, },
logout() { logout() {
storage.removeItem("token"); storage.removeItem("token");
this.socket.token = null; this.socket.token = null;
@ -102,19 +119,59 @@ export default {
toast.success("Logout Successfully") toast.success("Logout Successfully")
}) })
}, },
add(monitor, callback) { add(monitor, callback) {
socket.emit("add", monitor, callback) socket.emit("add", monitor, callback)
}, },
deleteMonitor(monitorID, callback) { deleteMonitor(monitorID, callback) {
socket.emit("deleteMonitor", monitorID, callback) socket.emit("deleteMonitor", monitorID, callback)
}, },
loadMonitor(monitorID) {
}
}, },
computed: { computed: {
lastHeartbeatList() {
let result = {}
for (let monitorID in this.heartbeatList) {
let index = this.heartbeatList[monitorID].length - 1;
result[monitorID] = this.heartbeatList[monitorID][index];
}
return result;
},
statusList() {
let result = {}
let unknown = {
text: "Unknown",
color: "secondary"
}
for (let monitorID in this.lastHeartbeatList) {
let lastHeartBeat = this.lastHeartbeatList[monitorID]
if (! lastHeartBeat) {
result[monitorID] = unknown;
} else if (lastHeartBeat.status === 1) {
result[monitorID] = {
text: "Up",
color: "primary"
};
} else if (lastHeartBeat.status === 0) {
result[monitorID] = {
text: "Down",
color: "danger"
};
} else {
result[monitorID] = unknown;
}
}
return result;
}
} }
} }

View file

@ -23,20 +23,7 @@
</div> </div>
<div class="col-6"> <div class="col-6">
<div class="hp-bar"> <HeartbeatBar size="small" :monitor-id="item.id" />
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div> </div>
</div> </div>
@ -54,8 +41,11 @@
<script> <script>
import HeartbeatBar from "../components/HeartbeatBar.vue";
export default { export default {
components: { components: {
HeartbeatBar
}, },
data() { data() {
return { return {
@ -78,6 +68,8 @@ export default {
.list { .list {
margin-top: 25px; margin-top: 25px;
height: auto;
min-height: calc(100vh - 200px);
.item { .item {
display: block; display: block;
@ -92,6 +84,8 @@ export default {
.info { .info {
white-space: nowrap; white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
&:hover { &:hover {
@ -118,6 +112,10 @@ export default {
border-radius: 50rem; border-radius: 50rem;
transition: all ease-in-out 0.15s; transition: all ease-in-out 0.15s;
&.empty {
background-color: aliceblue;
}
&:hover { &:hover {
opacity: 0.8; opacity: 0.8;
transform: scale(1.5); transform: scale(1.5);

View file

@ -61,15 +61,19 @@
<div class="col"> <div class="col">
<h3>Up</h3> <h3>Up</h3>
<span class="num">2</span> <span class="num">{{ stats.up }}</span>
</div> </div>
<div class="col"> <div class="col">
<h3>Down</h3> <h3>Down</h3>
<span class="num text-danger">0</span> <span class="num text-danger">{{ stats.down }}</span>
</div>
<div class="col">
<h3>Unknown</h3>
<span class="num text-secondary">{{ stats.unknown }}</span>
</div> </div>
<div class="col"> <div class="col">
<h3>Pause</h3> <h3>Pause</h3>
<span class="num">0</span> <span class="num text-secondary">{{ stats.pause }}</span>
</div> </div>
</div> </div>
</div> </div>
@ -107,7 +111,37 @@
<script> <script>
export default { export default {
computed: { computed: {
stats() {
let result = {
up: 0,
down: 0,
unknown: 0,
pause: 0,
};
for (let monitorID in this.$root.monitorList) {
let beat = this.$root.lastHeartbeatList[monitorID];
let monitor = this.$root.monitorList[monitorID]
if (monitor && ! monitor.active) {
result.pause++;
} else if (beat) {
if (beat.status === 1) {
result.up++;
} else if (beat.status === 0) {
result.down++;
} else {
result.unknown++;
}
} else {
console.log(monitorID + " Unknown?")
console.log(beat)
result.unknown++;
}
}
return result;
},
} }
} }
</script> </script>

View file

@ -1,6 +1,6 @@
<template> <template>
<h1>{{ monitor.name }}</h1> <h1> {{ monitor.name }}</h1>
<h2>{{ monitor.url }}</h2> <p class="url"><a :href="monitor.url" target="_blank" v-if="monitor.type === 'http'">{{ monitor.url }}</a></p>
<div class="functions"> <div class="functions">
<button class="btn btn-light" @click="pauseDialog" v-if="monitor.active">Pause</button> <button class="btn btn-light" @click="pauseDialog" v-if="monitor.active">Pause</button>
@ -11,14 +11,15 @@
<div class="shadow-box"> <div class="shadow-box">
<HeartbeatBar />
<div class="row"> <div class="row">
<div class="col-md-8"> <div class="col-md-8">
<HeartbeatBar :monitor-id="monitor.id" />
<span class="word">Check every {{ monitor.interval }} seconds.</span>
</div> </div>
<div class="col-md-4"> <div class="col-md-4 text-center">
<span class="badge rounded-pill" :class=" 'bg-' + status.color " style="font-size: 30px">{{ status.text }}</span>
</div> </div>
</div> </div>
</div> </div>
@ -53,15 +54,28 @@ export default {
}, },
computed: { computed: {
monitor() { monitor() {
let id = parseInt(this.$route.params.id) let id = this.$route.params.id
return this.$root.monitorList[id];
for (let monitor of this.$root.monitorList) {
if (monitor.id === id) {
return monitor;
}
}
return {};
}, },
lastHeartBeat() {
if (this.monitor.id in this.$root.lastHeartbeatList && this.$root.lastHeartbeatList[this.monitor.id]) {
return this.$root.lastHeartbeatList[this.monitor.id]
} else {
return { status: -1 }
}
},
status() {
if (this.$root.statusList[this.monitor.id]) {
return this.$root.statusList[this.monitor.id]
} else {
return {
}
}
}
}, },
methods: { methods: {
pauseDialog() { pauseDialog() {
@ -97,9 +111,14 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../assets/vars.scss"; @import "../assets/vars.scss";
h2 { .url {
color: $primary; color: $primary;
margin-bottom: 20px; margin-bottom: 20px;
font-weight: bold;
a {
color: $primary;
}
} }
.functions { .functions {
@ -112,4 +131,9 @@ h2 {
padding: 20px; padding: 20px;
margin-top: 25px; margin-top: 25px;
} }
.word {
color: #AAA;
font-size: 14px;
}
</style> </style>

View file

@ -29,7 +29,7 @@
<div class="mb-3"> <div class="mb-3">
<label for="interval" class="form-label">Heartbeat Interval (Every {{ monitor.interval }} seconds)</label> <label for="interval" class="form-label">Heartbeat Interval (Every {{ monitor.interval }} seconds)</label>
<input type="number" class="form-control" id="interval" v-model="monitor.interval" required min="20" step="20"> <input type="number" class="form-control" id="interval" v-model="monitor.interval" required min="20">
</div> </div>
<div> <div>
@ -59,24 +59,7 @@ export default {
}, },
mounted() { mounted() {
this.init();
if (this.isAdd) {
this.monitor = {
type: "http",
name: "",
url: "https://",
interval: 60,
}
} else {
this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => {
if (res.ok) {
this.monitor = res.monitor;
} else {
toast.error(res.msg)
}
})
}
}, },
data() { data() {
return { return {
@ -90,9 +73,33 @@ export default {
}, },
isAdd() { isAdd() {
return this.$route.path === "/add"; return this.$route.path === "/add";
},
isEdit() {
return this.$route.path.startsWith("/edit");
} }
}, },
methods: { methods: {
init() {
if (this.isAdd) {
console.log("??????")
this.monitor = {
type: "http",
name: "",
url: "https://",
interval: 60,
}
} else if (this.isEdit) {
this.$root.getSocket().emit("getMonitor", this.$route.params.id, (res) => {
if (res.ok) {
this.monitor = res.monitor;
} else {
toast.error(res.msg)
}
})
}
},
submit() { submit() {
this.processing = true; this.processing = true;
@ -109,10 +116,18 @@ export default {
}) })
} else { } else {
this.$root.getSocket().emit("editMonitor", this.monitor, (res) => {
this.processing = false;
this.$root.toastRes(res)
})
} }
} }
},
watch: {
'$route.fullPath' () {
this.init();
} }
},
} }
</script> </script>