diff --git a/src/webui/src/components/react/servers.tsx b/src/webui/src/components/react/servers.tsx index e65981c..286cb30 100644 --- a/src/webui/src/components/react/servers.tsx +++ b/src/webui/src/components/react/servers.tsx @@ -1,161 +1,202 @@ import { api } from '@/api'; import { useEffect, Fragment } from 'react'; import Loader from '@/components/react/loader'; import Header from '@/components/react/header'; import { version } from '../../../package.json'; import { useArray, classNames, isVersionTooFar, startDuration } from '@/helpers'; -const getStatus = (remote: string, status: string) => { +const getStatus = (remote: string, status: string): string => { const badge = { updated: 'bg-emerald-700/40 text-emerald-400', behind: 'bg-gray-700/40 text-gray-400', critical: 'bg-red-700/40 text-red-400' }; - if (isVersionTooFar(version, remote.slice(1))) { + if (remote == 'v0.0.0') { + return badge['behind']; + } else if (isVersionTooFar(version, remote.slice(1))) { return badge['behind']; } else if (remote == `v${version}`) { return badge['updated']; } else { return badge[status ?? 'critical']; } }; +const skeleton = { + os: { name: '' }, + version: { + pkg: 'v0.0.0', + hash: 'none', + build_date: 'none', + target: '' + }, + daemon: { + pid: 'none', + running: false, + uptime: 0, + process_count: 'none' + } +}; + +const getServerIcon = (base: string, distro: string): string => { + const distroList = [ + 'Alpine', + 'Arch', + 'Amazon', + 'Macos', + 'Linux', + 'Fedora', + 'Debian', + 'CentOS', + 'NixOS', + 'FreeBSD', + 'OracleLinux', + 'Pop', + 'Raspbian', + 'Redhat', + 'Ubuntu' + ]; + + const isDistroKnown = distroList.includes(distro); + return `${base}/distro/${isDistroKnown ? distro : 'Unknown'}.svg`; +}; + const Index = (props: { base: string }) => { const items = useArray([]); const badge = { online: 'bg-emerald-400/10 text-emerald-400', offline: 'bg-red-500/10 text-red-500' }; async function fetch() { items.clear(); const metrics = await api.get(props.base + '/daemon/metrics').json(); items.push({ ...metrics, name: 'local' }); try { const servers = await api.get(props.base + '/daemon/servers').json(); await servers.forEach(async (name) => { - const metrics = await api.get(props.base + `/remote/${name}/metrics`).json(); - items.push({ ...metrics, name }); + api + .get(props.base + `/remote/${name}/metrics`) + .json() + .then((metrics) => items.push({ ...metrics, name })) + .catch(() => items.push({ ...skeleton, name })); }); } catch {} } useEffect(() => { fetch(); }, []); if (items.isEmpty()) { return ; } else { return (
{items.value.sort().map((server) => ( (window.location.href = props.base + '/status/' + server.name)}> ))}
Server Version Build Hash Process Id Count Status Uptime
- +
{server.name == 'local' ? 'Internal' : server.name}
{server.version.pkg}
- {server.version.target}_{server.version.build_date} + {server.version.target} {server.version.build_date}
{server.version.hash.slice(0, 16)}
{server.daemon.pid} {server.daemon.process_count}
- {startDuration(server.daemon.uptime, false)} + {server.daemon.uptime == 0 ? 'none' : startDuration(server.daemon.uptime, false)}
{server.daemon.running ? 'Online' : 'Offline'}
- {startDuration(server.daemon.uptime, false)} + {server.daemon.uptime == 0 ? 'none' : startDuration(server.daemon.uptime, false)}
); } }; export default Index; diff --git a/src/webui/src/components/react/status.tsx b/src/webui/src/components/react/status.tsx index 84541b6..6b8ae4f 100644 --- a/src/webui/src/components/react/status.tsx +++ b/src/webui/src/components/react/status.tsx @@ -1,198 +1,211 @@ import { Line } from 'react-chartjs-2'; import { SSE, api, headers } from '@/api'; import Loader from '@/components/react/loader'; import { useEffect, useState, useRef, Fragment } from 'react'; import { classNames, isRunning, formatMemory, startDuration, useArray } from '@/helpers'; import { Chart, CategoryScale, LinearScale, PointElement, LineElement, Filler } from 'chart.js'; Chart.register(CategoryScale, LinearScale, PointElement, LineElement, Filler); const bytesToSize = (bytes: number, precision: number) => { if (isNaN(bytes) || bytes === 0) return '0b'; const sizes = ['b', 'kb', 'mb', 'gb', 'tb']; const kilobyte = 1024; const index = Math.floor(Math.log(bytes) / Math.log(kilobyte)); const size = (bytes / Math.pow(kilobyte, index)).toFixed(precision); return size + sizes[index]; }; const Status = (props: { name: string; base: string }) => { const bufferLength = 21; const memoryUsage = useArray([], bufferLength); const cpuPercentage = useArray([], bufferLength); + const [item, setItem] = useState(); const [loaded, setLoaded] = useState(false); const [live, setLive] = useState(null); const options = { responsive: true, maintainAspectRatio: false, - animation: { duration: 0.5 }, + animation: { duration: 0 }, layout: { padding: { left: 0, right: 0, bottom: 0, top: 0 } }, plugins: { tooltips: { enabled: false }, title: { display: false } }, elements: { point: { radius: 0 }, line: { tension: 0.5, borderWidth: 1 } }, scales: { x: { display: false }, y: { display: false, suggestedMin: 0 } }, data: { labels: Array(20).fill(''), datasets: [{ fill: true, data: Array(20).fill(0) }] } }; const chartContainerStyle = { borderRadius: '0 0 0.5rem 0.5rem', marginBottom: '0.5px', zIndex: 1 }; const cpuChart = { labels: Array(20).fill(''), datasets: [ { fill: true, data: cpuPercentage.value, borderColor: '#0284c7', backgroundColor: (ctx: any) => { const chart = ctx.chart; const { ctx: context, chartArea } = chart; if (!chartArea) { return null; } const gradient = context.createLinearGradient(0, chartArea.bottom, 0, chartArea.top); gradient.addColorStop(0, 'rgba(14, 165, 233, 0.1)'); gradient.addColorStop(1, 'rgba(14, 165, 233, 0.5)'); return gradient; } } ] }; const memoryChart = { labels: Array(20).fill(''), datasets: [ { fill: true, data: memoryUsage.value, borderColor: '#0284c7', backgroundColor: (ctx: any) => { const chart = ctx.chart; const { ctx: context, chartArea } = chart; if (!chartArea) { return null; } const gradient = context.createLinearGradient(0, chartArea.bottom, 0, chartArea.top); gradient.addColorStop(0, 'rgba(14, 165, 233, 0.1)'); gradient.addColorStop(1, 'rgba(14, 165, 233, 0.5)'); return gradient; } } ] }; const openConnection = () => { let retryTimeout; let hasRun = false; const source = new SSE(`${props.base}/live/daemon/${props.server}/metrics`, { headers }); setLive(source); source.onmessage = (event) => { const data = JSON.parse(event.data); + setItem(data); + memoryUsage.pushMax(data.raw.memory_usage.rss); cpuPercentage.pushMax(data.raw.cpu_percent + 1); if (!hasRun) { setLoaded(true); hasRun = true; } }; source.onerror = (error) => { source.close(); retryTimeout = setTimeout(() => { openConnection(); }, 5000); }; return retryTimeout; }; useEffect(() => { const retryTimeout = openConnection(); return () => { live && live.close(); clearTimeout(retryTimeout); }; }, []); - const stats = [ - { name: 'Status', stat: 'Online' }, - { name: 'Proccess', stat: '3' }, - { name: 'Errors', stat: '10' }, - { name: 'Crashes', stat: '2' } - ]; - if (!loaded) { return ; } else { + const stats = [ + { name: 'Uptime', stat: startDuration(item.daemon.uptime, false) }, + { name: 'Count', stat: item.daemon.process_count }, + { name: 'Version', stat: item.version.pkg }, + { name: 'Process Id', stat: item.daemon.pid }, + { name: 'Build date', stat: item.version.build_date }, + { name: 'Hash', stat: item.version.hash.slice(0, 18) }, + { name: 'Platform', stat: `${item.os.name} ${item.os.version} (${item.os.arch})` }, + { name: 'Daemon', stat: item.daemon.daemon_type } + ]; + return ( -

Overview

-
- {stats.map((item: any) => ( -
-
{item.name}
-
{item.stat}
-
- ))} -
-

Metrics

-
-
+
+ + + {props.name != 'local' ? props.name : 'Internal'} + +
+
+
CPU Usage
{cpuPercentage.value.slice(-1)[0].toFixed(2)} %
-
+
-
+
Memory Usage
{bytesToSize(memoryUsage.value.slice(-1)[0], 2)}
-
+
+
+ {stats.map((item: any) => ( +
+
{item.name}
+
{item.stat}
+
+ ))} +
); } }; export default Status;