mirror of
https://github.com/csehviktor/status-monitor.git
synced 2025-08-08 18:06:14 +02:00
improve home page
This commit is contained in:
@@ -1,4 +1,9 @@
|
|||||||
|
import { UptimeMessage } from "@/services/types";
|
||||||
|
import { formatRelativeTime, isAgentOnline } from "@/services/utils";
|
||||||
|
import { DottedProgressBar } from "./Progressbar";
|
||||||
|
|
||||||
export type AgentOverviewCardProps = {
|
export type AgentOverviewCardProps = {
|
||||||
|
count: number;
|
||||||
title: string;
|
title: string;
|
||||||
icon: {
|
icon: {
|
||||||
ref: React.ReactNode;
|
ref: React.ReactNode;
|
||||||
@@ -19,7 +24,9 @@ export function AgentOverviewCard({
|
|||||||
<p className="text-sm text-gray-400 uppercase tracking-wider">
|
<p className="text-sm text-gray-400 uppercase tracking-wider">
|
||||||
{props.title}
|
{props.title}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-2xl font-bold text-white">0</p>
|
<p className="text-2xl font-bold text-white">
|
||||||
|
{props.count}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="w-12 h-12 rounded-lg flex items-center justify-center"
|
className="w-12 h-12 rounded-lg flex items-center justify-center"
|
||||||
@@ -34,3 +41,62 @@ export function AgentOverviewCard({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AgentCardProps = {
|
||||||
|
data: UptimeMessage;
|
||||||
|
onClick?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function AgentCard({ props }: { props: AgentCardProps }) {
|
||||||
|
const status = isAgentOnline(props.data)
|
||||||
|
? {
|
||||||
|
name: "online",
|
||||||
|
color: "bg-green-500",
|
||||||
|
textColor: "text-green-400",
|
||||||
|
borderColor: "border-green-500/20",
|
||||||
|
bgColor: "bg-green-500/10",
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
name: "offline",
|
||||||
|
color: "bg-red-500",
|
||||||
|
textColor: "text-red-400",
|
||||||
|
borderColor: "border-red-500/20",
|
||||||
|
bgColor: "bg-red-500/10",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={() => props.onClick?.()}
|
||||||
|
className="bg-[#111111] border border-[#262626] rounded-lg p-6 hover:bg-[#151515] transition-all cursor-pointer group"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div
|
||||||
|
className={`px-2 py-1 rounded-md text-xs font-medium ${status.bgColor} ${status.textColor} border ${status.borderColor}`}
|
||||||
|
>
|
||||||
|
{status.name.toUpperCase()}
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold text-white transition-colors">
|
||||||
|
{props.data.agent}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-3 flex-1 justify-end">
|
||||||
|
<div className="flex flex-col max-w-lg gap-y-2">
|
||||||
|
<DottedProgressBar
|
||||||
|
color={status.color}
|
||||||
|
percentage={props.data.uptime}
|
||||||
|
totalDots={20}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-between items-center text-xs">
|
||||||
|
<span>
|
||||||
|
{formatRelativeTime(props.data.last_seen)}
|
||||||
|
</span>
|
||||||
|
<span>{props.data.uptime.toFixed(2)}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
24
ui/src/components/Progressbar.tsx
Normal file
24
ui/src/components/Progressbar.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export function DottedProgressBar({
|
||||||
|
color,
|
||||||
|
percentage,
|
||||||
|
totalDots = 10,
|
||||||
|
}: {
|
||||||
|
color: string;
|
||||||
|
percentage: number;
|
||||||
|
totalDots?: number;
|
||||||
|
}) {
|
||||||
|
const filledDots = Math.round((percentage / 100) * totalDots);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center space-x-2">
|
||||||
|
{Array.from({ length: totalDots }).map((_, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`w-[14px] h-[14px] rounded-full transition-all duration-300 ${
|
||||||
|
index < filledDots ? color : "bg-[#232323]"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,27 @@
|
|||||||
import { AgentOverviewCard } from "@/components/AgentCard";
|
"use client";
|
||||||
|
|
||||||
|
import { AgentCard, AgentOverviewCard } from "@/components/AgentCard";
|
||||||
import { Header } from "@/components/Header";
|
import { Header } from "@/components/Header";
|
||||||
|
import { UptimeMessage } from "@/services/types";
|
||||||
|
import { isAgentOnline } from "@/services/utils";
|
||||||
import { Box } from "lucide-react";
|
import { Box } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export function HomePage() {
|
export function HomePage() {
|
||||||
|
const [agents, setAgents] = useState<UptimeMessage[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("http://localhost:3000/agents")
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
setAgents(data);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const totalAgents = agents.length;
|
||||||
|
const onlineAgents = agents.filter((agent) => isAgentOnline(agent)).length;
|
||||||
|
const offlineAgents = totalAgents - onlineAgents;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Header
|
<Header
|
||||||
@@ -13,9 +32,12 @@ export function HomePage() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<main className="max-w-7xl mx-auto py-8">
|
<main className="max-w-7xl mx-auto py-8">
|
||||||
|
<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">
|
||||||
<AgentOverviewCard
|
<AgentOverviewCard
|
||||||
props={{
|
props={{
|
||||||
|
count: totalAgents,
|
||||||
title: "total agents",
|
title: "total agents",
|
||||||
icon: {
|
icon: {
|
||||||
ref: <Box />,
|
ref: <Box />,
|
||||||
@@ -26,6 +48,7 @@ export function HomePage() {
|
|||||||
/>
|
/>
|
||||||
<AgentOverviewCard
|
<AgentOverviewCard
|
||||||
props={{
|
props={{
|
||||||
|
count: onlineAgents,
|
||||||
title: "online",
|
title: "online",
|
||||||
icon: {
|
icon: {
|
||||||
ref: (
|
ref: (
|
||||||
@@ -38,6 +61,7 @@ export function HomePage() {
|
|||||||
/>
|
/>
|
||||||
<AgentOverviewCard
|
<AgentOverviewCard
|
||||||
props={{
|
props={{
|
||||||
|
count: offlineAgents,
|
||||||
title: "offline",
|
title: "offline",
|
||||||
icon: {
|
icon: {
|
||||||
ref: (
|
ref: (
|
||||||
@@ -53,7 +77,11 @@ export function HomePage() {
|
|||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h2 className="text-xl font-semibold mb-4">Agents</h2>
|
<h2 className="text-xl font-semibold mb-4">Agents</h2>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"></div>
|
<div className="space-y-6">
|
||||||
|
{agents.map((agent, index) => (
|
||||||
|
<AgentCard key={index} props={{ data: agent }} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -54,5 +54,5 @@ export interface StatusMessage {
|
|||||||
export interface UptimeMessage {
|
export interface UptimeMessage {
|
||||||
agent: string;
|
agent: string;
|
||||||
uptime: number;
|
uptime: number;
|
||||||
last_seen: Date;
|
last_seen: string;
|
||||||
}
|
}
|
||||||
|
|||||||
28
ui/src/services/utils.ts
Normal file
28
ui/src/services/utils.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { UptimeMessage } from "@/services/types";
|
||||||
|
|
||||||
|
export function isAgentOnline(data: UptimeMessage): boolean {
|
||||||
|
const timeDiff = new Date().getTime() - new Date(data.last_seen).getTime();
|
||||||
|
|
||||||
|
return timeDiff < 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formatRelativeTime = (timestamp: string): string => {
|
||||||
|
const now = new Date();
|
||||||
|
const past = new Date(timestamp);
|
||||||
|
const diffInSeconds = Math.floor((now.getTime() - past.getTime()) / 1000);
|
||||||
|
|
||||||
|
if (diffInSeconds < 5) {
|
||||||
|
return "now";
|
||||||
|
} else if (diffInSeconds < 60) {
|
||||||
|
return `${diffInSeconds}s ago`;
|
||||||
|
} else if (diffInSeconds < 3600) {
|
||||||
|
const minutes = Math.floor(diffInSeconds / 60);
|
||||||
|
return `${minutes}m ago`;
|
||||||
|
} else if (diffInSeconds < 86400) {
|
||||||
|
const hours = Math.floor(diffInSeconds / 3600);
|
||||||
|
return `${hours}h ago`;
|
||||||
|
} else {
|
||||||
|
const days = Math.floor(diffInSeconds / 86400);
|
||||||
|
return `${days}d ago`;
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user