@runloop/rl-cli 1.2.0 → 1.3.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 +28 -8
- package/dist/commands/blueprint/list.js +97 -28
- package/dist/commands/blueprint/prune.js +7 -19
- package/dist/commands/devbox/create.js +3 -0
- package/dist/commands/devbox/list.js +44 -65
- package/dist/commands/menu.js +2 -1
- package/dist/commands/network-policy/create.js +27 -0
- package/dist/commands/network-policy/delete.js +21 -0
- package/dist/commands/network-policy/get.js +15 -0
- package/dist/commands/network-policy/list.js +494 -0
- package/dist/commands/object/list.js +516 -24
- package/dist/commands/snapshot/list.js +90 -29
- package/dist/components/Banner.js +109 -8
- package/dist/components/ConfirmationPrompt.js +45 -0
- package/dist/components/DevboxActionsMenu.js +42 -6
- package/dist/components/DevboxCard.js +1 -1
- package/dist/components/DevboxCreatePage.js +95 -81
- package/dist/components/DevboxDetailPage.js +218 -272
- package/dist/components/LogsViewer.js +8 -1
- package/dist/components/MainMenu.js +35 -4
- package/dist/components/NavigationTips.js +24 -0
- package/dist/components/NetworkPolicyCreatePage.js +264 -0
- package/dist/components/OperationsMenu.js +9 -1
- package/dist/components/ResourceActionsMenu.js +5 -1
- package/dist/components/ResourceDetailPage.js +204 -0
- package/dist/components/ResourceListView.js +19 -2
- package/dist/components/StatusBadge.js +2 -2
- package/dist/components/Table.js +6 -8
- package/dist/components/form/FormActionButton.js +7 -0
- package/dist/components/form/FormField.js +7 -0
- package/dist/components/form/FormListManager.js +112 -0
- package/dist/components/form/FormSelect.js +34 -0
- package/dist/components/form/FormTextInput.js +8 -0
- package/dist/components/form/index.js +8 -0
- package/dist/hooks/useViewportHeight.js +38 -20
- package/dist/router/Router.js +23 -1
- package/dist/screens/BlueprintDetailScreen.js +337 -0
- package/dist/screens/MenuScreen.js +6 -0
- package/dist/screens/NetworkPolicyCreateScreen.js +7 -0
- package/dist/screens/NetworkPolicyDetailScreen.js +247 -0
- package/dist/screens/NetworkPolicyListScreen.js +7 -0
- package/dist/screens/ObjectDetailScreen.js +377 -0
- package/dist/screens/ObjectListScreen.js +7 -0
- package/dist/screens/SnapshotDetailScreen.js +208 -0
- package/dist/services/blueprintService.js +30 -11
- package/dist/services/networkPolicyService.js +108 -0
- package/dist/services/objectService.js +101 -0
- package/dist/services/snapshotService.js +39 -3
- package/dist/store/blueprintStore.js +4 -10
- package/dist/store/index.js +1 -0
- package/dist/store/networkPolicyStore.js +83 -0
- package/dist/store/objectStore.js +92 -0
- package/dist/store/snapshotStore.js +4 -8
- package/dist/utils/commands.js +47 -0
- package/package.json +2 -2
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* DevboxDetailPage - Detail page for devboxes
|
|
4
|
+
* Uses the generic ResourceDetailPage component with devbox-specific customizations
|
|
5
|
+
*/
|
|
2
6
|
import React from "react";
|
|
3
|
-
import {
|
|
7
|
+
import { Text } from "ink";
|
|
4
8
|
import figures from "figures";
|
|
5
|
-
import { Header } from "./Header.js";
|
|
6
|
-
import { StatusBadge } from "./StatusBadge.js";
|
|
7
|
-
import { Breadcrumb } from "./Breadcrumb.js";
|
|
8
9
|
import { DevboxActionsMenu } from "./DevboxActionsMenu.js";
|
|
9
10
|
import { StateHistory } from "./StateHistory.js";
|
|
11
|
+
import { ResourceDetailPage, } from "./ResourceDetailPage.js";
|
|
10
12
|
import { getDevboxUrl } from "../utils/url.js";
|
|
11
13
|
import { colors } from "../utils/theme.js";
|
|
12
|
-
import { useViewportHeight } from "../hooks/useViewportHeight.js";
|
|
13
|
-
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
|
|
14
14
|
import { getDevbox } from "../services/devboxService.js";
|
|
15
15
|
// Format time ago in a succinct way
|
|
16
16
|
const formatTimeAgo = (timestamp) => {
|
|
@@ -32,59 +32,11 @@ const formatTimeAgo = (timestamp) => {
|
|
|
32
32
|
const years = Math.floor(months / 12);
|
|
33
33
|
return `${years}y ago`;
|
|
34
34
|
};
|
|
35
|
-
// Truncate long strings to prevent layout issues
|
|
36
|
-
const truncateString = (str, maxLength) => {
|
|
37
|
-
if (str.length <= maxLength)
|
|
38
|
-
return str;
|
|
39
|
-
return str.substring(0, maxLength - 3) + "...";
|
|
40
|
-
};
|
|
41
35
|
export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
|
|
42
|
-
const isMounted = React.useRef(true);
|
|
43
|
-
// Track mounted state
|
|
44
|
-
React.useEffect(() => {
|
|
45
|
-
isMounted.current = true;
|
|
46
|
-
return () => {
|
|
47
|
-
isMounted.current = false;
|
|
48
|
-
};
|
|
49
|
-
}, []);
|
|
50
|
-
// Local state for devbox data (updated by polling)
|
|
51
|
-
const [currentDevbox, setCurrentDevbox] = React.useState(initialDevbox);
|
|
52
|
-
const [showDetailedInfo, setShowDetailedInfo] = React.useState(false);
|
|
53
|
-
const [detailScroll, setDetailScroll] = React.useState(0);
|
|
54
36
|
const [showActions, setShowActions] = React.useState(false);
|
|
55
|
-
const [
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
// Skip polling if showing actions, detailed info, or not mounted
|
|
59
|
-
if (showActions || showDetailedInfo)
|
|
60
|
-
return;
|
|
61
|
-
const interval = setInterval(async () => {
|
|
62
|
-
// Only poll when not in actions/detail mode and component is mounted
|
|
63
|
-
if (!showActions && !showDetailedInfo && isMounted.current) {
|
|
64
|
-
try {
|
|
65
|
-
const updatedDevbox = await getDevbox(initialDevbox.id);
|
|
66
|
-
// Only update if still mounted
|
|
67
|
-
if (isMounted.current) {
|
|
68
|
-
setCurrentDevbox(updatedDevbox);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
// Silently ignore polling errors to avoid disrupting user experience
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}, 3000); // Poll every 3 seconds
|
|
76
|
-
return () => clearInterval(interval);
|
|
77
|
-
}, [initialDevbox.id, showActions, showDetailedInfo]);
|
|
78
|
-
// Calculate viewport for detailed info view:
|
|
79
|
-
// - Breadcrumb (3 lines + marginBottom): 4 lines
|
|
80
|
-
// - Header (title + underline + marginBottom): 3 lines
|
|
81
|
-
// - Status box (content + marginBottom): 2 lines
|
|
82
|
-
// - Content box (marginTop + border + paddingY top/bottom + border + marginBottom): 6 lines
|
|
83
|
-
// - Help bar (marginTop + content): 2 lines
|
|
84
|
-
// - Safety buffer: 1 line
|
|
85
|
-
// Total: 18 lines
|
|
86
|
-
const detailViewport = useViewportHeight({ overhead: 18, minHeight: 10 });
|
|
87
|
-
const selectedDevbox = currentDevbox;
|
|
37
|
+
const [selectedOperationKey, setSelectedOperationKey] = React.useState(null);
|
|
38
|
+
const [currentDevbox, setCurrentDevbox] = React.useState(initialDevbox);
|
|
39
|
+
// All possible operations for devboxes
|
|
88
40
|
const allOperations = [
|
|
89
41
|
{
|
|
90
42
|
key: "logs",
|
|
@@ -151,9 +103,9 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
|
|
|
151
103
|
},
|
|
152
104
|
];
|
|
153
105
|
// Filter operations based on devbox status
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
const status =
|
|
106
|
+
const getFilteredOperations = (devbox) => {
|
|
107
|
+
return allOperations.filter((op) => {
|
|
108
|
+
const status = devbox.status;
|
|
157
109
|
// When suspended: logs and resume
|
|
158
110
|
if (status === "suspended") {
|
|
159
111
|
return op.key === "resume" || op.key === "logs";
|
|
@@ -170,125 +122,190 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
|
|
|
170
122
|
}
|
|
171
123
|
// Default for transitional states (provisioning, initializing)
|
|
172
124
|
return op.key === "logs" || op.key === "delete";
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
setDetailScroll(0);
|
|
196
|
-
}
|
|
197
|
-
else if (input === "j" || input === "s" || key.downArrow) {
|
|
198
|
-
// Scroll down in detailed info
|
|
199
|
-
setDetailScroll(detailScroll + 1);
|
|
200
|
-
}
|
|
201
|
-
else if (input === "k" || input === "w" || key.upArrow) {
|
|
202
|
-
// Scroll up in detailed info
|
|
203
|
-
setDetailScroll(Math.max(0, detailScroll - 1));
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
// Build detail sections for the devbox
|
|
128
|
+
const buildDetailSections = (devbox) => {
|
|
129
|
+
const sections = [];
|
|
130
|
+
const lp = devbox.launch_parameters;
|
|
131
|
+
// Calculate uptime
|
|
132
|
+
const uptime = devbox.create_time_ms
|
|
133
|
+
? Math.floor((Date.now() - devbox.create_time_ms) / 1000 / 60)
|
|
134
|
+
: null;
|
|
135
|
+
// Details section
|
|
136
|
+
const detailFields = [];
|
|
137
|
+
// Created / Ended time
|
|
138
|
+
if (devbox.create_time_ms) {
|
|
139
|
+
const createTime = new Date(devbox.create_time_ms).toLocaleString();
|
|
140
|
+
const timeAgo = formatTimeAgo(devbox.create_time_ms);
|
|
141
|
+
if (devbox.end_time_ms) {
|
|
142
|
+
const endTime = new Date(devbox.end_time_ms).toLocaleString();
|
|
143
|
+
detailFields.push({
|
|
144
|
+
label: "Created",
|
|
145
|
+
value: `${createTime} → ${endTime}`,
|
|
146
|
+
});
|
|
204
147
|
}
|
|
205
|
-
else
|
|
206
|
-
|
|
207
|
-
|
|
148
|
+
else {
|
|
149
|
+
detailFields.push({
|
|
150
|
+
label: "Created",
|
|
151
|
+
value: `${createTime} (${timeAgo})`,
|
|
152
|
+
});
|
|
208
153
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
154
|
+
}
|
|
155
|
+
// Resources
|
|
156
|
+
if (lp?.resource_size_request ||
|
|
157
|
+
lp?.custom_cpu_cores ||
|
|
158
|
+
lp?.custom_gb_memory ||
|
|
159
|
+
lp?.custom_disk_size ||
|
|
160
|
+
lp?.architecture) {
|
|
161
|
+
const resources = [
|
|
162
|
+
lp?.resource_size_request,
|
|
163
|
+
lp?.architecture,
|
|
164
|
+
lp?.custom_cpu_cores && `${lp.custom_cpu_cores}VCPU`,
|
|
165
|
+
lp?.custom_gb_memory && `${lp.custom_gb_memory}GB RAM`,
|
|
166
|
+
lp?.custom_disk_size && `${lp.custom_disk_size}GB DISC`,
|
|
167
|
+
]
|
|
168
|
+
.filter(Boolean)
|
|
169
|
+
.join(" • ");
|
|
170
|
+
detailFields.push({
|
|
171
|
+
label: "Resources",
|
|
172
|
+
value: resources,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
// Lifetime with remaining time
|
|
176
|
+
if (lp?.keep_alive_time_seconds) {
|
|
177
|
+
const lifetimeStr = lp.keep_alive_time_seconds < 3600
|
|
178
|
+
? `${Math.floor(lp.keep_alive_time_seconds / 60)}m`
|
|
179
|
+
: `${Math.floor(lp.keep_alive_time_seconds / 3600)}h ${Math.floor((lp.keep_alive_time_seconds % 3600) / 60)}m`;
|
|
180
|
+
let remainingText = "";
|
|
181
|
+
if (uptime !== null && devbox.status === "running") {
|
|
182
|
+
const maxLifetimeMinutes = Math.floor(lp.keep_alive_time_seconds / 60);
|
|
183
|
+
const remainingMinutes = maxLifetimeMinutes - uptime;
|
|
184
|
+
if (remainingMinutes <= 0) {
|
|
185
|
+
remainingText = " (Expired)";
|
|
186
|
+
}
|
|
187
|
+
else if (remainingMinutes < 60) {
|
|
188
|
+
remainingText = ` (${remainingMinutes}m remaining)`;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
const hours = Math.floor(remainingMinutes / 60);
|
|
192
|
+
const mins = remainingMinutes % 60;
|
|
193
|
+
remainingText = ` (${hours}h ${mins}m remaining)`;
|
|
194
|
+
}
|
|
212
195
|
}
|
|
213
|
-
|
|
196
|
+
detailFields.push({
|
|
197
|
+
label: "Lifetime",
|
|
198
|
+
value: lifetimeStr + remainingText,
|
|
199
|
+
});
|
|
214
200
|
}
|
|
215
|
-
//
|
|
216
|
-
if (
|
|
217
|
-
|
|
201
|
+
// User
|
|
202
|
+
if (lp?.user_parameters) {
|
|
203
|
+
const username = lp.user_parameters.username || "default";
|
|
204
|
+
const uid = lp.user_parameters.uid != null && lp.user_parameters.uid !== 0
|
|
205
|
+
? ` (UID: ${lp.user_parameters.uid})`
|
|
206
|
+
: "";
|
|
207
|
+
detailFields.push({
|
|
208
|
+
label: "User",
|
|
209
|
+
value: username + uid,
|
|
210
|
+
});
|
|
218
211
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
212
|
+
// Source
|
|
213
|
+
if (devbox.blueprint_id || devbox.snapshot_id) {
|
|
214
|
+
detailFields.push({
|
|
215
|
+
label: "Source",
|
|
216
|
+
value: (_jsx(Text, { color: colors.success, children: devbox.blueprint_id || devbox.snapshot_id })),
|
|
217
|
+
});
|
|
222
218
|
}
|
|
223
|
-
|
|
224
|
-
|
|
219
|
+
// Network Policy
|
|
220
|
+
if (lp?.network_policy_id) {
|
|
221
|
+
detailFields.push({
|
|
222
|
+
label: "Network Policy",
|
|
223
|
+
value: _jsx(Text, { color: colors.info, children: lp.network_policy_id }),
|
|
224
|
+
});
|
|
225
225
|
}
|
|
226
|
-
|
|
227
|
-
|
|
226
|
+
// Initiator
|
|
227
|
+
if (devbox.initiator_id) {
|
|
228
|
+
detailFields.push({
|
|
229
|
+
label: "Initiator",
|
|
230
|
+
value: _jsx(Text, { color: colors.secondary, children: devbox.initiator_id }),
|
|
231
|
+
});
|
|
228
232
|
}
|
|
229
|
-
|
|
230
|
-
|
|
233
|
+
// Capabilities
|
|
234
|
+
const hasCapabilities = devbox.capabilities &&
|
|
235
|
+
devbox.capabilities.filter((c) => c !== "unknown").length > 0;
|
|
236
|
+
if (hasCapabilities) {
|
|
237
|
+
detailFields.push({
|
|
238
|
+
label: "Capabilities",
|
|
239
|
+
value: devbox.capabilities
|
|
240
|
+
.filter((c) => c !== "unknown")
|
|
241
|
+
.join(", "),
|
|
242
|
+
});
|
|
231
243
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
244
|
+
if (detailFields.length > 0) {
|
|
245
|
+
sections.push({
|
|
246
|
+
title: "Details",
|
|
247
|
+
icon: figures.squareSmallFilled,
|
|
248
|
+
color: colors.warning,
|
|
249
|
+
fields: detailFields,
|
|
250
|
+
});
|
|
239
251
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
openCommand = `start "${url}"`;
|
|
252
|
-
}
|
|
253
|
-
else {
|
|
254
|
-
openCommand = `xdg-open "${url}"`;
|
|
255
|
-
}
|
|
256
|
-
exec(openCommand);
|
|
257
|
-
};
|
|
258
|
-
openBrowser();
|
|
252
|
+
// Metadata section
|
|
253
|
+
if (devbox.metadata && Object.keys(devbox.metadata).length > 0) {
|
|
254
|
+
sections.push({
|
|
255
|
+
title: "Metadata",
|
|
256
|
+
icon: figures.identical,
|
|
257
|
+
color: colors.secondary,
|
|
258
|
+
fields: Object.entries(devbox.metadata).map(([key, value]) => ({
|
|
259
|
+
label: key,
|
|
260
|
+
value: value,
|
|
261
|
+
})),
|
|
262
|
+
});
|
|
259
263
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
264
|
+
// Error section
|
|
265
|
+
if (devbox.failure_reason) {
|
|
266
|
+
sections.push({
|
|
267
|
+
title: "Error",
|
|
268
|
+
icon: figures.cross,
|
|
269
|
+
color: colors.error,
|
|
270
|
+
fields: [
|
|
271
|
+
{
|
|
272
|
+
label: "Failure Reason",
|
|
273
|
+
value: devbox.failure_reason,
|
|
274
|
+
color: colors.error,
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
return sections;
|
|
280
|
+
};
|
|
281
|
+
// Build detailed info lines for full details view
|
|
282
|
+
const buildDetailLines = (devbox) => {
|
|
266
283
|
const lines = [];
|
|
267
284
|
const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
|
|
268
285
|
// Core Information
|
|
269
286
|
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Devbox Details" }, "core-title"));
|
|
270
|
-
lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "ID: ",
|
|
271
|
-
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Name: ",
|
|
272
|
-
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Status: ", capitalize(
|
|
273
|
-
if (
|
|
274
|
-
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Created: ", new Date(
|
|
287
|
+
lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "ID: ", devbox.id] }, "core-id"));
|
|
288
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Name: ", devbox.name || "(none)"] }, "core-name"));
|
|
289
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Status: ", capitalize(devbox.status)] }, "core-status"));
|
|
290
|
+
if (devbox.create_time_ms) {
|
|
291
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Created: ", new Date(devbox.create_time_ms).toLocaleString()] }, "core-created"));
|
|
275
292
|
}
|
|
276
|
-
if (
|
|
277
|
-
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Ended: ", new Date(
|
|
293
|
+
if (devbox.end_time_ms) {
|
|
294
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Ended: ", new Date(devbox.end_time_ms).toLocaleString()] }, "core-ended"));
|
|
278
295
|
}
|
|
279
296
|
lines.push(_jsx(Text, { children: " " }, "core-space"));
|
|
280
297
|
// Capabilities
|
|
281
|
-
if (
|
|
298
|
+
if (devbox.capabilities && devbox.capabilities.length > 0) {
|
|
282
299
|
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Capabilities" }, "cap-title"));
|
|
283
|
-
|
|
300
|
+
devbox.capabilities.forEach((cap, idx) => {
|
|
284
301
|
lines.push(_jsxs(Text, { dimColor: true, children: [" ", figures.pointer, " ", cap] }, `cap-${idx}`));
|
|
285
302
|
});
|
|
286
303
|
lines.push(_jsx(Text, { children: " " }, "cap-space"));
|
|
287
304
|
}
|
|
288
305
|
// Launch Parameters
|
|
289
|
-
if (
|
|
306
|
+
if (devbox.launch_parameters) {
|
|
290
307
|
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Launch Parameters" }, "launch-title"));
|
|
291
|
-
const lp =
|
|
308
|
+
const lp = devbox.launch_parameters;
|
|
292
309
|
if (lp.resource_size_request) {
|
|
293
310
|
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Resource Size Request: ", lp.resource_size_request] }, "launch-size-req"));
|
|
294
311
|
}
|
|
@@ -315,14 +332,9 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
|
|
|
315
332
|
}
|
|
316
333
|
if (lp.launch_commands && lp.launch_commands.length > 0) {
|
|
317
334
|
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Launch Commands:"] }, "launch-launch-cmds"));
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
// {" "}
|
|
322
|
-
// {figures.pointer} {cmd}
|
|
323
|
-
// </Text>,
|
|
324
|
-
// );
|
|
325
|
-
// });
|
|
335
|
+
lp.launch_commands.forEach((cmd, idx) => {
|
|
336
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", figures.pointer, " ", cmd] }, `launch-cmd-${idx}`));
|
|
337
|
+
});
|
|
326
338
|
}
|
|
327
339
|
if (lp.required_services && lp.required_services.length > 0) {
|
|
328
340
|
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Required Services: ", lp.required_services.join(", ")] }, "launch-services"));
|
|
@@ -330,59 +342,57 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
|
|
|
330
342
|
if (lp.user_parameters) {
|
|
331
343
|
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "User Parameters:"] }, "launch-user"));
|
|
332
344
|
if (lp.user_parameters.username) {
|
|
333
|
-
lines.push(_jsxs(Text, { dimColor: true, children: ["
|
|
345
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Username: ", lp.user_parameters.username] }, "user-name"));
|
|
334
346
|
}
|
|
335
347
|
if (lp.user_parameters.uid) {
|
|
336
|
-
lines.push(_jsxs(Text, { dimColor: true, children: ["
|
|
348
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "UID: ", lp.user_parameters.uid] }, "user-uid"));
|
|
337
349
|
}
|
|
338
350
|
}
|
|
339
351
|
lines.push(_jsx(Text, { children: " " }, "launch-space"));
|
|
340
352
|
}
|
|
341
353
|
// Source
|
|
342
|
-
if (
|
|
354
|
+
if (devbox.blueprint_id || devbox.snapshot_id) {
|
|
343
355
|
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Source" }, "source-title"));
|
|
344
|
-
if (
|
|
345
|
-
lines.push(_jsxs(Text, { color: colors.idColor, children: [" ",
|
|
356
|
+
if (devbox.blueprint_id) {
|
|
357
|
+
lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "Blueprint: ", devbox.blueprint_id] }, "source-bp"));
|
|
346
358
|
}
|
|
347
|
-
if (
|
|
348
|
-
lines.push(_jsxs(Text, { color: colors.idColor, children: [" ",
|
|
359
|
+
if (devbox.snapshot_id) {
|
|
360
|
+
lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "Snapshot: ", devbox.snapshot_id] }, "source-snap"));
|
|
349
361
|
}
|
|
350
362
|
lines.push(_jsx(Text, { children: " " }, "source-space"));
|
|
351
363
|
}
|
|
352
364
|
// Initiator
|
|
353
|
-
if (
|
|
365
|
+
if (devbox.initiator_type) {
|
|
354
366
|
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Initiator" }, "init-title"));
|
|
355
|
-
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Type: ",
|
|
356
|
-
if (
|
|
357
|
-
lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "ID: ",
|
|
367
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Type: ", devbox.initiator_type] }, "init-type"));
|
|
368
|
+
if (devbox.initiator_id) {
|
|
369
|
+
lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "ID: ", devbox.initiator_id] }, "init-id"));
|
|
358
370
|
}
|
|
359
371
|
lines.push(_jsx(Text, { children: " " }, "init-space"));
|
|
360
372
|
}
|
|
361
373
|
// Status Details
|
|
362
|
-
if (
|
|
374
|
+
if (devbox.failure_reason || devbox.shutdown_reason) {
|
|
363
375
|
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Status Details" }, "status-title"));
|
|
364
|
-
if (
|
|
365
|
-
lines.push(_jsxs(Text, { color: colors.error,
|
|
376
|
+
if (devbox.failure_reason) {
|
|
377
|
+
lines.push(_jsxs(Text, { color: colors.error, children: [" ", "Failure Reason: ", devbox.failure_reason] }, "status-fail"));
|
|
366
378
|
}
|
|
367
|
-
if (
|
|
368
|
-
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Shutdown Initiator: ",
|
|
379
|
+
if (devbox.shutdown_reason) {
|
|
380
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Shutdown Initiator: ", devbox.shutdown_reason] }, "status-shut"));
|
|
369
381
|
}
|
|
370
382
|
lines.push(_jsx(Text, { children: " " }, "status-space"));
|
|
371
383
|
}
|
|
372
384
|
// Metadata
|
|
373
|
-
if (
|
|
374
|
-
Object.keys(selectedDevbox.metadata).length > 0) {
|
|
385
|
+
if (devbox.metadata && Object.keys(devbox.metadata).length > 0) {
|
|
375
386
|
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Metadata" }, "meta-title"));
|
|
376
|
-
Object.entries(
|
|
387
|
+
Object.entries(devbox.metadata).forEach(([key, value], idx) => {
|
|
377
388
|
lines.push(_jsxs(Text, { dimColor: true, children: [" ", key, ": ", value] }, `meta-${idx}`));
|
|
378
389
|
});
|
|
379
390
|
lines.push(_jsx(Text, { children: " " }, "meta-space"));
|
|
380
391
|
}
|
|
381
392
|
// State Transitions
|
|
382
|
-
if (
|
|
383
|
-
selectedDevbox.state_transitions.length > 0) {
|
|
393
|
+
if (devbox.state_transitions && devbox.state_transitions.length > 0) {
|
|
384
394
|
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "State History" }, "state-title"));
|
|
385
|
-
|
|
395
|
+
devbox.state_transitions.forEach((transition, idx) => {
|
|
386
396
|
const text = `${idx + 1}. ${capitalize(transition.status)}${transition.transition_time_ms ? ` at ${new Date(transition.transition_time_ms).toLocaleString()}` : ""}`;
|
|
387
397
|
lines.push(_jsxs(Text, { dimColor: true, children: [" ", text] }, `state-${idx}`));
|
|
388
398
|
});
|
|
@@ -390,102 +400,38 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
|
|
|
390
400
|
}
|
|
391
401
|
// Raw JSON (full)
|
|
392
402
|
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Raw JSON" }, "json-title"));
|
|
393
|
-
const jsonLines = JSON.stringify(
|
|
403
|
+
const jsonLines = JSON.stringify(devbox, null, 2).split("\n");
|
|
394
404
|
jsonLines.forEach((line, idx) => {
|
|
395
405
|
lines.push(_jsxs(Text, { dimColor: true, children: [" ", line] }, `json-${idx}`));
|
|
396
406
|
});
|
|
397
407
|
return lines;
|
|
398
408
|
};
|
|
399
|
-
//
|
|
409
|
+
// Handle operation selection
|
|
410
|
+
const handleOperation = (operation, _devbox) => {
|
|
411
|
+
setSelectedOperationKey(operation);
|
|
412
|
+
setShowActions(true);
|
|
413
|
+
};
|
|
414
|
+
// Polling function
|
|
415
|
+
const pollDevbox = React.useCallback(async () => {
|
|
416
|
+
const updated = await getDevbox(initialDevbox.id);
|
|
417
|
+
setCurrentDevbox(updated);
|
|
418
|
+
return updated;
|
|
419
|
+
}, [initialDevbox.id]);
|
|
420
|
+
// Show DevboxActionsMenu when an action is selected
|
|
400
421
|
if (showActions) {
|
|
401
|
-
|
|
402
|
-
return (_jsx(DevboxActionsMenu, { devbox: selectedDevbox, onBack: () => {
|
|
422
|
+
return (_jsx(DevboxActionsMenu, { devbox: currentDevbox, onBack: () => {
|
|
403
423
|
setShowActions(false);
|
|
404
|
-
|
|
424
|
+
setSelectedOperationKey(null);
|
|
405
425
|
}, breadcrumbItems: [
|
|
406
426
|
{ label: "Devboxes" },
|
|
407
|
-
{ label:
|
|
408
|
-
], initialOperation:
|
|
409
|
-
}
|
|
410
|
-
// Detailed info mode - full screen
|
|
411
|
-
if (showDetailedInfo) {
|
|
412
|
-
const detailLines = buildDetailLines();
|
|
413
|
-
const viewportHeight = detailViewport.viewportHeight;
|
|
414
|
-
const maxScroll = Math.max(0, detailLines.length - viewportHeight);
|
|
415
|
-
const actualScroll = Math.min(detailScroll, maxScroll);
|
|
416
|
-
const visibleLines = detailLines.slice(actualScroll, actualScroll + viewportHeight);
|
|
417
|
-
const hasMore = actualScroll + viewportHeight < detailLines.length;
|
|
418
|
-
const hasLess = actualScroll > 0;
|
|
419
|
-
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
420
|
-
{ label: "Devboxes" },
|
|
421
|
-
{ label: selectedDevbox.name || selectedDevbox.id },
|
|
422
|
-
{ label: "Full Details", active: true },
|
|
423
|
-
] }), _jsx(Header, { title: `${selectedDevbox.name || selectedDevbox.id} - Complete Information` }), _jsx(Box, { flexDirection: "column", marginBottom: 1, children: _jsxs(Box, { marginBottom: 1, children: [_jsx(StatusBadge, { status: selectedDevbox.status }), _jsx(Text, { children: " " }), _jsx(Text, { color: colors.idColor, children: selectedDevbox.id })] }) }), _jsx(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, borderStyle: "round", borderColor: colors.border, paddingX: 2, paddingY: 1, children: _jsx(Box, { flexDirection: "column", children: visibleLines }) }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Scroll \u2022 Line ", actualScroll + 1, "-", Math.min(actualScroll + viewportHeight, detailLines.length), " of", " ", detailLines.length] }), hasLess && _jsxs(Text, { color: colors.primary, children: [" ", figures.arrowUp] }), hasMore && _jsxs(Text, { color: colors.primary, children: [" ", figures.arrowDown] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 [q or esc] Back to Details"] })] })] }));
|
|
427
|
+
{ label: currentDevbox.name || currentDevbox.id },
|
|
428
|
+
], initialOperation: selectedOperationKey || undefined, skipOperationsMenu: true }));
|
|
424
429
|
}
|
|
425
|
-
//
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
{ label: selectedDevbox.name || selectedDevbox.id, active: true },
|
|
433
|
-
] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, paddingX: 1, children: [_jsxs(Box, { flexDirection: "row", flexWrap: "wrap", children: [_jsx(Text, { color: colors.primary, bold: true, children: truncateString(selectedDevbox.name || selectedDevbox.id, Math.max(20, detailViewport.terminalWidth - 35)) }), selectedDevbox.name && (_jsxs(Text, { color: colors.idColor, children: [" \u2022 ", selectedDevbox.id] }))] }), _jsxs(Box, { children: [_jsx(StatusBadge, { status: selectedDevbox.status, fullText: true }), uptime !== null && selectedDevbox.status === "running" && (_jsxs(Text, { color: colors.success, dimColor: true, children: [" ", "\u2022 Uptime:", " ", uptime < 60
|
|
434
|
-
? `${uptime}m`
|
|
435
|
-
: `${Math.floor(uptime / 60)}h ${uptime % 60}m`] })), selectedDevbox.status !== "running" &&
|
|
436
|
-
selectedDevbox.create_time_ms &&
|
|
437
|
-
selectedDevbox.end_time_ms && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022 Ran for:", " ", (() => {
|
|
438
|
-
const runtime = Math.floor((selectedDevbox.end_time_ms -
|
|
439
|
-
selectedDevbox.create_time_ms) /
|
|
440
|
-
1000);
|
|
441
|
-
if (runtime < 60)
|
|
442
|
-
return `${runtime}s`;
|
|
443
|
-
const mins = Math.floor(runtime / 60);
|
|
444
|
-
if (mins < 60)
|
|
445
|
-
return `${mins}m ${runtime % 60}s`;
|
|
446
|
-
const hours = Math.floor(mins / 60);
|
|
447
|
-
return `${hours}h ${mins % 60}m`;
|
|
448
|
-
})()] }))] })] }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: colors.warning, bold: true, children: [figures.squareSmallFilled, " Details"] }), _jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [selectedDevbox.create_time_ms && (_jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, children: "Created " }), _jsx(Text, { dimColor: true, children: formattedCreateTime }), selectedDevbox.end_time_ms ? (_jsxs(Text, { dimColor: true, children: [" ", figures.arrowRight, " ", new Date(selectedDevbox.end_time_ms).toLocaleString()] })) : (_jsxs(Text, { dimColor: true, children: [" (", createTimeAgo, ")"] }))] })), (lp?.resource_size_request ||
|
|
449
|
-
lp?.custom_cpu_cores ||
|
|
450
|
-
lp?.custom_gb_memory ||
|
|
451
|
-
lp?.custom_disk_size ||
|
|
452
|
-
lp?.architecture) && (_jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, children: "Resources " }), _jsx(Text, { dimColor: true, children: [
|
|
453
|
-
lp?.resource_size_request,
|
|
454
|
-
lp?.architecture,
|
|
455
|
-
lp?.custom_cpu_cores && `${lp.custom_cpu_cores}VCPU`,
|
|
456
|
-
lp?.custom_gb_memory && `${lp.custom_gb_memory}GB RAM`,
|
|
457
|
-
lp?.custom_disk_size && `${lp.custom_disk_size}GB DISC`,
|
|
458
|
-
]
|
|
459
|
-
.filter(Boolean)
|
|
460
|
-
.join(" • ") })] })), (lp?.keep_alive_time_seconds || lp?.user_parameters) && (_jsxs(Box, { children: [lp?.keep_alive_time_seconds && (_jsxs(_Fragment, { children: [_jsx(Text, { color: colors.textDim, children: "Lifetime " }), _jsx(Text, { dimColor: true, children: lp.keep_alive_time_seconds < 3600
|
|
461
|
-
? `${Math.floor(lp.keep_alive_time_seconds / 60)}m`
|
|
462
|
-
: `${Math.floor(lp.keep_alive_time_seconds / 3600)}h ${Math.floor((lp.keep_alive_time_seconds % 3600) / 60)}m` }), uptime !== null && selectedDevbox.status === "running" && (_jsxs(Text, { children: [" ", "\u2022", " ", (() => {
|
|
463
|
-
const maxLifetimeMinutes = Math.floor(lp.keep_alive_time_seconds / 60);
|
|
464
|
-
const remainingMinutes = maxLifetimeMinutes - uptime;
|
|
465
|
-
if (remainingMinutes <= 0) {
|
|
466
|
-
return _jsx(Text, { color: colors.error, children: "Expired" });
|
|
467
|
-
}
|
|
468
|
-
else if (remainingMinutes < 5) {
|
|
469
|
-
return (_jsxs(Text, { color: colors.error, children: [remainingMinutes, "m remaining"] }));
|
|
470
|
-
}
|
|
471
|
-
else if (remainingMinutes < 15) {
|
|
472
|
-
return (_jsxs(Text, { color: colors.warning, children: [remainingMinutes, "m remaining"] }));
|
|
473
|
-
}
|
|
474
|
-
else if (remainingMinutes < 60) {
|
|
475
|
-
return (_jsxs(Text, { color: colors.success, children: [remainingMinutes, "m remaining"] }));
|
|
476
|
-
}
|
|
477
|
-
else {
|
|
478
|
-
const hours = Math.floor(remainingMinutes / 60);
|
|
479
|
-
const mins = remainingMinutes % 60;
|
|
480
|
-
return (_jsxs(Text, { color: colors.success, children: [hours, "h ", mins, "m remaining"] }));
|
|
481
|
-
}
|
|
482
|
-
})()] })), lp?.user_parameters && (_jsx(Text, { color: colors.textDim, children: " \u2022 " }))] })), lp?.user_parameters && (_jsxs(_Fragment, { children: [!lp?.keep_alive_time_seconds && (_jsx(Text, { color: colors.textDim, children: "User " })), _jsx(Text, { color: colors.textDim, children: "User: " }), _jsxs(Text, { dimColor: true, children: [lp.user_parameters.username || "default", lp.user_parameters.uid != null &&
|
|
483
|
-
lp.user_parameters.uid !== 0 &&
|
|
484
|
-
` (UID: ${lp.user_parameters.uid})`] })] }))] })), (selectedDevbox.blueprint_id || selectedDevbox.snapshot_id) && (_jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, children: "Source " }), _jsx(Text, { color: colors.success, children: selectedDevbox.blueprint_id || selectedDevbox.snapshot_id })] })), selectedDevbox.initiator_id && (_jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, children: "Initiator " }), _jsx(Text, { color: colors.secondary, children: selectedDevbox.initiator_id })] })), hasCapabilities && (_jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, children: "Capabilities " }), _jsx(Text, { dimColor: true, children: selectedDevbox.capabilities
|
|
485
|
-
.filter((c) => c !== "unknown")
|
|
486
|
-
.join(", ") })] }))] })] }), selectedDevbox.metadata &&
|
|
487
|
-
Object.keys(selectedDevbox.metadata).length > 0 && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: colors.secondary, bold: true, children: [figures.identical, " Metadata"] }), _jsx(Box, { flexDirection: "column", paddingLeft: 2, children: Object.entries(selectedDevbox.metadata).map(([key, value]) => (_jsxs(Box, { children: [_jsx(Text, { color: colors.textDim, children: key }), _jsx(Text, { color: colors.textDim, children: ": " }), _jsx(Text, { color: colors.textDim, dimColor: true, children: value })] }, key))) })] })), selectedDevbox.failure_reason && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: colors.error, bold: true, children: [figures.cross, " Error"] }), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: colors.error, children: selectedDevbox.failure_reason }) })] })), _jsx(StateHistory, { stateTransitions: selectedDevbox.state_transitions, shutdownReason: selectedDevbox.shutdown_reason ?? undefined }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.play, " Actions"] }), _jsx(Box, { flexDirection: "column", paddingLeft: 2, children: operations.map((op, index) => {
|
|
488
|
-
const isSelected = index === selectedOperation;
|
|
489
|
-
return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? colors.primary : colors.textDim, children: [isSelected ? figures.pointer : " ", " "] }), _jsxs(Text, { color: isSelected ? op.color : colors.textDim, bold: isSelected, children: [op.icon, " ", op.label] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[", op.shortcut, "]"] })] }, op.key));
|
|
490
|
-
}) })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.arrowUp, figures.arrowDown, " Navigate \u2022 [Enter] Execute \u2022 [i] Full Details \u2022 [o] Browser \u2022 [q] Back"] }) })] }));
|
|
430
|
+
// Determine if we should poll based on status
|
|
431
|
+
const shouldPoll = currentDevbox.status === "running" ||
|
|
432
|
+
currentDevbox.status === "provisioning" ||
|
|
433
|
+
currentDevbox.status === "initializing" ||
|
|
434
|
+
currentDevbox.status === "resuming" ||
|
|
435
|
+
currentDevbox.status === "suspending";
|
|
436
|
+
return (_jsx(ResourceDetailPage, { resource: currentDevbox, resourceType: "Devboxes", getDisplayName: (d) => d.name || d.id, getId: (d) => d.id, getStatus: (d) => d.status, getUrl: (d) => getDevboxUrl(d.id), detailSections: buildDetailSections(currentDevbox), operations: getFilteredOperations(currentDevbox), onOperation: handleOperation, onBack: onBack, buildDetailLines: buildDetailLines, additionalContent: _jsx(StateHistory, { stateTransitions: currentDevbox.state_transitions, shutdownReason: currentDevbox.shutdown_reason ?? undefined }), pollResource: shouldPoll ? pollDevbox : undefined, pollInterval: 3000 }));
|
|
491
437
|
};
|