- Sidebar Infrastructure replaced with 6 named rows linking to /infrastructure?tab= - New /infrastructure page with left sub-nav and per-tab content panels: Builds — lists deployed Coolify apps with live status Databases — coming soon placeholder Services — coming soon placeholder Environment — variable table with masked values (scaffold) Domains — lists configured domains with SSL status Logs — dark terminal panel, ready to stream - Dim state on rows reflects whether data exists (e.g. no domains = dim) Made-with: Cursor
354 lines
15 KiB
TypeScript
354 lines
15 KiB
TypeScript
"use client";
|
|
|
|
import { Suspense, useState, useEffect } from "react";
|
|
import { useParams, useSearchParams, useRouter } from "next/navigation";
|
|
|
|
// ── Types ─────────────────────────────────────────────────────────────────────
|
|
|
|
interface InfraApp {
|
|
name: string;
|
|
domain?: string | null;
|
|
coolifyServiceUuid?: string | null;
|
|
}
|
|
|
|
interface ProjectData {
|
|
giteaRepo?: string;
|
|
giteaRepoUrl?: string;
|
|
apps?: InfraApp[];
|
|
}
|
|
|
|
// ── Tab definitions ───────────────────────────────────────────────────────────
|
|
|
|
const TABS = [
|
|
{ id: "builds", label: "Builds", icon: "⬡" },
|
|
{ id: "databases", label: "Databases", icon: "◫" },
|
|
{ id: "services", label: "Services", icon: "◎" },
|
|
{ id: "environment", label: "Environment", icon: "≡" },
|
|
{ id: "domains", label: "Domains", icon: "◬" },
|
|
{ id: "logs", label: "Logs", icon: "≈" },
|
|
] as const;
|
|
|
|
type TabId = typeof TABS[number]["id"];
|
|
|
|
// ── Shared empty state ────────────────────────────────────────────────────────
|
|
|
|
function ComingSoonPanel({ icon, title, description }: { icon: string; title: string; description: string }) {
|
|
return (
|
|
<div style={{
|
|
flex: 1, display: "flex", flexDirection: "column",
|
|
alignItems: "center", justifyContent: "center",
|
|
padding: 60, textAlign: "center", gap: 16,
|
|
}}>
|
|
<div style={{
|
|
width: 56, height: 56, borderRadius: 14, background: "#f0ece4",
|
|
display: "flex", alignItems: "center", justifyContent: "center",
|
|
fontSize: "1.5rem", color: "#b5b0a6",
|
|
}}>
|
|
{icon}
|
|
</div>
|
|
<div>
|
|
<div style={{ fontSize: "1rem", fontWeight: 600, color: "#1a1a1a", marginBottom: 6 }}>{title}</div>
|
|
<div style={{ fontSize: "0.82rem", color: "#a09a90", maxWidth: 340, lineHeight: 1.6 }}>{description}</div>
|
|
</div>
|
|
<div style={{
|
|
marginTop: 8, padding: "8px 18px",
|
|
background: "#1a1a1a", color: "#fff",
|
|
borderRadius: 7, fontSize: "0.78rem", fontWeight: 500,
|
|
opacity: 0.4, cursor: "default",
|
|
}}>
|
|
Coming soon
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ── Builds tab ────────────────────────────────────────────────────────────────
|
|
|
|
function BuildsTab({ project }: { project: ProjectData | null }) {
|
|
const apps = project?.apps ?? [];
|
|
if (apps.length === 0) {
|
|
return (
|
|
<ComingSoonPanel
|
|
icon="⬡"
|
|
title="No deployments yet"
|
|
description="Once your apps are deployed via Coolify, build history and deployment logs will appear here."
|
|
/>
|
|
);
|
|
}
|
|
return (
|
|
<div style={{ padding: 32, maxWidth: 720 }}>
|
|
<div style={{ fontSize: "0.72rem", fontWeight: 700, color: "#a09a90", letterSpacing: "0.08em", textTransform: "uppercase", marginBottom: 16 }}>
|
|
Deployed Apps
|
|
</div>
|
|
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
|
{apps.map(app => (
|
|
<div key={app.name} style={{
|
|
background: "#fff", border: "1px solid #e8e4dc", borderRadius: 10,
|
|
padding: "14px 18px", display: "flex", alignItems: "center", justifyContent: "space-between",
|
|
}}>
|
|
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
|
<span style={{ fontSize: "0.85rem", color: "#a09a90" }}>⬡</span>
|
|
<div>
|
|
<div style={{ fontSize: "0.82rem", fontWeight: 600, color: "#1a1a1a" }}>{app.name}</div>
|
|
{app.domain && (
|
|
<div style={{ fontSize: "0.72rem", color: "#a09a90", marginTop: 2 }}>{app.domain}</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
<span style={{ width: 7, height: 7, borderRadius: "50%", background: "#2e7d32", display: "inline-block" }} />
|
|
<span style={{ fontSize: "0.73rem", color: "#6b6560" }}>Running</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ── Databases tab ─────────────────────────────────────────────────────────────
|
|
|
|
function DatabasesTab() {
|
|
return (
|
|
<ComingSoonPanel
|
|
icon="◫"
|
|
title="Databases"
|
|
description="Provision and manage PostgreSQL, Redis, and other databases for your project. Connection strings and credentials will be auto-injected into your environment."
|
|
/>
|
|
);
|
|
}
|
|
|
|
// ── Services tab ──────────────────────────────────────────────────────────────
|
|
|
|
function ServicesTab() {
|
|
return (
|
|
<ComingSoonPanel
|
|
icon="◎"
|
|
title="Services"
|
|
description="Background workers, email delivery, queues, file storage, and third-party integrations will be configured and monitored here."
|
|
/>
|
|
);
|
|
}
|
|
|
|
// ── Environment tab ───────────────────────────────────────────────────────────
|
|
|
|
function EnvironmentTab({ project }: { project: ProjectData | null }) {
|
|
return (
|
|
<div style={{ padding: 32, maxWidth: 720 }}>
|
|
<div style={{ fontSize: "0.72rem", fontWeight: 700, color: "#a09a90", letterSpacing: "0.08em", textTransform: "uppercase", marginBottom: 16 }}>
|
|
Environment Variables & Secrets
|
|
</div>
|
|
<div style={{
|
|
background: "#fff", border: "1px solid #e8e4dc", borderRadius: 10,
|
|
overflow: "hidden", marginBottom: 20,
|
|
}}>
|
|
{/* Header row */}
|
|
<div style={{
|
|
display: "grid", gridTemplateColumns: "1fr 1fr auto",
|
|
padding: "10px 18px", background: "#faf8f5",
|
|
borderBottom: "1px solid #e8e4dc",
|
|
fontSize: "0.68rem", fontWeight: 700, color: "#a09a90",
|
|
letterSpacing: "0.06em", textTransform: "uppercase",
|
|
}}>
|
|
<span>Key</span><span>Value</span><span />
|
|
</div>
|
|
{/* Placeholder rows */}
|
|
{["DATABASE_URL", "NEXTAUTH_SECRET", "GITEA_API_TOKEN"].map(k => (
|
|
<div key={k} style={{
|
|
display: "grid", gridTemplateColumns: "1fr 1fr auto",
|
|
padding: "11px 18px", borderBottom: "1px solid #f0ece4",
|
|
alignItems: "center",
|
|
}}>
|
|
<span style={{ fontFamily: "IBM Plex Mono, monospace", fontSize: "0.73rem", color: "#1a1a1a" }}>{k}</span>
|
|
<span style={{ fontFamily: "IBM Plex Mono, monospace", fontSize: "0.73rem", color: "#b5b0a6", letterSpacing: 2 }}>••••••••</span>
|
|
<button style={{ background: "none", border: "none", cursor: "pointer", color: "#a09a90", fontSize: "0.72rem", padding: "2px 6px" }}>Edit</button>
|
|
</div>
|
|
))}
|
|
<div style={{ padding: "11px 18px", borderTop: "1px solid #f0ece4" }}>
|
|
<button style={{
|
|
background: "none", border: "1px dashed #d4cfc8", borderRadius: 6,
|
|
padding: "6px 14px", fontSize: "0.75rem", color: "#a09a90",
|
|
cursor: "pointer", width: "100%",
|
|
}}>
|
|
+ Add variable
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div style={{ fontSize: "0.75rem", color: "#b5b0a6", lineHeight: 1.6 }}>
|
|
Variables are encrypted at rest and auto-injected into deployed containers. Secrets are never exposed in logs.
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ── Domains tab ───────────────────────────────────────────────────────────────
|
|
|
|
function DomainsTab({ project }: { project: ProjectData | null }) {
|
|
const apps = (project?.apps ?? []).filter(a => a.domain);
|
|
return (
|
|
<div style={{ padding: 32, maxWidth: 720 }}>
|
|
<div style={{ fontSize: "0.72rem", fontWeight: 700, color: "#a09a90", letterSpacing: "0.08em", textTransform: "uppercase", marginBottom: 16 }}>
|
|
Domains & SSL
|
|
</div>
|
|
{apps.length > 0 ? (
|
|
<div style={{ display: "flex", flexDirection: "column", gap: 10, marginBottom: 20 }}>
|
|
{apps.map(app => (
|
|
<div key={app.name} style={{
|
|
background: "#fff", border: "1px solid #e8e4dc", borderRadius: 10,
|
|
padding: "14px 18px", display: "flex", alignItems: "center", justifyContent: "space-between",
|
|
}}>
|
|
<div>
|
|
<div style={{ fontFamily: "IBM Plex Mono, monospace", fontSize: "0.8rem", color: "#1a1a1a", fontWeight: 500 }}>
|
|
{app.domain}
|
|
</div>
|
|
<div style={{ fontSize: "0.7rem", color: "#a09a90", marginTop: 3 }}>{app.name}</div>
|
|
</div>
|
|
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
<span style={{ width: 7, height: 7, borderRadius: "50%", background: "#2e7d32", display: "inline-block" }} />
|
|
<span style={{ fontSize: "0.73rem", color: "#6b6560" }}>SSL active</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div style={{
|
|
background: "#fff", border: "1px dashed #d4cfc8", borderRadius: 10,
|
|
padding: "32px 24px", textAlign: "center", marginBottom: 20,
|
|
}}>
|
|
<div style={{ fontSize: "0.82rem", color: "#a09a90" }}>No custom domains configured</div>
|
|
<div style={{ fontSize: "0.73rem", color: "#b5b0a6", marginTop: 6 }}>Deploy an app first, then point a domain here.</div>
|
|
</div>
|
|
)}
|
|
<button style={{
|
|
background: "#1a1a1a", color: "#fff", border: "none",
|
|
borderRadius: 8, padding: "9px 20px",
|
|
fontSize: "0.78rem", fontWeight: 500, cursor: "pointer",
|
|
opacity: 0.5,
|
|
}}>
|
|
+ Add domain
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ── Logs tab ──────────────────────────────────────────────────────────────────
|
|
|
|
function LogsTab({ project }: { project: ProjectData | null }) {
|
|
const apps = project?.apps ?? [];
|
|
if (apps.length === 0) {
|
|
return (
|
|
<ComingSoonPanel
|
|
icon="≈"
|
|
title="No logs yet"
|
|
description="Runtime logs, request traces, and error reports from your deployed services will stream here."
|
|
/>
|
|
);
|
|
}
|
|
return (
|
|
<div style={{ padding: 32, maxWidth: 900 }}>
|
|
<div style={{ fontSize: "0.72rem", fontWeight: 700, color: "#a09a90", letterSpacing: "0.08em", textTransform: "uppercase", marginBottom: 16 }}>
|
|
Runtime Logs
|
|
</div>
|
|
<div style={{
|
|
background: "#1e1e1e", borderRadius: 10, padding: "20px 24px",
|
|
fontFamily: "IBM Plex Mono, monospace", fontSize: "0.73rem", color: "#d4d4d4",
|
|
lineHeight: 1.6, minHeight: 200,
|
|
}}>
|
|
<div style={{ color: "#6a9955" }}>{"# Logs will stream here once connected to Coolify"}</div>
|
|
<div style={{ color: "#569cd6", marginTop: 8 }}>{"→ Select a service to tail its log output"}</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ── Inner page ────────────────────────────────────────────────────────────────
|
|
|
|
function InfrastructurePageInner() {
|
|
const params = useParams();
|
|
const searchParams = useSearchParams();
|
|
const router = useRouter();
|
|
const projectId = params.projectId as string;
|
|
const workspace = params.workspace as string;
|
|
|
|
const activeTab = (searchParams.get("tab") ?? "builds") as TabId;
|
|
const [project, setProject] = useState<ProjectData | null>(null);
|
|
|
|
useEffect(() => {
|
|
fetch(`/api/projects/${projectId}/apps`)
|
|
.then(r => r.json())
|
|
.then(d => setProject({ apps: d.apps ?? [], giteaRepo: d.giteaRepo, giteaRepoUrl: d.giteaRepoUrl }))
|
|
.catch(() => {});
|
|
}, [projectId]);
|
|
|
|
const setTab = (id: TabId) => {
|
|
router.push(`/${workspace}/project/${projectId}/infrastructure?tab=${id}`, { scroll: false });
|
|
};
|
|
|
|
return (
|
|
<div style={{ display: "flex", height: "100%", fontFamily: "Outfit, sans-serif", overflow: "hidden" }}>
|
|
|
|
{/* ── Left sub-nav ── */}
|
|
<div style={{
|
|
width: 190, flexShrink: 0,
|
|
borderRight: "1px solid #e8e4dc",
|
|
background: "#faf8f5",
|
|
display: "flex", flexDirection: "column",
|
|
padding: "16px 8px",
|
|
gap: 2,
|
|
overflow: "auto",
|
|
}}>
|
|
<div style={{
|
|
fontSize: "0.6rem", fontWeight: 700, color: "#b5b0a6",
|
|
letterSpacing: "0.1em", textTransform: "uppercase",
|
|
padding: "0 8px 10px",
|
|
}}>
|
|
Infrastructure
|
|
</div>
|
|
{TABS.map(tab => {
|
|
const active = activeTab === tab.id;
|
|
return (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => setTab(tab.id)}
|
|
style={{
|
|
display: "flex", alignItems: "center", gap: 9,
|
|
padding: "7px 10px", borderRadius: 6,
|
|
background: active ? "#f0ece4" : "transparent",
|
|
border: "none", cursor: "pointer", width: "100%", textAlign: "left",
|
|
color: active ? "#1a1a1a" : "#6b6560",
|
|
fontSize: "0.8rem", fontWeight: active ? 600 : 450,
|
|
transition: "background 0.1s",
|
|
}}
|
|
onMouseEnter={e => { if (!active) (e.currentTarget as HTMLElement).style.background = "#f6f4f0"; }}
|
|
onMouseLeave={e => { if (!active) (e.currentTarget as HTMLElement).style.background = "transparent"; }}
|
|
>
|
|
<span style={{ fontSize: "0.75rem", opacity: 0.65, width: 16, textAlign: "center" }}>{tab.icon}</span>
|
|
{tab.label}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* ── Content ── */}
|
|
<div style={{ flex: 1, overflow: "auto", display: "flex", flexDirection: "column" }}>
|
|
{activeTab === "builds" && <BuildsTab project={project} />}
|
|
{activeTab === "databases" && <DatabasesTab />}
|
|
{activeTab === "services" && <ServicesTab />}
|
|
{activeTab === "environment" && <EnvironmentTab project={project} />}
|
|
{activeTab === "domains" && <DomainsTab project={project} />}
|
|
{activeTab === "logs" && <LogsTab project={project} />}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ── Export ────────────────────────────────────────────────────────────────────
|
|
|
|
export default function InfrastructurePage() {
|
|
return (
|
|
<Suspense fallback={<div style={{ display: "flex", height: "100%", alignItems: "center", justifyContent: "center", color: "#a09a90", fontFamily: "Outfit, sans-serif", fontSize: "0.85rem" }}>Loading…</div>}>
|
|
<InfrastructurePageInner />
|
|
</Suspense>
|
|
);
|
|
}
|