89 lines
3.5 KiB
TypeScript
89 lines
3.5 KiB
TypeScript
'use client';
|
|
|
|
import Link from 'next/link';
|
|
import { useEffect, useState } from 'react';
|
|
import { apiFetch } from '@/lib/api';
|
|
import { StatusPill } from '@/components/status-pill';
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
interface ServerRow {
|
|
id: string;
|
|
name: string;
|
|
slug: string;
|
|
status: string;
|
|
publicUrl: string | null;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export default function ServersPage() {
|
|
const [servers, setServers] = useState<ServerRow[] | null>(null);
|
|
const [q, setQ] = useState('');
|
|
|
|
useEffect(() => {
|
|
apiFetch<{ servers: ServerRow[] }>('/v1/servers').then((r) => setServers(r.servers));
|
|
}, []);
|
|
|
|
const filtered = servers?.filter((s) =>
|
|
q ? s.name.toLowerCase().includes(q.toLowerCase()) || s.slug.includes(q.toLowerCase()) : true,
|
|
);
|
|
|
|
return (
|
|
<div className="mx-auto max-w-7xl px-6 py-8">
|
|
<div className="flex items-baseline justify-between">
|
|
<div>
|
|
<h1 className="text-[22px] font-semibold tracking-tight">Servers</h1>
|
|
<p className="mt-1 text-[13px] text-[--color-fg-muted]">All your hosted MCP servers.</p>
|
|
</div>
|
|
<Link href="/servers/new">
|
|
<Button variant="primary" size="md">+ New server</Button>
|
|
</Link>
|
|
</div>
|
|
<div className="mt-6">
|
|
<input
|
|
value={q}
|
|
onChange={(e) => setQ(e.target.value)}
|
|
placeholder="Search by name or slug…"
|
|
className="h-8 w-72 rounded-md border border-[--color-border] bg-[--color-bg-subtle] px-2.5 text-[13px] placeholder:text-[--color-fg-subtle] focus:border-[--color-accent] focus:outline-none"
|
|
/>
|
|
</div>
|
|
<div className="panel mt-4">
|
|
{!filtered && <div className="px-4 py-4 text-[12.5px] text-[--color-fg-muted]">Loading…</div>}
|
|
{filtered && filtered.length === 0 && (
|
|
<div className="px-4 py-12 text-center text-[13px] text-[--color-fg-muted]">No servers match.</div>
|
|
)}
|
|
{filtered && filtered.length > 0 && (
|
|
<table className="w-full text-[12.5px]">
|
|
<thead className="border-b border-[--color-border] text-[--color-fg-subtle]">
|
|
<tr>
|
|
<th className="px-4 py-2 text-left font-medium">Name</th>
|
|
<th className="px-4 py-2 text-left font-medium">Slug</th>
|
|
<th className="px-4 py-2 text-left font-medium">Status</th>
|
|
<th className="px-4 py-2 text-left font-medium">URL</th>
|
|
<th className="px-4 py-2 text-left font-medium">Updated</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{filtered.map((s) => (
|
|
<tr key={s.id} className="border-b border-[--color-border] last:border-0 hover:bg-[--color-bg-subtle]">
|
|
<td className="px-4 py-2.5">
|
|
<Link href={`/servers/${s.id}`} className="font-medium hover:text-[--color-accent]">
|
|
{s.name}
|
|
</Link>
|
|
</td>
|
|
<td className="mono px-4 py-2.5 text-[--color-fg-muted]">{s.slug}</td>
|
|
<td className="px-4 py-2.5"><StatusPill status={s.status as never} /></td>
|
|
<td className="mono px-4 py-2.5 text-[--color-fg-muted]">{s.publicUrl ?? '—'}</td>
|
|
<td className="px-4 py-2.5 text-[--color-fg-muted]">
|
|
{new Date(s.updatedAt).toLocaleString()}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|