Basic dashboard

Signed-off-by: Romein van Buren <romein@vburen.nl>
This commit is contained in:
Romein van Buren 2022-07-11 11:20:44 +02:00
parent b3fd94b02a
commit c561a00954
Signed by: romein
GPG Key ID: 0EFF8478ADDF6C49
7 changed files with 230 additions and 31 deletions

View File

@ -4,16 +4,20 @@ const { minify: minifyCSS } = require('csso');
const { rollup } = require('rollup');
const commonjs = require('@rollup/plugin-commonjs');
const css = require('rollup-plugin-css-only');
const replace = require('@rollup/plugin-replace');
const { default: resolve } = require('@rollup/plugin-node-resolve');
const svelte = require('rollup-plugin-svelte');
const { terser } = require('rollup-plugin-terser');
async function build() {
async function build(server) {
const serverDomain = server.settings.domain || 'localhost';
const serverPort = server.settings.port || 80;
const serverBase = `${serverDomain}:${serverPort}`;
let cssOutput = '';
try {
const bundle = await rollup({
input: __dirname + '/gui/dashboard/index.js',
input: __dirname + '/../gui/dashboard/index.js',
plugins: [
// Svelte
svelte({
@ -39,6 +43,14 @@ async function build() {
// Minify
terser(),
// Replace env vars
replace({
preventAssignment: false,
values: {
'__SERVER__': serverBase,
},
}),
],
});

72
dashboard/socket.js Normal file
View File

@ -0,0 +1,72 @@
'use strict';
const { makeId } = require('core/makeid');
const decoder = new TextDecoder('utf-8');
let uws;
async function createDashboardSocket(server) {
uws = server.ws({
route: '/statusdashboard/socket',
onOpen: async ws => {
function sendTime() {
ws.send(JSON.stringify({
cmd: 'time',
time: new Date().getTime(),
}));
}
sendTime();
setInterval(sendTime, 5000);
async function sendStatuses() {
const services = await server.storage
.store('smartyellow/webservice')
.find()
.toArray();
const heartbeats = await server.storage
.store('smartyellow/webserviceheartbeat')
.find({ webservice: { $in: services.map(s => s.id) } })
.sort({ date: -1 })
.toArray();
const mappedServices = {};
for (const s of services) {
const lastBeat = heartbeats.find(h => h.webservice === s.id);
mappedServices[s.id] = {
name: s.name,
lastBeat: lastBeat,
};
}
ws.send(JSON.stringify({
cmd: 'data',
data: mappedServices,
}));
}
sendStatuses();
setInterval(sendStatuses, 5000);
},
onUpgrade: async () => ({ id: makeId(10) }),
onMessage: async (ws, msg) => {
msg = JSON.parse(decoder.decode(msg));
console.log('msg', msg);
if (!msg || !msg.command) {
return;
}
switch (msg.command) {
case 'data':
ws.send('data');
return;
default:
return;
}
},
});
}
module.exports = createDashboardSocket;

View File

@ -1,19 +1,69 @@
<script>
import { onMount } from 'svelte';
import TileRawValue from './tile-rawvalue.svelte';
onMount(() => console.log('test'));
let lastUpdated = new Date();
let lastUpdatedFormatted = '';
let services = {};
let loading = true;
$:console.log(services);
onMount(() => {
const ws = new WebSocket('ws://__SERVER__/statusdashboard/socket');
ws.onmessage = async evt => {
const data = JSON.parse(evt.data || '""');
switch (data.cmd) {
case 'time':
lastUpdated = new Date(data.time);
lastUpdatedFormatted = lastUpdated.toLocaleTimeString('en-GB', {
timeStyle: 'short',
});
break;
case 'data':
services = data.data;
loading = false;
break;
default:
break;
}
}
});
</script>
<div class="center">
<div class="ratio">
<div class="tiles">
<TileRawValue title="Last updated" value={lastUpdatedFormatted} />
{#if !loading}
{#each Object.entries(services) as [ id, service ] (id)}
{@const isDown = service.lastBeat.down}
<TileRawValue
title={service.name.en}
value={isDown ? 'down' : 'up'}
color={isDown ? 'red' : 'green'}
/>
{/each}
{:else}
loading
{/if}
</div>
</div>
</div>
<style>
:global(html), :global(body) {
--radius: 10px;
--tile-bg: #181818;
--red: red;
--green: green;
--radius: 10px;
--cols: 4;
--rows: 3;
background-color: #000;
color: #fff;
@ -40,4 +90,13 @@
transform: translate(-50%,-50%);
width: 100vw;
}
.tiles {
margin: 1rem;
display: grid;
grid-template-columns: repeat(var(--cols), 1fr);
grid-template-rows: repeat(var(--rows), 1fr);
gap: 1rem;
justify-items: stretch;
}
</style>

View File

@ -0,0 +1,29 @@
<script>
import Tile from './tile.svelte';
export let title;
export let subtitle;
export let color;
export let value;
</script>
<Tile {title} {subtitle}>
<div class="value {color}">
{value}
</div>
</Tile>
<style>
.value {
font-size: 3rem;
font-weight: 600;
}
.red {
color: var(--red);
}
.green {
color: var(--green);
}
</style>

View File

@ -4,27 +4,38 @@
</script>
<div class="tile">
{#if title}
<div class="title">{title}</div>
{#if title || subtitle}
<div class="desc">
{#if title}
<div class="title">{title}</div>
{/if}
{#if subtitle}
<div class="subtitle">{subtitle}</div>
{/if}
</div>
{/if}
{#if subtitle}
<div class="subtitle">{subtitle}</div>
{/if}
<slot />
</div>
<style>
.desc {
font-size: 1.3rem;
margin-bottom: 1rem;
}
.tile {
padding: 1rem;
background-color: var(--tile-bg);
border-radius: var(--radius);
}
.title, .subtitle {
font-size: 1.3rem;
.title {
font-weight: 200;
}
.title {
font-weight: 600;
.subtitle {
font-weight: 100;
}
</style>

View File

@ -2,7 +2,8 @@
const { fork } = require('child_process');
const { processOutage } = require('./lib/processoutage');
const buildDashboard = require('./builddashboard');
const buildDashboard = require('./dashboard/build');
const createDashboardSocket = require('./dashboard/socket');
const { minifyHtml } = require('core/strings');
const guiCluster = 'web service status';
@ -210,6 +211,15 @@ module.exports = {
],
hooks: ({ server, settings }) => [
{ id: 'startDashboardSocket',
event: 'boot',
order: 100,
purpose: 'Start the websocket for the dashboard after server has booted',
handler: async ({ server }) => {
createDashboardSocket(server);
},
},
{ id: 'autotestOnSave',
order: 500,
event: 'saveEntity',
@ -535,24 +545,29 @@ module.exports = {
{ route: '/statusdashboard',
method: 'get',
handler: async (req, res) => {
if (!renderedDashboard) {
renderedDashboard = await buildDashboard();
try {
if (!renderedDashboard) {
renderedDashboard = await buildDashboard(server);
}
const dashboardHtml = minifyHtml(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Web service status dashboard</title>
<style>${renderedDashboard.css || ''}</style>
<script>${renderedDashboard.code || ''}</script>
</head>
<body></body>
</html>
`);
res.send(dashboardHtml);
}
catch (error) {
server.error('could not compile web service status dashboard', error);
}
const dashboardHtml = minifyHtml(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Web service status dashboard</title>
<style>${renderedDashboard.css || ''}</style>
<script>${renderedDashboard.code || ''}</script>
</head>
<body></body>
</html>
`);
res.send(dashboardHtml);
},
},

View File

@ -17,6 +17,7 @@
},
"homepage": "https://github.com/smartyellow/status#readme",
"dependencies": {
"@rollup/plugin-replace": "^4.0.0",
"csso": "^5.0.3",
"rollup-plugin-css-only": "^3.1.0"
}