Concept refactoring; accept tiles and props from other plugins through hooks.

This commit is contained in:
Romein van Buren 2023-02-23 14:47:51 +01:00
parent 59d36ed208
commit 086da49c67
Signed by: romein
GPG Key ID: 0EFF8478ADDF6C49
5 changed files with 160 additions and 127 deletions

View File

@ -5,6 +5,7 @@ html, body {
--grey: grey; --grey: grey;
--red: #f00; --red: #f00;
--green: #1d641d; --green: #1d641d;
--orange: #bf5900;
--dark: #0a0a0a; --dark: #0a0a0a;
--radius: 10px; --radius: 10px;
--cols: 4; --cols: 4;
@ -22,6 +23,7 @@ html, body {
--tile-bg: #dedede; --tile-bg: #dedede;
--dark: #c2c2c2; --dark: #c2c2c2;
--green: #377137; --green: #377137;
--orange: #bf5900;
} }
.mb { .mb {

View File

@ -1,92 +1,114 @@
<script> <script>
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import TileRawValue from './tile-rawvalue.svelte'; import Tile from './tile.svelte';
import Settings from './settings.svelte'; import Settings from './settings.svelte';
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate';
import { settings, shuffle } from './lib'; import { settings, shuffle } from './lib';
import { connect } from './apiclient'; import { connect } from './apiclient';
const [ send, receive ] = shuffle; const [ send, receive ] = shuffle;
let size = ($settings.cols || 4) * ($settings.rows || 3); let maxNumberOfTilesOnPage = ($settings.cols || 4) * ($settings.rows || 3);
let placesLeft = size;
let pageNum = -1; let pageNum = -1;
let pageCount = 1; let pageCount = 1;
let tiles = []; let allTiles = [];
let tilesOnPage = [];
let time = ''; let time = '';
let globalData = {};
let hasData = false; let hasData = false;
function tileProps(service) { function previousPage() {
const props = { pageNum--;
title: service.name.en, if (pageNum < 0) {
subtitle: service.cluster, pageNum = pageCount - 1;
date: service.lastBeat?.date ? new Date(service.lastBeat.date) : undefined,
since: service.checked ? new Date(service.checked) : undefined,
};
if (!service.lastBeat?.date) {
props.value = 'no data';
props.color = 'grey';
props.sort = 20;
} }
else if (service.lastBeat.down) {
props.value = 'down';
props.color = 'red';
props.sort = 0;
}
else {
props.value = 'up';
props.color = 'green';
props.sort = 10;
} }
return props; function nextPage() {
}
function organiseGrid() {
let servicesTemp = [];
const { servicesUp, servicesDown, servicesUnknown, total } = globalData;
const upOrUnknown = [ ...servicesUp, ...servicesUnknown ];
servicesTemp = servicesDown.slice(0, size);
pageCount = Math.ceil(upOrUnknown.length / size);
placesLeft = size - servicesTemp.length;
pageCount = Math.ceil(upOrUnknown.length / placesLeft);
if (pageNum === -1 || total >= size) {
pageNum++; pageNum++;
if (pageNum >= pageCount) { if (pageNum >= pageCount) {
pageNum = 0; pageNum = 0;
} }
} }
const offset = placesLeft * pageNum; function organiseGrid(goToNextPage = true) {
if (placesLeft > 0) { const newTiles = allTiles.sort((a, b) => b.prio - a.prio);
servicesTemp.push( const pinnedTilesCount = allTiles.filter(t => t.prio > 0).length;
...upOrUnknown.slice(offset, placesLeft + offset) const placesLeft = maxNumberOfTilesOnPage - pinnedTilesCount;
); pageCount = Math.ceil(newTiles.length / placesLeft);
if (((newTiles.length >= placesLeft) && goToNextPage) || (pageNum === -1)) {
nextPage();
} }
tiles = servicesTemp; tilesOnPage = [
...newTiles.slice(0, pinnedTilesCount),
...newTiles.slice(pinnedTilesCount + (pageNum * placesLeft)),
].slice(0, maxNumberOfTilesOnPage);
}
function keydown(event) {
switch (event.code) {
case 'ArrowLeft':
case 'PageUp':
event.preventDefault();
previousPage();
organiseGrid(false);
break;
case 'ArrowRight':
case 'PageDown':
event.preventDefault();
nextPage();
organiseGrid(false);
break;
case 'Home':
event.preventDefault();
pageNum = 0;
organiseGrid(false);
break;
case 'End':
event.preventDefault();
pageNum = pageCount - 1;
organiseGrid(false);
break;
default:
if (event.code.startsWith('Digit')) {
event.preventDefault();
let num = parseInt(event.code.slice(5));
if (!isNaN(num)) {
if (num > pageCount) {
num = pageCount;
}
pageNum = num - 1;
organiseGrid(false);
}
}
break;
}
} }
onMount(async () => { onMount(async () => {
await connect({ await connect({
onData: data => { onData: data => {
globalData = data; allTiles = data.tiles?.map(tile => {
if (tile?.service?.checked) {
tile.service.checked = new Date(tile.service.checked);
}
return tile;
});
organiseGrid(); organiseGrid();
hasData = true; hasData = true;
}, },
}); });
const clockInterval = setInterval(() => { const clockInterval = setInterval(() => {
time = new Date().toLocaleTimeString('en-GB', { time = new Date().toLocaleTimeString('en-GB', { timeStyle: 'medium' });
timeStyle: 'medium',
});
}, 100); }, 100);
settings.subscribe(s => { settings.subscribe(s => {
size = (s.cols || 4) * (s.rows || 3); maxNumberOfTilesOnPage = (s.cols || 4) * (s.rows || 3);
if (hasData) { if (hasData) {
organiseGrid(); organiseGrid();
} }
@ -96,6 +118,8 @@
}); });
</script> </script>
<svelte:window on:keydown={keydown} />
<div <div
class="center theme-{$settings.theme}" class="center theme-{$settings.theme}"
style=" style="
@ -106,13 +130,13 @@
> >
<div class="ratio"> <div class="ratio">
<div class="tiles"> <div class="tiles">
{#each tiles as tile (tile.id)} {#each tilesOnPage || [] as tile (tile.serviceId)}
<div <div
in:receive={{ key: tile.id }} in:receive={{ key: tile.serviceId }}
out:send={{ key: tile.id }} out:send={{ key: tile.serviceId }}
animate:flip={{ duration: $settings.animate ? (d => Math.sqrt(d) * 120) : 0 }} animate:flip={{ duration: $settings.animate ? (d => Math.sqrt(d) * 120) : 0 }}
> >
<TileRawValue {...tileProps(tile)} /> <Tile {tile} />
</div> </div>
{/each} {/each}
</div> </div>

View File

@ -1,24 +0,0 @@
<script>
import Tile from './tile.svelte';
export let title;
export let subtitle;
export let color;
export let value;
export let date;
export let since;
</script>
<Tile {title} {subtitle} {color} {date} {since}>
<div class="value {color}">
{value}
</div>
</Tile>
<style>
.value {
font-size: 3em;
font-weight: 600;
margin-top: auto;
}
</style>

View File

@ -2,25 +2,21 @@
import { formatDuration } from './lib'; import { formatDuration } from './lib';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
export let title; export let tile;
export let subtitle;
export let color;
export let date;
export let since;
let formattedDuration = ''; let formattedDuration = '';
$: formattedDate = date ? date.toLocaleTimeString('en-GB', { $: formattedDate = tile.service?.checked ? tile.service.checked.toLocaleTimeString('en-GB', {
timeStyle: 'short', timeStyle: 'short',
}) : ''; }) : '';
onMount(() => { onMount(() => {
function updateDuration() { function updateDuration() {
formattedDuration = formatDuration( formattedDuration = formatDuration(
new Date().getTime() - since.getTime() new Date().getTime() - tile.since.getTime()
); );
} }
if (since) { if (tile.since) {
updateDuration(); updateDuration();
const interval = setInterval(updateDuration, 100); const interval = setInterval(updateDuration, 100);
return () => clearInterval(interval); return () => clearInterval(interval);
@ -28,22 +24,33 @@
}); });
</script> </script>
<div class="tile {color}"> <div class="tile prio{tile.prio}">
{#if title || subtitle}
<div class="desc"> <div class="desc">
{#if title} {#if tile.service?.name?.en}
<div class="title">{title}</div> <div class="title">{tile.service.name.en}</div>
{/if} {/if}
{#if subtitle} {#if tile.service?.cluster}
<div class="subtitle">{subtitle}</div> <div class="subtitle">{tile.service.cluster}</div>
{/if} {/if}
{#if tile.badges?.length}
<div class="badges">
{#each tile.badges as badge}
<span class="badge">{badge}</span>
{/each}
</div> </div>
{/if} {/if}
</div>
<div class="bottom"> <div class="bottom">
<div class="content"><slot /></div> <div class="content">
{#if date || since} <div class="statustext">
{tile.statusText}
</div>
</div>
{#if tile.date || tile.since}
<div class="time"> <div class="time">
{#if formattedDate}<div>{formattedDate}</div>{/if} {#if formattedDate}<div>{formattedDate}</div>{/if}
{#if formattedDuration}<div>{formattedDuration}</div>{/if} {#if formattedDuration}<div>{formattedDuration}</div>{/if}
@ -62,19 +69,25 @@
flex-direction: column; flex-direction: column;
} }
.tile.red { .tile.prio2 {
background-color: var(--red); background-color: var(--red);
border-color: var(--red); border-color: var(--red);
color: #fff; color: #fff;
} }
.tile.green { .tile.prio1 {
background-color: var(--orange);
border-color: var(--orange);
color: #fff;
}
.tile.prio0 {
border-color: var(--green); border-color: var(--green);
background-color: var(--green); background-color: var(--green);
color: #fff; color: #fff;
} }
.tile.grey { .tile.prio-1 {
opacity: 0.5; opacity: 0.5;
border-color: var(--grey); border-color: var(--grey);
} }
@ -98,6 +111,18 @@
opacity: 0.7; opacity: 0.7;
} }
.badges {
margin-top: 0.2em;
}
.badges .badge {
display: inline-block;
margin-right: 0.3em;
background-color: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(0, 0, 0, 0.4);
padding: 0.2em 0.4em;
border-radius: 0.4em;
}
.bottom { .bottom {
display: flex; display: flex;
margin-top: auto; margin-top: auto;
@ -117,4 +142,10 @@
flex-grow: 1; flex-grow: 1;
display: flex; display: flex;
} }
.statustext {
font-size: 3em;
font-weight: 600;
margin-top: auto;
}
</style> </style>

View File

@ -375,32 +375,38 @@ module.exports = {
.sort({ date: -1 }) .sort({ date: -1 })
.toArray(); .toArray();
const servicesUp = []; const tiles = [];
const servicesDown = [];
const servicesUnknown = [];
downIdsAfter = []; downIdsAfter = [];
for (let service of services) { for (let service of services) {
const beat = heartbeats.find(b => b.webservice === service.id); const beat = heartbeats.find(b => b.webservice === service.id);
service = mapService(service, beat); service = mapService(service, beat);
const tile = {
service: service,
serviceId: service.id,
badges: [],
prio: -1,
};
if (!beat) { if (!beat) {
servicesUnknown.push(service); tile.prio = -1; // no data (grey)
tile.statusText = 'no data';
} }
else if (beat.down) { else if (beat.down) {
servicesDown.push(service); tile.prio = 2; // down (red)
downIdsAfter.push(service.id); tile.statusText = 'down';
downIdsAfter.push(tile.serviceId);
} }
else { else {
servicesUp.push(service); tile.prio = 0; // ok (green)
} tile.statusText = 'ok';
} }
const total = [ tiles.push(tile);
...servicesUp, }
...servicesDown,
...servicesUnknown, // Let other plugins enrich dashboard tiles with custom badges and priorities.
].length; await server.executePostHooks('pupulateDashboardTiles', { tiles });
let newOutage = false; let newOutage = false;
for (const id of downIdsAfter) { for (const id of downIdsAfter) {
@ -408,20 +414,14 @@ module.exports = {
newOutage = true; newOutage = true;
} }
} }
downIdsBefore = JSON.parse(JSON.stringify(downIdsAfter)); downIdsBefore = [ ...downIdsAfter ];
try { try {
if (newOutage) { if (newOutage) {
ws.send(JSON.stringify({ cmd: 'bell' })); ws.send(JSON.stringify({ cmd: 'bell' }));
} }
ws.send(JSON.stringify({ ws.send(JSON.stringify({ cmd: 'data', tiles: tiles }));
cmd: 'data',
servicesUp,
servicesDown,
servicesUnknown,
total,
}));
} }
catch { catch {
return; return;
@ -804,7 +804,7 @@ module.exports = {
method: 'get', method: 'get',
handler: async (req, res) => { handler: async (req, res) => {
// const cacheValid = !!renderedDashboard; // const cacheValid = !!renderedDashboard;
const cacheValid = true; const cacheValid = false;
if (!cacheValid) { if (!cacheValid) {
// Build dashboard // Build dashboard
let cssOutput = ''; let cssOutput = '';