first commit
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user