Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2707476
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
15 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
rPMC Process Management Controller
Attached
Detach File
Event Timeline
Log In to Comment