225 lines
7.0 KiB
TypeScript
225 lines
7.0 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useMemo } from "react";
|
|
import { usePoll } from "@/lib/use-poll";
|
|
import type { Container, OverviewStats } from "@/lib/types";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { DataTable, Column } from "@/components/data-table";
|
|
import { ContainerLogsModal } from "@/components/container-logs-modal";
|
|
import { StatusBadge } from "@/components/status-badge";
|
|
import { postAPI } from "@/lib/api";
|
|
|
|
interface OlaresPod {
|
|
name: string;
|
|
namespace: string;
|
|
status: string;
|
|
restarts: number;
|
|
age: string;
|
|
}
|
|
|
|
export default function InfrastructurePage() {
|
|
const { data: containers } = usePoll<Container[]>(
|
|
"/api/containers",
|
|
30000
|
|
);
|
|
const { data: overview } = usePoll<OverviewStats>(
|
|
"/api/stats/overview",
|
|
60000
|
|
);
|
|
const { data: pods } = usePoll<OlaresPod[]>("/api/olares/pods", 30000);
|
|
|
|
const [logsTarget, setLogsTarget] = useState<{
|
|
id: string;
|
|
name: string;
|
|
endpoint: string;
|
|
} | null>(null);
|
|
|
|
const endpoints = useMemo(() => {
|
|
if (!containers) return [];
|
|
return [...new Set(containers.map((c) => c.endpoint))];
|
|
}, [containers]);
|
|
|
|
const containerColumns: Column<Container>[] = [
|
|
{
|
|
key: "name",
|
|
label: "Name",
|
|
render: (row) => (
|
|
<span className="font-medium text-foreground">{row.name}</span>
|
|
),
|
|
},
|
|
{
|
|
key: "state",
|
|
label: "State",
|
|
render: (row) => (
|
|
<StatusBadge
|
|
color={row.state === "running" ? "green" : "red"}
|
|
label={row.state}
|
|
/>
|
|
),
|
|
},
|
|
{ key: "status", label: "Status" },
|
|
{ key: "endpoint", label: "Endpoint" },
|
|
{
|
|
key: "image",
|
|
label: "Image",
|
|
render: (row) => (
|
|
<span className="truncate max-w-[200px] block">{row.image}</span>
|
|
),
|
|
},
|
|
];
|
|
|
|
const podColumns: Column<OlaresPod>[] = [
|
|
{ key: "name", label: "Pod" },
|
|
{ key: "namespace", label: "Namespace" },
|
|
{
|
|
key: "status",
|
|
label: "Status",
|
|
render: (row) => (
|
|
<StatusBadge
|
|
color={row.status === "Running" ? "green" : "amber"}
|
|
label={row.status}
|
|
/>
|
|
),
|
|
},
|
|
{ key: "restarts", label: "Restarts" },
|
|
{ key: "age", label: "Age" },
|
|
];
|
|
|
|
const gpu = overview?.gpu;
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
<h1 className="text-lg font-semibold">Infrastructure</h1>
|
|
|
|
{/* Container Table */}
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm font-medium">Containers</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<DataTable<Container>
|
|
data={containers ?? []}
|
|
columns={containerColumns}
|
|
searchKey="name"
|
|
filterKey="endpoint"
|
|
filterOptions={endpoints}
|
|
actions={(row) => (
|
|
<div className="flex gap-1">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-6 text-[10px] px-2 border-white/[0.08] hover:bg-white/[0.06]"
|
|
onClick={() =>
|
|
setLogsTarget({
|
|
id: row.id,
|
|
name: row.name,
|
|
endpoint: row.endpoint,
|
|
})
|
|
}
|
|
>
|
|
Logs
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-6 text-[10px] px-2 border-white/[0.08] hover:bg-white/[0.06]"
|
|
onClick={() =>
|
|
postAPI(
|
|
`/api/containers/${row.endpoint}/${row.id}/restart`
|
|
)
|
|
}
|
|
>
|
|
Restart
|
|
</Button>
|
|
</div>
|
|
)}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Row 2: Olares Pods + GPU */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm font-medium">Olares Pods</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<DataTable<OlaresPod>
|
|
data={pods ?? []}
|
|
columns={podColumns}
|
|
searchKey="name"
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-sm font-medium">GPU Status</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{!gpu ? (
|
|
<p className="text-xs text-muted-foreground">Loading...</p>
|
|
) : gpu.available ? (
|
|
<>
|
|
<p className="text-sm font-medium text-foreground">
|
|
{gpu.name}
|
|
</p>
|
|
{gpu.vram_used_mb != null && gpu.vram_total_mb != null && (
|
|
<div>
|
|
<div className="flex justify-between text-xs text-muted-foreground mb-1.5">
|
|
<span>VRAM</span>
|
|
<span>
|
|
{(gpu.vram_used_mb / 1024).toFixed(1)} /{" "}
|
|
{(gpu.vram_total_mb / 1024).toFixed(1)} GB
|
|
</span>
|
|
</div>
|
|
<div className="glass-bar-track h-3">
|
|
<div
|
|
className="h-full glass-bar-fill bg-gradient-to-r from-blue-500 to-violet-500 transition-all duration-700"
|
|
style={{
|
|
width: `${(gpu.vram_used_mb / gpu.vram_total_mb) * 100}%`,
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div className="grid grid-cols-3 gap-4 text-xs">
|
|
<div>
|
|
<p className="text-muted-foreground/70">Temperature</p>
|
|
<p className="text-foreground text-lg font-semibold stat-glow">
|
|
{gpu.temp_c ?? "\u2014"}°C
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-muted-foreground/70">Power</p>
|
|
<p className="text-foreground text-lg font-semibold stat-glow">
|
|
{gpu.power_w ?? "\u2014"}W
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-muted-foreground/70">Utilization</p>
|
|
<p className="text-foreground text-lg font-semibold stat-glow">
|
|
{gpu.utilization_pct ?? "\u2014"}%
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<p className="text-xs text-muted-foreground">GPU not available</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Logs Modal */}
|
|
<ContainerLogsModal
|
|
containerId={logsTarget?.id ?? null}
|
|
containerName={logsTarget?.name ?? ""}
|
|
endpoint={logsTarget?.endpoint ?? ""}
|
|
onClose={() => setLogsTarget(null)}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|