Express API server on :3100 exposing all Coolify operations: - CRUD for apps, env vars, servers - Full upsert pipeline (create/update + env + route + deploy) - Drift detection, Traefik route management via SSH - Scalar API docs at /reference, OpenAPI 3.1 spec UI: New Coolify page with app cards, deploy/delete actions, env var expansion. Sidebar nav + React Query hooks + fetch client. Both UI and LLM/CLI use the same HTTP endpoints. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
93 lines
3.5 KiB
JavaScript
93 lines
3.5 KiB
JavaScript
import { useState } from 'react';
|
|
import { Rocket, Trash2, ChevronDown, ChevronRight, ExternalLink } from 'lucide-react';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import { StatusDot } from '@/components/shared/status-dot';
|
|
|
|
const buildpackColor = {
|
|
dockercompose: 'secondary',
|
|
nixpacks: 'outline',
|
|
dockerfile: 'outline',
|
|
};
|
|
|
|
function statusColor(status) {
|
|
if (!status) return 'gray';
|
|
const s = status.toLowerCase();
|
|
if (s.includes('running') || s.includes('healthy')) return 'green';
|
|
if (s.includes('stopped') || s.includes('exited')) return 'red';
|
|
if (s.includes('building') || s.includes('starting')) return 'yellow';
|
|
return 'gray';
|
|
}
|
|
|
|
export function CoolifyAppCard({ app, onDeploy, onDelete }) {
|
|
const [expanded, setExpanded] = useState(false);
|
|
|
|
return (
|
|
<Card className="overflow-hidden">
|
|
<CardHeader className="p-4 pb-2">
|
|
<div className="flex items-center justify-between">
|
|
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
|
<StatusDot color={statusColor(app.status)} />
|
|
{app.name}
|
|
</CardTitle>
|
|
<Badge variant={buildpackColor[app.build_pack] || 'outline'} className="text-[10px]">
|
|
{app.build_pack}
|
|
</Badge>
|
|
</div>
|
|
</CardHeader>
|
|
|
|
<CardContent className="p-4 pt-0 space-y-2">
|
|
{/* Port + domain */}
|
|
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
|
{app._host_port && <span>:{app._host_port}</span>}
|
|
{app.git_branch && <span>{app.git_branch}</span>}
|
|
{app.fqdn && (
|
|
<a
|
|
href={app.fqdn}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="flex items-center gap-1 text-primary hover:underline"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
{app.fqdn.replace(/^https?:\/\//, '')}
|
|
<ExternalLink className="h-3 w-3" />
|
|
</a>
|
|
)}
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="flex items-center gap-2 pt-1">
|
|
<Button variant="ghost" size="sm" className="h-7 text-xs gap-1" onClick={() => onDeploy(app)}>
|
|
<Rocket className="h-3 w-3" /> Deploy
|
|
</Button>
|
|
<Button variant="ghost" size="sm" className="h-7 text-xs gap-1 text-destructive" onClick={() => onDelete(app)}>
|
|
<Trash2 className="h-3 w-3" /> Delete
|
|
</Button>
|
|
<button
|
|
className="ml-auto text-xs text-muted-foreground hover:text-foreground flex items-center gap-1"
|
|
onClick={() => setExpanded(!expanded)}
|
|
>
|
|
{expanded ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
|
|
Envs
|
|
</button>
|
|
</div>
|
|
|
|
{/* Expandable env vars */}
|
|
{expanded && app._envs && (
|
|
<div className="rounded-md bg-secondary/50 p-2 text-[11px] font-mono space-y-0.5 max-h-40 overflow-auto">
|
|
{app._envs.length === 0 && <span className="text-muted-foreground">No env vars</span>}
|
|
{app._envs.map((env) => (
|
|
<div key={env.id || env.key} className="flex gap-2">
|
|
<span className="text-primary">{env.key}</span>
|
|
<span className="text-muted-foreground">=</span>
|
|
<span className="truncate">{env.value}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|