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
52 changes: 35 additions & 17 deletions src/commands/devbox/list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -423,28 +423,46 @@ const ListDevboxesUI = ({
const endIndex = startIndex + devboxes.length;

// Filter operations based on devbox status
const hasTunnel = !!(
selectedDevbox?.tunnel && selectedDevbox.tunnel.tunnel_key
);
const operations = selectedDevbox
? allOperations.filter((op) => {
const devboxStatus = selectedDevbox.status;
? allOperations
.filter((op) => {
const devboxStatus = selectedDevbox.status;

if (devboxStatus === "suspended") {
return op.key === "resume" || op.key === "logs";
}
if (devboxStatus === "suspended") {
return op.key === "resume" || op.key === "logs";
}

if (
devboxStatus !== "running" &&
devboxStatus !== "provisioning" &&
devboxStatus !== "initializing"
) {
return op.key === "logs";
}
if (
devboxStatus !== "running" &&
devboxStatus !== "provisioning" &&
devboxStatus !== "initializing"
) {
return op.key === "logs";
}

if (devboxStatus === "running") {
return op.key !== "resume";
}
if (devboxStatus === "running") {
return op.key !== "resume";
}

return op.key === "logs" || op.key === "delete";
})
return op.key === "logs" || op.key === "delete";
})
.map((op) => {
// Dynamic tunnel label based on whether tunnel is active
if (op.key === "tunnel") {
return hasTunnel
? {
...op,
label: "Tunnel (Active)",
color: colors.success,
icon: figures.tick,
}
: op;
}
return op;
})
: allOperations;

useInput((input, key) => {
Expand Down
61 changes: 39 additions & 22 deletions src/components/DevboxActionsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
createSSHKey,
} from "../services/devboxService.js";
import { StreamingLogsViewer } from "./StreamingLogsViewer.js";
import { DevboxView } from "@runloop/api-client/resources/devboxes.mjs";

type Operation =
| "exec"
Expand All @@ -37,7 +38,7 @@
| null;

interface DevboxActionsMenuProps {
devbox: any;
devbox: DevboxView;
onBack: () => void;
breadcrumbItems?: Array<{ label: string; active?: boolean }>;
initialOperation?: string; // Operation to execute immediately
Expand Down Expand Up @@ -194,32 +195,48 @@
];

// Filter operations based on devbox status
const hasTunnel = !!(devbox?.tunnel && devbox.tunnel.tunnel_key);
const operations = devbox
? allOperations.filter((op) => {
const status = devbox.status;
? allOperations
.filter((op) => {
const status = devbox.status;

// When suspended: logs and resume
if (status === "suspended") {
return op.key === "resume" || op.key === "logs";
}
// When suspended: logs and resume
if (status === "suspended") {
return op.key === "resume" || op.key === "logs";
}

// When not running (shutdown, failure, etc): only logs
if (
status !== "running" &&
status !== "provisioning" &&
status !== "initializing"
) {
return op.key === "logs";
}
// When not running (shutdown, failure, etc): only logs
if (
status !== "running" &&
status !== "provisioning" &&
status !== "initializing"
) {
return op.key === "logs";
}

// When running: everything except resume
if (status === "running") {
return op.key !== "resume";
}
// When running: everything except resume
if (status === "running") {
return op.key !== "resume";
}

// Default for transitional states (provisioning, initializing)
return op.key === "logs" || op.key === "delete";
})
// Default for transitional states (provisioning, initializing)
return op.key === "logs" || op.key === "delete";
})
.map((op) => {
// Dynamic tunnel label based on whether tunnel is active
if (op.key === "tunnel") {
return hasTunnel
? {
...op,
label: "Tunnel (Active)",
color: colors.success,
icon: figures.tick,
}
: op;
}
return op;
})
: allOperations;

// Auto-execute operations that don't need input (except delete which needs confirmation)
Expand Down Expand Up @@ -471,10 +488,10 @@
input === "o" &&
operationResult &&
typeof operationResult === "object" &&
(operationResult as any).__customRender === "tunnel"

Check warning on line 491 in src/components/DevboxActionsMenu.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
) {
// Open tunnel URL in browser
const tunnelUrl = (operationResult as any).__tunnelUrl;

Check warning on line 494 in src/components/DevboxActionsMenu.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
if (tunnelUrl) {
const openBrowser = async () => {
const { exec } = await import("child_process");
Expand Down Expand Up @@ -505,46 +522,46 @@
(key.upArrow || input === "k") &&
operationResult &&
typeof operationResult === "object" &&
(operationResult as any).__customRender === "exec"

Check warning on line 525 in src/components/DevboxActionsMenu.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
) {
setExecScroll(Math.max(0, execScroll - 1));
} else if (
(key.downArrow || input === "j") &&
operationResult &&
typeof operationResult === "object" &&
(operationResult as any).__customRender === "exec"

Check warning on line 532 in src/components/DevboxActionsMenu.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
) {
setExecScroll(execScroll + 1);
} else if (
key.pageUp &&
operationResult &&
typeof operationResult === "object" &&
(operationResult as any).__customRender === "exec"

Check warning on line 539 in src/components/DevboxActionsMenu.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
) {
setExecScroll(Math.max(0, execScroll - 10));
} else if (
key.pageDown &&
operationResult &&
typeof operationResult === "object" &&
(operationResult as any).__customRender === "exec"

Check warning on line 546 in src/components/DevboxActionsMenu.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
) {
setExecScroll(execScroll + 10);
} else if (
input === "g" &&
operationResult &&
typeof operationResult === "object" &&
(operationResult as any).__customRender === "exec"

Check warning on line 553 in src/components/DevboxActionsMenu.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
) {
setExecScroll(0);
} else if (
input === "G" &&
operationResult &&
typeof operationResult === "object" &&
(operationResult as any).__customRender === "exec"

Check warning on line 560 in src/components/DevboxActionsMenu.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
) {
const lines = [
...((operationResult as any).stdout || "").split("\n"),

Check warning on line 563 in src/components/DevboxActionsMenu.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
...((operationResult as any).stderr || "").split("\n"),

Check warning on line 564 in src/components/DevboxActionsMenu.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
];
const maxScroll = Math.max(
0,
Expand Down
39 changes: 36 additions & 3 deletions src/components/DevboxCreatePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ type FormField =
| "metadata"
| "source"
| "network_policy_id"
| "tunnel_auth_mode"
| "gateways";

// Gateway configuration for devbox
Expand Down Expand Up @@ -94,6 +95,7 @@ interface FormData {
blueprint_id: string;
snapshot_id: string;
network_policy_id: string;
tunnel_auth_mode: "none" | "open" | "authenticated";
gateways: GatewaySpec[];
}

Expand All @@ -107,6 +109,7 @@ const resourceSizes = [
"XX_LARGE",
"CUSTOM_SIZE",
] as const;
const tunnelAuthModes = ["none", "open", "authenticated"] as const;

export const DevboxCreatePage = ({
onBack,
Expand All @@ -127,6 +130,7 @@ export const DevboxCreatePage = ({
blueprint_id: initialBlueprintId || "",
snapshot_id: initialSnapshotId || "",
network_policy_id: "",
tunnel_auth_mode: "none",
gateways: [],
});
const [metadataKey, setMetadataKey] = React.useState("");
Expand Down Expand Up @@ -242,6 +246,12 @@ export const DevboxCreatePage = ({
type: "picker",
placeholder: "Select a network policy...",
},
{
key: "tunnel_auth_mode",
label: "Tunnel (optional)",
type: "select",
placeholder: "none",
},
{
key: "gateways",
label: "Gateways (optional)",
Expand Down Expand Up @@ -273,6 +283,13 @@ export const DevboxCreatePage = ({
currentField === "resource_size",
);

const handleTunnelNav = useFormSelectNavigation(
formData.tunnel_auth_mode,
tunnelAuthModes,
(value) => setFormData({ ...formData, tunnel_auth_mode: value }),
currentField === "tunnel_auth_mode",
);

const handleSourceTypeNav = useFormSelectNavigation(
sourceTypeToggle,
sourceTypes,
Expand Down Expand Up @@ -378,6 +395,7 @@ export const DevboxCreatePage = ({
// Handle select field navigation using shared hooks
if (handleArchitectureNav(input, key)) return;
if (handleResourceSizeNav(input, key)) return;
if (handleTunnelNav(input, key)) return;
if (handleSourceTypeNav(input, key)) return;

// Navigation (up/down arrows and tab/shift+tab)
Expand Down Expand Up @@ -767,6 +785,13 @@ export const DevboxCreatePage = ({
createParams.gateways = gateways;
}

// Add tunnel configuration if not "none"
if (formData.tunnel_auth_mode !== "none") {
createParams.tunnel = {
auth_mode: formData.tunnel_auth_mode,
};
}

const devbox = await client.devboxes.create(createParams);
setResult(devbox);
} catch (err) {
Expand Down Expand Up @@ -1253,14 +1278,22 @@ export const DevboxCreatePage = ({

if (field.type === "select") {
const value = fieldData as string;
let options: readonly string[];
if (field.key === "architecture") {
options = architectures;
} else if (field.key === "resource_size") {
options = resourceSizes;
} else if (field.key === "tunnel_auth_mode") {
options = tunnelAuthModes;
} else {
options = [];
}
return (
<FormSelect
key={field.key}
label={field.label}
value={value || ""}
options={
field.key === "architecture" ? architectures : resourceSizes
}
options={options}
onChange={(newValue) =>
setFormData({ ...formData, [field.key]: newValue })
}
Expand Down
Loading