140 lines
5.0 KiB
TypeScript
140 lines
5.0 KiB
TypeScript
"use client";
|
|
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import Link from "next/link";
|
|
import { api, Server, ServerStatus } from "@/lib/api";
|
|
import { Badge, Button, Card } from "@/components/ui";
|
|
import { Table, Thead, Tbody, Tr, Th, Td } from "@/components/ui";
|
|
|
|
function statusVariant(status: ServerStatus) {
|
|
switch (status) {
|
|
case "active":
|
|
return "success";
|
|
case "pending":
|
|
return "warning";
|
|
case "offline":
|
|
return "danger";
|
|
}
|
|
}
|
|
|
|
function formatLastSeen(dateStr: string): string {
|
|
const date = new Date(dateStr);
|
|
const now = new Date();
|
|
const diffMs = now.getTime() - date.getTime();
|
|
const diffSec = Math.floor(diffMs / 1000);
|
|
const diffMin = Math.floor(diffSec / 60);
|
|
const diffHour = Math.floor(diffMin / 60);
|
|
const diffDay = Math.floor(diffHour / 24);
|
|
|
|
if (diffSec < 60) return `${diffSec}s ago`;
|
|
if (diffMin < 60) return `${diffMin}m ago`;
|
|
if (diffHour < 24) return `${diffHour}h ago`;
|
|
return `${diffDay}d ago`;
|
|
}
|
|
|
|
export default function ServersPage() {
|
|
const { data: servers, isLoading, error } = useQuery({
|
|
queryKey: ["servers"],
|
|
queryFn: api.listServers,
|
|
refetchInterval: 30_000,
|
|
});
|
|
|
|
return (
|
|
<div className="p-8">
|
|
<div className="mb-6 flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-text-primary">Servers</h1>
|
|
<p className="mt-1 text-sm text-text-secondary">
|
|
{servers?.length ?? 0} registered server{servers?.length !== 1 ? "s" : ""}
|
|
</p>
|
|
</div>
|
|
<Link href="/servers/new">
|
|
<Button variant="primary">
|
|
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
|
</svg>
|
|
Add Server
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
|
|
<Card padding={false}>
|
|
{isLoading ? (
|
|
<div className="flex items-center justify-center py-20">
|
|
<div className="h-8 w-8 animate-spin rounded-full border-2 border-border border-t-accent" />
|
|
</div>
|
|
) : error ? (
|
|
<div className="py-20 text-center text-danger">
|
|
Failed to load servers. Is the backend running?
|
|
</div>
|
|
) : servers && servers.length > 0 ? (
|
|
<Table>
|
|
<Thead>
|
|
<Tr>
|
|
<Th>Hostname</Th>
|
|
<Th>IP Address</Th>
|
|
<Th>OS</Th>
|
|
<Th>Status</Th>
|
|
<Th>Last Seen</Th>
|
|
<Th />
|
|
</Tr>
|
|
</Thead>
|
|
<Tbody>
|
|
{servers.map((server: Server) => (
|
|
<Tr key={server.server_id}>
|
|
<Td>
|
|
<span className="font-medium text-text-primary">
|
|
{server.hostname}
|
|
</span>
|
|
</Td>
|
|
<Td>
|
|
<span className="font-mono text-text-secondary">
|
|
{server.ip_address}
|
|
</span>
|
|
</Td>
|
|
<Td>
|
|
<span className="text-text-secondary">{server.os_info}</span>
|
|
</Td>
|
|
<Td>
|
|
<Badge variant={statusVariant(server.status)}>
|
|
{server.status}
|
|
</Badge>
|
|
</Td>
|
|
<Td>
|
|
<span className="text-text-secondary">
|
|
{server.last_seen
|
|
? formatLastSeen(server.last_seen)
|
|
: "Never"}
|
|
</span>
|
|
</Td>
|
|
<Td>
|
|
<Link href={`/servers/${server.server_id}`}>
|
|
<Button variant="ghost" size="sm">
|
|
View →
|
|
</Button>
|
|
</Link>
|
|
</Td>
|
|
</Tr>
|
|
))}
|
|
</Tbody>
|
|
</Table>
|
|
) : (
|
|
<div className="py-20 text-center">
|
|
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-surface-2">
|
|
<svg className="h-6 w-6 text-text-secondary" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M5.25 14.25h13.5m-13.5 0a3 3 0 01-3-3m3 3a3 3 0 100 6h13.5a3 3 0 100-6m-16.5-3a3 3 0 013-3h13.5a3 3 0 013 3m-19.5 0a4.5 4.5 0 01.9-2.7L5.737 5.1a3.375 3.375 0 012.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 01.9 2.7m0 0a3 3 0 01-3 3m0 3h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008zm-3 6h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008z" />
|
|
</svg>
|
|
</div>
|
|
<p className="text-text-secondary">No servers registered yet.</p>
|
|
<Link href="/servers/new">
|
|
<Button variant="primary" size="sm" className="mt-4">
|
|
Add your first server
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|