Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions apps/web/e2e/console.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,10 @@ export async function openConsole(page: Page, request: APIRequestContext, hash =

/** Selects the ready build returned by the E2B discovery fixture. */
export async function selectFixtureE2BBuild(page: Page) {
await page.getByLabel("E2B API key").fill("fixture-private-key");
await page.getByLabel("Template", { exact: true }).selectOption("template");
await page.getByLabel("Template build").selectOption("template:94be54a1-138c-4f30-bc87-b13686272dbe");
await page.getByLabel("API key").fill("fixture-private-key");
const template = page.getByLabel("Template build");
await template.first().selectOption("template");
await template.nth(1).selectOption("template:94be54a1-138c-4f30-bc87-b13686272dbe");
}

/** Makes the next matching write fail once with the given status. */
Expand Down
5 changes: 3 additions & 2 deletions apps/web/e2e/fixture-console.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -342,8 +342,9 @@ async function sandboxRoute(request, response, path, url) {
["https://api.e2b.app", "e2b.app"],
].some(([apiURL, domain]) => input.configuration?.api_url === apiURL && input.configuration?.domain === domain);
if (input.credential?.api_key !== "fixture-private-key" || !knownEndpoint) return error(response, 400, "Invalid E2B connection.");
if (!input.query?.template) return send(response, 200, { templates: [{ id: "template", names: ["fixture-runtime"] }] });
return send(response, 200, { builds: [{ id: "94be54a1-138c-4f30-bc87-b13686272dbe", cpus: 2, memory_mib: 2048 }] });
if (input.query?.field !== "template") return error(response, 400, "Invalid discovery field.");
if (input.query.path.length === 0) return send(response, 200, { options: [{ value: "template", label: "fixture-runtime · template", complete: false }] });
return send(response, 200, { options: [{ value: "template:94be54a1-138c-4f30-bc87-b13686272dbe", label: "94be54a1-138c-4f30-bc87-b13686272dbe", resources: { cpus: 2, memory_mib: 2048 }, complete: true }] });
}
// Retired even for authenticated callers; never reinterpret maintenance as reset.
if (path === "/deployment/maintenance") return error(response, 404, "Not found.");
Expand Down
6 changes: 2 additions & 4 deletions apps/web/e2e/monitoring.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,13 +250,11 @@ test("shows a node's host on Nodes and opens a sandbox in a dialog from Sandbox

test("shows E2B's cloud instead of machines", async ({ page, request }) => {
await openConsole(page, request, "sandbox-metrics", { sandbox: "e2b" });
await expect(page.getByRole("heading", { name: "E2B cloud" })).toBeVisible();
// Each sandbox takes the template build's size, disk included.
await expect(page.getByText("10 GiB disk")).toBeVisible();
await expect(page.getByRole("heading", { name: "Provider cloud" })).toBeVisible();
await expect(page.getByRole("button", { name: "Add node" })).toHaveCount(0);
await expect(page.getByRole("columnheader", { name: "Node", exact: true })).toHaveCount(0);

await page.getByRole("navigation").getByRole("button", { name: "Nodes", exact: true }).click();
await expect(page.getByRole("heading", { name: "Nodes", level: 1 })).toBeVisible();
await expect(page.getByText("E2B runs sandboxes in its cloud. There are no nodes to manage.")).toBeVisible();
await expect(page.getByText("The provider runs sandboxes in its cloud. There are no nodes to manage.")).toBeVisible();
});
25 changes: 12 additions & 13 deletions apps/web/e2e/nodes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,13 +229,13 @@ test("preselects microsandbox and asks once before switching to Docker", async (
await page.getByRole("button", { name: "Own machines" }).click();
const microsandbox = page.getByRole("button", { name: "microsandbox Recommended" });
const docker = page.getByRole("button", { name: "Docker", exact: true });
const confirm = page.getByRole("dialog", { name: "Use Docker instead of microsandbox?" });
const confirm = page.getByRole("dialog", { name: "Use Docker?" });
const sizeStep = page.getByRole("heading", { name: "How big is each sandbox?" });
await expect(microsandbox).toHaveAttribute("aria-pressed", "true");

// Keeping microsandbox is the default action; it leaves microsandbox selected.
await docker.click();
const keep = confirm.getByRole("button", { name: "Keep microsandbox" });
const keep = confirm.getByRole("button", { name: "Back" });
await expect(keep).toBeFocused();
await keep.click();
await expect(confirm).toBeHidden();
Expand All @@ -258,8 +258,8 @@ test("saves E2B without opening Add node, as it has no machines", async ({ page,
if (sent.method() === "POST" && sent.url().endsWith("/core/v1/sandbox/deployment")) submitted = sent.postDataJSON() as Record<string, unknown>;
});
await openConsole(page, request, "settings?id=sandbox", { sandbox: "none" });
await page.getByRole("button", { name: "E2B cloud" }).click();
await expect(page.getByLabel("E2B provider")).toHaveValue("sandbase");
await page.getByRole("button", { name: "E2B", exact: true }).click();
await expect(page.getByLabel("Service")).toHaveValue("0");
await expect(page.getByLabel("Sandbox API URL")).toHaveValue("https://sandbox.sandbase.ai");
await expect(page.getByLabel("Sandbox data-plane domain")).toHaveValue("sandbox.sandbase.ai");
const keyConsole = page.getByRole("link", { name: "Console → API keys" });
Expand All @@ -283,21 +283,20 @@ test("saves E2B without opening Add node, as it has no machines", async ({ page,

test("uses the official E2B preset and clears a selected build when the key changes", async ({ page, request }) => {
await openConsole(page, request, "settings?id=sandbox", { sandbox: "none" });
await page.getByRole("button", { name: "E2B cloud" }).click();
await page.getByLabel("E2B provider").selectOption("official");
await page.getByRole("button", { name: "E2B", exact: true }).click();
await page.getByLabel("Service").selectOption("1");
await expect(page.getByLabel("Sandbox API URL")).toHaveValue("https://api.e2b.app");
await expect(page.getByLabel("Sandbox data-plane domain")).toHaveValue("e2b.app");
const keyConsole = page.getByRole("link", { name: "Console → API keys" });
await expect(keyConsole).toHaveAttribute("href", "https://e2b.dev/dashboard?tab=api-keys");
await page.getByLabel("Sandbox API URL").fill("https://custom.e2b.app");
await expect(keyConsole).toHaveCount(0);
await page.getByLabel("E2B provider").selectOption("custom");
await page.getByLabel("Service").selectOption("-1");
await expect(keyConsole).toHaveCount(0);
await page.getByLabel("E2B provider").selectOption("official");
await page.getByLabel("Service").selectOption("1");
await selectFixtureE2BBuild(page);
await expect(page.getByRole("button", { name: "Next" })).toBeEnabled();
await page.getByLabel("E2B API key").fill("changed-fixture-key");
await expect(page.getByLabel("Template", { exact: true })).toHaveValue("");
await page.getByLabel("API key").fill("changed-fixture-key");
await expect(page.getByLabel("Template build")).toHaveValue("");
await expect(page.getByRole("button", { name: "Next" })).toBeDisabled();
});
Expand All @@ -308,8 +307,8 @@ test("shows the retained E2B build while a replacement key is checked", async ({
const edit = page.getByRole("dialog", { name: "Change resources" });
const saved = "oac-runtime:0f1e2d3c-4b5a-6978-8a9b-0c1d2e3f4a5b";
await expect(edit.getByLabel("Template build")).toHaveValue(saved);
await edit.getByLabel("E2B API key").fill("replacement-fixture-key");
await expect(edit.getByLabel("Template build")).toHaveValue(saved);
await edit.getByLabel("API key").fill("replacement-fixture-key");
await expect(edit.getByLabel("Template build").first()).toContainText(saved);
await expect(edit.getByRole("button", { name: "Next" })).toBeEnabled();
});

Expand All @@ -330,7 +329,7 @@ test("edits only the saved backend, preserving a custom size", async ({ page, re
await openConsole(page, request, "settings?id=sandbox");
await page.getByRole("button", { name: "Change resources" }).click();
await expect(page.getByRole("button", { name: "Own machines" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "E2B cloud" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "E2B", exact: true })).toHaveCount(0);
await page.getByRole("button", { name: /^Current/ }).click();
await page.getByRole("button", { name: "Save configuration" }).click();
await expect.poll(() => submitted).toMatchObject({ provider: "docker", expected_generation: 1, resources: current.resources });
Expand Down
6 changes: 3 additions & 3 deletions apps/web/e2e/public-url.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ test("sets up sandboxes with Core's address read-only, never sending it", async

test("explains an E2B rejection in the wizard, with a link to domain setup", async ({ page, request }) => {
await openConsole(page, request, "settings?id=sandbox", { sandbox: "none", installation: "local" });
await page.getByRole("button", { name: "E2B cloud" }).click();
await page.getByRole("button", { name: "E2B", exact: true }).click();
await selectFixtureE2BBuild(page);
await page.getByRole("button", { name: "Next" }).click();
const address = page.getByRole("definition").filter({ hasText: "http://127.0.0.1:8091" });
await expect(address).toContainText("Set a public address before connecting remote nodes");
await expect(address).toContainText("Set a public address before connecting; E2B sandboxes need an HTTPS one.");
await page.getByRole("button", { name: "Save configuration" }).click();
const rejection = page.locator(".wizard-rejection");
await expect(rejection).toContainText("E2B sandboxes need a public HTTPS address.");
await expect(rejection).toContainText("This sandbox provider needs a public HTTPS address.");
await expect(rejection.getByRole("button", { name: "Managed in Settings" })).toBeVisible();
// Nothing was saved and nothing is uncertain: no dialog, and the wizard stays on its review.
await expect(page.getByRole("dialog")).toHaveCount(0);
Expand Down
6 changes: 3 additions & 3 deletions apps/web/e2e/sandbox-generation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ async function saveConfiguration(page: Page) {
}
async function editE2B(page: Page, key?: string) {
await page.getByRole("button", { name: "Change resources", exact: true }).click();
await expect(page.getByLabel("E2B API key", { exact: true })).toHaveValue("");
if (key !== undefined) await page.getByLabel("E2B API key", { exact: true }).fill(key);
await expect(page.getByLabel("API key", { exact: true })).toHaveValue("");
if (key !== undefined) await page.getByLabel("API key", { exact: true }).fill(key);
await page.getByRole("button", { name: "Next", exact: true }).click();
}

Expand Down Expand Up @@ -212,7 +212,7 @@ test("a different E2B team leaves the committed configuration intact and require
await editE2B(page, "fixture-other-team-key");
const rejected = await saveConfiguration(page);
expect(rejected.response.status()).toBe(409);
await expect(page.locator(".wizard-rejection")).toContainText("Reset before changing teams.");
await expect(page.locator(".wizard-rejection")).toContainText("Reset before changing accounts.");
await expect(page.locator(".wizard-rejection")).toContainText("the saved configuration is unchanged");
expect(await deploymentRead(page)).toEqual(before);
await expect(page.getByRole("button", { name: "Save configuration", exact: true })).toBeDisabled();
Expand Down
7 changes: 0 additions & 7 deletions apps/web/src/features/metrics/MetricsView.css
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,6 @@
gap: 24px;
}

/* An E2B template build is long: the figure cuts it off, and the full build is in its tooltip. */
.cloud-template {
font-family: var(--font-mono);
font-size: 12px;
font-weight: 400;
}

/* A figure that opens the Session log filtered to it; it keeps its cell's colour. */
.figure-link {
padding: 0;
Expand Down
22 changes: 3 additions & 19 deletions apps/web/src/features/metrics/SandboxMetricsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import { RUNTIME_SNAPSHOT_REFRESH_MS } from "../dashboard/runtime-snapshot";
import { FleetReadNotice } from "../fleet/FleetReadNotice";
import { SandboxResetNotice } from "../fleet/SandboxResetNotice";
import { fleetSnapshot, useSandboxFleet } from "../fleet/use-sandbox-fleet";
import { sandboxSize, templateBuildStatus } from "../sandbox/deployment-specification";
import {
hostedRuntimeRows,
hostedRuntimeUsage,
Expand Down Expand Up @@ -131,17 +130,12 @@ export function SandboxMetricsPage() {
);
}

/** An E2B deployment: what runs in its cloud now, and with what. */
/** A direct deployment: what runs in the Provider's cloud now, and with what. */
function CloudSection({ deployment }: { deployment: SandboxDeployment }) {
const { t, i18n } = useTranslation("metrics");
const locale = i18n.resolvedLanguage;
const { navigate } = useConsoleNavigation();
// An E2B selection may adopt its template build's size instead of saving one.
const resources = sandboxSize(deployment);
const build = deployment.metadata?.template_build;
const disk = build?.resources.root_disk_mib ?? null;
const status = templateBuildStatus(build);
const template = deployment.configuration?.template;
const resources = deployment.specification?.resources;
return (
<Section
headingId="cloud-heading"
Expand All @@ -157,18 +151,8 @@ function CloudSection({ deployment }: { deployment: SandboxDeployment }) {
label={t("sandbox.cloud.size")}
value={resources ? <>
{t("sandbox.cores", { value: formatInteger(resources.cpus, locale) })} · {formatBytes(resources.memory_mib * 2 ** 20)}
{disk !== null ? <span className="kpi-unit">{t("sandbox.cloud.disk", { disk: formatBytes(disk * 2 ** 20) })}</span> : null}
</> : MISSING}
/>
<Kpi
label={t("sandbox.cloud.template")}
help={t("sandbox.cloud.templateHelp")}
tone={status === "notReady" ? "warning" : undefined}
value={<>
{t(`sandbox.cloud.build.${status}`)}
{template ? <code className="kpi-unit cloud-template" title={template}>{template}</code> : null}
</>}
/>
</KpiStrip>
</Section>
);
Expand Down Expand Up @@ -389,7 +373,7 @@ function RuntimeDialog({ row, showNode, onClose }: { row: HostedRuntimeRow | nul
<div><dt>{t("sandbox.status")}</dt><dd><StatusDot tone={lifecycleTone(shown)} label={lifecycleLabel(shown, t)} /></dd></div>
<div><dt>{t("sandbox.configurationGeneration")}</dt><dd>{shown.deploymentGeneration ?? MISSING}</dd></div>
<div><dt>{t("sandbox.uptime")}</dt><dd>{formatDuration(shown.uptimeSeconds)}</dd></div>
{/* Only E2B reports a sandbox's disk. */}
{/* Disk is shown only when the provider reports it. */}
{observation.disk ? <div><dt>{t("sandbox.disk")}</dt><dd>{formatBytes(observation.disk.usage_bytes)} / {formatBytes(observation.disk.limit_bytes)}</dd></div> : null}
</dl>
{shown.session
Expand Down
5 changes: 2 additions & 3 deletions apps/web/src/features/overview/FleetOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,13 @@ export function overviewNodes(nodes: readonly SandboxNode[]): SandboxNode[] {
.map(({ node }) => node);
}

/** E2B's cloud in place of machines: what Core holds there. */
/** The provider's cloud in place of machines: what Core holds there. */
export interface CloudHost {
running: number;
pending: number;
template: string | null;
}

/** A compact fleet inventory: Core, then E2B's cloud or the nodes; each row opens its page. */
/** A compact fleet inventory: Core, then the provider's cloud or the nodes; each row opens its page. */
export function FleetOverview({ nodes, cloud, coreLabel, coreTone, onOpenNode, onOpenCloud, onOpenCore }: {
nodes: readonly SandboxNode[];
/** A direct deployment: Core links to the Provider's cloud instead of to machines. */
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/features/overview/OverviewPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export function OverviewPage() {
const activity = useMemo(() => (sessions ? sessionActivity(sessions.map((entry) => entry.value), now) : null), [now, sessions]);
const attention = useMemo(() => (sessions ? attentionSessions(sessions, activityStart(now)) : null), [now, sessions]);
const capacity = fleet ? capacitySummary(fleet.nodes) : null;
// An E2B deployment has no machines: capacity is what Core holds in E2B's cloud.
// A direct deployment has no machines: capacity is what Core holds in the Provider's cloud.
const cloud = cloudHost(fleet);
const failuresLastHour = sessions ? recentFailures(sessions.map((entry) => entry.value), now) : null;

Expand Down Expand Up @@ -316,10 +316,10 @@ function fleetDetail(state: FleetState, t: TFunction<"overview">): string {

function cloudHost(fleet: FleetSnapshot | null): CloudHost | null {
if (fleet?.deployment.mode !== "direct") return null;
return { running: fleet.deployment.resources.allocations, pending: fleet.deployment.resources.pending, template: fleet.deployment.configuration?.template || null };
return { running: fleet.deployment.resources.allocations, pending: fleet.deployment.resources.pending };
}

/** Core and its sandbox nodes (or E2B's cloud) as an inventory; each row opens its page. */
/** Core and its sandbox nodes (or the Provider's cloud) as an inventory; each row opens its page. */
function FleetCard({ fleetState, core, localOnly }: { fleetState: FleetState; core: CoreStatus; localOnly: boolean }) {
const { t } = useTranslation("overview");
const { navigate } = useConsoleNavigation();
Expand Down
9 changes: 3 additions & 6 deletions apps/web/src/features/overview/getting-started.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,13 @@ describe("Getting started steps", () => {
expect(steps.sandboxes).toMatchObject({ state: "todo", action: "nodes", cloud: provider === "e2b" });
}
});
it("counts sandboxes ready with a saved deployment and a ready node, or a saved E2B deployment whose build is not reported unready", () => {
it("counts sandboxes ready with a saved deployment and a ready node, or a saved direct deployment", () => {
expect(sandboxes(fleet(deployment({ provider: "", mode: "" }), []))).toMatchObject({ state: "todo", action: "setup" });
expect(sandboxes(fleet(deployment(), []))).toMatchObject({ state: "todo", action: "add-node" });
expect(sandboxes(fleet(deployment(), [node("n1", { provider_ready: false }), node("n2", { online: false })]))).toMatchObject({ state: "todo", action: "nodes" });
expect(sandboxes(fleet(deployment()))).toMatchObject({ state: "done" });
const e2b = (status: string | null) => deployment({ provider: "e2b", mode: "direct", configuration: { template: "t", api_url: "https://api.e2b.app", domain: "e2b.app" } , credential_configured: true, metadata: { template_build: { status, resources: { cpus: 2, memory_mib: 2048, root_disk_mib: null } } } });
expect(sandboxes(fleet(e2b("building"), []))).toMatchObject({ state: "todo", cloud: true });
expect(sandboxes(fleet(e2b("ready"), []))).toMatchObject({ state: "done", cloud: true });
// Saved before Core recorded the build: Core admitted it, so it counts as ready.
expect(sandboxes(fleet(e2b(null), []))).toMatchObject({ state: "done", cloud: true });
const direct = deployment({ provider: "e2b", mode: "direct", configuration: { template: "t", api_url: "https://api.e2b.app", domain: "e2b.app" }, credential_configured: true });
expect(sandboxes(fleet(direct, []))).toMatchObject({ state: "done", cloud: true });
expect(sandboxes({ status: "loading" }).state).toBeNull();
expect(sandboxes({ status: "failed", error: new Error("down") }).state).toBe("unknown");
});
Expand Down
Loading
Loading