@runloop/rl-cli 1.8.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -7
- package/dist/cli.js +0 -0
- package/dist/commands/blueprint/delete.js +21 -0
- package/dist/commands/blueprint/list.js +226 -174
- package/dist/commands/blueprint/prune.js +13 -28
- package/dist/commands/devbox/create.js +41 -0
- package/dist/commands/devbox/list.js +142 -110
- package/dist/commands/devbox/rsync.js +69 -41
- package/dist/commands/devbox/scp.js +180 -39
- package/dist/commands/devbox/tunnel.js +4 -19
- package/dist/commands/gateway-config/create.js +53 -0
- package/dist/commands/gateway-config/delete.js +21 -0
- package/dist/commands/gateway-config/get.js +18 -0
- package/dist/commands/gateway-config/list.js +493 -0
- package/dist/commands/gateway-config/update.js +70 -0
- package/dist/commands/snapshot/list.js +11 -2
- package/dist/commands/snapshot/prune.js +265 -0
- package/dist/components/BenchmarkMenu.js +23 -3
- package/dist/components/DetailedInfoView.js +20 -0
- package/dist/components/DevboxActionsMenu.js +26 -62
- package/dist/components/DevboxCreatePage.js +763 -15
- package/dist/components/DevboxDetailPage.js +73 -24
- package/dist/components/GatewayConfigCreatePage.js +272 -0
- package/dist/components/LogsViewer.js +6 -40
- package/dist/components/ResourceDetailPage.js +143 -160
- package/dist/components/ResourceListView.js +3 -33
- package/dist/components/ResourcePicker.js +234 -0
- package/dist/components/SecretCreatePage.js +71 -27
- package/dist/components/SettingsMenu.js +12 -2
- package/dist/components/StateHistory.js +1 -20
- package/dist/components/StatusBadge.js +9 -2
- package/dist/components/StreamingLogsViewer.js +8 -42
- package/dist/components/form/FormTextInput.js +4 -2
- package/dist/components/resourceDetailTypes.js +18 -0
- package/dist/hooks/useInputHandler.js +103 -0
- package/dist/router/Router.js +79 -2
- package/dist/screens/BenchmarkDetailScreen.js +163 -0
- package/dist/screens/BenchmarkJobCreateScreen.js +524 -0
- package/dist/screens/BenchmarkJobDetailScreen.js +614 -0
- package/dist/screens/BenchmarkJobListScreen.js +479 -0
- package/dist/screens/BenchmarkListScreen.js +266 -0
- package/dist/screens/BenchmarkMenuScreen.js +6 -0
- package/dist/screens/BenchmarkRunDetailScreen.js +258 -22
- package/dist/screens/BenchmarkRunListScreen.js +21 -1
- package/dist/screens/BlueprintDetailScreen.js +5 -1
- package/dist/screens/DevboxCreateScreen.js +2 -2
- package/dist/screens/GatewayConfigDetailScreen.js +236 -0
- package/dist/screens/GatewayConfigListScreen.js +7 -0
- package/dist/screens/ScenarioRunDetailScreen.js +6 -0
- package/dist/screens/SecretDetailScreen.js +26 -2
- package/dist/screens/SettingsMenuScreen.js +3 -0
- package/dist/screens/SnapshotDetailScreen.js +6 -0
- package/dist/services/agentService.js +42 -0
- package/dist/services/benchmarkJobService.js +122 -0
- package/dist/services/benchmarkService.js +47 -0
- package/dist/services/gatewayConfigService.js +153 -0
- package/dist/services/scenarioService.js +34 -0
- package/dist/store/benchmarkJobStore.js +66 -0
- package/dist/store/benchmarkStore.js +63 -0
- package/dist/store/gatewayConfigStore.js +83 -0
- package/dist/utils/browser.js +22 -0
- package/dist/utils/clipboard.js +41 -0
- package/dist/utils/commands.js +105 -9
- package/dist/utils/gatewayConfigValidation.js +58 -0
- package/dist/utils/time.js +121 -0
- package/package.json +43 -43
|
@@ -10,9 +10,19 @@ import { SuccessMessage } from "./SuccessMessage.js";
|
|
|
10
10
|
import { Breadcrumb } from "./Breadcrumb.js";
|
|
11
11
|
import { NavigationTips } from "./NavigationTips.js";
|
|
12
12
|
import { MetadataDisplay } from "./MetadataDisplay.js";
|
|
13
|
+
import { ResourcePicker, createTextColumn } from "./ResourcePicker.js";
|
|
14
|
+
import { formatTimeAgo } from "./ResourceListView.js";
|
|
15
|
+
import { getStatusDisplay } from "./StatusBadge.js";
|
|
13
16
|
import { FormTextInput, FormSelect, FormActionButton, useFormSelectNavigation, } from "./form/index.js";
|
|
14
17
|
import { colors } from "../utils/theme.js";
|
|
15
18
|
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
|
|
19
|
+
import { listBlueprints } from "../services/blueprintService.js";
|
|
20
|
+
import { listSnapshots } from "../services/snapshotService.js";
|
|
21
|
+
import { listNetworkPolicies } from "../services/networkPolicyService.js";
|
|
22
|
+
import { listGatewayConfigs } from "../services/gatewayConfigService.js";
|
|
23
|
+
import { SecretCreatePage } from "./SecretCreatePage.js";
|
|
24
|
+
import { GatewayConfigCreatePage } from "./GatewayConfigCreatePage.js";
|
|
25
|
+
const sourceTypes = ["blueprint", "snapshot"];
|
|
16
26
|
const architectures = ["arm64", "x86_64"];
|
|
17
27
|
const resourceSizes = [
|
|
18
28
|
"X_SMALL",
|
|
@@ -23,6 +33,7 @@ const resourceSizes = [
|
|
|
23
33
|
"XX_LARGE",
|
|
24
34
|
"CUSTOM_SIZE",
|
|
25
35
|
];
|
|
36
|
+
const tunnelAuthModes = ["none", "open", "authenticated"];
|
|
26
37
|
export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initialSnapshotId, }) => {
|
|
27
38
|
const [currentField, setCurrentField] = React.useState("create");
|
|
28
39
|
const [formData, setFormData] = React.useState({
|
|
@@ -37,6 +48,8 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
37
48
|
blueprint_id: initialBlueprintId || "",
|
|
38
49
|
snapshot_id: initialSnapshotId || "",
|
|
39
50
|
network_policy_id: "",
|
|
51
|
+
tunnel_auth_mode: "none",
|
|
52
|
+
gateways: [],
|
|
40
53
|
});
|
|
41
54
|
const [metadataKey, setMetadataKey] = React.useState("");
|
|
42
55
|
const [metadataValue, setMetadataValue] = React.useState("");
|
|
@@ -46,6 +59,29 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
46
59
|
const [creating, setCreating] = React.useState(false);
|
|
47
60
|
const [result, setResult] = React.useState(null);
|
|
48
61
|
const [error, setError] = React.useState(null);
|
|
62
|
+
// Source picker states (toggle between blueprint/snapshot)
|
|
63
|
+
const [sourceTypeToggle, setSourceTypeToggle] = React.useState(initialSnapshotId ? "snapshot" : "blueprint");
|
|
64
|
+
const [showBlueprintPicker, setShowBlueprintPicker] = React.useState(false);
|
|
65
|
+
const [showSnapshotPicker, setShowSnapshotPicker] = React.useState(false);
|
|
66
|
+
const [showNetworkPolicyPicker, setShowNetworkPolicyPicker] = React.useState(false);
|
|
67
|
+
const [selectedBlueprintName, setSelectedBlueprintName] = React.useState("");
|
|
68
|
+
const [selectedSnapshotName, setSelectedSnapshotName] = React.useState("");
|
|
69
|
+
const [selectedNetworkPolicyName, setSelectedNetworkPolicyName] = React.useState("");
|
|
70
|
+
// Gateway picker states
|
|
71
|
+
const [showGatewayPicker, setShowGatewayPicker] = React.useState(false);
|
|
72
|
+
const [showSecretPicker, setShowSecretPicker] = React.useState(false);
|
|
73
|
+
const [inGatewaySection, setInGatewaySection] = React.useState(false);
|
|
74
|
+
const [gatewayEnvPrefix, setGatewayEnvPrefix] = React.useState("");
|
|
75
|
+
const [selectedGatewayIndex, setSelectedGatewayIndex] = React.useState(0);
|
|
76
|
+
const [pendingGateway, setPendingGateway] = React.useState(null);
|
|
77
|
+
const [pendingSecret, setPendingSecret] = React.useState(null);
|
|
78
|
+
const [showInlineSecretCreate, setShowInlineSecretCreate] = React.useState(false);
|
|
79
|
+
const [showInlineGatewayConfigCreate, setShowInlineGatewayConfigCreate] = React.useState(false);
|
|
80
|
+
// Gateway attach form: when active, shows a mini-form to configure a gateway
|
|
81
|
+
const [gatewayFormActive, setGatewayFormActive] = React.useState(false);
|
|
82
|
+
const [gatewayFormField, setGatewayFormField] = React.useState("attach");
|
|
83
|
+
const gatewayFormFields = ["attach", "gateway", "envName", "secret"];
|
|
84
|
+
const gatewayFormFieldIndex = gatewayFormFields.indexOf(gatewayFormField);
|
|
49
85
|
const baseFields = [
|
|
50
86
|
{ key: "create", label: "Devbox Create", type: "action" },
|
|
51
87
|
{ key: "name", label: "Name", type: "text", placeholder: "my-devbox" },
|
|
@@ -83,22 +119,28 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
83
119
|
placeholder: "3600",
|
|
84
120
|
},
|
|
85
121
|
{
|
|
86
|
-
key: "
|
|
87
|
-
label: "
|
|
88
|
-
type: "
|
|
89
|
-
placeholder: "
|
|
122
|
+
key: "source",
|
|
123
|
+
label: "Source (optional)",
|
|
124
|
+
type: "source",
|
|
125
|
+
placeholder: "Select Blueprint or Snapshot...",
|
|
90
126
|
},
|
|
91
127
|
{
|
|
92
|
-
key: "
|
|
93
|
-
label: "
|
|
94
|
-
type: "
|
|
95
|
-
placeholder: "
|
|
128
|
+
key: "network_policy_id",
|
|
129
|
+
label: "Network Policy (optional)",
|
|
130
|
+
type: "picker",
|
|
131
|
+
placeholder: "Select a network policy...",
|
|
96
132
|
},
|
|
97
133
|
{
|
|
98
|
-
key: "
|
|
99
|
-
label: "
|
|
100
|
-
type: "
|
|
101
|
-
placeholder: "
|
|
134
|
+
key: "tunnel_auth_mode",
|
|
135
|
+
label: "Tunnel (optional)",
|
|
136
|
+
type: "select",
|
|
137
|
+
placeholder: "none",
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
key: "gateways",
|
|
141
|
+
label: "AI Gateway Configs (optional)",
|
|
142
|
+
type: "gateways",
|
|
143
|
+
placeholder: "Configure API credential proxying...",
|
|
102
144
|
},
|
|
103
145
|
{ key: "metadata", label: "Metadata (optional)", type: "metadata" },
|
|
104
146
|
];
|
|
@@ -109,6 +151,8 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
109
151
|
// Select navigation handlers using shared hook
|
|
110
152
|
const handleArchitectureNav = useFormSelectNavigation(formData.architecture, architectures, (value) => setFormData({ ...formData, architecture: value }), currentField === "architecture");
|
|
111
153
|
const handleResourceSizeNav = useFormSelectNavigation(formData.resource_size || "SMALL", resourceSizes, (value) => setFormData({ ...formData, resource_size: value }), currentField === "resource_size");
|
|
154
|
+
const handleTunnelNav = useFormSelectNavigation(formData.tunnel_auth_mode, tunnelAuthModes, (value) => setFormData({ ...formData, tunnel_auth_mode: value }), currentField === "tunnel_auth_mode");
|
|
155
|
+
const handleSourceTypeNav = useFormSelectNavigation(sourceTypeToggle, sourceTypes, (value) => setSourceTypeToggle(value), currentField === "source");
|
|
112
156
|
// Main form input handler - active when not in metadata section
|
|
113
157
|
useInput((input, key) => {
|
|
114
158
|
// Handle result screen
|
|
@@ -155,6 +199,43 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
155
199
|
setSelectedMetadataIndex(0);
|
|
156
200
|
return;
|
|
157
201
|
}
|
|
202
|
+
// Enter key on gateways field to enter gateway section
|
|
203
|
+
if (currentField === "gateways" && key.return) {
|
|
204
|
+
setInGatewaySection(true);
|
|
205
|
+
setSelectedGatewayIndex(0);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
// Enter key on source field to open the appropriate picker
|
|
209
|
+
if (currentField === "source" && key.return) {
|
|
210
|
+
// If something is already selected, open that type's picker to change it
|
|
211
|
+
const hasBlueprint = !!(selectedBlueprintName || formData.blueprint_id);
|
|
212
|
+
const hasSnapshot = !!(selectedSnapshotName || formData.snapshot_id);
|
|
213
|
+
if (hasBlueprint) {
|
|
214
|
+
setShowBlueprintPicker(true);
|
|
215
|
+
}
|
|
216
|
+
else if (hasSnapshot) {
|
|
217
|
+
setShowSnapshotPicker(true);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
// Nothing selected, use the toggle value
|
|
221
|
+
if (sourceTypeToggle === "blueprint") {
|
|
222
|
+
setShowBlueprintPicker(true);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
setShowSnapshotPicker(true);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
// Delete key on source field to clear selection
|
|
231
|
+
if (currentField === "source" && (input === "d" || key.delete)) {
|
|
232
|
+
handleClearSource();
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (currentField === "network_policy_id" && key.return) {
|
|
236
|
+
setShowNetworkPolicyPicker(true);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
158
239
|
// Handle Enter on any field to submit
|
|
159
240
|
if (key.return) {
|
|
160
241
|
handleCreate();
|
|
@@ -165,6 +246,10 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
165
246
|
return;
|
|
166
247
|
if (handleResourceSizeNav(input, key))
|
|
167
248
|
return;
|
|
249
|
+
if (handleTunnelNav(input, key))
|
|
250
|
+
return;
|
|
251
|
+
if (handleSourceTypeNav(input, key))
|
|
252
|
+
return;
|
|
168
253
|
// Navigation (up/down arrows and tab/shift+tab)
|
|
169
254
|
if ((key.upArrow || (key.tab && key.shift)) && currentFieldIndex > 0) {
|
|
170
255
|
setCurrentField(fields[currentFieldIndex - 1].key);
|
|
@@ -175,7 +260,118 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
175
260
|
setCurrentField(fields[currentFieldIndex + 1].key);
|
|
176
261
|
return;
|
|
177
262
|
}
|
|
178
|
-
}, {
|
|
263
|
+
}, {
|
|
264
|
+
isActive: !inMetadataSection &&
|
|
265
|
+
!inGatewaySection &&
|
|
266
|
+
!showBlueprintPicker &&
|
|
267
|
+
!showSnapshotPicker &&
|
|
268
|
+
!showNetworkPolicyPicker &&
|
|
269
|
+
!showGatewayPicker &&
|
|
270
|
+
!showSecretPicker &&
|
|
271
|
+
!showInlineSecretCreate &&
|
|
272
|
+
!showInlineGatewayConfigCreate,
|
|
273
|
+
});
|
|
274
|
+
// Handle blueprint selection
|
|
275
|
+
const handleBlueprintSelect = React.useCallback((blueprints) => {
|
|
276
|
+
if (blueprints.length > 0) {
|
|
277
|
+
const blueprint = blueprints[0];
|
|
278
|
+
setFormData((prev) => ({
|
|
279
|
+
...prev,
|
|
280
|
+
blueprint_id: blueprint.id,
|
|
281
|
+
snapshot_id: "",
|
|
282
|
+
}));
|
|
283
|
+
setSelectedBlueprintName(blueprint.name || blueprint.id);
|
|
284
|
+
setSelectedSnapshotName("");
|
|
285
|
+
}
|
|
286
|
+
setShowBlueprintPicker(false);
|
|
287
|
+
}, []);
|
|
288
|
+
// Handle snapshot selection
|
|
289
|
+
const handleSnapshotSelect = React.useCallback((snapshots) => {
|
|
290
|
+
if (snapshots.length > 0) {
|
|
291
|
+
const snapshot = snapshots[0];
|
|
292
|
+
setFormData((prev) => ({
|
|
293
|
+
...prev,
|
|
294
|
+
snapshot_id: snapshot.id,
|
|
295
|
+
blueprint_id: "",
|
|
296
|
+
}));
|
|
297
|
+
setSelectedSnapshotName(snapshot.name || snapshot.id);
|
|
298
|
+
setSelectedBlueprintName("");
|
|
299
|
+
}
|
|
300
|
+
setShowSnapshotPicker(false);
|
|
301
|
+
}, []);
|
|
302
|
+
// Handle network policy selection
|
|
303
|
+
const handleNetworkPolicySelect = React.useCallback((policies) => {
|
|
304
|
+
if (policies.length > 0) {
|
|
305
|
+
const policy = policies[0];
|
|
306
|
+
setFormData((prev) => ({ ...prev, network_policy_id: policy.id }));
|
|
307
|
+
setSelectedNetworkPolicyName(policy.name || policy.id);
|
|
308
|
+
}
|
|
309
|
+
setShowNetworkPolicyPicker(false);
|
|
310
|
+
}, []);
|
|
311
|
+
// Handle gateway config selection
|
|
312
|
+
const handleGatewaySelect = React.useCallback((configs) => {
|
|
313
|
+
if (configs.length > 0) {
|
|
314
|
+
const config = configs[0];
|
|
315
|
+
const configName = config.name || config.id;
|
|
316
|
+
setPendingGateway({
|
|
317
|
+
id: config.id,
|
|
318
|
+
name: configName,
|
|
319
|
+
endpoint: config.endpoint || "",
|
|
320
|
+
});
|
|
321
|
+
// Auto-fill ENV name from config name (uppercase, underscores, no GWS_ prefix)
|
|
322
|
+
const autoEnvName = configName
|
|
323
|
+
.toUpperCase()
|
|
324
|
+
.replace(/[^A-Z0-9]+/g, "_")
|
|
325
|
+
.replace(/^_|_$/g, "");
|
|
326
|
+
setGatewayEnvPrefix(autoEnvName);
|
|
327
|
+
setShowGatewayPicker(false);
|
|
328
|
+
// Move to env name field in the form
|
|
329
|
+
setGatewayFormField("envName");
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
setShowGatewayPicker(false);
|
|
333
|
+
}
|
|
334
|
+
}, []);
|
|
335
|
+
// Handle secret selection for gateway
|
|
336
|
+
const handleSecretSelect = React.useCallback((secrets) => {
|
|
337
|
+
if (secrets.length > 0) {
|
|
338
|
+
const secret = secrets[0];
|
|
339
|
+
setPendingSecret({ id: secret.id, name: secret.name || secret.id });
|
|
340
|
+
}
|
|
341
|
+
setShowSecretPicker(false);
|
|
342
|
+
// Return to the form at the attach button
|
|
343
|
+
setGatewayFormField("attach");
|
|
344
|
+
}, []);
|
|
345
|
+
// Attach the configured gateway to the devbox
|
|
346
|
+
const handleAttachGateway = React.useCallback(() => {
|
|
347
|
+
if (!pendingGateway || !pendingSecret || !gatewayEnvPrefix.trim())
|
|
348
|
+
return;
|
|
349
|
+
const newGateway = {
|
|
350
|
+
envPrefix: gatewayEnvPrefix.trim(),
|
|
351
|
+
gateway: pendingGateway.id,
|
|
352
|
+
gatewayName: pendingGateway.name,
|
|
353
|
+
gatewayEndpoint: pendingGateway.endpoint,
|
|
354
|
+
secret: pendingSecret.id,
|
|
355
|
+
secretName: pendingSecret.name,
|
|
356
|
+
};
|
|
357
|
+
setFormData((prev) => ({
|
|
358
|
+
...prev,
|
|
359
|
+
gateways: [...prev.gateways, newGateway],
|
|
360
|
+
}));
|
|
361
|
+
// Reset form
|
|
362
|
+
setPendingGateway(null);
|
|
363
|
+
setPendingSecret(null);
|
|
364
|
+
setGatewayEnvPrefix("");
|
|
365
|
+
setGatewayFormActive(false);
|
|
366
|
+
setGatewayFormField("attach");
|
|
367
|
+
setSelectedGatewayIndex(0);
|
|
368
|
+
}, [pendingGateway, pendingSecret, gatewayEnvPrefix]);
|
|
369
|
+
// Handle clearing source
|
|
370
|
+
const handleClearSource = React.useCallback(() => {
|
|
371
|
+
setFormData((prev) => ({ ...prev, blueprint_id: "", snapshot_id: "" }));
|
|
372
|
+
setSelectedBlueprintName("");
|
|
373
|
+
setSelectedSnapshotName("");
|
|
374
|
+
}, []);
|
|
179
375
|
// Metadata section input handler - active when in metadata section
|
|
180
376
|
useInput((input, key) => {
|
|
181
377
|
const metadataKeys = Object.keys(formData.metadata);
|
|
@@ -265,6 +461,117 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
265
461
|
setMetadataInputMode(null);
|
|
266
462
|
}
|
|
267
463
|
}, { isActive: inMetadataSection });
|
|
464
|
+
// Gateway section input handler - active when in gateway section
|
|
465
|
+
useInput((input, key) => {
|
|
466
|
+
// === Gateway attach form mode ===
|
|
467
|
+
if (gatewayFormActive) {
|
|
468
|
+
// envName field is a text input - only handle navigation keys
|
|
469
|
+
if (gatewayFormField === "envName") {
|
|
470
|
+
if (key.upArrow || (key.tab && key.shift)) {
|
|
471
|
+
setGatewayFormField("gateway");
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (key.downArrow || (key.tab && !key.shift)) {
|
|
475
|
+
setGatewayFormField("secret");
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (key.escape) {
|
|
479
|
+
// Cancel the form
|
|
480
|
+
setPendingGateway(null);
|
|
481
|
+
setPendingSecret(null);
|
|
482
|
+
setGatewayEnvPrefix("");
|
|
483
|
+
setGatewayFormActive(false);
|
|
484
|
+
setGatewayFormField("attach");
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
// Let TextInput handle other keys
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
// Navigation between form fields
|
|
491
|
+
if (key.upArrow || (key.tab && key.shift)) {
|
|
492
|
+
const prevIdx = Math.max(0, gatewayFormFieldIndex - 1);
|
|
493
|
+
setGatewayFormField(gatewayFormFields[prevIdx]);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
if (key.downArrow || (key.tab && !key.shift)) {
|
|
497
|
+
const nextIdx = Math.min(gatewayFormFields.length - 1, gatewayFormFieldIndex + 1);
|
|
498
|
+
setGatewayFormField(gatewayFormFields[nextIdx]);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
// Enter on specific fields
|
|
502
|
+
if (key.return) {
|
|
503
|
+
if (gatewayFormField === "gateway") {
|
|
504
|
+
setShowGatewayPicker(true);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (gatewayFormField === "secret") {
|
|
508
|
+
setShowSecretPicker(true);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
if (gatewayFormField === "attach") {
|
|
512
|
+
handleAttachGateway();
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (key.escape || input === "q") {
|
|
517
|
+
// Cancel the form
|
|
518
|
+
setPendingGateway(null);
|
|
519
|
+
setPendingSecret(null);
|
|
520
|
+
setGatewayEnvPrefix("");
|
|
521
|
+
setGatewayFormActive(false);
|
|
522
|
+
setGatewayFormField("attach");
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
// === List navigation mode (existing gateways + attach/done) ===
|
|
528
|
+
const gatewayCount = formData.gateways.length;
|
|
529
|
+
const maxIndex = gatewayCount + 1; // Attach + existing items + Done
|
|
530
|
+
if (key.upArrow && selectedGatewayIndex > 0) {
|
|
531
|
+
setSelectedGatewayIndex(selectedGatewayIndex - 1);
|
|
532
|
+
}
|
|
533
|
+
else if (key.downArrow && selectedGatewayIndex < maxIndex) {
|
|
534
|
+
setSelectedGatewayIndex(selectedGatewayIndex + 1);
|
|
535
|
+
}
|
|
536
|
+
else if (key.return) {
|
|
537
|
+
if (selectedGatewayIndex === 0) {
|
|
538
|
+
// Open the attach form
|
|
539
|
+
setPendingGateway(null);
|
|
540
|
+
setPendingSecret(null);
|
|
541
|
+
setGatewayEnvPrefix("");
|
|
542
|
+
setGatewayFormActive(true);
|
|
543
|
+
setGatewayFormField("gateway");
|
|
544
|
+
}
|
|
545
|
+
else if (selectedGatewayIndex === maxIndex) {
|
|
546
|
+
// Done
|
|
547
|
+
setInGatewaySection(false);
|
|
548
|
+
setSelectedGatewayIndex(0);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
else if ((input === "d" || key.delete) &&
|
|
552
|
+
selectedGatewayIndex >= 1 &&
|
|
553
|
+
selectedGatewayIndex <= gatewayCount) {
|
|
554
|
+
// Remove gateway at index
|
|
555
|
+
const indexToDelete = selectedGatewayIndex - 1;
|
|
556
|
+
const newGateways = [...formData.gateways];
|
|
557
|
+
newGateways.splice(indexToDelete, 1);
|
|
558
|
+
setFormData({ ...formData, gateways: newGateways });
|
|
559
|
+
const newLength = newGateways.length;
|
|
560
|
+
if (selectedGatewayIndex > newLength) {
|
|
561
|
+
setSelectedGatewayIndex(Math.max(0, newLength));
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
else if (key.escape || input === "q") {
|
|
565
|
+
setInGatewaySection(false);
|
|
566
|
+
setSelectedGatewayIndex(0);
|
|
567
|
+
}
|
|
568
|
+
}, {
|
|
569
|
+
isActive: inGatewaySection &&
|
|
570
|
+
!showGatewayPicker &&
|
|
571
|
+
!showSecretPicker &&
|
|
572
|
+
!showInlineSecretCreate &&
|
|
573
|
+
!showInlineGatewayConfigCreate,
|
|
574
|
+
});
|
|
268
575
|
// Validate custom resource configuration
|
|
269
576
|
const validateCustomResources = () => {
|
|
270
577
|
if (formData.resource_size !== "CUSTOM_SIZE") {
|
|
@@ -342,6 +649,23 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
342
649
|
if (Object.keys(launchParameters).length > 0) {
|
|
343
650
|
createParams.launch_parameters = launchParameters;
|
|
344
651
|
}
|
|
652
|
+
// Add gateway specifications
|
|
653
|
+
if (formData.gateways.length > 0) {
|
|
654
|
+
const gateways = {};
|
|
655
|
+
for (const gw of formData.gateways) {
|
|
656
|
+
gateways[gw.envPrefix] = {
|
|
657
|
+
gateway: gw.gateway,
|
|
658
|
+
secret: gw.secret,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
createParams.gateways = gateways;
|
|
662
|
+
}
|
|
663
|
+
// Add tunnel configuration if not "none"
|
|
664
|
+
if (formData.tunnel_auth_mode !== "none") {
|
|
665
|
+
createParams.tunnel = {
|
|
666
|
+
auth_mode: formData.tunnel_auth_mode,
|
|
667
|
+
};
|
|
668
|
+
}
|
|
345
669
|
const devbox = await client.devboxes.create(createParams);
|
|
346
670
|
setResult(devbox);
|
|
347
671
|
}
|
|
@@ -367,6 +691,331 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
367
691
|
if (creating) {
|
|
368
692
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes" }, { label: "Create", active: true }] }), _jsx(SpinnerComponent, { message: "Creating devbox..." })] }));
|
|
369
693
|
}
|
|
694
|
+
// Blueprint picker screen
|
|
695
|
+
if (showBlueprintPicker) {
|
|
696
|
+
const blueprintColumns = [
|
|
697
|
+
{
|
|
698
|
+
key: "statusIcon",
|
|
699
|
+
label: "",
|
|
700
|
+
width: 2,
|
|
701
|
+
render: (blueprint, _index, isSelected) => {
|
|
702
|
+
const statusDisplay = getStatusDisplay(blueprint.status || "");
|
|
703
|
+
return (_jsxs(Text, { color: isSelected ? "white" : statusDisplay.color, bold: true, inverse: isSelected, wrap: "truncate", children: [statusDisplay.icon, " "] }));
|
|
704
|
+
},
|
|
705
|
+
},
|
|
706
|
+
createTextColumn("id", "ID", (blueprint) => blueprint.id, {
|
|
707
|
+
width: 25,
|
|
708
|
+
color: colors.idColor,
|
|
709
|
+
}),
|
|
710
|
+
createTextColumn("name", "Name", (blueprint) => blueprint.name || "", { width: 30 }),
|
|
711
|
+
{
|
|
712
|
+
key: "status",
|
|
713
|
+
label: "Status",
|
|
714
|
+
width: 12,
|
|
715
|
+
render: (blueprint, _index, isSelected) => {
|
|
716
|
+
const statusDisplay = getStatusDisplay(blueprint.status || "");
|
|
717
|
+
const padded = statusDisplay.text.slice(0, 12).padEnd(12, " ");
|
|
718
|
+
return (_jsx(Text, { color: isSelected ? "white" : statusDisplay.color, bold: true, inverse: isSelected, wrap: "truncate", children: padded }));
|
|
719
|
+
},
|
|
720
|
+
},
|
|
721
|
+
createTextColumn("created", "Created", (blueprint) => blueprint.create_time_ms
|
|
722
|
+
? formatTimeAgo(blueprint.create_time_ms)
|
|
723
|
+
: "", { width: 18, color: colors.textDim }),
|
|
724
|
+
];
|
|
725
|
+
// Filter out failed blueprints
|
|
726
|
+
const failedStatuses = ["failure", "build_failed", "failed"];
|
|
727
|
+
return (_jsx(ResourcePicker, { config: {
|
|
728
|
+
title: "Select Blueprint",
|
|
729
|
+
fetchPage: async (params) => {
|
|
730
|
+
const result = await listBlueprints({
|
|
731
|
+
limit: params.limit,
|
|
732
|
+
startingAfter: params.startingAt,
|
|
733
|
+
search: params.search,
|
|
734
|
+
});
|
|
735
|
+
// Filter out failed blueprints
|
|
736
|
+
const validBlueprints = result.blueprints.filter((bp) => !failedStatuses.includes(bp.status || ""));
|
|
737
|
+
return {
|
|
738
|
+
items: validBlueprints,
|
|
739
|
+
hasMore: result.hasMore,
|
|
740
|
+
totalCount: validBlueprints.length,
|
|
741
|
+
};
|
|
742
|
+
},
|
|
743
|
+
getItemId: (blueprint) => blueprint.id,
|
|
744
|
+
getItemLabel: (blueprint) => blueprint.name || blueprint.id,
|
|
745
|
+
columns: blueprintColumns,
|
|
746
|
+
mode: "single",
|
|
747
|
+
emptyMessage: "No blueprints found (failed blueprints are hidden)",
|
|
748
|
+
searchPlaceholder: "Search blueprints...",
|
|
749
|
+
breadcrumbItems: [
|
|
750
|
+
{ label: "Devboxes" },
|
|
751
|
+
{ label: "Create" },
|
|
752
|
+
{ label: "Select Blueprint", active: true },
|
|
753
|
+
],
|
|
754
|
+
}, onSelect: handleBlueprintSelect, onCancel: () => setShowBlueprintPicker(false), initialSelected: formData.blueprint_id ? [formData.blueprint_id] : [] }));
|
|
755
|
+
}
|
|
756
|
+
// Snapshot picker screen
|
|
757
|
+
if (showSnapshotPicker) {
|
|
758
|
+
const snapshotColumns = [
|
|
759
|
+
createTextColumn("id", "ID", (snapshot) => snapshot.id, {
|
|
760
|
+
width: 25,
|
|
761
|
+
color: colors.idColor,
|
|
762
|
+
}),
|
|
763
|
+
createTextColumn("name", "Name", (snapshot) => snapshot.name || "", { width: 30 }),
|
|
764
|
+
createTextColumn("status", "Status", (snapshot) => snapshot.status || "", { width: 12 }),
|
|
765
|
+
createTextColumn("created", "Created", (snapshot) => snapshot.create_time_ms ? formatTimeAgo(snapshot.create_time_ms) : "", { width: 18, color: colors.textDim }),
|
|
766
|
+
];
|
|
767
|
+
return (_jsx(ResourcePicker, { config: {
|
|
768
|
+
title: "Select Snapshot",
|
|
769
|
+
fetchPage: async (params) => {
|
|
770
|
+
const result = await listSnapshots({
|
|
771
|
+
limit: params.limit,
|
|
772
|
+
startingAfter: params.startingAt,
|
|
773
|
+
});
|
|
774
|
+
return {
|
|
775
|
+
items: result.snapshots,
|
|
776
|
+
hasMore: result.hasMore,
|
|
777
|
+
totalCount: result.totalCount,
|
|
778
|
+
};
|
|
779
|
+
},
|
|
780
|
+
getItemId: (snapshot) => snapshot.id,
|
|
781
|
+
getItemLabel: (snapshot) => snapshot.name || snapshot.id,
|
|
782
|
+
columns: snapshotColumns,
|
|
783
|
+
mode: "single",
|
|
784
|
+
emptyMessage: "No snapshots found",
|
|
785
|
+
searchPlaceholder: "Search snapshots...",
|
|
786
|
+
breadcrumbItems: [
|
|
787
|
+
{ label: "Devboxes" },
|
|
788
|
+
{ label: "Create" },
|
|
789
|
+
{ label: "Select Snapshot", active: true },
|
|
790
|
+
],
|
|
791
|
+
}, onSelect: handleSnapshotSelect, onCancel: () => setShowSnapshotPicker(false), initialSelected: formData.snapshot_id ? [formData.snapshot_id] : [] }));
|
|
792
|
+
}
|
|
793
|
+
// Network policy picker screen
|
|
794
|
+
if (showNetworkPolicyPicker) {
|
|
795
|
+
// Helper to get egress type label
|
|
796
|
+
const getEgressLabel = (egress) => {
|
|
797
|
+
if (egress.allow_all)
|
|
798
|
+
return "Allow All";
|
|
799
|
+
if (egress.allowed_hostnames?.length === 0)
|
|
800
|
+
return "Deny All";
|
|
801
|
+
return `Custom (${egress.allowed_hostnames?.length || 0})`;
|
|
802
|
+
};
|
|
803
|
+
const buildNetworkPolicyColumns = (tw) => {
|
|
804
|
+
const fixedWidth = 6;
|
|
805
|
+
const idWidth = 25;
|
|
806
|
+
const egressWidth = 15;
|
|
807
|
+
const timeWidth = 18;
|
|
808
|
+
const baseWidth = fixedWidth + idWidth + egressWidth + timeWidth;
|
|
809
|
+
const nameWidth = Math.min(80, Math.max(15, tw - baseWidth));
|
|
810
|
+
return [
|
|
811
|
+
createTextColumn("id", "ID", (policy) => policy.id, {
|
|
812
|
+
width: idWidth + 1,
|
|
813
|
+
color: colors.idColor,
|
|
814
|
+
}),
|
|
815
|
+
createTextColumn("name", "Name", (policy) => policy.name || "", { width: nameWidth }),
|
|
816
|
+
createTextColumn("egress", "Egress", (policy) => getEgressLabel(policy.egress), { width: egressWidth }),
|
|
817
|
+
createTextColumn("created", "Created", (policy) => policy.create_time_ms ? formatTimeAgo(policy.create_time_ms) : "", { width: timeWidth, color: colors.textDim }),
|
|
818
|
+
];
|
|
819
|
+
};
|
|
820
|
+
return (_jsx(ResourcePicker, { config: {
|
|
821
|
+
title: "Select Network Policy",
|
|
822
|
+
fetchPage: async (params) => {
|
|
823
|
+
const result = await listNetworkPolicies({
|
|
824
|
+
limit: params.limit,
|
|
825
|
+
startingAfter: params.startingAt,
|
|
826
|
+
search: params.search,
|
|
827
|
+
});
|
|
828
|
+
return {
|
|
829
|
+
items: result.networkPolicies,
|
|
830
|
+
hasMore: result.hasMore,
|
|
831
|
+
totalCount: result.totalCount,
|
|
832
|
+
};
|
|
833
|
+
},
|
|
834
|
+
getItemId: (policy) => policy.id,
|
|
835
|
+
getItemLabel: (policy) => policy.name || policy.id,
|
|
836
|
+
columns: buildNetworkPolicyColumns,
|
|
837
|
+
mode: "single",
|
|
838
|
+
emptyMessage: "No network policies found",
|
|
839
|
+
searchPlaceholder: "Search network policies...",
|
|
840
|
+
breadcrumbItems: [
|
|
841
|
+
{ label: "Devboxes" },
|
|
842
|
+
{ label: "Create" },
|
|
843
|
+
{ label: "Select Network Policy", active: true },
|
|
844
|
+
],
|
|
845
|
+
}, onSelect: handleNetworkPolicySelect, onCancel: () => setShowNetworkPolicyPicker(false), initialSelected: formData.network_policy_id ? [formData.network_policy_id] : [] }));
|
|
846
|
+
}
|
|
847
|
+
// Inline gateway config creation screen (from gateway attach flow)
|
|
848
|
+
if (showInlineGatewayConfigCreate) {
|
|
849
|
+
return (_jsx(GatewayConfigCreatePage, { onBack: () => {
|
|
850
|
+
setShowInlineGatewayConfigCreate(false);
|
|
851
|
+
// Return to gateway picker
|
|
852
|
+
setShowGatewayPicker(true);
|
|
853
|
+
}, onCreate: (config) => {
|
|
854
|
+
setShowInlineGatewayConfigCreate(false);
|
|
855
|
+
// Auto-select the newly created gateway config
|
|
856
|
+
const configName = config.name || config.id;
|
|
857
|
+
setPendingGateway({
|
|
858
|
+
id: config.id,
|
|
859
|
+
name: configName,
|
|
860
|
+
endpoint: config.endpoint || "",
|
|
861
|
+
});
|
|
862
|
+
const autoEnvName = configName
|
|
863
|
+
.toUpperCase()
|
|
864
|
+
.replace(/[^A-Z0-9]+/g, "_")
|
|
865
|
+
.replace(/^_|_$/g, "");
|
|
866
|
+
setGatewayEnvPrefix(autoEnvName);
|
|
867
|
+
setShowGatewayPicker(false);
|
|
868
|
+
setGatewayFormField("envName");
|
|
869
|
+
} }));
|
|
870
|
+
}
|
|
871
|
+
// Gateway config picker screen
|
|
872
|
+
if (showGatewayPicker) {
|
|
873
|
+
const buildGatewayColumns = (tw) => {
|
|
874
|
+
const fixedWidth = 6;
|
|
875
|
+
const idWidth = 25;
|
|
876
|
+
const timeWidth = 20;
|
|
877
|
+
const showEndpoint = tw >= 100;
|
|
878
|
+
const endpointWidth = Math.max(20, tw >= 140 ? 40 : 25);
|
|
879
|
+
const baseWidth = fixedWidth + idWidth + timeWidth;
|
|
880
|
+
const optionalWidth = showEndpoint ? endpointWidth : 0;
|
|
881
|
+
const nameWidth = Math.min(80, Math.max(15, tw - baseWidth - optionalWidth));
|
|
882
|
+
return [
|
|
883
|
+
createTextColumn("id", "ID", (config) => config.id, {
|
|
884
|
+
width: idWidth + 1,
|
|
885
|
+
color: colors.idColor,
|
|
886
|
+
}),
|
|
887
|
+
createTextColumn("name", "Name", (config) => config.name || "", { width: nameWidth }),
|
|
888
|
+
...(showEndpoint
|
|
889
|
+
? [
|
|
890
|
+
createTextColumn("endpoint", "Endpoint", (config) => config.endpoint || "", { width: endpointWidth, color: colors.textDim }),
|
|
891
|
+
]
|
|
892
|
+
: []),
|
|
893
|
+
createTextColumn("created", "Created", (config) => config.create_time_ms ? formatTimeAgo(config.create_time_ms) : "", { width: timeWidth, color: colors.textDim }),
|
|
894
|
+
];
|
|
895
|
+
};
|
|
896
|
+
return (_jsx(ResourcePicker, { config: {
|
|
897
|
+
title: "Select AI Gateway Config",
|
|
898
|
+
fetchPage: async (params) => {
|
|
899
|
+
const result = await listGatewayConfigs({
|
|
900
|
+
limit: params.limit,
|
|
901
|
+
startingAfter: params.startingAt,
|
|
902
|
+
search: params.search,
|
|
903
|
+
});
|
|
904
|
+
return {
|
|
905
|
+
items: result.gatewayConfigs,
|
|
906
|
+
hasMore: result.hasMore,
|
|
907
|
+
totalCount: result.totalCount,
|
|
908
|
+
};
|
|
909
|
+
},
|
|
910
|
+
getItemId: (config) => config.id,
|
|
911
|
+
getItemLabel: (config) => config.name || config.id,
|
|
912
|
+
columns: buildGatewayColumns,
|
|
913
|
+
mode: "single",
|
|
914
|
+
emptyMessage: "No AI gateway configs found",
|
|
915
|
+
searchPlaceholder: "Search AI gateway configs...",
|
|
916
|
+
breadcrumbItems: [
|
|
917
|
+
{ label: "Devboxes" },
|
|
918
|
+
{ label: "Create" },
|
|
919
|
+
{ label: "Attach AI Gateway Config" },
|
|
920
|
+
{ label: "Select Config", active: true },
|
|
921
|
+
],
|
|
922
|
+
onCreateNew: () => {
|
|
923
|
+
setShowGatewayPicker(false);
|
|
924
|
+
setShowInlineGatewayConfigCreate(true);
|
|
925
|
+
},
|
|
926
|
+
createNewLabel: "Create AI gateway config",
|
|
927
|
+
}, onSelect: handleGatewaySelect, onCancel: () => {
|
|
928
|
+
setShowGatewayPicker(false);
|
|
929
|
+
// Return to the form at the gateway field
|
|
930
|
+
setGatewayFormField("gateway");
|
|
931
|
+
}, initialSelected: [] }, "gateway-config-picker"));
|
|
932
|
+
}
|
|
933
|
+
// Inline secret creation screen (from gateway flow)
|
|
934
|
+
if (showInlineSecretCreate) {
|
|
935
|
+
return (_jsx(SecretCreatePage, { onBack: () => {
|
|
936
|
+
setShowInlineSecretCreate(false);
|
|
937
|
+
// Return to secret picker
|
|
938
|
+
setShowSecretPicker(true);
|
|
939
|
+
}, onCreate: (secret) => {
|
|
940
|
+
setShowInlineSecretCreate(false);
|
|
941
|
+
// Store as pending secret and return to the attach form
|
|
942
|
+
setPendingSecret({
|
|
943
|
+
id: secret.id,
|
|
944
|
+
name: secret.name || secret.id,
|
|
945
|
+
});
|
|
946
|
+
setShowSecretPicker(false);
|
|
947
|
+
setGatewayFormField("attach");
|
|
948
|
+
} }));
|
|
949
|
+
}
|
|
950
|
+
// Secret picker screen (for gateway)
|
|
951
|
+
if (showSecretPicker) {
|
|
952
|
+
const buildSecretColumns = (tw) => {
|
|
953
|
+
const fixedWidth = 6;
|
|
954
|
+
const idWidth = 30;
|
|
955
|
+
const timeWidth = 20;
|
|
956
|
+
const baseWidth = fixedWidth + idWidth + timeWidth;
|
|
957
|
+
const nameWidth = Math.min(80, Math.max(15, tw - baseWidth));
|
|
958
|
+
return [
|
|
959
|
+
createTextColumn("id", "ID", (secret) => secret.id, {
|
|
960
|
+
width: idWidth + 1,
|
|
961
|
+
color: colors.idColor,
|
|
962
|
+
}),
|
|
963
|
+
createTextColumn("name", "Name", (secret) => secret.name || "", { width: nameWidth }),
|
|
964
|
+
createTextColumn("created", "Created", (secret) => secret.create_time_ms ? formatTimeAgo(secret.create_time_ms) : "", { width: timeWidth, color: colors.textDim }),
|
|
965
|
+
];
|
|
966
|
+
};
|
|
967
|
+
return (_jsx(ResourcePicker, { config: {
|
|
968
|
+
title: "Select Secret for Gateway",
|
|
969
|
+
fetchPage: async (params) => {
|
|
970
|
+
const client = getClient();
|
|
971
|
+
// Secrets API doesn't support cursor pagination, so we fetch all
|
|
972
|
+
// and do client-side pagination by slicing to the requested page
|
|
973
|
+
const page = await client.secrets.list({
|
|
974
|
+
limit: 1000,
|
|
975
|
+
});
|
|
976
|
+
const allSecrets = (page.secrets || []).map((s) => ({
|
|
977
|
+
id: s.id,
|
|
978
|
+
name: s.name,
|
|
979
|
+
create_time_ms: s.create_time_ms,
|
|
980
|
+
}));
|
|
981
|
+
// Client-side cursor pagination
|
|
982
|
+
let startIdx = 0;
|
|
983
|
+
if (params.startingAt) {
|
|
984
|
+
const cursorIdx = allSecrets.findIndex((s) => s.id === params.startingAt);
|
|
985
|
+
if (cursorIdx >= 0) {
|
|
986
|
+
startIdx = cursorIdx + 1;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
const sliced = allSecrets.slice(startIdx, startIdx + params.limit);
|
|
990
|
+
return {
|
|
991
|
+
items: sliced,
|
|
992
|
+
hasMore: startIdx + params.limit < allSecrets.length,
|
|
993
|
+
totalCount: allSecrets.length,
|
|
994
|
+
};
|
|
995
|
+
},
|
|
996
|
+
getItemId: (secret) => secret.id,
|
|
997
|
+
getItemLabel: (secret) => secret.name || secret.id,
|
|
998
|
+
columns: buildSecretColumns,
|
|
999
|
+
mode: "single",
|
|
1000
|
+
emptyMessage: "No secrets found",
|
|
1001
|
+
searchPlaceholder: "Search secrets...",
|
|
1002
|
+
breadcrumbItems: [
|
|
1003
|
+
{ label: "Devboxes" },
|
|
1004
|
+
{ label: "Create" },
|
|
1005
|
+
{ label: "Attach AI Gateway Config" },
|
|
1006
|
+
{ label: "Select Secret", active: true },
|
|
1007
|
+
],
|
|
1008
|
+
onCreateNew: () => {
|
|
1009
|
+
setShowSecretPicker(false);
|
|
1010
|
+
setShowInlineSecretCreate(true);
|
|
1011
|
+
},
|
|
1012
|
+
createNewLabel: "Create secret",
|
|
1013
|
+
}, onSelect: handleSecretSelect, onCancel: () => {
|
|
1014
|
+
setShowSecretPicker(false);
|
|
1015
|
+
// Return to the form at the secret field
|
|
1016
|
+
setGatewayFormField("secret");
|
|
1017
|
+
}, initialSelected: [] }, "secret-picker"));
|
|
1018
|
+
}
|
|
370
1019
|
// Form screen
|
|
371
1020
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes" }, { label: "Create", active: true }] }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: fields.map((field) => {
|
|
372
1021
|
const isActive = currentField === field.key;
|
|
@@ -379,7 +1028,49 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
379
1028
|
}
|
|
380
1029
|
if (field.type === "select") {
|
|
381
1030
|
const value = fieldData;
|
|
382
|
-
|
|
1031
|
+
let options;
|
|
1032
|
+
if (field.key === "architecture") {
|
|
1033
|
+
options = architectures;
|
|
1034
|
+
}
|
|
1035
|
+
else if (field.key === "resource_size") {
|
|
1036
|
+
options = resourceSizes;
|
|
1037
|
+
}
|
|
1038
|
+
else if (field.key === "tunnel_auth_mode") {
|
|
1039
|
+
options = tunnelAuthModes;
|
|
1040
|
+
}
|
|
1041
|
+
else {
|
|
1042
|
+
options = [];
|
|
1043
|
+
}
|
|
1044
|
+
return (_jsx(FormSelect, { label: field.label, value: value || "", options: options, onChange: (newValue) => setFormData({ ...formData, [field.key]: newValue }), isActive: isActive }, field.key));
|
|
1045
|
+
}
|
|
1046
|
+
if (field.type === "source") {
|
|
1047
|
+
// Check if either blueprint or snapshot is selected
|
|
1048
|
+
const selectedBlueprintValue = selectedBlueprintName || formData.blueprint_id;
|
|
1049
|
+
const selectedSnapshotValue = selectedSnapshotName || formData.snapshot_id;
|
|
1050
|
+
const hasBlueprint = !!selectedBlueprintValue;
|
|
1051
|
+
const hasSnapshot = !!selectedSnapshotValue;
|
|
1052
|
+
const hasSelection = hasBlueprint || hasSnapshot;
|
|
1053
|
+
// If something is selected, show it clearly with its type
|
|
1054
|
+
if (hasSelection) {
|
|
1055
|
+
const selectedType = hasBlueprint ? "Blueprint" : "Snapshot";
|
|
1056
|
+
const selectedValue = hasBlueprint
|
|
1057
|
+
? selectedBlueprintValue
|
|
1058
|
+
: selectedSnapshotValue;
|
|
1059
|
+
return (_jsxs(Box, { marginBottom: 0, children: [_jsxs(Text, { color: isActive ? colors.primary : colors.textDim, children: [isActive ? figures.pointer : " ", " ", field.label, ":", " "] }), _jsxs(Text, { color: colors.success, children: [selectedType, ": "] }), _jsx(Text, { color: colors.idColor, children: selectedValue }), isActive && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Enter to change, d to clear]"] }))] }, field.key));
|
|
1060
|
+
}
|
|
1061
|
+
// Nothing selected - show toggle to choose type
|
|
1062
|
+
return (_jsxs(Box, { marginBottom: 0, children: [_jsxs(Text, { color: isActive ? colors.primary : colors.textDim, children: [isActive ? figures.pointer : " ", " ", field.label, ":", " "] }), _jsxs(Text, { color: isActive ? colors.text : colors.textDim, children: [isActive ? figures.arrowLeft : "", " "] }), _jsx(Text, { color: sourceTypeToggle === "blueprint"
|
|
1063
|
+
? colors.primary
|
|
1064
|
+
: colors.textDim, bold: sourceTypeToggle === "blueprint", children: "Blueprint" }), _jsx(Text, { color: colors.textDim, children: " / " }), _jsx(Text, { color: sourceTypeToggle === "snapshot"
|
|
1065
|
+
? colors.primary
|
|
1066
|
+
: colors.textDim, bold: sourceTypeToggle === "snapshot", children: "Snapshot" }), _jsxs(Text, { color: isActive ? colors.text : colors.textDim, children: [" ", isActive ? figures.arrowRight : ""] }), isActive && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Enter to select]"] }))] }, field.key));
|
|
1067
|
+
}
|
|
1068
|
+
if (field.type === "picker") {
|
|
1069
|
+
const value = fieldData;
|
|
1070
|
+
const displayName = field.key === "network_policy_id"
|
|
1071
|
+
? selectedNetworkPolicyName || value
|
|
1072
|
+
: value;
|
|
1073
|
+
return (_jsxs(Box, { marginBottom: 0, children: [_jsxs(Text, { color: isActive ? colors.primary : colors.textDim, children: [isActive ? figures.pointer : " ", " ", field.label, ":", " "] }), displayName ? (_jsx(Text, { color: colors.idColor, children: displayName })) : (_jsx(Text, { color: colors.textDim, dimColor: true, children: field.placeholder || "(none)" })), isActive && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Enter to ", displayName ? "change" : "select", "]"] }))] }, field.key));
|
|
383
1074
|
}
|
|
384
1075
|
if (field.type === "metadata") {
|
|
385
1076
|
if (!inMetadataSection) {
|
|
@@ -414,9 +1105,66 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
414
1105
|
? `[Tab] Switch field • [Enter] ${metadataInputMode === "key" ? "Next" : "Save"} • [esc] Cancel`
|
|
415
1106
|
: `${figures.arrowUp}${figures.arrowDown} Navigate • [Enter] ${selectedMetadataIndex === 0 ? "Add" : selectedMetadataIndex === maxIndex ? "Done" : "Edit"} • [d] Delete • [esc] Back` }) })] }, field.key));
|
|
416
1107
|
}
|
|
1108
|
+
if (field.type === "gateways") {
|
|
1109
|
+
if (!inGatewaySection) {
|
|
1110
|
+
// Collapsed view
|
|
1111
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 0, children: [_jsxs(Box, { children: [_jsxs(Text, { color: isActive ? colors.primary : colors.textDim, children: [isActive ? figures.pointer : " ", " ", field.label, ":", " "] }), _jsxs(Text, { color: colors.text, children: [formData.gateways.length, " configured"] }), isActive && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Enter to manage]"] }))] }), formData.gateways.length > 0 && (_jsx(Box, { marginLeft: 2, flexDirection: "column", children: formData.gateways.map((gw, idx) => (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.pointer, " ENV: ", gw.envPrefix, " | Config:", " ", gw.gatewayName, " (", gw.gatewayEndpoint, ") | Secret:", " ", gw.secretName] }, idx))) }))] }, field.key));
|
|
1112
|
+
}
|
|
1113
|
+
// Expanded gateway section view
|
|
1114
|
+
const gatewayCount = formData.gateways.length;
|
|
1115
|
+
const maxGatewayIndex = gatewayCount + 1;
|
|
1116
|
+
const canAttach = !!pendingGateway && !!pendingSecret && !!gatewayEnvPrefix.trim();
|
|
1117
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.primary, paddingX: 1, paddingY: 1, marginBottom: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " Configure AI Gateway Configs for Devbox"] }), gatewayFormActive && (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: colors.success, paddingX: 1, children: [_jsx(Text, { color: colors.success, bold: true, children: "Attach AI Gateway Config" }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: gatewayFormField === "attach"
|
|
1118
|
+
? canAttach
|
|
1119
|
+
? colors.success
|
|
1120
|
+
: colors.primary
|
|
1121
|
+
: colors.textDim, children: [gatewayFormField === "attach"
|
|
1122
|
+
? figures.pointer
|
|
1123
|
+
: " ", " "] }), _jsx(Text, { color: gatewayFormField === "attach"
|
|
1124
|
+
? canAttach
|
|
1125
|
+
? colors.success
|
|
1126
|
+
: colors.primary
|
|
1127
|
+
: colors.textDim, bold: gatewayFormField === "attach", children: canAttach
|
|
1128
|
+
? `${figures.tick} Attach Gateway`
|
|
1129
|
+
: `${figures.ellipsis} Attach Gateway (fill fields below)` })] }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: gatewayFormField === "gateway"
|
|
1130
|
+
? colors.primary
|
|
1131
|
+
: colors.textDim, children: [gatewayFormField === "gateway" ? figures.pointer : " ", " ", "AI Gateway Config:", " "] }), pendingGateway ? (_jsxs(Text, { color: colors.success, children: [pendingGateway.name, " ", _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["(", pendingGateway.endpoint, ")"] })] })) : (_jsx(Text, { color: colors.textDim, dimColor: true, children: "(none selected)" })), gatewayFormField === "gateway" && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Enter to select]"] }))] }), _jsxs(Box, { children: [_jsxs(Text, { color: gatewayFormField === "envName"
|
|
1132
|
+
? colors.primary
|
|
1133
|
+
: colors.textDim, children: [gatewayFormField === "envName" ? figures.pointer : " ", " ", "ENV Name:", " "] }), gatewayFormField === "envName" ? (_jsx(TextInput, { value: gatewayEnvPrefix || "", onChange: setGatewayEnvPrefix, placeholder: "ANTHROPIC" })) : (_jsx(Text, { color: gatewayEnvPrefix ? colors.text : colors.textDim, dimColor: !gatewayEnvPrefix, children: gatewayEnvPrefix || "(auto-filled from config)" }))] }), _jsxs(Box, { children: [_jsxs(Text, { color: gatewayFormField === "secret"
|
|
1134
|
+
? colors.primary
|
|
1135
|
+
: colors.textDim, children: [gatewayFormField === "secret" ? figures.pointer : " ", " ", "Secret:", " "] }), pendingSecret ? (_jsx(Text, { color: colors.success, children: pendingSecret.name })) : (_jsx(Text, { color: colors.textDim, dimColor: true, children: "(none selected)" })), gatewayFormField === "secret" && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Enter to select]"] }))] }), _jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: colors.border, paddingX: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: gatewayFormField === "envName"
|
|
1136
|
+
? `Type to edit • ${figures.arrowUp}${figures.arrowDown} Navigate • [esc] Cancel`
|
|
1137
|
+
: gatewayFormField === "attach"
|
|
1138
|
+
? canAttach
|
|
1139
|
+
? `[Enter] Attach • ${figures.arrowUp}${figures.arrowDown} Navigate • [esc] Cancel`
|
|
1140
|
+
: `${figures.arrowUp}${figures.arrowDown} Navigate • [esc] Cancel`
|
|
1141
|
+
: `[Enter] Select • ${figures.arrowUp}${figures.arrowDown} Navigate • [esc] Cancel` }) })] })), !gatewayFormActive && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: selectedGatewayIndex === 0
|
|
1142
|
+
? colors.primary
|
|
1143
|
+
: colors.textDim, children: [selectedGatewayIndex === 0
|
|
1144
|
+
? figures.pointer
|
|
1145
|
+
: " ", " "] }), _jsx(Text, { color: selectedGatewayIndex === 0
|
|
1146
|
+
? colors.success
|
|
1147
|
+
: colors.textDim, bold: selectedGatewayIndex === 0, children: "+ Attach AI gateway config" })] }), gatewayCount > 0 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: formData.gateways.map((gw, index) => {
|
|
1148
|
+
const itemIndex = index + 1;
|
|
1149
|
+
const isGatewaySelected = selectedGatewayIndex === itemIndex;
|
|
1150
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { color: isGatewaySelected
|
|
1151
|
+
? colors.primary
|
|
1152
|
+
: colors.textDim, children: [isGatewaySelected
|
|
1153
|
+
? figures.pointer
|
|
1154
|
+
: " ", " "] }), _jsxs(Text, { color: isGatewaySelected
|
|
1155
|
+
? colors.primary
|
|
1156
|
+
: colors.textDim, bold: isGatewaySelected, children: ["ENV: ", gw.envPrefix] })] }), _jsxs(Box, { marginLeft: 3, flexDirection: "column", children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Gateway Config: ", gw.gatewayName, " (", gw.gateway, ")"] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Endpoint: ", gw.gatewayEndpoint] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Secret: ", gw.secretName, " (", gw.secret, ")"] })] })] }, gw.envPrefix));
|
|
1157
|
+
}) })), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: selectedGatewayIndex === maxGatewayIndex
|
|
1158
|
+
? colors.primary
|
|
1159
|
+
: colors.textDim, children: [selectedGatewayIndex === maxGatewayIndex
|
|
1160
|
+
? figures.pointer
|
|
1161
|
+
: " ", " "] }), _jsxs(Text, { color: selectedGatewayIndex === maxGatewayIndex
|
|
1162
|
+
? colors.success
|
|
1163
|
+
: colors.textDim, bold: selectedGatewayIndex === maxGatewayIndex, children: [figures.tick, " Done"] })] }), _jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: colors.border, paddingX: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: `${figures.arrowUp}${figures.arrowDown} Navigate • [Enter] ${selectedGatewayIndex === 0 ? "Attach" : selectedGatewayIndex === maxGatewayIndex ? "Done" : "Select"} • [d] Remove • [esc] Back` }) })] }))] }, field.key));
|
|
1164
|
+
}
|
|
417
1165
|
return null;
|
|
418
1166
|
}) }), formData.resource_size === "CUSTOM_SIZE" &&
|
|
419
|
-
validateCustomResources() && (_jsxs(Box, { borderStyle: "round", borderColor: colors.error, paddingX: 1, paddingY: 0, marginTop: 1, children: [_jsxs(Text, { color: colors.error, bold: true, children: [figures.cross, " Validation Error"] }), _jsx(Text, { color: colors.error, dimColor: true, children: validateCustomResources() })] })), !inMetadataSection && (_jsx(NavigationTips, { showArrows: true, tips: [
|
|
1167
|
+
validateCustomResources() && (_jsxs(Box, { borderStyle: "round", borderColor: colors.error, paddingX: 1, paddingY: 0, marginTop: 1, children: [_jsxs(Text, { color: colors.error, bold: true, children: [figures.cross, " Validation Error"] }), _jsx(Text, { color: colors.error, dimColor: true, children: validateCustomResources() })] })), !inMetadataSection && !inGatewaySection && (_jsx(NavigationTips, { showArrows: true, tips: [
|
|
420
1168
|
{ key: "Enter", label: "Create" },
|
|
421
1169
|
{ key: "q", label: "Cancel" },
|
|
422
1170
|
] }))] }));
|