Files
dockship/src/app/deployments/page.tsx
T
2026-02-15 17:40:24 +01:00

161 lines
5.3 KiB
TypeScript

"use client";
import { useMemo } from "react";
import { DashboardLayout } from "@/components/dashboard/DashboardLayout";
import {
Card,
CardContent,
CardDescription,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Plus, Play, Pause, Square, RotateCcw } from "lucide-react";
import {
getDeployments,
removeDeployment,
toggleDeployment,
} from "@/lib/dockship/deploy";
import { getApps } from "@/lib/dockship/application";
import { cn } from "@/lib/utils";
import DeployAppDialog from "@/components/dialogs/DeployApp";
import { useAsync, useAsyncInterval } from "@/hooks/use-async";
import RemoveDialog from "@/components/dialogs/Remove";
import { Application } from "@/lib/types";
const statusDot = {
running: "bg-chart-2",
pending: "bg-chart-1",
stopped: "bg-slate-400 dark:bg-slate-600",
} as const;
export default function DeploymentsPage() {
const { value: apps } = useAsyncInterval("apps", [], getApps, 5000);
const { value: deployments, loading } = useAsync(
"deployments",
[],
getDeployments,
[apps],
);
const appByContainer = useMemo(() => {
const map = new Map<string, Application>();
for (const app of apps) {
for (const container of app.deployments) {
map.set(container, app);
}
}
return map;
}, [apps]);
const changeDeploymentStatus = async (
deployId: string,
action: "start" | "stop",
) => {
console.log(`Toggling deployment ${deployId} with action ${action}`);
await toggleDeployment(deployId, action);
};
const remove = async (deployId: string) => {
console.log(`Removing deployment ${deployId}`);
await removeDeployment(deployId);
};
return (
<DashboardLayout
title="Deployments"
subtitle="Deployment history and management"
>
{/* Header */}
<div className="flex items-center justify-between mb-6">
<Badge>
{loading
? "Loading…"
: `${deployments.length} deployment${deployments.length > 0 && "s"}`}
</Badge>
<DeployAppDialog>
<Button size="sm" className="gap-2">
<Plus className="w-4 h-4" />
New Deployment
</Button>
</DeployAppDialog>
</div>
<div className="divide-y divide-border flex flex-col-reverse gap-5">
{deployments.map((dep) => {
return (
<Card key={`${dep.image}-${dep.container ?? "latest"}`}>
<CardContent>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span
className={cn(
"size-2 rounded-full animate-pulse",
statusDot[dep.status as keyof typeof statusDot] ??
statusDot.stopped,
)}
/>
<CardTitle>
{dep.name} @{" "}
<span className="text-primary-foreground">
{appByContainer.get(dep.container)?.name ??
"Unknown app"}
</span>
</CardTitle>
<CardDescription className="flex gap-3">
{dep.ports.map(([to, from]) => (
<span key={`${from}-${to}`}>
{from} → {to}
</span>
))}
</CardDescription>
</div>
<p className="hidden xl:block text-muted-foreground">
{dep.image}
</p>
<div className="flex justify-end gap-1">
<Button
variant="ghost"
size="icon-sm"
onClick={async () =>
await changeDeploymentStatus(
dep.container,
dep.status === "stopped" ? "start" : "stop",
)
}
>
{dep.status === "stopped" ? (
<Play className="w-3 h-3" />
) : dep.status === "running" ? (
<Square className="w-3 h-3" />
) : (
<Square className="w-3 h-3 text-primary" />
)}
</Button>
<DeployAppDialog
defaultApp={appByContainer.get(dep.container)?.id}
ports={dep.ports.map(([to, from]) => ({
containerPort: from.toString(),
hostPort: to.toString(),
protocol: "tcp",
}))}
nodeId={dep.nodeId}
>
<Button variant="ghost" size="icon-sm">
<RotateCcw className="w-3 h-3" />
</Button>
</DeployAppDialog>
<RemoveDialog remove={() => remove(dep.container)} />
</div>
</div>
</CardContent>
</Card>
);
})}
</div>
</DashboardLayout>
);
}