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