finish ui

This commit is contained in:
csehviktor
2025-07-15 03:08:44 +02:00
parent e778f7934e
commit 96df5453b9
7 changed files with 142 additions and 70 deletions

View File

@@ -10,7 +10,7 @@ export function LineChartCard({
}) { }) {
return ( return (
<div className="bg-[#0d0d0d] border border-[#191919] rounded-lg p-6"> <div className="bg-[#0d0d0d] border border-[#191919] rounded-lg p-6">
{data.labels.length > 0 ? ( {data.labels.length > 1 ? (
<> <>
<h3 className="text-lg font-semibold mb-4">{title}</h3> <h3 className="text-lg font-semibold mb-4">{title}</h3>
<div className="h-48"> <div className="h-48">

View File

@@ -0,0 +1,34 @@
import { TimePeriod } from "@/services/store";
type TimePeriodSelectorProps = {
currentPeriod: TimePeriod | "realtime";
onChange: (period: TimePeriod | "realtime") => void;
};
export function TimePeriodSelector({
currentPeriod,
onChange,
}: TimePeriodSelectorProps) {
const periods: { value: TimePeriod | "realtime"; label: string }[] = [
{ value: "realtime", label: "Real time" },
{ value: "hour", label: "Past hour" },
{ value: "day", label: "Past day" },
{ value: "week", label: "Past week" },
{ value: "month", label: "Past month" },
{ value: "all", label: "All" },
];
return (
<div className="space-x-3">
{periods.map((period) => (
<button
key={period.value}
onClick={() => onChange(period.value)}
className={`px-4 py-2 text-md font-medium rounded-full bg-[#0d0d0d] border border-[#101010] ${currentPeriod === period.value && "text-purple-500 font-semibold"}`}
>
{period.label}
</button>
))}
</div>
);
}

View File

@@ -18,55 +18,57 @@ type ChartDataReturns = {
networkData: ChartData; networkData: ChartData;
}; };
export function useChartData(data: StatusMessage | null): ChartDataReturns { export function useChartData(
data: StatusMessage | StatusMessage[] | null,
): ChartDataReturns {
const currentData = Array.isArray(data) ? data : getRealtimeData();
useEffect(() => { useEffect(() => {
if (!data) return; if (!data || Array.isArray(data)) return;
addDataPoint(data); addDataPoint(data);
}, [data]); }, [data]);
const realtimeData = getRealtimeData();
return { return {
cpuData: { cpuData: {
labels: realtimeData.map((p) => formatTimestamp(p.timestamp)), labels: currentData.map((p) => formatTimestamp(p.timestamp)),
datasets: [ datasets: [
{ {
label: "Total CPU (%)", label: "Total CPU (%)",
data: realtimeData.map(({ metrics }) => metrics.cpu.usage), data: currentData.map(({ metrics }) => metrics.cpu.usage),
color: "#3b82f6", color: "#3b82f6",
}, },
{ {
label: "User (%)", label: "User (%)",
data: realtimeData.map( data: currentData.map(
({ metrics }) => metrics.cpu.breakdown.user, ({ metrics }) => metrics.cpu.breakdown.user,
), ),
color: "#10b981", color: "#10b981",
}, },
{ {
label: "System (%)", label: "System (%)",
data: realtimeData.map( data: currentData.map(
({ metrics }) => metrics.cpu.breakdown.system, ({ metrics }) => metrics.cpu.breakdown.system,
), ),
color: "#f59e0b", color: "#f59e0b",
}, },
{ {
label: "I/O Wait (%)", label: "I/O Wait (%)",
data: realtimeData.map( data: currentData.map(
({ metrics }) => metrics.cpu.breakdown.iowait, ({ metrics }) => metrics.cpu.breakdown.iowait,
), ),
color: "#ef4444", color: "#ef4444",
}, },
{ {
label: "Steal (%)", label: "Steal (%)",
data: realtimeData.map( data: currentData.map(
({ metrics }) => metrics.cpu.breakdown.steal, ({ metrics }) => metrics.cpu.breakdown.steal,
), ),
color: "#8b5cf6", color: "#8b5cf6",
}, },
{ {
label: "Idle (%)", label: "Idle (%)",
data: realtimeData.map( data: currentData.map(
({ metrics }) => metrics.cpu.breakdown.idle, ({ metrics }) => metrics.cpu.breakdown.idle,
), ),
color: "#6b7280", color: "#6b7280",
@@ -74,11 +76,11 @@ export function useChartData(data: StatusMessage | null): ChartDataReturns {
], ],
}, },
memoryData: { memoryData: {
labels: realtimeData.map((p) => formatTimestamp(p.timestamp)), labels: currentData.map((p) => formatTimestamp(p.timestamp)),
datasets: [ datasets: [
{ {
label: "Memory Usage (%)", label: "Memory Usage (%)",
data: realtimeData.map(({ metrics }) => data: currentData.map(({ metrics }) =>
calcPercentage( calcPercentage(
metrics.memory.used, metrics.memory.used,
metrics.memory.total, metrics.memory.total,
@@ -88,7 +90,7 @@ export function useChartData(data: StatusMessage | null): ChartDataReturns {
}, },
{ {
label: "Swap Usage (%)", label: "Swap Usage (%)",
data: realtimeData.map(({ metrics }) => data: currentData.map(({ metrics }) =>
calcPercentage( calcPercentage(
metrics.memory.swap_used, metrics.memory.swap_used,
metrics.memory.swap_total, metrics.memory.swap_total,
@@ -99,16 +101,16 @@ export function useChartData(data: StatusMessage | null): ChartDataReturns {
], ],
}, },
networkData: { networkData: {
labels: realtimeData.map((p) => formatTimestamp(p.timestamp)), labels: currentData.map((p) => formatTimestamp(p.timestamp)),
datasets: [ datasets: [
{ {
label: "Upload (bps)", label: "Upload (bps)",
data: realtimeData.map(({ metrics }) => metrics.network.up), data: currentData.map(({ metrics }) => metrics.network.up),
color: "#ef4444", color: "#ef4444",
}, },
{ {
label: "Download (bps)", label: "Download (bps)",
data: realtimeData.map( data: currentData.map(
({ metrics }) => metrics.network.down, ({ metrics }) => metrics.network.down,
), ),
color: "#3b82f6", color: "#3b82f6",

View File

@@ -0,0 +1,25 @@
import type { WebsocketStatus } from "@/components/ConnectionStatus";
import type { StatusMessage } from "@/services/types";
import { initializeConnection } from "@/services/websocket";
import { useEffect, useState } from "react";
export function useWebsocket(url: string) {
const [status, setStatus] = useState<WebsocketStatus>();
const [message, setMessage] = useState<StatusMessage | null>();
useEffect(() => {
initializeConnection(url, {
onMessage: (data) => {
const parsed = JSON.parse(data);
setStatus("connected");
setMessage(parsed);
},
onOpen: () => setStatus("connecting"),
onClose: () => setStatus("disconnected"),
onError: () => setStatus("error"),
});
}, [url]);
return { status, message };
}

View File

@@ -1,51 +1,45 @@
import { import { ConnectionStatus } from "@/components/ConnectionStatus";
type WebsocketStatus,
ConnectionStatus,
} from "@/components/ConnectionStatus";
import type { StatusMessage } from "@/services/types";
import { Header } from "@/components/Header"; import { Header } from "@/components/Header";
import { DonutChart } from "@/components/DonutChart"; import { DonutChart } from "@/components/DonutChart";
import { LineChartCard } from "@/components/LineChartCard"; import { LineChartCard } from "@/components/LineChartCard";
import { MetricCard } from "@/components/MetricCard"; import { MetricCard } from "@/components/MetricCard";
import { SysinfoCard } from "@/components/SysinfoCard"; import { SysinfoCard } from "@/components/SysinfoCard";
import { useChartData } from "@/hooks/useChartData"; import { useChartData } from "@/hooks/useChartData";
import { getLastMessage, setLastMessage } from "@/services/store";
import { import {
formatBits, formatBits,
formatPercentage, formatPercentage,
calcPercentage, calcPercentage,
formatBytes, formatBytes,
} from "@/services/utils"; } from "@/services/utils";
import { initializeConnection } from "@/services/websocket";
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { useWebsocket } from "@/hooks/useWebsocket";
import { getHistoricalData, TimePeriod } from "@/services/store";
import { useEffect, useState } from "react";
import { StatusMessage } from "@/services/types";
import { TimePeriodSelector } from "@/components/TimePeriodSelector";
export function AgentPage() { export function AgentPage() {
const { agent } = useParams(); const { agent } = useParams();
const [status, setStatus] = useState<WebsocketStatus>(); const { status, message } = useWebsocket(`ws://localhost:3000/ws/${agent}`);
const [message, setMessage] = useState<StatusMessage | null>(
getLastMessage(), const [period, setPeriod] = useState<TimePeriod | "realtime">("all");
); const [history, setHistory] = useState<StatusMessage[] | null>(null);
useEffect(() => { useEffect(() => {
if (!agent) return; const fetchData = async () => {
const data = await getHistoricalData(
`http://localhost:3000/history/${agent}/${period}`,
);
setHistory(data);
};
initializeConnection(agent, { fetchData();
onMessage: (data) => { }, [agent, period]);
const parsed = JSON.parse(data);
setStatus("connected");
setMessage(parsed);
setLastMessage(parsed);
},
onOpen: () => setStatus("connecting"),
onClose: () => setStatus("disconnected"),
onError: () => setStatus("error"),
});
}, [agent]);
const { metrics } = message ?? {}; const { metrics } = message ?? {};
const { cpuData, memoryData, networkData } = useChartData(message); const { cpuData, memoryData, networkData } = useChartData(
period === "realtime" ? message! : history,
);
const getMetricStatus = (percentage: number | undefined) => { const getMetricStatus = (percentage: number | undefined) => {
if (!percentage) return "nil"; if (!percentage) return "nil";
@@ -86,6 +80,8 @@ export function AgentPage() {
/> />
<main className="max-w-7xl mx-auto py-8"> <main className="max-w-7xl mx-auto py-8">
<div className="mx-2"> <div className="mx-2">
<h2 className="text-xl font-semibold mb-4">Overview</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
<MetricCard <MetricCard
props={{ props={{
@@ -170,9 +166,21 @@ export function AgentPage() {
</div> </div>
</MetricCard> </MetricCard>
<div className="flex flex-col my-10">
<h2 className="text-xl font-semibold mb-4">Details</h2>
<div className="mt-3">
<TimePeriodSelector
currentPeriod={period}
onChange={setPeriod}
/>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 my-8"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 my-8">
<LineChartCard title="CPU Usage" data={cpuData} /> <LineChartCard title="CPU Usage" data={cpuData} />
<LineChartCard title="Memory Usage" data={memoryData} /> <LineChartCard
title="Memory Usage"
data={memoryData}
/>
<LineChartCard <LineChartCard
title="Network Activity" title="Network Activity"
data={networkData} data={networkData}
@@ -181,6 +189,7 @@ export function AgentPage() {
<SysinfoCard sysinfo={metrics?.system_info} /> <SysinfoCard sysinfo={metrics?.system_info} />
</div> </div>
</div> </div>
</div>
</main> </main>
</div> </div>
); );

View File

@@ -1,26 +1,28 @@
import type { StatusMessage } from "@/services/types"; import type { StatusMessage } from "@/services/types";
import { atom } from "nanostores";
const store = atom<StatusMessage | null>(null); export type TimePeriod = "all" | "hour" | "day" | "week" | "month";
export function getLastMessage(): StatusMessage | null { let data: StatusMessage[] = [];
return store.get();
}
export function setLastMessage(message: StatusMessage) {
store.set(message);
}
const realtimeData: StatusMessage[] = [];
const maxRealtimePoints = 50; const maxRealtimePoints = 50;
export function addDataPoint(value: StatusMessage) { export function addDataPoint(value: StatusMessage) {
realtimeData.push(value); data.push(value);
if (realtimeData.length > maxRealtimePoints) { if (data.length > maxRealtimePoints) {
realtimeData.shift(); data.shift();
} }
} }
export function getRealtimeData(): StatusMessage[] { export function getRealtimeData(): StatusMessage[] {
return realtimeData ?? []; return data ?? [];
}
export function setHistoricalData(value: StatusMessage[]) {
data = value;
}
export async function getHistoricalData(url: string): Promise<StatusMessage[]> {
return await fetch(url)
.then((res) => res.json())
.then((data) => data as StatusMessage[]);
} }

View File

@@ -5,8 +5,8 @@ type Callbacks = {
onClose?: () => void; onClose?: () => void;
}; };
export function initializeConnection(agent: string, callbacks: Callbacks) { export function initializeConnection(url: string, callbacks: Callbacks) {
const ws = new WebSocket(`ws://localhost:3000/ws/${agent}`); const ws = new WebSocket(url);
ws.onopen = () => callbacks.onOpen?.(); ws.onopen = () => callbacks.onOpen?.();
ws.onmessage = (event) => callbacks.onMessage(event.data); ws.onmessage = (event) => callbacks.onMessage(event.data);