Page MenuHomePhorge

No OneTemporary

Size
15 KB
Referenced Files
None
Subscribers
None
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 <Loader />;
} else {
return (
<Fragment>
<Header name="Servers" description="A list of all the servers in your daemon config.">
<button
type="button"
onClick={fetch}
className="transition inline-flex items-center justify-center space-x-1.5 border focus:outline-none focus:ring-0 focus:ring-offset-0 focus:z-10 shrink-0 border-zinc-900 hover:border-zinc-800 bg-zinc-950 text-zinc-50 hover:bg-zinc-900 px-4 py-2 text-sm font-semibold rounded-lg">
Refresh
</button>
</Header>
<table className="w-full whitespace-nowrap text-left">
<colgroup>
<col className="w-full sm:w-3/12" />
<col className="lg:w-[10%]" />
<col className="lg:w-2/12" />
<col className="lg:w-2/12" />
<col className="lg:w-1/12" />
<col className="lg:w-1/12" />
<col className="lg:w-1/12" />
<col className="lg:w-1/12" />
</colgroup>
<thead className="sticky top-0 z-10 bg-zinc-950 bg-opacity-75 backdrop-blur backdrop-filter border-b border-white/10 text-sm leading-6 text-white">
<tr>
<th scope="col" className="py-2 pl-4 pr-8 font-semibold sm:pl-6 lg:pl-8">
Server
</th>
<th scope="col" className="py-2 pl-0 pr-8 font-semibold table-cell">
Version
</th>
<th scope="col" className="hidden py-2 pl-0 pr-8 font-semibold sm:table-cell">
Build
</th>
<th scope="col" className="hidden py-2 pl-0 pr-8 font-semibold sm:table-cell">
Hash
</th>
<th scope="col" className="hidden py-2 pl-0 pr-8 font-semibold sm:table-cell">
Process Id
</th>
<th scope="col" className="hidden py-2 pl-0 pr-8 font-semibold md:table-cell lg:pr-20">
Count
</th>
<th scope="col" className="hidden py-2 pl-0 pr-4 font-semibold md:table-cell lg:pr-20">
Status
</th>
<th scope="col" className="py-2 pl-0 pr-4 text-right font-semibold sm:table-cell sm:pr-6 lg:pr-8">
Uptime
</th>
</tr>
</thead>
<tbody className="divide-y divide-white/5 border-b border-white/5">
{items.value.sort().map((server) => (
<tr
className="hover:bg-zinc-800/30 transition cursor-pointer"
key={server.name}
onClick={() => (window.location.href = props.base + '/status/' + server.name)}>
<td className="py-4 pl-4 pr-8 sm:pl-6 lg:pl-8">
<div className="flex items-center gap-x-4">
- <img
- src="https://upload.wikimedia.org/wikipedia/commons/thumb/3/3f/Fedora_logo.svg/1024px-Fedora_logo.svg.png"
- className="h-8 w-8 rounded-full bg-gray-800"
- />
+ <img src={getServerIcon(props.base, server.os.name)} className="h-8 w-8 rounded-full bg-zinc-900" />
<div className="truncate text-sm font-medium leading-6 text-white">{server.name == 'local' ? 'Internal' : server.name}</div>
</div>
</td>
<td className="py-4 pl-0 pr-4 table-cell sm:pr-8">
<div className="flex gap-x-3">
<div
className={classNames(
getStatus(server.version.pkg, server.version.status),
'rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset ring-white/10'
)}>
{server.version.pkg}
</div>
</div>
</td>
<td className="hidden py-4 pl-0 pr-4 sm:table-cell sm:pr-8">
<div className="font-mono text-sm leading-6 text-gray-400">
- {server.version.target}_{server.version.build_date}
+ {server.version.target} {server.version.build_date}
</div>
</td>
<td className="hidden py-4 pl-0 pr-4 sm:table-cell sm:pr-8">
<div className="font-mono text-sm leading-6 text-gray-400">{server.version.hash.slice(0, 16)}</div>
</td>
<td className="hidden py-4 pl-0 pr-8 text-sm leading-6 text-gray-400 md:table-cell lg:pr-20 font-mono">{server.daemon.pid}</td>
<td className="hidden py-4 pl-0 pr-8 text-sm leading-6 text-gray-400 md:table-cell lg:pr-20">{server.daemon.process_count}</td>
<td className="py-4 pl-0 pr-4 text-sm leading-6 sm:pr-8 lg:pr-20">
<div className="flex items-center justify-end gap-x-2 sm:justify-start">
- <span className="text-gray-400 sm:hidden">{startDuration(server.daemon.uptime, false)}</span>
+ <span className="text-gray-400 sm:hidden">{server.daemon.uptime == 0 ? 'none' : startDuration(server.daemon.uptime, false)}</span>
<div className={classNames(badge[server.daemon.running ? 'online' : 'offline'], 'flex-none rounded-full p-1')}>
<div className="h-1.5 w-1.5 rounded-full bg-current" />
</div>
<div className="hidden text-white sm:block">{server.daemon.running ? 'Online' : 'Offline'}</div>
</div>
</td>
<td className="hidden py-4 pl-0 pr-4 text-right text-sm leading-6 text-gray-400 sm:table-cell sm:pr-6 lg:pr-8">
- {startDuration(server.daemon.uptime, false)}
+ {server.daemon.uptime == 0 ? 'none' : startDuration(server.daemon.uptime, false)}
</td>
</tr>
))}
</tbody>
</table>
</Fragment>
);
}
};
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<any>();
const [loaded, setLoaded] = useState(false);
const [live, setLive] = useState<SSE | null>(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 <Loader />;
} 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 (
<Fragment>
- <h3 className="ml-8 mt-6 mb-5 text-2xl font-bold leading-6 text-zinc-200">Overview</h3>
- <dl className="grid grid-cols-1 gap-5 sm:grid-cols-4 px-5">
- {stats.map((item: any) => (
- <div key={item.name} className="overflow-hidden rounded-lg bg-zinc-900/25 border border-zinc-800 px-4 py-5 shadow sm:p-6">
- <dt className="truncate text-sm font-medium text-zinc-400">{item.name}</dt>
- <dd className="mt-1 text-3xl font-semibold tracking-tight text-zinc-100">{item.stat}</dd>
- </div>
- ))}
- </dl>
- <h3 className="ml-8 mt-8 mb-5 text-2xl font-bold leading-6 text-zinc-200">Metrics</h3>
- <dl className="grid grid-cols-1 gap-5 sm:grid-cols-2 px-5">
- <div className="overflow-hidden rounded-lg bg-zinc-900/25 border border-zinc-800 shadow">
+ <div className="absolute top-2 right-3 z-[200]">
+ <span className="inline-flex items-center gap-x-1.5 rounded-md px-2 py-1 text-xs font-medium text-white ring-1 ring-inset ring-zinc-800">
+ <svg viewBox="0 0 6 6" aria-hidden="true" className="h-1.5 w-1.5 fill-green-400">
+ <circle r={3} cx={3} cy={3} />
+ </svg>
+ {props.name != 'local' ? props.name : 'Internal'}
+ </span>
+ </div>
+ <dl className="mt-8 grid grid-cols-1 gap-5 sm:grid-cols-2 px-5">
+ <div className="overflow-hidden rounded-lg bg-zinc-900/20 border border-zinc-800 shadow">
<dt className="truncate text-sm font-bold text-zinc-400 pt-4 px-4">CPU Usage</dt>
<dt className="truncate text-xl font-bold text-zinc-100 p-1 px-4">
{cpuPercentage.value.slice(-1)[0].toFixed(2)}
<span className="text-base text-zinc-400">%</span>
</dt>
- <dd className="mt-2 text-3xl font-semibold tracking-tight text-zinc-100 h-52" style={chartContainerStyle}>
+ <dd className="mt-2 text-3xl font-semibold tracking-tight text-zinc-100 h-96" style={chartContainerStyle}>
<Line data={cpuChart} options={options} />
</dd>
</div>
- <div className="overflow-hidden rounded-lg bg-zinc-900/25 border border-zinc-800 shadow">
+ <div className="overflow-hidden rounded-lg bg-zinc-900/20 border border-zinc-800 shadow">
<dt className="truncate text-sm font-bold text-zinc-400 pt-4 px-4">Memory Usage</dt>
<dt className="truncate text-xl font-bold text-zinc-100 p-1 px-4">{bytesToSize(memoryUsage.value.slice(-1)[0], 2)}</dt>
- <dd className="mt-2 text-3xl font-semibold tracking-tight text-zinc-100 h-52" style={chartContainerStyle}>
+ <dd className="mt-2 text-3xl font-semibold tracking-tight text-zinc-100 h-96" style={chartContainerStyle}>
<Line data={memoryChart} options={options} />
</dd>
</div>
</dl>
+ <dl className="mt-5 pb-5 grid grid-cols-2 gap-5 lg:grid-cols-4 px-5 h-3/10">
+ {stats.map((item: any) => (
+ <div key={item.name} className="overflow-hidden rounded-lg bg-zinc-900/20 border border-zinc-800 px-4 py-5 shadow sm:p-6">
+ <dt className="truncate text-sm font-medium text-zinc-400">{item.name}</dt>
+ <dd className="mt-1 text-2xl font-semibold tracking-tight text-zinc-100">{item.stat}</dd>
+ </div>
+ ))}
+ </dl>
</Fragment>
);
}
};
export default Status;

File Metadata

Mime Type
text/x-diff
Expires
Sun, Feb 1, 3:24 PM (1 d, 7 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
494824
Default Alt Text
(15 KB)

Event Timeline