feat(logs): add Dev Previews to server logs page
This commit is contained in:
@@ -21,26 +21,40 @@ export default function LogsPage() {
|
||||
const { anatomy, loading } = useAnatomy(projectId, { pollMs: 8000 });
|
||||
const live = anatomy?.hosting.live ?? [];
|
||||
|
||||
const [activeUuid, setActiveUuid] = useState<string | null>(null);
|
||||
const previews = anatomy?.hosting.previews ?? [];
|
||||
|
||||
const [activeItem, setActiveItem] = useState<{
|
||||
type: "app" | "preview";
|
||||
id: string;
|
||||
} | null>(null);
|
||||
const [logs, setLogs] = useState<string | null>(null);
|
||||
const [logsLoading, setLogsLoading] = useState(false);
|
||||
|
||||
// Auto-select first app if none selected
|
||||
// Auto-select first available item
|
||||
useEffect(() => {
|
||||
if (live.length > 0 && !activeUuid) {
|
||||
setActiveUuid(live[0].uuid);
|
||||
if (activeItem) return;
|
||||
if (live.length > 0) {
|
||||
setActiveItem({ type: "app", id: live[0].uuid });
|
||||
} else if (previews.length > 0) {
|
||||
setActiveItem({ type: "preview", id: previews[0].id });
|
||||
}
|
||||
}, [live, activeUuid]);
|
||||
}, [live, previews, activeItem]);
|
||||
|
||||
const fetchLogs = async (uuid: string) => {
|
||||
const fetchLogs = async (item: { type: "app" | "preview"; id: string }) => {
|
||||
setLogsLoading(true);
|
||||
try {
|
||||
const action = item.type === "app" ? "apps.logs" : "dev_server.logs";
|
||||
const payloadParams =
|
||||
item.type === "app"
|
||||
? { uuid: item.id, lines: 100 }
|
||||
: { id: item.id, lines: 100, projectId };
|
||||
|
||||
const r = await fetch(`/api/mcp`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
action: "apps.logs",
|
||||
params: { uuid, lines: 100 },
|
||||
action,
|
||||
params: payloadParams,
|
||||
}),
|
||||
});
|
||||
const d = await r.json();
|
||||
@@ -77,10 +91,10 @@ export default function LogsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch when active app changes
|
||||
// Fetch when active item changes
|
||||
useEffect(() => {
|
||||
if (activeUuid) fetchLogs(activeUuid);
|
||||
}, [activeUuid]);
|
||||
if (activeItem) fetchLogs(activeItem);
|
||||
}, [activeItem]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -120,12 +134,12 @@ export default function LogsPage() {
|
||||
>
|
||||
<Loader2 size={15} className="animate-spin" /> Loading…
|
||||
</div>
|
||||
) : live.length === 0 ? (
|
||||
) : live.length === 0 && previews.length === 0 ? (
|
||||
<div style={{ padding: 24 }}>
|
||||
<EmptyState
|
||||
icon={<Activity size={22} />}
|
||||
title="No apps running"
|
||||
hint="Once you deploy an app, its runtime logs will appear here."
|
||||
title="No active servers"
|
||||
hint="Once you deploy an app or start a dev server, its runtime logs will appear here."
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
@@ -145,59 +159,51 @@ export default function LogsPage() {
|
||||
flexDirection: "column",
|
||||
borderRight: `1px solid ${THEME.borderSoft}`,
|
||||
overflowY: "auto",
|
||||
paddingBottom: 24,
|
||||
}}
|
||||
>
|
||||
<header
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: "10px 20px",
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
background: THEME.cardBg,
|
||||
zIndex: 10,
|
||||
height: "41px",
|
||||
boxSizing: "border-box",
|
||||
borderTopLeftRadius: THEME.radius,
|
||||
}}
|
||||
>
|
||||
<h3
|
||||
style={{
|
||||
fontSize: "0.95rem",
|
||||
fontWeight: 600,
|
||||
color: THEME.ink,
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
Apps
|
||||
</h3>
|
||||
</header>
|
||||
{live.length > 0 && (
|
||||
<div style={{ padding: "16px 20px 8px" }}>
|
||||
<h3
|
||||
style={{
|
||||
fontSize: "0.7rem",
|
||||
fontWeight: 600,
|
||||
color: THEME.muted,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.06em",
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
Live Apps
|
||||
</h3>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 4,
|
||||
padding: "0 20px 20px 20px",
|
||||
padding: "0 20px",
|
||||
}}
|
||||
>
|
||||
{live.map((app) => (
|
||||
<button
|
||||
key={app.uuid}
|
||||
onClick={() => setActiveUuid(app.uuid)}
|
||||
onClick={() => setActiveItem({ type: "app", id: app.uuid })}
|
||||
style={{
|
||||
textAlign: "left",
|
||||
padding: "10px 14px",
|
||||
padding: "8px 12px",
|
||||
background:
|
||||
activeUuid === app.uuid
|
||||
activeItem?.id === app.uuid
|
||||
? THEME.subtleBg
|
||||
: "transparent",
|
||||
border: "1px solid transparent",
|
||||
borderRadius: THEME.radiusSm,
|
||||
cursor: "pointer",
|
||||
fontSize: "0.875rem",
|
||||
fontWeight: activeUuid === app.uuid ? 500 : 400,
|
||||
color: activeUuid === app.uuid ? THEME.ink : THEME.mid,
|
||||
fontWeight: activeItem?.id === app.uuid ? 500 : 400,
|
||||
color:
|
||||
activeItem?.id === app.uuid ? THEME.ink : THEME.mid,
|
||||
transition: "all 0.1s ease",
|
||||
}}
|
||||
>
|
||||
@@ -205,6 +211,58 @@ export default function LogsPage() {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{previews.length > 0 && (
|
||||
<div style={{ padding: "24px 20px 8px" }}>
|
||||
<h3
|
||||
style={{
|
||||
fontSize: "0.7rem",
|
||||
fontWeight: 600,
|
||||
color: THEME.muted,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.06em",
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
Dev Previews
|
||||
</h3>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 4,
|
||||
padding: "0 20px",
|
||||
}}
|
||||
>
|
||||
{previews.map((preview) => (
|
||||
<button
|
||||
key={preview.id}
|
||||
onClick={() =>
|
||||
setActiveItem({ type: "preview", id: preview.id })
|
||||
}
|
||||
style={{
|
||||
textAlign: "left",
|
||||
padding: "8px 12px",
|
||||
background:
|
||||
activeItem?.id === preview.id
|
||||
? THEME.subtleBg
|
||||
: "transparent",
|
||||
border: "1px solid transparent",
|
||||
borderRadius: THEME.radiusSm,
|
||||
cursor: "pointer",
|
||||
fontSize: "0.875rem",
|
||||
fontWeight: activeItem?.id === preview.id ? 500 : 400,
|
||||
color:
|
||||
activeItem?.id === preview.id ? THEME.ink : THEME.mid,
|
||||
transition: "all 0.1s ease",
|
||||
}}
|
||||
>
|
||||
{preview.name || `Port ${preview.port}`}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Log Viewer Column */}
|
||||
@@ -236,7 +294,11 @@ export default function LogsPage() {
|
||||
color: THEME.mid,
|
||||
}}
|
||||
>
|
||||
{live.find((a) => a.uuid === activeUuid)?.name ?? "Logs"}
|
||||
{activeItem?.type === "app"
|
||||
? (live.find((a) => a.uuid === activeItem.id)?.name ??
|
||||
"Logs")
|
||||
: previews.find((p) => p.id === activeItem?.id)?.name ||
|
||||
"Dev Server Logs"}
|
||||
</span>
|
||||
<SecondaryButton
|
||||
icon={
|
||||
@@ -246,7 +308,7 @@ export default function LogsPage() {
|
||||
<RefreshCw size={13} />
|
||||
)
|
||||
}
|
||||
onClick={() => activeUuid && fetchLogs(activeUuid)}
|
||||
onClick={() => activeItem && fetchLogs(activeItem)}
|
||||
disabled={logsLoading}
|
||||
style={{ padding: "4px 8px", fontSize: "0.75rem" }}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user