feat: refine project container detail panels

This commit is contained in:
KodekaTeam committed 2026-06-14 01:31:46 +07:00
1 parent 2815de9e14
commit cb77dab219
12 files changed
+118 -441

No files matched your search

@@ -2,6 +2,7 @@ import SearchField from "@/components/SearchField";
import {
Box,
ChevronDown,
CloudSyncIcon,
Database,
Loader2,
Plus,
@@ -163,6 +164,23 @@ export default function EnvironmentContainersToolbar({
<Database size={14} style={{ color: "var(--accent-green)" }} />
Deploy Database
</button>
<button
type="button"
role="menuitem"
className="btn btn-ghost"
onClick={handleDeployDatabase}
style={{
justifyContent: "flex-start",
fontSize: 12,
color: "var(--text-secondary)",
}}
>
<CloudSyncIcon
size={14}
style={{ color: "var(--accent-red)" }}
/>
Import form Sync
</button>
</div>
) : null}
</div>
@@ -12,7 +12,9 @@ interface AdvancedSummaryCardProps {
item: AdvancedSummary;
}
export default function AdvancedSummaryCard({ item }: AdvancedSummaryCardProps) {
export default function AdvancedSummaryCard({
item,
}: AdvancedSummaryCardProps) {
return (
<section
className="card"
@@ -1,15 +1,11 @@
import {
AlertTriangle,
Clock3,
Cpu,
Database,
Loader2,
RefreshCcw,
Settings2,
ShieldCheck,
ToggleLeft,
ToggleRight,
Trash2,
} from "lucide-react";
import type {
AdvancedAuditEvent,
@@ -231,7 +227,7 @@ export default function AdvancedTabPanel({
</PanelShell>
</div>
<div
{/* <div
style={{
display: "grid",
gridTemplateColumns:
@@ -255,13 +251,13 @@ export default function AdvancedTabPanel({
))}
</div>
</PanelShell>
</div>
</div> */}
<div
{/* <div
style={{
display: "grid",
gridTemplateColumns:
"minmax(min(620px, 100%), 1.2fr) minmax(min(360px, 100%), 0.8fr)",
"minmax(min(620px, 100%), 1.2fr) minmax(min(360px, 100%), 1.2fr)",
gap: 12,
alignItems: "start",
}}
@@ -271,7 +267,7 @@ export default function AdvancedTabPanel({
style={{
display: "grid",
gridTemplateColumns:
"repeat(auto-fit, minmax(min(180px, 100%), 1fr))",
"repeat(auto-fit, minmax(min(280px, 100%), 1fr))",
gap: 10,
}}
>
@@ -355,90 +351,7 @@ export default function AdvancedTabPanel({
))}
</div>
</PanelShell>
</div>
<PanelShell title="Danger Zone">
<div
style={{
display: "grid",
gridTemplateColumns: "auto minmax(0, 1fr) auto",
gap: 12,
alignItems: "center",
border: "1px solid rgba(239,68,68,0.35)",
borderRadius: 7,
background: "rgba(239,68,68,0.06)",
padding: 13,
}}
>
<span
style={{
width: 40,
height: 40,
borderRadius: 10,
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
color: "var(--text-danger)",
background: "rgba(239,68,68,0.1)",
}}
>
<AlertTriangle size={20} />
</span>
<div style={{ minWidth: 0 }}>
<p
style={{
margin: 0,
color: "var(--text-primary)",
fontSize: 13,
fontWeight: 800,
}}
>
Destructive actions require confirmation
</p>
<p
style={{
marginTop: 4,
color: "var(--text-muted)",
fontSize: 12,
lineHeight: 1.45,
}}
>
Remove, reset, and cleanup operations stay behind explicit
confirmation dialogs before they execute.
</p>
</div>
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
<button
type="button"
className="btn btn-ghost"
disabled={actionBusy}
aria-busy={activeAction === "rebuild"}
onClick={onReset}
>
{activeAction === "rebuild" ? (
<Loader2 size={14} className="animate-spin" />
) : (
<Settings2 size={14} />
)}
Reset
</button>
<button
type="button"
className="btn btn-danger"
disabled={actionBusy}
aria-busy={activeAction === "remove"}
onClick={onRemove}
>
{activeAction === "remove" ? (
<Loader2 size={14} className="animate-spin" />
) : (
<Trash2 size={14} />
)}
Remove
</button>
</div>
</div>
</PanelShell>
</div> */}
</section>
);
}
@@ -9,7 +9,6 @@ import {
ExternalLink,
GitBranch,
GitCommit,
Package,
Rocket,
XCircle,
} from "lucide-react";
@@ -242,7 +241,6 @@ export default function DeploymentsTabPanel({
<th>Branch</th>
<th>Duration</th>
<th>Deployed At</th>
<th style={{ textAlign: "right" }}>Actions</th>
</tr>
</thead>
<tbody>
@@ -269,16 +267,6 @@ export default function DeploymentsTabPanel({
</td>
<td>{deployment.duration}</td>
<td>{deployment.deployedAt}</td>
<td style={{ textAlign: "right" }}>
<button
type="button"
className="btn btn-ghost"
style={{ padding: "4px 7px" }}
aria-label={`Open deployment ${deployment.version}`}
>
<ExternalLink size={13} />
</button>
</td>
</tr>
))}
</tbody>
@@ -356,27 +344,6 @@ export default function DeploymentsTabPanel({
</div>
</PanelShell>
</div>
<PanelShell title="Release Notes">
<div
style={{
display: "grid",
gridTemplateColumns: "auto minmax(0, 1fr)",
gap: 10,
alignItems: "start",
color: "var(--text-secondary)",
fontSize: 12,
lineHeight: 1.6,
}}
>
<Package size={16} style={{ color: "var(--accent-blue)" }} />
<p style={{ margin: 0 }}>
Deployment history is currently derived from the real container
record. A permanent deployment history API is not available yet, so
only the current deployment snapshot is shown here.
</p>
</div>
</PanelShell>
</section>
);
}
@@ -1,14 +1,11 @@
"use client";
import { useMemo, useState } from "react";
import TablePagination from "@/components/TablePagination";
import { useTablePagination } from "@/lib/use-table-pagination";
import {
CheckCircle2,
ClipboardCheck,
KeyRound,
Loader2,
LockKeyhole,
RotateCcw,
Save,
ShieldAlert,
@@ -178,7 +175,7 @@ export default function EnvironmentTabPanel({
return (
<section style={{ display: "flex", flexDirection: "column", gap: 12 }}>
<div
{/* <div
style={{
display: "grid",
gridTemplateColumns:
@@ -189,7 +186,7 @@ export default function EnvironmentTabPanel({
{environment.summaries.map((summary) => (
<EnvironmentSummaryCard key={summary.label} item={summary} />
))}
</div>
</div> */}
<PanelShell title="Project .env Editor">
<div
@@ -215,7 +212,7 @@ export default function EnvironmentTabPanel({
spellCheck={false}
style={{
width: "100%",
minHeight: 260,
minHeight: 480,
resize: "vertical",
border: "1px solid var(--border)",
borderRadius: 7,
@@ -255,7 +252,8 @@ export default function EnvironmentTabPanel({
});
setEditorNotice({
tone: "info",
message: "Editor content has been reset to the last loaded .env.",
message:
"Editor content has been reset to the last loaded .env.",
});
}}
>
@@ -444,245 +442,6 @@ export default function EnvironmentTabPanel({
</div>
</div>
</PanelShell>
<div
style={{
display: "grid",
gridTemplateColumns:
"minmax(min(660px, 100%), 1.35fr) minmax(min(340px, 100%), 0.65fr)",
gap: 12,
alignItems: "start",
}}
>
<PanelShell title={`Variables (${environment.variables.length})`}>
{environment.variables.length > 0 ? (
<>
<div style={{ overflowX: "auto" }}>
<table className="data-table" style={{ minWidth: 760 }}>
<thead>
<tr>
<th>Key</th>
<th>Value</th>
<th>Scope</th>
<th>Source</th>
<th>Required</th>
<th>Sensitive</th>
</tr>
</thead>
<tbody>
{pagination.paginatedItems.map((variable) => (
<tr key={variable.id}>
<td style={{ fontFamily: "var(--font--code)" }}>
<KeyRound size={13} />
{variable.key}
</td>
<td style={{ fontFamily: "var(--font--code)" }}>
{variable.value}
</td>
<td>{variable.scope}</td>
<td>{variable.source}</td>
<td>
<span
className={`ui-badge ${
variable.required ? "badge-warning" : ""
}`}
>
{variable.required ? "Required" : "Optional"}
</span>
</td>
<td>
<span
className={`ui-badge ${
variable.sensitive ? "badge-online" : ""
}`}
>
{variable.sensitive ? "Masked" : "Plain"}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
<TablePagination
currentPage={pagination.currentPage}
totalPages={pagination.totalPages}
totalItems={pagination.totalItems}
startItem={pagination.startItem}
endItem={pagination.endItem}
itemLabel="variables"
onPageChange={pagination.setCurrentPage}
/>
</>
) : (
<div
style={{
minHeight: 180,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: 10,
textAlign: "center",
color: "var(--text-muted)",
border: "1px dashed var(--border)",
borderRadius: 7,
background: "var(--bg-input)",
padding: 18,
}}
>
<KeyRound size={22} style={{ color: "var(--accent-blue)" }} />
<div>
<p
style={{
margin: 0,
color: "var(--text-primary)",
fontSize: 13,
fontWeight: 700,
}}
>
No environment variables detected
</p>
<p style={{ marginTop: 4, fontSize: 12 }}>
This container has no stored deploy variables or runtime
inspect variables available.
</p>
</div>
</div>
)}
</PanelShell>
<PanelShell title="Validation">
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
{environment.checks.map((check) => (
<div
key={check.label}
style={{
display: "grid",
gridTemplateColumns: "auto minmax(0, 1fr)",
gap: 10,
alignItems: "start",
padding: "10px 11px",
border: "1px solid var(--border)",
borderRadius: 7,
background: "var(--bg-input)",
}}
>
<span
style={{
color:
check.status === "Ready"
? "var(--accent-green)"
: check.status === "Missing"
? "var(--text-danger)"
: "var(--accent-yellow)",
display: "inline-flex",
marginTop: 1,
}}
>
{check.status === "Ready" ? (
<CheckCircle2 size={15} />
) : check.status === "Missing" ? (
<ShieldAlert size={15} />
) : (
<LockKeyhole size={15} />
)}
</span>
<div style={{ minWidth: 0 }}>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 8,
}}
>
<p
style={{
margin: 0,
color: "var(--text-primary)",
fontSize: 12,
fontWeight: 700,
}}
>
{check.label}
</p>
<span className={`ui-badge ${checkClass[check.status]}`}>
{check.status}
</span>
</div>
<p
style={{
marginTop: 4,
color: "var(--text-muted)",
fontSize: 11,
lineHeight: 1.45,
}}
>
{check.description}
</p>
</div>
</div>
))}
</div>
</PanelShell>
</div>
<div
style={{
display: "grid",
gridTemplateColumns:
"repeat(auto-fit, minmax(min(360px, 100%), 1fr))",
gap: 12,
alignItems: "start",
}}
>
{environment.groups.map((group) => (
<PanelShell key={group.title} title={group.title}>
<p
style={{
marginBottom: 12,
color: "var(--text-muted)",
fontSize: 12,
lineHeight: 1.5,
}}
>
{group.description}
</p>
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
{group.items.map((item) => (
<div
key={item.label}
style={{
display: "grid",
gridTemplateColumns:
"minmax(120px, 0.45fr) minmax(0, 1fr)",
gap: 12,
alignItems: "center",
fontSize: 12,
}}
>
<span style={{ color: "var(--text-muted)" }}>
{item.label}
</span>
<span
style={{
color: "var(--text-primary)",
fontFamily: "var(--font--code)",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
textAlign: "right",
}}
>
{item.value}
</span>
</div>
))}
</div>
</PanelShell>
))}
</div>
</section>
);
}
@@ -28,21 +28,25 @@ const levelColor: Record<LogsStreamItem["level"], string> = {
DEBUG: "#38bdf8",
};
const sourceStatusClass: Record<LogsTabData["sources"][number]["status"], string> =
{
Streaming: "badge-online",
Paused: "badge-warning",
Unavailable: "badge-danger",
};
const sourceStatusClass: Record<
LogsTabData["sources"][number]["status"],
string
> = {
Streaming: "badge-online",
Paused: "badge-warning",
Unavailable: "badge-danger",
};
const summaryToneColor: Record<LogsTabData["summaries"][number]["tone"], string> =
{
blue: "var(--accent-blue)",
green: "var(--accent-green)",
purple: "var(--accent-purple)",
amber: "var(--accent-yellow)",
cyan: "var(--accent-cyan)",
};
const summaryToneColor: Record<
LogsTabData["summaries"][number]["tone"],
string
> = {
blue: "var(--accent-blue)",
green: "var(--accent-green)",
purple: "var(--accent-purple)",
amber: "var(--accent-yellow)",
cyan: "var(--accent-cyan)",
};
export default function LogsTabPanel({
logs,
@@ -198,8 +202,7 @@ export default function LogsTabPanel({
<div
style={{
display: "grid",
gridTemplateColumns:
"minmax(min(360px, 100%), 1fr) auto auto auto",
gridTemplateColumns: "minmax(min(360px, 100%), 1fr) auto auto auto",
gap: 8,
alignItems: "center",
marginBottom: 10,
@@ -295,7 +298,7 @@ export default function LogsTabPanel({
key={line.id}
style={{
display: "grid",
gridTemplateColumns: "72px 54px 110px minmax(360px, 1fr) auto",
gridTemplateColumns: "72px 54px 180px minmax(360px, 1fr) auto",
gap: 10,
whiteSpace: "nowrap",
}}
@@ -306,9 +309,7 @@ export default function LogsTabPanel({
<span style={{ color: levelColor[line.level], fontWeight: 800 }}>
{line.level}
</span>
<span style={{ color: "var(--accent-cyan)" }}>
{line.source}
</span>
<span style={{ color: "var(--accent-cyan)" }}>{line.source}</span>
<span>{line.message}</span>
<span style={{ color: "var(--text-muted)" }}>
{line.traceId ?? ""}
@@ -356,7 +357,7 @@ export default function LogsTabPanel({
</div>
</PanelShell>
<div
{/* <div
style={{
display: "grid",
gridTemplateColumns:
@@ -402,7 +403,9 @@ export default function LogsTabPanel({
{source.type} - {source.lines} lines - {source.retention}
</p>
</div>
<span className={`ui-badge ${sourceStatusClass[source.status]}`}>
<span
className={`ui-badge ${sourceStatusClass[source.status]}`}
>
{source.status}
</span>
</div>
@@ -455,7 +458,7 @@ export default function LogsTabPanel({
))}
</div>
</PanelShell>
</div>
</div> */}
</section>
);
}
@@ -22,13 +22,19 @@ export default function AppOverview({
<section
style={{
display: "grid",
gridTemplateColumns:
"repeat(auto-fit, minmax(min(360px, 100%), 1fr))",
gridTemplateColumns: "repeat(auto-fit, minmax(min(360px, 100%), 1fr))",
gap: 12,
minWidth: 0,
}}
>
<div style={{ display: "flex", flexDirection: "column", gap: 12, minWidth: 0 }}>
<div
style={{
display: "flex",
flexDirection: "column",
gap: 12,
minWidth: 0,
}}
>
<div
style={{
display: "grid",
@@ -40,16 +46,16 @@ export default function AppOverview({
<DeploymentPanel deployment={app.deployment} />
<HealthPanel health={app.health} />
</div>
<RuntimeContainersPanel containers={app.runtimeContainers} />
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 12, minWidth: 0 }}>
<div
style={{
display: "flex",
flexDirection: "column",
gap: 12,
minWidth: 0,
}}
>
<DomainsPanel domains={app.domains} />
<RecentLogsPanel
logs={app.logs}
autoRefresh={logsAutoRefresh}
refreshing={logsRefreshing}
onAutoRefreshChange={onLogsAutoRefreshChange}
/>
</div>
</section>
);
@@ -79,13 +79,13 @@ export default function RecentLogsPanel({
</div>
))}
</div>
<button
{/* <button
type="button"
className="btn btn-ghost"
style={{ width: "100%", marginTop: 10, fontSize: 12 }}
>
View all logs
</button>
</button> */}
</PanelShell>
);
}
@@ -181,7 +181,7 @@ export default function RuntimeTabPanel({ runtime }: RuntimeTabPanelProps) {
) : (
<div
style={{
minHeight: 156,
minHeight: 210,
display: "flex",
flexDirection: "column",
alignItems: "center",
@@ -1,15 +1,7 @@
"use client";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
AlertTriangle,
History,
Loader2,
Play,
RotateCw,
ShieldAlert,
TerminalSquare,
} from "lucide-react";
import { Loader2, RotateCw, TerminalSquare } from "lucide-react";
import { terminal as terminalApi } from "@/lib/api";
import { createClientId } from "@/lib/random-id";
import { sanitizeTerminalStreamChunk } from "@/lib/terminal-output";
@@ -541,7 +533,7 @@ export default function TerminalTabPanel({ terminal }: TerminalTabPanelProps) {
return (
<section style={{ display: "flex", flexDirection: "column", gap: 12 }}>
<div
{/* <div
style={{
display: "grid",
gridTemplateColumns:
@@ -608,7 +600,7 @@ export default function TerminalTabPanel({ terminal }: TerminalTabPanelProps) {
</div>
</div>
))}
</div>
</div> */}
<PanelShell
title="Container Terminal"
@@ -688,7 +680,7 @@ export default function TerminalTabPanel({ terminal }: TerminalTabPanelProps) {
</div>
</PanelShell>
<div
{/* <div
style={{
display: "grid",
gridTemplateColumns:
@@ -803,9 +795,9 @@ export default function TerminalTabPanel({ terminal }: TerminalTabPanelProps) {
))}
</div>
</PanelShell>
</div>
</div> */}
<div
{/* <div
style={{
display: "grid",
gridTemplateColumns:
@@ -884,7 +876,7 @@ export default function TerminalTabPanel({ terminal }: TerminalTabPanelProps) {
))}
</div>
</PanelShell>
</div>
</div> */}
</section>
);
}
@@ -40,7 +40,8 @@ function formatBytes(value: number | null) {
}
function formatCpu(nanoCpus: number | null, cpuQuota: number | null) {
if (nanoCpus && nanoCpus > 0) return `${(nanoCpus / 1_000_000_000).toFixed(2)} vCPU`;
if (nanoCpus && nanoCpus > 0)
return `${(nanoCpus / 1_000_000_000).toFixed(2)} vCPU`;
if (cpuQuota && cpuQuota > 0) return `${cpuQuota} quota`;
return "Unlimited";
}
@@ -105,16 +106,16 @@ export function createAdvancedData(
tone: restartName === "no" ? "amber" : "green",
},
{
label: "Runtime Guard",
value: runtimeLoaded ? "Loaded" : "Unavailable",
subvalue: runtimeLoaded ? "Docker inspect available" : "Using DB record",
tone: runtimeLoaded ? "cyan" : "amber",
label: "Root Filesystem",
value: readOnlyRootfs ? "Read-only" : "Writable",
subvalue: "Docker HostConfig.ReadonlyRootfs",
tone: readOnlyRootfs ? "green" : "amber",
},
{
label: "Danger Zone",
value: "Protected",
subvalue: "Confirmation required",
tone: "purple",
label: "Auto Remove",
value: autoRemove ? "Enabled" : "Disabled",
subvalue: "Docker HostConfig.AutoRemove",
tone: autoRemove ? "amber" : "blue",
},
],
metadata: [
@@ -134,7 +135,9 @@ export function createAdvancedData(
label: "CPU Limit",
value: formatCpu(nanoCpus, cpuQuota),
usage: detail ? Math.min(100, Math.round(detail.stats.cpuPercent)) : 0,
helper: runtimeLoaded ? "Current Docker CPU sampling" : "Runtime unavailable",
helper: runtimeLoaded
? "Current Docker CPU sampling"
: "Runtime unavailable",
},
{
label: "Memory Limit",
@@ -147,7 +150,10 @@ export function createAdvancedData(
value: pidsLimit && pidsLimit > 0 ? String(pidsLimit) : "Unlimited",
usage:
pidsLimit && pidsLimit > 0
? Math.min(100, Math.round((detail?.stats.pids ?? 0) / pidsLimit * 100))
? Math.min(
100,
Math.round(((detail?.stats.pids ?? 0) / pidsLimit) * 100),
)
: 0,
helper: `${detail?.stats.pids ?? 0} process ID(s) currently visible`,
},
@@ -202,7 +208,8 @@ export function createAdvancedData(
{
id: "reconcile-runtime",
label: "Reconcile runtime state",
description: "Stored metadata can be refreshed from Docker runtime data.",
description:
"Stored metadata can be refreshed from Docker runtime data.",
enabled: true,
},
],
@@ -53,6 +53,16 @@ function getBranch(container: Container) {
return "-";
}
function getImageTag(image: string) {
if (!image.trim()) return "-";
if (image.includes("@")) return "digest";
const lastSegment = image.split("/").at(-1) ?? image;
const tagIndex = lastSegment.lastIndexOf(":");
return tagIndex >= 0 ? lastSegment.slice(tagIndex + 1) || "latest" : "latest";
}
export function createDeploymentsData(
container: Container,
detail: ContainerDetails | null,
@@ -88,10 +98,10 @@ export function createDeploymentsData(
tone: isSuccessful ? "green" : "amber",
},
{
label: "Success Rate",
value: isSuccessful ? "100%" : "0%",
subvalue: "Based on current container record",
tone: isSuccessful ? "green" : "amber",
label: "Image Tag",
value: getImageTag(container.image),
subvalue: container.image,
tone: "purple",
},
{
label: "Deploy Mode",