@runloop/rl-cli 0.9.0 → 0.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/dist/commands/devbox/list.js +31 -9
- package/dist/components/DevboxCreatePage.js +3 -3
- package/dist/components/DevboxDetailPage.js +61 -11
- package/dist/components/MetadataDisplay.js +12 -2
- package/dist/components/ResourceListView.js +24 -11
- package/dist/components/StateHistory.js +54 -14
- package/dist/components/StatusBadge.js +51 -20
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { Box, Text, useInput, useApp } from "ink";
|
|
4
4
|
import TextInput from "ink-text-input";
|
|
@@ -149,8 +149,21 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
|
|
|
149
149
|
const ABSOLUTE_MAX_NAME = 80;
|
|
150
150
|
const ABSOLUTE_MAX_ID = 50;
|
|
151
151
|
const columns = [
|
|
152
|
+
// Status icon column - visual indicator for quick scanning
|
|
153
|
+
{
|
|
154
|
+
key: "statusIcon",
|
|
155
|
+
label: "",
|
|
156
|
+
width: statusIconWidth,
|
|
157
|
+
render: (devbox, _index, isSelected) => {
|
|
158
|
+
const statusDisplay = getStatusDisplay(devbox?.status);
|
|
159
|
+
const statusColor = statusDisplay.color === colors.textDim
|
|
160
|
+
? colors.info
|
|
161
|
+
: statusDisplay.color;
|
|
162
|
+
return (_jsxs(Text, { color: isSelected ? "white" : statusColor, bold: true, dimColor: false, inverse: isSelected, wrap: "truncate", children: [statusDisplay.icon, " "] }));
|
|
163
|
+
},
|
|
164
|
+
},
|
|
152
165
|
createTextColumn("name", "Name", (devbox) => {
|
|
153
|
-
const name = String(devbox?.name ||
|
|
166
|
+
const name = String(devbox?.name || "");
|
|
154
167
|
const safeMax = Math.min(nameWidth || 15, ABSOLUTE_MAX_NAME);
|
|
155
168
|
return name.length > safeMax
|
|
156
169
|
? name.substring(0, Math.max(1, safeMax - 3)) + "..."
|
|
@@ -171,14 +184,22 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
|
|
|
171
184
|
dimColor: false,
|
|
172
185
|
bold: false,
|
|
173
186
|
}),
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}, {
|
|
187
|
+
// Status text column with color matching the icon
|
|
188
|
+
{
|
|
189
|
+
key: "status",
|
|
190
|
+
label: "Status",
|
|
179
191
|
width: statusTextWidth,
|
|
180
|
-
|
|
181
|
-
|
|
192
|
+
render: (devbox, _index, isSelected) => {
|
|
193
|
+
const statusDisplay = getStatusDisplay(devbox?.status);
|
|
194
|
+
const statusColor = statusDisplay.color === colors.textDim
|
|
195
|
+
? colors.info
|
|
196
|
+
: statusDisplay.color;
|
|
197
|
+
const safeWidth = Math.max(1, statusTextWidth);
|
|
198
|
+
const truncated = statusDisplay.text.slice(0, safeWidth);
|
|
199
|
+
const padded = truncated.padEnd(safeWidth, " ");
|
|
200
|
+
return (_jsx(Text, { color: isSelected ? "white" : statusColor, bold: true, dimColor: false, inverse: isSelected, wrap: "truncate", children: padded }));
|
|
201
|
+
},
|
|
202
|
+
},
|
|
182
203
|
createTextColumn("created", "Created", (devbox) => {
|
|
183
204
|
const time = formatTimeAgo(devbox?.create_time_ms || Date.now());
|
|
184
205
|
const text = String(time || "-");
|
|
@@ -217,6 +238,7 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
|
|
|
217
238
|
}
|
|
218
239
|
return columns;
|
|
219
240
|
}, [
|
|
241
|
+
statusIconWidth,
|
|
220
242
|
nameWidth,
|
|
221
243
|
idWidth,
|
|
222
244
|
statusTextWidth,
|
|
@@ -15,12 +15,12 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
15
15
|
const [currentField, setCurrentField] = React.useState("create");
|
|
16
16
|
const [formData, setFormData] = React.useState({
|
|
17
17
|
name: "",
|
|
18
|
-
architecture: "
|
|
18
|
+
architecture: "x86_64",
|
|
19
19
|
resource_size: "SMALL",
|
|
20
20
|
custom_cpu: "",
|
|
21
21
|
custom_memory: "",
|
|
22
22
|
custom_disk: "",
|
|
23
|
-
keep_alive: "3600",
|
|
23
|
+
keep_alive: "3600", // 1 hour
|
|
24
24
|
metadata: {},
|
|
25
25
|
blueprint_id: initialBlueprintId || "",
|
|
26
26
|
snapshot_id: initialSnapshotId || "",
|
|
@@ -381,7 +381,7 @@ export const DevboxCreatePage = ({ onBack, onCreate, initialBlueprintId, initial
|
|
|
381
381
|
if (field.type === "metadata") {
|
|
382
382
|
if (!inMetadataSection) {
|
|
383
383
|
// Collapsed view
|
|
384
|
-
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: [Object.keys(formData.metadata).length, " item(s)"] }), isActive && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Enter to manage]"] }))] }), Object.keys(formData.metadata).length > 0 && (_jsx(Box, { marginLeft: 2, children: _jsx(MetadataDisplay, { metadata: formData.metadata, showBorder: false }) }))] }, field.key));
|
|
384
|
+
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: [Object.keys(formData.metadata).length, " item(s)"] }), isActive && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Enter to manage]"] }))] }), Object.keys(formData.metadata).length > 0 && (_jsx(Box, { marginLeft: 2, children: _jsx(MetadataDisplay, { metadata: formData.metadata, title: "", showBorder: false, compact: true }) }))] }, field.key));
|
|
385
385
|
}
|
|
386
386
|
// Expanded metadata section view
|
|
387
387
|
const metadataKeys = Object.keys(formData.metadata);
|
|
@@ -4,7 +4,6 @@ import { Box, Text, useInput } from "ink";
|
|
|
4
4
|
import figures from "figures";
|
|
5
5
|
import { Header } from "./Header.js";
|
|
6
6
|
import { StatusBadge } from "./StatusBadge.js";
|
|
7
|
-
import { MetadataDisplay } from "./MetadataDisplay.js";
|
|
8
7
|
import { Breadcrumb } from "./Breadcrumb.js";
|
|
9
8
|
import { DevboxActionsMenu } from "./DevboxActionsMenu.js";
|
|
10
9
|
import { StateHistory } from "./StateHistory.js";
|
|
@@ -33,6 +32,12 @@ const formatTimeAgo = (timestamp) => {
|
|
|
33
32
|
const years = Math.floor(months / 12);
|
|
34
33
|
return `${years}y ago`;
|
|
35
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
|
+
};
|
|
36
41
|
export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
|
|
37
42
|
const isMounted = React.useRef(true);
|
|
38
43
|
// Track mounted state
|
|
@@ -360,7 +365,7 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
|
|
|
360
365
|
lines.push(_jsxs(Text, { color: colors.error, dimColor: true, children: [" ", "Failure Reason: ", selectedDevbox.failure_reason] }, "status-fail"));
|
|
361
366
|
}
|
|
362
367
|
if (selectedDevbox.shutdown_reason) {
|
|
363
|
-
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Shutdown
|
|
368
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Shutdown Initiator: ", selectedDevbox.shutdown_reason] }, "status-shut"));
|
|
364
369
|
}
|
|
365
370
|
lines.push(_jsx(Text, { children: " " }, "status-space"));
|
|
366
371
|
}
|
|
@@ -425,16 +430,61 @@ export const DevboxDetailPage = ({ devbox: initialDevbox, onBack, }) => {
|
|
|
425
430
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
426
431
|
{ label: "Devboxes" },
|
|
427
432
|
{ label: selectedDevbox.name || selectedDevbox.id, active: true },
|
|
428
|
-
] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: colors.primary, bold: true, children: selectedDevbox.name || selectedDevbox.id
|
|
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
|
|
429
434
|
? `${uptime}m`
|
|
430
|
-
: `${Math.floor(uptime / 60)}h ${uptime % 60}m`] }),
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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) => {
|
|
438
488
|
const isSelected = index === selectedOperation;
|
|
439
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));
|
|
440
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"] }) })] }));
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
2
3
|
import { Box, Text } from "ink";
|
|
3
4
|
import figures from "figures";
|
|
4
5
|
import { colors } from "../utils/theme.js";
|
|
5
|
-
const renderKeyValueBadge = (keyText, value, color) => (_jsxs(Box, { borderStyle: "round", borderColor: color, paddingX: 1, marginRight: 1, children: [_jsx(Text, { color: color, bold: true, children: keyText }), _jsx(Text, { color: color, children: "
|
|
6
|
+
const renderKeyValueBadge = (keyText, value, color) => (_jsxs(Box, { borderStyle: "round", borderColor: color, paddingX: 1, marginRight: 1, children: [_jsx(Text, { color: color, bold: true, children: keyText }), _jsx(Text, { color: color, children: "= " }), _jsx(Text, { color: color, children: value })] }));
|
|
7
|
+
const renderCompactKeyValue = (keyText, value, color, isLast) => (_jsxs(Text, { children: [_jsx(Text, { color: color, bold: true, children: keyText }), _jsx(Text, { color: colors.textDim, children: "=" }), _jsx(Text, { color: color, children: value }), !isLast && _jsx(Text, { color: colors.textDim, children: " \u00B7 " })] }));
|
|
6
8
|
// Generate color for each key based on hash
|
|
7
9
|
const getColorForKey = (key, index) => {
|
|
8
10
|
const colorList = [
|
|
@@ -15,11 +17,19 @@ const getColorForKey = (key, index) => {
|
|
|
15
17
|
];
|
|
16
18
|
return colorList[index % colorList.length];
|
|
17
19
|
};
|
|
18
|
-
export const MetadataDisplay = ({ metadata, title = "Metadata", showBorder = false, selectedKey, }) => {
|
|
20
|
+
export const MetadataDisplay = ({ metadata, title = "Metadata", showBorder = false, selectedKey, compact = false, }) => {
|
|
19
21
|
const entries = Object.entries(metadata);
|
|
20
22
|
if (entries.length === 0) {
|
|
21
23
|
return null;
|
|
22
24
|
}
|
|
25
|
+
if (compact) {
|
|
26
|
+
return (_jsxs(Box, { flexDirection: "column", children: [title && (_jsxs(Text, { color: colors.accent3, bold: true, children: [figures.identical, " ", title] })), _jsx(Box, { flexDirection: "row", flexWrap: "wrap", children: entries.map(([key, value], index) => {
|
|
27
|
+
const color = getColorForKey(key, index);
|
|
28
|
+
const isSelected = selectedKey === key;
|
|
29
|
+
const isLast = index === entries.length - 1;
|
|
30
|
+
return (_jsxs(React.Fragment, { children: [isSelected && (_jsx(Text, { color: colors.primary, bold: true, children: figures.pointer })), renderCompactKeyValue(key, value, isSelected ? colors.primary : color, isLast)] }, key));
|
|
31
|
+
}) })] }));
|
|
32
|
+
}
|
|
23
33
|
const content = (_jsxs(Box, { flexDirection: "row", alignItems: "center", flexWrap: "wrap", gap: 1, children: [title && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.accent3, bold: true, children: [figures.identical, " ", title] }), _jsx(Text, { children: " " })] })), entries.map(([key, value], index) => {
|
|
24
34
|
const color = getColorForKey(key, index);
|
|
25
35
|
const isSelected = selectedKey === key;
|
|
@@ -10,25 +10,38 @@ import { Table } from "./Table.js";
|
|
|
10
10
|
import { colors } from "../utils/theme.js";
|
|
11
11
|
import { useViewportHeight } from "../hooks/useViewportHeight.js";
|
|
12
12
|
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
|
|
13
|
-
// Format time ago
|
|
13
|
+
// Format time ago - concise with ISO-style date for older items
|
|
14
14
|
export const formatTimeAgo = (timestamp) => {
|
|
15
15
|
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
|
16
|
+
const date = new Date(timestamp);
|
|
17
|
+
const now = new Date();
|
|
18
|
+
// Format time as HH:MM:SS (24h)
|
|
19
|
+
const time = date.toTimeString().slice(0, 8);
|
|
20
|
+
// Less than 1 minute
|
|
16
21
|
if (seconds < 60)
|
|
17
|
-
return `${seconds}s ago`;
|
|
22
|
+
return `${time} (${seconds}s ago)`;
|
|
18
23
|
const minutes = Math.floor(seconds / 60);
|
|
24
|
+
// Less than 1 hour
|
|
19
25
|
if (minutes < 60)
|
|
20
|
-
return `${minutes}m ago`;
|
|
26
|
+
return `${time} (${minutes}m ago)`;
|
|
21
27
|
const hours = Math.floor(minutes / 60);
|
|
28
|
+
// Less than 24 hours - show time + relative
|
|
22
29
|
if (hours < 24)
|
|
23
|
-
return `${hours}
|
|
30
|
+
return `${time} (${hours}hr ago)`;
|
|
24
31
|
const days = Math.floor(hours / 24);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
const sameYear = date.getFullYear() === now.getFullYear();
|
|
33
|
+
// Format date parts
|
|
34
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
35
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
36
|
+
const year = date.getFullYear();
|
|
37
|
+
// Date format: MM-DD or YYYY-MM-DD if different year
|
|
38
|
+
const dateStr = `${month}-${day}`;
|
|
39
|
+
// 1-7 days - show date + time + relative
|
|
40
|
+
if (days <= 7) {
|
|
41
|
+
return `${dateStr} ${time} (${days}d)`;
|
|
42
|
+
}
|
|
43
|
+
// More than 7 days - just date + time, no relative
|
|
44
|
+
return `${dateStr} ${time}`;
|
|
32
45
|
};
|
|
33
46
|
export function ResourceListView({ config }) {
|
|
34
47
|
const { exit: inkExit } = useApp();
|
|
@@ -2,6 +2,37 @@ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import figures from "figures";
|
|
4
4
|
import { colors } from "../utils/theme.js";
|
|
5
|
+
import { getStatusDisplay } from "./StatusBadge.js";
|
|
6
|
+
// Format shutdown reason into human-readable text
|
|
7
|
+
const formatShutdownReason = (reason) => {
|
|
8
|
+
switch (reason) {
|
|
9
|
+
case "api_shutdown":
|
|
10
|
+
return "API call";
|
|
11
|
+
case "idle_timeout":
|
|
12
|
+
return "Idle timeout";
|
|
13
|
+
case "keep_alive_timeout":
|
|
14
|
+
return "Max lifetime expired";
|
|
15
|
+
case "max_lifetime":
|
|
16
|
+
case "max_lifetime_exceeded":
|
|
17
|
+
return "Max lifetime exceeded";
|
|
18
|
+
case "user_initiated":
|
|
19
|
+
return "User initiated";
|
|
20
|
+
case "system_maintenance":
|
|
21
|
+
return "System maintenance";
|
|
22
|
+
case "resource_limit":
|
|
23
|
+
return "Resource limits";
|
|
24
|
+
case "entrypoint_exit":
|
|
25
|
+
return "Entrypoint exited";
|
|
26
|
+
case "idle":
|
|
27
|
+
return "Idle";
|
|
28
|
+
case "error":
|
|
29
|
+
case "failure":
|
|
30
|
+
return "Error";
|
|
31
|
+
default:
|
|
32
|
+
// Convert snake_case to readable text
|
|
33
|
+
return reason.replace(/_/g, " ");
|
|
34
|
+
}
|
|
35
|
+
};
|
|
5
36
|
// Format time ago in a succinct way
|
|
6
37
|
const formatTimeAgo = (timestamp) => {
|
|
7
38
|
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
|
@@ -38,28 +69,30 @@ const formatDuration = (milliseconds) => {
|
|
|
38
69
|
};
|
|
39
70
|
// Capitalize first letter of a string
|
|
40
71
|
const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
72
|
+
// Terminal states that don't need duration shown (no new state coming)
|
|
73
|
+
const TERMINAL_STATES = ["shutdown", "failure"];
|
|
74
|
+
export const StateHistory = ({ stateTransitions, shutdownReason, }) => {
|
|
75
|
+
if (!stateTransitions || stateTransitions.length === 0) {
|
|
45
76
|
return null;
|
|
46
77
|
}
|
|
47
|
-
//
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
78
|
+
// Check if there are more than 5 transitions
|
|
79
|
+
const totalTransitions = stateTransitions.length;
|
|
80
|
+
const hasMore = totalTransitions > 5;
|
|
81
|
+
// Get last 5 transitions (oldest first - chronological order)
|
|
82
|
+
const lastFive = stateTransitions
|
|
83
|
+
.slice(-5)
|
|
51
84
|
.map((transition, idx, arr) => {
|
|
52
85
|
const transitionTime = transition.transition_time_ms;
|
|
53
|
-
// Calculate duration: time until next transition, or until now if it's the
|
|
86
|
+
// Calculate duration: time until next transition, or until now if it's the last state
|
|
54
87
|
let duration = 0;
|
|
55
88
|
if (transitionTime) {
|
|
56
|
-
if (idx ===
|
|
89
|
+
if (idx === arr.length - 1) {
|
|
57
90
|
// Most recent state - duration is from transition time to now
|
|
58
91
|
duration = Date.now() - transitionTime;
|
|
59
92
|
}
|
|
60
93
|
else {
|
|
61
|
-
//
|
|
62
|
-
const nextTransition = arr[idx
|
|
94
|
+
// Earlier state - duration is from this transition to the next one
|
|
95
|
+
const nextTransition = arr[idx + 1];
|
|
63
96
|
const nextTransitionTime = nextTransition.transition_time_ms;
|
|
64
97
|
if (nextTransitionTime) {
|
|
65
98
|
duration = nextTransitionTime - transitionTime;
|
|
@@ -73,8 +106,15 @@ export const StateHistory = ({ stateTransitions }) => {
|
|
|
73
106
|
};
|
|
74
107
|
})
|
|
75
108
|
.filter((state) => state.transitionTime); // Only show states with valid timestamps
|
|
76
|
-
if (
|
|
109
|
+
if (lastFive.length === 0) {
|
|
77
110
|
return null;
|
|
78
111
|
}
|
|
79
|
-
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1,
|
|
112
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: colors.success, bold: true, children: [figures.info, " State History", hasMore && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "(", totalTransitions - 5, " earlier)"] }))] }), _jsx(Box, { flexDirection: "column", paddingLeft: 2, children: lastFive.map((state, idx) => {
|
|
113
|
+
const statusDisplay = getStatusDisplay(state.status || "");
|
|
114
|
+
const isLastState = idx === lastFive.length - 1;
|
|
115
|
+
const isTerminalState = TERMINAL_STATES.includes(state.status);
|
|
116
|
+
const showDuration = state.duration > 0 && !(isLastState && isTerminalState);
|
|
117
|
+
const isShutdownState = state.status === "shutdown";
|
|
118
|
+
return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: statusDisplay.color, children: [statusDisplay.icon, " "] }), _jsx(Text, { color: isLastState ? statusDisplay.color : colors.textDim, bold: isLastState, children: capitalize(state.status || "unknown") }), state.transitionTime && (_jsxs(_Fragment, { children: [_jsxs(Text, { dimColor: true, children: [" ", "at ", new Date(state.transitionTime).toLocaleString(), " ", _jsxs(Text, { color: colors.textDim, children: ["(", formatTimeAgo(state.transitionTime), ")"] }), showDuration && (_jsxs(_Fragment, { children: [" ", "\u2022 Duration:", " ", _jsx(Text, { color: colors.secondary, children: formatDuration(state.duration) })] }))] }), isShutdownState && shutdownReason && (_jsxs(_Fragment, { children: [_jsx(Text, { color: colors.textDim, children: " due to " }), _jsx(Text, { color: colors.warning, children: formatShutdownReason(shutdownReason) })] }))] }))] }, idx));
|
|
119
|
+
}) })] }));
|
|
80
120
|
};
|
|
@@ -8,87 +8,118 @@ export const getStatusDisplay = (status) => {
|
|
|
8
8
|
icon: figures.questionMarkPrefix,
|
|
9
9
|
color: colors.textDim,
|
|
10
10
|
text: "UNKNOWN ",
|
|
11
|
+
label: "Unknown",
|
|
11
12
|
};
|
|
12
13
|
}
|
|
13
14
|
switch (status) {
|
|
15
|
+
// === ACTIVE STATE ===
|
|
14
16
|
case "running":
|
|
15
17
|
return {
|
|
16
18
|
icon: figures.circleFilled,
|
|
17
19
|
color: colors.success,
|
|
18
20
|
text: "RUNNING ",
|
|
21
|
+
label: "Running",
|
|
19
22
|
};
|
|
23
|
+
// === STARTING UP (transitioning to active) ===
|
|
20
24
|
case "provisioning":
|
|
21
25
|
return {
|
|
22
|
-
icon: figures.
|
|
26
|
+
icon: figures.arrowUp,
|
|
23
27
|
color: colors.warning,
|
|
24
28
|
text: "PROVISION ",
|
|
29
|
+
label: "Provisioning",
|
|
25
30
|
};
|
|
26
31
|
case "initializing":
|
|
27
32
|
return {
|
|
28
|
-
icon: figures.
|
|
33
|
+
icon: figures.arrowUp,
|
|
29
34
|
color: colors.primary,
|
|
30
35
|
text: "INITIALIZE",
|
|
36
|
+
label: "Initializing",
|
|
31
37
|
};
|
|
38
|
+
case "resuming":
|
|
39
|
+
return {
|
|
40
|
+
icon: figures.arrowUp,
|
|
41
|
+
color: colors.primary,
|
|
42
|
+
text: "RESUMING ",
|
|
43
|
+
label: "Resuming",
|
|
44
|
+
};
|
|
45
|
+
// === SHUTTING DOWN (transitioning to inactive) ===
|
|
46
|
+
case "suspending":
|
|
47
|
+
return {
|
|
48
|
+
icon: figures.arrowDown,
|
|
49
|
+
color: colors.warning,
|
|
50
|
+
text: "SUSPENDING",
|
|
51
|
+
label: "Suspending",
|
|
52
|
+
};
|
|
53
|
+
// === INACTIVE STATES ===
|
|
32
54
|
case "suspended":
|
|
33
55
|
return {
|
|
34
56
|
icon: figures.circleDotted,
|
|
35
57
|
color: colors.warning,
|
|
36
58
|
text: "SUSPENDED ",
|
|
59
|
+
label: "Suspended",
|
|
37
60
|
};
|
|
38
|
-
case "failure":
|
|
39
|
-
return { icon: figures.cross, color: colors.error, text: "FAILED " };
|
|
40
61
|
case "shutdown":
|
|
41
62
|
return {
|
|
42
63
|
icon: figures.circle,
|
|
43
64
|
color: colors.textDim,
|
|
44
65
|
text: "SHUTDOWN ",
|
|
66
|
+
label: "Shutdown",
|
|
45
67
|
};
|
|
46
|
-
|
|
68
|
+
// === ERROR STATES ===
|
|
69
|
+
case "failure":
|
|
47
70
|
return {
|
|
48
|
-
icon: figures.
|
|
49
|
-
color: colors.
|
|
50
|
-
text: "
|
|
71
|
+
icon: figures.cross,
|
|
72
|
+
color: colors.error,
|
|
73
|
+
text: "FAILED ",
|
|
74
|
+
label: "Failed",
|
|
51
75
|
};
|
|
52
|
-
case "
|
|
76
|
+
case "build_failed":
|
|
77
|
+
case "failed":
|
|
53
78
|
return {
|
|
54
|
-
icon: figures.
|
|
55
|
-
color: colors.
|
|
56
|
-
text: "
|
|
79
|
+
icon: figures.cross,
|
|
80
|
+
color: colors.error,
|
|
81
|
+
text: "FAILED ",
|
|
82
|
+
label: "Failed",
|
|
57
83
|
};
|
|
84
|
+
// === BUILD STATES (for blueprints) ===
|
|
58
85
|
case "ready":
|
|
59
86
|
return {
|
|
60
|
-
icon: figures.
|
|
87
|
+
icon: figures.tick,
|
|
61
88
|
color: colors.success,
|
|
62
89
|
text: "READY ",
|
|
90
|
+
label: "Ready",
|
|
63
91
|
};
|
|
64
92
|
case "build_complete":
|
|
65
93
|
case "building_complete":
|
|
66
94
|
return {
|
|
67
|
-
icon: figures.
|
|
95
|
+
icon: figures.tick,
|
|
68
96
|
color: colors.success,
|
|
69
97
|
text: "COMPLETE ",
|
|
98
|
+
label: "Build Complete",
|
|
70
99
|
};
|
|
71
100
|
case "building":
|
|
72
101
|
return {
|
|
73
|
-
icon: figures.
|
|
102
|
+
icon: figures.arrowUp,
|
|
74
103
|
color: colors.warning,
|
|
75
104
|
text: "BUILDING ",
|
|
105
|
+
label: "Building: In Progress",
|
|
76
106
|
};
|
|
77
|
-
case "build_failed":
|
|
78
|
-
case "failed":
|
|
79
|
-
return { icon: figures.cross, color: colors.error, text: "FAILED " };
|
|
80
107
|
default:
|
|
81
108
|
// Truncate and pad any unknown status to 10 chars to match column width
|
|
82
109
|
const truncated = status.toUpperCase().slice(0, 10);
|
|
83
110
|
const padded = truncated.padEnd(10, " ");
|
|
111
|
+
// Capitalize first letter for label
|
|
112
|
+
const label = status.charAt(0).toUpperCase() + status.slice(1);
|
|
84
113
|
return {
|
|
85
114
|
icon: figures.questionMarkPrefix,
|
|
86
115
|
color: colors.textDim,
|
|
87
116
|
text: padded,
|
|
117
|
+
label: label,
|
|
88
118
|
};
|
|
89
119
|
}
|
|
90
120
|
};
|
|
91
|
-
export const StatusBadge = ({ status, showText = true }) => {
|
|
121
|
+
export const StatusBadge = ({ status, showText = true, fullText = false, }) => {
|
|
92
122
|
const statusDisplay = getStatusDisplay(status);
|
|
93
|
-
|
|
123
|
+
const displayText = fullText ? statusDisplay.label : statusDisplay.text;
|
|
124
|
+
return (_jsxs(_Fragment, { children: [_jsx(Text, { color: statusDisplay.color, children: statusDisplay.icon }), showText && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { color: statusDisplay.color, children: displayText })] }))] }));
|
|
94
125
|
};
|