mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-07 03:00:29 +00:00
feat(web): show sandbox status dot on the run overview panel
The Sandbox cell now renders a colored status dot before the resource summary, with a tooltip explaining the state on hover. The dot reuses the data already fetched for CPU/memory, so no new API call. Falls back to the state label when resources are unavailable. Lifts the per-state display map into a shared lib/sandbox-state module so the overview panel and the dedicated sandbox page stay consistent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
81d1715e22
commit
f2efaf70fb
4 changed files with 152 additions and 26 deletions
|
|
@ -19,6 +19,7 @@ function render(props: Partial<RunSummaryPanelViewProps> = {}) {
|
|||
const full: RunSummaryPanelViewProps = {
|
||||
run: null,
|
||||
runLoading: false,
|
||||
sandboxState: null,
|
||||
sandboxResources: null,
|
||||
sandboxLoading: false,
|
||||
artifactsCount: null,
|
||||
|
|
@ -118,11 +119,20 @@ describe("RunSummaryPanelView", () => {
|
|||
test("renders sandbox CPU and memory", () => {
|
||||
const tree = render({
|
||||
run: makeRun(),
|
||||
sandboxState: "running",
|
||||
sandboxResources: { cpu_cores: 4, memory_bytes: 8 * 1024 * 1024 * 1024 } as any,
|
||||
});
|
||||
expect(instanceText(cellAfterLabel(tree, "Sandbox"))).toBe("4 CPU · 8 GiB");
|
||||
});
|
||||
|
||||
test("renders sandbox status dot and falls back to state label without resources", () => {
|
||||
const tree = render({ run: makeRun(), sandboxState: "stopped" });
|
||||
const cell = cellAfterLabel(tree, "Sandbox");
|
||||
expect(instanceText(cell)).toBe("Stopped");
|
||||
const dot = cell.find((node) => node.props["aria-label"] === "Sandbox Stopped");
|
||||
expect(dot.props.className).toContain("bg-fg-muted");
|
||||
});
|
||||
|
||||
test("renders artifacts count when positive", () => {
|
||||
const tree = render({ run: makeRun(), artifactsCount: 3 });
|
||||
expect(instanceText(cellAfterLabel(tree, "Artifacts"))).toBe("3");
|
||||
|
|
|
|||
|
|
@ -7,7 +7,12 @@ import {
|
|||
QuestionMarkCircleIcon,
|
||||
ServerIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import type { Principal, Run, SandboxResources } from "@qltysh/fabro-api-client";
|
||||
import type {
|
||||
Principal,
|
||||
Run,
|
||||
SandboxResources,
|
||||
SandboxState,
|
||||
} from "@qltysh/fabro-api-client";
|
||||
|
||||
import {
|
||||
formatBytesAsMemory,
|
||||
|
|
@ -15,6 +20,8 @@ import {
|
|||
formatUsdMicros,
|
||||
} from "../lib/format";
|
||||
import { useRun, useRunArtifacts, useRunSandboxDetails } from "../lib/queries";
|
||||
import { SANDBOX_STATE_DISPLAY } from "../lib/sandbox-state";
|
||||
import { Tooltip } from "./ui";
|
||||
|
||||
const LABEL_CLASS =
|
||||
"text-[10px] font-medium uppercase tracking-[0.08em] text-fg-muted";
|
||||
|
|
@ -104,15 +111,46 @@ function createdByDisplay(actor: Principal): CreatedByDisplay {
|
|||
export interface RunSummaryPanelViewProps {
|
||||
run: Run | null;
|
||||
runLoading: boolean;
|
||||
sandboxState: SandboxState | null;
|
||||
sandboxResources: SandboxResources | null;
|
||||
sandboxLoading: boolean;
|
||||
artifactsCount: number | null;
|
||||
artifactsLoading: boolean;
|
||||
}
|
||||
|
||||
function SandboxValue({
|
||||
state,
|
||||
resources,
|
||||
}: {
|
||||
state: SandboxState;
|
||||
resources: SandboxResources | null;
|
||||
}) {
|
||||
const display = SANDBOX_STATE_DISPLAY[state] ?? SANDBOX_STATE_DISPLAY.unknown;
|
||||
const cpu = resources?.cpu_cores;
|
||||
const memory = resources?.memory_bytes;
|
||||
const valueText =
|
||||
cpu != null && memory != null
|
||||
? `${formatCpuCores(cpu)} CPU · ${formatBytesAsMemory(memory)}`
|
||||
: display.label;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip label={display.description}>
|
||||
<span
|
||||
role="img"
|
||||
aria-label={`Sandbox ${display.label}`}
|
||||
className={`size-2 rounded-full ${display.dot}`}
|
||||
/>
|
||||
</Tooltip>
|
||||
<span className={VALUE_CLASS}>{valueText}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function RunSummaryPanelView({
|
||||
run,
|
||||
runLoading,
|
||||
sandboxState,
|
||||
sandboxResources,
|
||||
sandboxLoading,
|
||||
artifactsCount,
|
||||
|
|
@ -159,13 +197,8 @@ export function RunSummaryPanelView({
|
|||
<Cell label="Sandbox">
|
||||
{sandboxLoading ? (
|
||||
<Skeleton widthClass="w-24" />
|
||||
) : sandboxResources &&
|
||||
sandboxResources.cpu_cores != null &&
|
||||
sandboxResources.memory_bytes != null ? (
|
||||
<span className={VALUE_CLASS}>
|
||||
{formatCpuCores(sandboxResources.cpu_cores)} CPU ·{" "}
|
||||
{formatBytesAsMemory(sandboxResources.memory_bytes)}
|
||||
</span>
|
||||
) : sandboxState ? (
|
||||
<SandboxValue state={sandboxState} resources={sandboxResources} />
|
||||
) : (
|
||||
<EmDash />
|
||||
)}
|
||||
|
|
@ -204,6 +237,7 @@ export function RunSummaryPanel({ runId }: { runId: string }) {
|
|||
<RunSummaryPanelView
|
||||
run={runQuery.data ?? null}
|
||||
runLoading={runQuery.isLoading && !runQuery.data}
|
||||
sandboxState={sandboxQuery.data?.state ?? null}
|
||||
sandboxResources={sandboxQuery.data?.resources ?? null}
|
||||
sandboxLoading={sandboxQuery.isLoading && !sandboxQuery.data}
|
||||
artifactsCount={artifactsQuery.data?.data.length ?? null}
|
||||
|
|
|
|||
98
apps/fabro-web/app/lib/sandbox-state.ts
Normal file
98
apps/fabro-web/app/lib/sandbox-state.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import type { SandboxState } from "@qltysh/fabro-api-client";
|
||||
|
||||
export interface SandboxStateDisplay {
|
||||
/** Short human-readable label, e.g. "Running". */
|
||||
label: string;
|
||||
/** One-sentence explanation shown on hover. */
|
||||
description: string;
|
||||
/** Tailwind background class for the status dot. */
|
||||
dot: string;
|
||||
/** Tailwind text color class matching the dot. */
|
||||
text: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display metadata for every normalized sandbox lifecycle state. Shared by the
|
||||
* run overview summary panel and the dedicated sandbox page so the dot color,
|
||||
* label, and hover copy stay consistent.
|
||||
*/
|
||||
export const SANDBOX_STATE_DISPLAY: Record<SandboxState, SandboxStateDisplay> = {
|
||||
unknown: {
|
||||
label: "Unknown",
|
||||
description: "The sandbox state could not be determined.",
|
||||
dot: "bg-fg-muted",
|
||||
text: "text-fg-muted",
|
||||
},
|
||||
provisioning: {
|
||||
label: "Provisioning",
|
||||
description: "The sandbox is being provisioned.",
|
||||
dot: "bg-amber",
|
||||
text: "text-amber",
|
||||
},
|
||||
starting: {
|
||||
label: "Starting",
|
||||
description: "The sandbox is starting up.",
|
||||
dot: "bg-amber",
|
||||
text: "text-amber",
|
||||
},
|
||||
running: {
|
||||
label: "Running",
|
||||
description: "The sandbox is running.",
|
||||
dot: "bg-teal-500",
|
||||
text: "text-teal-500",
|
||||
},
|
||||
stopping: {
|
||||
label: "Stopping",
|
||||
description: "The sandbox is shutting down.",
|
||||
dot: "bg-amber",
|
||||
text: "text-amber",
|
||||
},
|
||||
stopped: {
|
||||
label: "Stopped",
|
||||
description: "The sandbox is stopped.",
|
||||
dot: "bg-fg-muted",
|
||||
text: "text-fg-muted",
|
||||
},
|
||||
paused: {
|
||||
label: "Paused",
|
||||
description: "The sandbox is paused.",
|
||||
dot: "bg-amber",
|
||||
text: "text-amber",
|
||||
},
|
||||
deleting: {
|
||||
label: "Deleting",
|
||||
description: "The sandbox is being deleted.",
|
||||
dot: "bg-amber",
|
||||
text: "text-amber",
|
||||
},
|
||||
deleted: {
|
||||
label: "Deleted",
|
||||
description: "The sandbox has been deleted.",
|
||||
dot: "bg-coral",
|
||||
text: "text-coral",
|
||||
},
|
||||
archived: {
|
||||
label: "Archived",
|
||||
description: "The sandbox has been archived.",
|
||||
dot: "bg-fg-muted",
|
||||
text: "text-fg-muted",
|
||||
},
|
||||
restoring: {
|
||||
label: "Restoring",
|
||||
description: "The sandbox is being restored.",
|
||||
dot: "bg-amber",
|
||||
text: "text-amber",
|
||||
},
|
||||
resizing: {
|
||||
label: "Resizing",
|
||||
description: "The sandbox resources are being resized.",
|
||||
dot: "bg-amber",
|
||||
text: "text-amber",
|
||||
},
|
||||
error: {
|
||||
label: "Error",
|
||||
description: "The sandbox encountered an error.",
|
||||
dot: "bg-coral",
|
||||
text: "text-coral",
|
||||
},
|
||||
};
|
||||
|
|
@ -10,11 +10,11 @@ import {
|
|||
formatCpuCores,
|
||||
} from "../lib/format";
|
||||
import { useRunSandboxDetails } from "../lib/queries";
|
||||
import { SANDBOX_STATE_DISPLAY } from "../lib/sandbox-state";
|
||||
import type {
|
||||
SandboxDetails,
|
||||
SandboxNetwork,
|
||||
SandboxResources,
|
||||
SandboxState,
|
||||
} from "@qltysh/fabro-api-client";
|
||||
import FilesystemPanel from "./run-sandbox/filesystem-panel";
|
||||
import ServicesPanel from "./run-sandbox/services-panel";
|
||||
|
|
@ -40,22 +40,6 @@ function vncTabAvailable(provider: string | null | undefined): boolean {
|
|||
|
||||
const EMPTY_VALUE = "—";
|
||||
|
||||
const STATE_DISPLAY: Record<SandboxState, { label: string; dot: string; text: string }> = {
|
||||
unknown: { label: "Unknown", dot: "bg-fg-muted", text: "text-fg-muted" },
|
||||
provisioning: { label: "Provisioning", dot: "bg-amber", text: "text-amber" },
|
||||
starting: { label: "Starting", dot: "bg-amber", text: "text-amber" },
|
||||
running: { label: "Running", dot: "bg-teal-500", text: "text-teal-500" },
|
||||
stopping: { label: "Stopping", dot: "bg-amber", text: "text-amber" },
|
||||
stopped: { label: "Stopped", dot: "bg-fg-muted", text: "text-fg-muted" },
|
||||
paused: { label: "Paused", dot: "bg-amber", text: "text-amber" },
|
||||
deleting: { label: "Deleting", dot: "bg-amber", text: "text-amber" },
|
||||
deleted: { label: "Deleted", dot: "bg-coral", text: "text-coral" },
|
||||
archived: { label: "Archived", dot: "bg-fg-muted", text: "text-fg-muted" },
|
||||
restoring: { label: "Restoring", dot: "bg-amber", text: "text-amber" },
|
||||
resizing: { label: "Resizing", dot: "bg-amber", text: "text-amber" },
|
||||
error: { label: "Error", dot: "bg-coral", text: "text-coral" },
|
||||
};
|
||||
|
||||
function nullable(value: string | null | undefined): string {
|
||||
return value && value.length > 0 ? value : EMPTY_VALUE;
|
||||
}
|
||||
|
|
@ -149,7 +133,7 @@ function Panel({ title, children }: PanelProps) {
|
|||
}
|
||||
|
||||
function StatusStrip({ details }: { details: SandboxDetails }) {
|
||||
const display = STATE_DISPLAY[details.state] ?? STATE_DISPLAY.unknown;
|
||||
const display = SANDBOX_STATE_DISPLAY[details.state] ?? SANDBOX_STATE_DISPLAY.unknown;
|
||||
const provider = details.sandbox.provider;
|
||||
const showNative =
|
||||
details.native_state &&
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue