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",
"scripts": {
"dev": "vite",
"dev-server": "node server/server.js",
"build": "vite build",
"serve": "vite preview"
},

View file

@ -1,6 +1,18 @@
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");
/**
* status:
* 0 = DOWN
* 1 = UP
*/
class Monitor extends BeanModel {
toJSON() {
@ -16,9 +28,36 @@ class Monitor extends BeanModel {
}
start(io) {
const beat = () => {
const beat = async () => {
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();

View file

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

View file

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

View file

@ -1,7 +1,7 @@
<template>
<div class="wrap" :style="wrapStyle" ref="wrap">
<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>
@ -11,12 +11,15 @@
export default {
props: {
size: {
type: String,
default: "big"
},
monitorId: Number
},
data() {
return {
i: 1,
beatList: [
],
beatWidth: 10,
beatHeight: 30,
hoverScale: 1.5,
@ -25,26 +28,38 @@ export default {
maxBeat: -1,
}
},
destroyed() {
unmounted() {
window.removeEventListener("resize", this.resize);
},
mounted() {
if (this.size === "small") {
this.beatWidth = 5.6;
this.beatMargin = 2.4;
this.beatHeight = 16
}
window.addEventListener("resize", this.resize);
this.resize();
setInterval(() => {
this.beatList.push(this.i++)
}, 3000)
},
methods: {
resize() {
if (this.$refs.wrap) {
this.maxBeat = Math.floor(this.$refs.wrap.clientWidth / (this.beatWidth + this.beatMargin * 2))
}
}
},
computed: {
beatList() {
if (! (this.monitorId in this.$root.heartbeatList)) {
this.$root.heartbeatList[this.monitorId] = [];
}
return this.$root.heartbeatList[this.monitorId]
},
shortBeatList() {
let placeholders = []
let start = this.beatList.length - this.maxBeat;
if (this.move) {
@ -52,10 +67,16 @@ export default {
}
if (start < 0) {
// Add empty placeholder
for (let i = start; i < 0; i++) {
placeholders.push(0)
}
start = 0;
}
return this.beatList.slice(start)
return placeholders.concat(this.beatList.slice(start))
},
wrapStyle() {
@ -104,7 +125,6 @@ export default {
watch: {
beatList: {
handler(val, oldVal) {
console.log("add beat2")
this.move = true;
setTimeout(() => {
@ -131,14 +151,17 @@ export default {
display: inline-block;
background-color: $primary;
border-radius: 50rem;
transition: all ease-in-out 0.15s;
&.new-beat {
&.empty {
background-color: aliceblue;
}
&.down {
background-color: $danger;
}
&:hover {
&:not(.empty):hover {
transition: all ease-in-out 0.15s;
opacity: 0.8;
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 Layout from './layouts/Layout.vue'
import EmptyLayout from './layouts/EmptyLayout.vue'
import Settings from "./pages/Settings.vue";
import Dashboard from "./pages/Dashboard.vue";
import DashboardHome from "./pages/DashboardHome.vue";
@ -30,15 +31,21 @@ const routes = [
component: DashboardHome,
children: [
{
path: ':id',
path: '/dashboard/:id',
component: EmptyLayout,
children: [
{
path: '',
component: Details,
},
{
path: '/add',
path: '/edit/:id',
component: EditMonitor,
},
]
},
{
path: '/edit/:id',
path: '/add',
component: EditMonitor,
},
]

View file

@ -21,7 +21,10 @@ export default {
],
importantHeartbeatList: [
]
],
heartbeatList: {
},
}
},
@ -34,6 +37,16 @@ export default {
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', () => {
this.socket.connected = false;
});
@ -53,9 +66,11 @@ export default {
},
methods: {
getSocket() {
return socket;
},
toastRes(res) {
if (res.ok) {
toast.success(res.msg);
@ -63,6 +78,7 @@ export default {
toast.error(res.msg);
}
},
login(username, password, callback) {
socket.emit("login", {
username,
@ -81,18 +97,19 @@ export default {
callback(res)
})
},
loginByToken(token) {
socket.emit("loginByToken", token, (res) => {
this.allowLoginDialog = true;
if (! res.ok) {
this.logout()
console.log(res.msg)
} else {
this.loggedIn = true;
}
})
},
logout() {
storage.removeItem("token");
this.socket.token = null;
@ -102,19 +119,59 @@ export default {
toast.success("Logout Successfully")
})
},
add(monitor, callback) {
socket.emit("add", monitor, callback)
},
deleteMonitor(monitorID, callback) {
socket.emit("deleteMonitor", monitorID, callback)
},
loadMonitor(monitorID) {
}
},
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 class="col-6">
<div class="hp-bar">
<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>
<HeartbeatBar size="small" :monitor-id="item.id" />
</div>
</div>
@ -54,8 +41,11 @@
<script>
import HeartbeatBar from "../components/HeartbeatBar.vue";
export default {
components: {
HeartbeatBar
},
data() {
return {
@ -78,6 +68,8 @@ export default {
.list {
margin-top: 25px;
height: auto;
min-height: calc(100vh - 200px);
.item {
display: block;
@ -92,6 +84,8 @@ export default {
.info {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&:hover {
@ -118,6 +112,10 @@ export default {
border-radius: 50rem;
transition: all ease-in-out 0.15s;
&.empty {
background-color: aliceblue;
}
&:hover {
opacity: 0.8;
transform: scale(1.5);

View file

@ -61,15 +61,19 @@
<div class="col">
<h3>Up</h3>
<span class="num">2</span>
<span class="num">{{ stats.up }}</span>
</div>
<div class="col">
<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 class="col">
<h3>Pause</h3>
<span class="num">0</span>
<span class="num text-secondary">{{ stats.pause }}</span>
</div>
</div>
</div>
@ -107,7 +111,37 @@
<script>
export default {
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>

View file

@ -1,6 +1,6 @@
<template>
<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">
<button class="btn btn-light" @click="pauseDialog" v-if="monitor.active">Pause</button>
@ -11,14 +11,15 @@
<div class="shadow-box">
<HeartbeatBar />
<div class="row">
<div class="col-md-8">
<HeartbeatBar :monitor-id="monitor.id" />
<span class="word">Check every {{ monitor.interval }} seconds.</span>
</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>
@ -53,15 +54,28 @@ export default {
},
computed: {
monitor() {
let id = parseInt(this.$route.params.id)
for (let monitor of this.$root.monitorList) {
if (monitor.id === id) {
return monitor;
}
}
return {};
let id = this.$route.params.id
return this.$root.monitorList[id];
},
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: {
pauseDialog() {
@ -97,9 +111,14 @@ export default {
<style lang="scss" scoped>
@import "../assets/vars.scss";
h2 {
.url {
color: $primary;
margin-bottom: 20px;
font-weight: bold;
a {
color: $primary;
}
}
.functions {
@ -112,4 +131,9 @@ h2 {
padding: 20px;
margin-top: 25px;
}
.word {
color: #AAA;
font-size: 14px;
}
</style>

View file

@ -29,7 +29,7 @@
<div class="mb-3">
<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>
@ -59,24 +59,7 @@ export default {
},
mounted() {
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)
}
})
}
this.init();
},
data() {
return {
@ -90,9 +73,33 @@ export default {
},
isAdd() {
return this.$route.path === "/add";
},
isEdit() {
return this.$route.path.startsWith("/edit");
}
},
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() {
this.processing = true;
@ -109,10 +116,18 @@ export default {
})
} else {
this.$root.getSocket().emit("editMonitor", this.monitor, (res) => {
this.processing = false;
this.$root.toastRes(res)
})
}
}
},
watch: {
'$route.fullPath' () {
this.init();
}
},
}
</script>