@runloop/rl-cli 1.7.0 → 1.8.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/menu.js +2 -1
- package/dist/commands/secret/list.js +379 -4
- package/dist/components/BenchmarkMenu.js +88 -0
- package/dist/components/MainMenu.js +63 -22
- package/dist/components/SecretCreatePage.js +185 -0
- package/dist/components/SettingsMenu.js +85 -0
- package/dist/components/StatusBadge.js +73 -0
- package/dist/components/StreamingLogsViewer.js +114 -53
- package/dist/router/Router.js +21 -1
- package/dist/screens/BenchmarkMenuScreen.js +23 -0
- package/dist/screens/BenchmarkRunDetailScreen.js +189 -0
- package/dist/screens/BenchmarkRunListScreen.js +255 -0
- package/dist/screens/MenuScreen.js +5 -2
- package/dist/screens/ScenarioRunDetailScreen.js +220 -0
- package/dist/screens/ScenarioRunListScreen.js +245 -0
- package/dist/screens/SecretCreateScreen.js +7 -0
- package/dist/screens/SecretDetailScreen.js +198 -0
- package/dist/screens/SecretListScreen.js +7 -0
- package/dist/screens/SettingsMenuScreen.js +23 -0
- package/dist/services/benchmarkService.js +73 -0
- package/dist/store/benchmarkStore.js +120 -0
- package/dist/store/betaFeatureStore.js +47 -0
- package/dist/store/index.js +1 -0
- package/dist/utils/config.js +8 -0
- package/package.json +1 -1
|
@@ -5,7 +5,6 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
5
5
|
*/
|
|
6
6
|
import React from "react";
|
|
7
7
|
import { Box, Text, useInput } from "ink";
|
|
8
|
-
import Spinner from "ink-spinner";
|
|
9
8
|
import figures from "figures";
|
|
10
9
|
import { Breadcrumb } from "./Breadcrumb.js";
|
|
11
10
|
import { NavigationTips } from "./NavigationTips.js";
|
|
@@ -14,6 +13,21 @@ import { useViewportHeight } from "../hooks/useViewportHeight.js";
|
|
|
14
13
|
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
|
|
15
14
|
import { parseAnyLogEntry } from "../utils/logFormatter.js";
|
|
16
15
|
import { getDevboxLogs } from "../services/devboxService.js";
|
|
16
|
+
// Color maps - defined outside component to avoid recreation on every render
|
|
17
|
+
const levelColorMap = {
|
|
18
|
+
red: colors.error,
|
|
19
|
+
yellow: colors.warning,
|
|
20
|
+
blue: colors.primary,
|
|
21
|
+
gray: colors.textDim,
|
|
22
|
+
};
|
|
23
|
+
const sourceColorMap = {
|
|
24
|
+
magenta: "#d33682",
|
|
25
|
+
cyan: colors.info,
|
|
26
|
+
green: colors.success,
|
|
27
|
+
yellow: colors.warning,
|
|
28
|
+
gray: colors.textDim,
|
|
29
|
+
white: colors.text,
|
|
30
|
+
};
|
|
17
31
|
export const StreamingLogsViewer = ({ devboxId, breadcrumbItems = [{ label: "Logs", active: true }], onBack, }) => {
|
|
18
32
|
const [logs, setLogs] = React.useState([]);
|
|
19
33
|
const [loading, setLoading] = React.useState(true);
|
|
@@ -25,15 +39,40 @@ export const StreamingLogsViewer = ({ devboxId, breadcrumbItems = [{ label: "Log
|
|
|
25
39
|
const [isPolling, setIsPolling] = React.useState(true);
|
|
26
40
|
// Refs for cleanup
|
|
27
41
|
const pollIntervalRef = React.useRef(null);
|
|
28
|
-
// Calculate viewport
|
|
29
|
-
const logsViewport = useViewportHeight({ overhead:
|
|
42
|
+
// Calculate viewport - overhead increased to reduce overdraw/flashing
|
|
43
|
+
const logsViewport = useViewportHeight({ overhead: 11, minHeight: 10 });
|
|
30
44
|
// Handle Ctrl+C
|
|
31
45
|
useExitOnCtrlC();
|
|
32
|
-
// Fetch logs function
|
|
46
|
+
// Fetch logs function - only update state if logs actually changed
|
|
33
47
|
const fetchLogs = React.useCallback(async () => {
|
|
34
48
|
try {
|
|
35
49
|
const newLogs = await getDevboxLogs(devboxId);
|
|
36
|
-
|
|
50
|
+
// Only update logs state if the logs have actually changed
|
|
51
|
+
// This prevents unnecessary re-renders that cause flashing in non-tmux terminals
|
|
52
|
+
setLogs((prevLogs) => {
|
|
53
|
+
// Quick length check first
|
|
54
|
+
if (prevLogs.length !== newLogs.length) {
|
|
55
|
+
return newLogs;
|
|
56
|
+
}
|
|
57
|
+
// If same length, check if last log entry is different (most common case for streaming)
|
|
58
|
+
if (newLogs.length > 0) {
|
|
59
|
+
const prevLast = prevLogs[prevLogs.length - 1];
|
|
60
|
+
const newLast = newLogs[newLogs.length - 1];
|
|
61
|
+
// Compare by timestamp and message for efficiency
|
|
62
|
+
if (prevLast &&
|
|
63
|
+
newLast &&
|
|
64
|
+
"timestamp" in prevLast &&
|
|
65
|
+
"timestamp" in newLast &&
|
|
66
|
+
"message" in prevLast &&
|
|
67
|
+
"message" in newLast &&
|
|
68
|
+
prevLast.timestamp === newLast.timestamp &&
|
|
69
|
+
prevLast.message === newLast.message) {
|
|
70
|
+
// Logs haven't changed, return previous state to avoid re-render
|
|
71
|
+
return prevLogs;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return newLogs;
|
|
75
|
+
});
|
|
37
76
|
setError(null);
|
|
38
77
|
if (loading)
|
|
39
78
|
setLoading(false);
|
|
@@ -216,57 +255,79 @@ export const StreamingLogsViewer = ({ devboxId, breadcrumbItems = [{ label: "Log
|
|
|
216
255
|
}
|
|
217
256
|
const hasMore = actualScroll + visibleLogs.length < logs.length;
|
|
218
257
|
const hasLess = actualScroll > 0;
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
258
|
+
// Build lines array - always render exactly viewportHeight lines for stable structure
|
|
259
|
+
// This prevents Ink from doing structural redraws that cause flashing
|
|
260
|
+
const renderLines = React.useMemo(() => {
|
|
261
|
+
const lines = [];
|
|
262
|
+
if (loading) {
|
|
263
|
+
// First line shows loading message
|
|
264
|
+
lines.push(_jsx(Box, { width: contentWidth, children: _jsx(Text, { color: colors.textDim, children: "\u25CF Loading logs..." }) }, "loading"));
|
|
265
|
+
}
|
|
266
|
+
else if (error) {
|
|
267
|
+
// First line shows error
|
|
268
|
+
lines.push(_jsx(Box, { width: contentWidth, children: _jsxs(Text, { color: colors.error, children: [figures.cross, " Error: ", error] }) }, "error"));
|
|
269
|
+
}
|
|
270
|
+
else if (logs.length === 0) {
|
|
271
|
+
// First line shows waiting message
|
|
272
|
+
lines.push(_jsx(Box, { width: contentWidth, children: _jsxs(Text, { color: colors.textDim, children: [isPolling ? "● " : "", "Waiting for logs..."] }) }, "waiting"));
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
// Render visible log entries
|
|
276
|
+
for (let i = 0; i < visibleLogs.length; i++) {
|
|
277
|
+
const log = visibleLogs[i];
|
|
278
|
+
const parts = parseAnyLogEntry(log);
|
|
279
|
+
const sanitizedMessage = sanitizeMessage(parts.message);
|
|
280
|
+
const MAX_MESSAGE_LENGTH = 1000;
|
|
281
|
+
const fullMessage = sanitizedMessage.length > MAX_MESSAGE_LENGTH
|
|
282
|
+
? sanitizedMessage.substring(0, MAX_MESSAGE_LENGTH) + "..."
|
|
283
|
+
: sanitizedMessage;
|
|
284
|
+
const cmd = parts.cmd
|
|
285
|
+
? `$ ${parts.cmd.substring(0, 40)}${parts.cmd.length > 40 ? "..." : ""} `
|
|
286
|
+
: "";
|
|
287
|
+
const exitCode = parts.exitCode !== null ? `exit=${parts.exitCode} ` : "";
|
|
288
|
+
const levelColor = levelColorMap[parts.levelColor] || colors.textDim;
|
|
289
|
+
const sourceColor = sourceColorMap[parts.sourceColor] || colors.textDim;
|
|
290
|
+
if (logsWrapMode) {
|
|
291
|
+
lines.push(_jsx(Box, { width: contentWidth, flexDirection: "column", children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: colors.textDim, dimColor: true, children: parts.timestamp }), _jsx(Text, { children: " " }), _jsx(Text, { color: levelColor, bold: parts.levelColor === "red", children: parts.level }), _jsx(Text, { children: " " }), _jsxs(Text, { color: sourceColor, children: ["[", parts.source, "]"] }), _jsx(Text, { children: " " }), parts.shellName && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["(", parts.shellName, ")", " "] })), cmd && _jsx(Text, { color: colors.info, children: cmd }), _jsx(Text, { children: fullMessage }), exitCode && (_jsxs(Text, { color: parts.exitCode === 0 ? colors.success : colors.error, children: [" ", exitCode] }))] }) }, i));
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
const shellPart = parts.shellName ? `(${parts.shellName}) ` : "";
|
|
295
|
+
const exitPart = exitCode ? ` ${exitCode}` : "";
|
|
296
|
+
const prefix = `${parts.timestamp} ${parts.level} [${parts.source}] ${shellPart}${cmd}`;
|
|
297
|
+
const suffix = exitPart;
|
|
298
|
+
const availableForMessage = contentWidth - prefix.length - suffix.length;
|
|
299
|
+
let displayMessage;
|
|
300
|
+
if (availableForMessage <= 3) {
|
|
301
|
+
displayMessage = "";
|
|
302
|
+
}
|
|
303
|
+
else if (fullMessage.length <= availableForMessage) {
|
|
304
|
+
displayMessage = fullMessage;
|
|
249
305
|
}
|
|
250
306
|
else {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const prefix = `${parts.timestamp} ${parts.level} [${parts.source}] ${shellPart}${cmd}`;
|
|
254
|
-
const suffix = exitPart;
|
|
255
|
-
const availableForMessage = contentWidth - prefix.length - suffix.length;
|
|
256
|
-
let displayMessage;
|
|
257
|
-
if (availableForMessage <= 3) {
|
|
258
|
-
displayMessage = "";
|
|
259
|
-
}
|
|
260
|
-
else if (fullMessage.length <= availableForMessage) {
|
|
261
|
-
displayMessage = fullMessage;
|
|
262
|
-
}
|
|
263
|
-
else {
|
|
264
|
-
displayMessage =
|
|
265
|
-
fullMessage.substring(0, availableForMessage - 3) + "...";
|
|
266
|
-
}
|
|
267
|
-
return (_jsx(Box, { width: contentWidth, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: colors.textDim, dimColor: true, children: parts.timestamp }), _jsx(Text, { children: " " }), _jsx(Text, { color: levelColor, bold: parts.levelColor === "red", children: parts.level }), _jsx(Text, { children: " " }), _jsxs(Text, { color: sourceColor, children: ["[", parts.source, "]"] }), _jsx(Text, { children: " " }), parts.shellName && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["(", parts.shellName, ")", " "] })), cmd && _jsx(Text, { color: colors.info, children: cmd }), _jsx(Text, { children: displayMessage }), exitCode && (_jsxs(Text, { color: parts.exitCode === 0 ? colors.success : colors.error, children: [" ", exitCode] }))] }) }, index));
|
|
307
|
+
displayMessage =
|
|
308
|
+
fullMessage.substring(0, availableForMessage - 3) + "...";
|
|
268
309
|
}
|
|
269
|
-
|
|
310
|
+
lines.push(_jsx(Box, { width: contentWidth, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: colors.textDim, dimColor: true, children: parts.timestamp }), _jsx(Text, { children: " " }), _jsx(Text, { color: levelColor, bold: parts.levelColor === "red", children: parts.level }), _jsx(Text, { children: " " }), _jsxs(Text, { color: sourceColor, children: ["[", parts.source, "]"] }), _jsx(Text, { children: " " }), parts.shellName && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["(", parts.shellName, ")", " "] })), cmd && _jsx(Text, { color: colors.info, children: cmd }), _jsx(Text, { children: displayMessage }), exitCode && (_jsxs(Text, { color: parts.exitCode === 0 ? colors.success : colors.error, children: [" ", exitCode] }))] }) }, i));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// Pad with empty lines to maintain consistent structure
|
|
315
|
+
// This prevents Ink from doing structural redraws
|
|
316
|
+
while (lines.length < viewportHeight) {
|
|
317
|
+
lines.push(_jsx(Box, { width: contentWidth, children: _jsx(Text, { children: " " }) }, `pad-${lines.length}`));
|
|
318
|
+
}
|
|
319
|
+
return lines;
|
|
320
|
+
}, [
|
|
321
|
+
loading,
|
|
322
|
+
error,
|
|
323
|
+
logs.length,
|
|
324
|
+
visibleLogs,
|
|
325
|
+
isPolling,
|
|
326
|
+
logsWrapMode,
|
|
327
|
+
contentWidth,
|
|
328
|
+
viewportHeight,
|
|
329
|
+
]);
|
|
330
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: breadcrumbItems }), _jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.border, paddingX: 1, height: viewportHeight + 2, children: renderLines }), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", logs.length] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "logs"] }), logs.length > 0 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [actualScroll + 1, "-", Math.min(actualScroll + visibleLogs.length, logs.length), " of", " ", logs.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", " "] }), isPolling ? (_jsx(Text, { color: colors.success, children: "\u25CF Live" })) : (_jsx(Text, { color: colors.textDim, children: "\u25CB Paused" })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsx(Text, { color: logsWrapMode ? colors.success : colors.textDim, bold: logsWrapMode, children: logsWrapMode ? "Wrap: ON" : "Wrap: OFF" }), copyStatus && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsx(Text, { color: colors.success, bold: true, children: copyStatus })] }))] }), _jsx(NavigationTips, { showArrows: true, tips: [
|
|
270
331
|
{ key: "g/G", label: "Top/Bottom" },
|
|
271
332
|
{ key: "p", label: isPolling ? "Pause" : "Resume" },
|
|
272
333
|
{ key: "w", label: "Wrap" },
|
package/dist/router/Router.js
CHANGED
|
@@ -10,6 +10,7 @@ import { useBlueprintStore } from "../store/blueprintStore.js";
|
|
|
10
10
|
import { useSnapshotStore } from "../store/snapshotStore.js";
|
|
11
11
|
import { useNetworkPolicyStore } from "../store/networkPolicyStore.js";
|
|
12
12
|
import { useObjectStore } from "../store/objectStore.js";
|
|
13
|
+
import { useBenchmarkStore } from "../store/benchmarkStore.js";
|
|
13
14
|
import { ErrorBoundary } from "../components/ErrorBoundary.js";
|
|
14
15
|
// Import screen components
|
|
15
16
|
import { MenuScreen } from "../screens/MenuScreen.js";
|
|
@@ -26,9 +27,18 @@ import { SnapshotDetailScreen } from "../screens/SnapshotDetailScreen.js";
|
|
|
26
27
|
import { NetworkPolicyListScreen } from "../screens/NetworkPolicyListScreen.js";
|
|
27
28
|
import { NetworkPolicyDetailScreen } from "../screens/NetworkPolicyDetailScreen.js";
|
|
28
29
|
import { NetworkPolicyCreateScreen } from "../screens/NetworkPolicyCreateScreen.js";
|
|
30
|
+
import { SettingsMenuScreen } from "../screens/SettingsMenuScreen.js";
|
|
31
|
+
import { SecretListScreen } from "../screens/SecretListScreen.js";
|
|
32
|
+
import { SecretDetailScreen } from "../screens/SecretDetailScreen.js";
|
|
33
|
+
import { SecretCreateScreen } from "../screens/SecretCreateScreen.js";
|
|
29
34
|
import { ObjectListScreen } from "../screens/ObjectListScreen.js";
|
|
30
35
|
import { ObjectDetailScreen } from "../screens/ObjectDetailScreen.js";
|
|
31
36
|
import { SSHSessionScreen } from "../screens/SSHSessionScreen.js";
|
|
37
|
+
import { BenchmarkMenuScreen } from "../screens/BenchmarkMenuScreen.js";
|
|
38
|
+
import { BenchmarkRunListScreen } from "../screens/BenchmarkRunListScreen.js";
|
|
39
|
+
import { BenchmarkRunDetailScreen } from "../screens/BenchmarkRunDetailScreen.js";
|
|
40
|
+
import { ScenarioRunListScreen } from "../screens/ScenarioRunListScreen.js";
|
|
41
|
+
import { ScenarioRunDetailScreen } from "../screens/ScenarioRunDetailScreen.js";
|
|
32
42
|
/**
|
|
33
43
|
* Router component that renders the current screen
|
|
34
44
|
* Implements memory cleanup on route changes
|
|
@@ -82,6 +92,16 @@ export function Router() {
|
|
|
82
92
|
useObjectStore.getState().clearAll();
|
|
83
93
|
}
|
|
84
94
|
break;
|
|
95
|
+
case "benchmark-menu":
|
|
96
|
+
case "benchmark-run-list":
|
|
97
|
+
case "benchmark-run-detail":
|
|
98
|
+
case "scenario-run-list":
|
|
99
|
+
case "scenario-run-detail":
|
|
100
|
+
if (!currentScreen.startsWith("benchmark") &&
|
|
101
|
+
!currentScreen.startsWith("scenario")) {
|
|
102
|
+
useBenchmarkStore.getState().clearAll();
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
85
105
|
}
|
|
86
106
|
}
|
|
87
107
|
prevScreenRef.current = currentScreen;
|
|
@@ -90,5 +110,5 @@ export function Router() {
|
|
|
90
110
|
// and mount new component, preventing race conditions during screen transitions.
|
|
91
111
|
// The key ensures React treats this as a completely new component tree.
|
|
92
112
|
// Wrap in ErrorBoundary to catch any Yoga WASM errors gracefully.
|
|
93
|
-
return (_jsxs(ErrorBoundary, { children: [currentScreen === "menu" && (_jsx(MenuScreen, { ...params }, currentScreen)), currentScreen === "devbox-list" && (_jsx(DevboxListScreen, { ...params }, currentScreen)), currentScreen === "devbox-detail" && (_jsx(DevboxDetailScreen, { ...params }, currentScreen)), currentScreen === "devbox-actions" && (_jsx(DevboxActionsScreen, { ...params }, currentScreen)), currentScreen === "devbox-exec" && (_jsx(DevboxExecScreen, { ...params }, currentScreen)), currentScreen === "devbox-create" && (_jsx(DevboxCreateScreen, { ...params }, currentScreen)), currentScreen === "blueprint-list" && (_jsx(BlueprintListScreen, { ...params }, currentScreen)), currentScreen === "blueprint-detail" && (_jsx(BlueprintDetailScreen, { ...params }, currentScreen)), currentScreen === "blueprint-logs" && (_jsx(BlueprintLogsScreen, { ...params }, currentScreen)), currentScreen === "snapshot-list" && (_jsx(SnapshotListScreen, { ...params }, currentScreen)), currentScreen === "snapshot-detail" && (_jsx(SnapshotDetailScreen, { ...params }, currentScreen)), currentScreen === "network-policy-list" && (_jsx(NetworkPolicyListScreen, { ...params }, currentScreen)), currentScreen === "network-policy-detail" && (_jsx(NetworkPolicyDetailScreen, { ...params }, currentScreen)), currentScreen === "network-policy-create" && (_jsx(NetworkPolicyCreateScreen, { ...params }, currentScreen)), currentScreen === "object-list" && (_jsx(ObjectListScreen, { ...params }, currentScreen)), currentScreen === "object-detail" && (_jsx(ObjectDetailScreen, { ...params }, currentScreen)), currentScreen === "ssh-session" && (_jsx(SSHSessionScreen, { ...params }, currentScreen))] }, `boundary-${currentScreen}`));
|
|
113
|
+
return (_jsxs(ErrorBoundary, { children: [currentScreen === "menu" && (_jsx(MenuScreen, { ...params }, currentScreen)), currentScreen === "settings-menu" && (_jsx(SettingsMenuScreen, { ...params }, currentScreen)), currentScreen === "devbox-list" && (_jsx(DevboxListScreen, { ...params }, currentScreen)), currentScreen === "devbox-detail" && (_jsx(DevboxDetailScreen, { ...params }, currentScreen)), currentScreen === "devbox-actions" && (_jsx(DevboxActionsScreen, { ...params }, currentScreen)), currentScreen === "devbox-exec" && (_jsx(DevboxExecScreen, { ...params }, currentScreen)), currentScreen === "devbox-create" && (_jsx(DevboxCreateScreen, { ...params }, currentScreen)), currentScreen === "blueprint-list" && (_jsx(BlueprintListScreen, { ...params }, currentScreen)), currentScreen === "blueprint-detail" && (_jsx(BlueprintDetailScreen, { ...params }, currentScreen)), currentScreen === "blueprint-logs" && (_jsx(BlueprintLogsScreen, { ...params }, currentScreen)), currentScreen === "snapshot-list" && (_jsx(SnapshotListScreen, { ...params }, currentScreen)), currentScreen === "snapshot-detail" && (_jsx(SnapshotDetailScreen, { ...params }, currentScreen)), currentScreen === "network-policy-list" && (_jsx(NetworkPolicyListScreen, { ...params }, currentScreen)), currentScreen === "network-policy-detail" && (_jsx(NetworkPolicyDetailScreen, { ...params }, currentScreen)), currentScreen === "network-policy-create" && (_jsx(NetworkPolicyCreateScreen, { ...params }, currentScreen)), currentScreen === "secret-list" && (_jsx(SecretListScreen, { ...params }, currentScreen)), currentScreen === "secret-detail" && (_jsx(SecretDetailScreen, { ...params }, currentScreen)), currentScreen === "secret-create" && (_jsx(SecretCreateScreen, { ...params }, currentScreen)), currentScreen === "object-list" && (_jsx(ObjectListScreen, { ...params }, currentScreen)), currentScreen === "object-detail" && (_jsx(ObjectDetailScreen, { ...params }, currentScreen)), currentScreen === "ssh-session" && (_jsx(SSHSessionScreen, { ...params }, currentScreen)), currentScreen === "benchmark-menu" && (_jsx(BenchmarkMenuScreen, { ...params }, currentScreen)), currentScreen === "benchmark-run-list" && (_jsx(BenchmarkRunListScreen, { ...params }, currentScreen)), currentScreen === "benchmark-run-detail" && (_jsx(BenchmarkRunDetailScreen, { ...params }, currentScreen)), currentScreen === "scenario-run-list" && (_jsx(ScenarioRunListScreen, { ...params }, currentScreen)), currentScreen === "scenario-run-detail" && (_jsx(ScenarioRunDetailScreen, { ...params }, currentScreen))] }, `boundary-${currentScreen}`));
|
|
94
114
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useNavigation } from "../store/navigationStore.js";
|
|
3
|
+
import { BenchmarkMenu } from "../components/BenchmarkMenu.js";
|
|
4
|
+
export function BenchmarkMenuScreen() {
|
|
5
|
+
const { navigate, goBack } = useNavigation();
|
|
6
|
+
const handleSelect = (key) => {
|
|
7
|
+
switch (key) {
|
|
8
|
+
case "benchmark-runs":
|
|
9
|
+
navigate("benchmark-run-list");
|
|
10
|
+
break;
|
|
11
|
+
case "scenario-runs":
|
|
12
|
+
navigate("scenario-run-list");
|
|
13
|
+
break;
|
|
14
|
+
default:
|
|
15
|
+
// Fallback for any other screen names
|
|
16
|
+
navigate(key);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const handleBack = () => {
|
|
20
|
+
goBack();
|
|
21
|
+
};
|
|
22
|
+
return _jsx(BenchmarkMenu, { onSelect: handleSelect, onBack: handleBack });
|
|
23
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* BenchmarkRunDetailScreen - Detail page for benchmark runs
|
|
4
|
+
* Uses the generic ResourceDetailPage component
|
|
5
|
+
*/
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { Text } from "ink";
|
|
8
|
+
import figures from "figures";
|
|
9
|
+
import { useNavigation } from "../store/navigationStore.js";
|
|
10
|
+
import { useBenchmarkStore, } from "../store/benchmarkStore.js";
|
|
11
|
+
import { ResourceDetailPage, formatTimestamp, } from "../components/ResourceDetailPage.js";
|
|
12
|
+
import { getBenchmarkRun } from "../services/benchmarkService.js";
|
|
13
|
+
import { SpinnerComponent } from "../components/Spinner.js";
|
|
14
|
+
import { ErrorMessage } from "../components/ErrorMessage.js";
|
|
15
|
+
import { Breadcrumb } from "../components/Breadcrumb.js";
|
|
16
|
+
import { colors } from "../utils/theme.js";
|
|
17
|
+
export function BenchmarkRunDetailScreen({ benchmarkRunId, }) {
|
|
18
|
+
const { goBack, navigate } = useNavigation();
|
|
19
|
+
const benchmarkRuns = useBenchmarkStore((state) => state.benchmarkRuns);
|
|
20
|
+
const [loading, setLoading] = React.useState(false);
|
|
21
|
+
const [error, setError] = React.useState(null);
|
|
22
|
+
const [fetchedRun, setFetchedRun] = React.useState(null);
|
|
23
|
+
// Find run in store first
|
|
24
|
+
const runFromStore = benchmarkRuns.find((r) => r.id === benchmarkRunId);
|
|
25
|
+
// Polling function
|
|
26
|
+
const pollRun = React.useCallback(async () => {
|
|
27
|
+
if (!benchmarkRunId)
|
|
28
|
+
return null;
|
|
29
|
+
return getBenchmarkRun(benchmarkRunId);
|
|
30
|
+
}, [benchmarkRunId]);
|
|
31
|
+
// Fetch run from API if not in store
|
|
32
|
+
React.useEffect(() => {
|
|
33
|
+
if (benchmarkRunId && !loading && !fetchedRun) {
|
|
34
|
+
setLoading(true);
|
|
35
|
+
setError(null);
|
|
36
|
+
getBenchmarkRun(benchmarkRunId)
|
|
37
|
+
.then((run) => {
|
|
38
|
+
setFetchedRun(run);
|
|
39
|
+
setLoading(false);
|
|
40
|
+
})
|
|
41
|
+
.catch((err) => {
|
|
42
|
+
setError(err);
|
|
43
|
+
setLoading(false);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}, [benchmarkRunId, loading, fetchedRun]);
|
|
47
|
+
// Use fetched run for full details, fall back to store for basic display
|
|
48
|
+
const run = fetchedRun || runFromStore;
|
|
49
|
+
// Show loading state
|
|
50
|
+
if (!run && benchmarkRunId && !error) {
|
|
51
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
52
|
+
{ label: "Home" },
|
|
53
|
+
{ label: "Benchmarks" },
|
|
54
|
+
{ label: "Benchmark Runs" },
|
|
55
|
+
{ label: "Loading...", active: true },
|
|
56
|
+
] }), _jsx(SpinnerComponent, { message: "Loading benchmark run details..." })] }));
|
|
57
|
+
}
|
|
58
|
+
// Show error state
|
|
59
|
+
if (error && !run) {
|
|
60
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
61
|
+
{ label: "Home" },
|
|
62
|
+
{ label: "Benchmarks" },
|
|
63
|
+
{ label: "Benchmark Runs" },
|
|
64
|
+
{ label: "Error", active: true },
|
|
65
|
+
] }), _jsx(ErrorMessage, { message: "Failed to load benchmark run details", error: error })] }));
|
|
66
|
+
}
|
|
67
|
+
// Show not found error
|
|
68
|
+
if (!run) {
|
|
69
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
70
|
+
{ label: "Home" },
|
|
71
|
+
{ label: "Benchmarks" },
|
|
72
|
+
{ label: "Benchmark Runs" },
|
|
73
|
+
{ label: "Not Found", active: true },
|
|
74
|
+
] }), _jsx(ErrorMessage, { message: `Benchmark run ${benchmarkRunId || "unknown"} not found`, error: new Error("Benchmark run not found") })] }));
|
|
75
|
+
}
|
|
76
|
+
// Build detail sections
|
|
77
|
+
const detailSections = [];
|
|
78
|
+
// Basic details section
|
|
79
|
+
const basicFields = [];
|
|
80
|
+
if (run.start_time_ms) {
|
|
81
|
+
basicFields.push({
|
|
82
|
+
label: "Started",
|
|
83
|
+
value: formatTimestamp(run.start_time_ms),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const endTimeMs = run.start_time_ms && run.duration_ms
|
|
87
|
+
? run.start_time_ms + run.duration_ms
|
|
88
|
+
: undefined;
|
|
89
|
+
if (endTimeMs) {
|
|
90
|
+
basicFields.push({
|
|
91
|
+
label: "Ended",
|
|
92
|
+
value: formatTimestamp(endTimeMs),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
if (run.benchmark_id) {
|
|
96
|
+
basicFields.push({
|
|
97
|
+
label: "Benchmark ID",
|
|
98
|
+
value: _jsx(Text, { color: colors.idColor, children: run.benchmark_id }),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (run.score !== undefined && run.score !== null) {
|
|
102
|
+
basicFields.push({
|
|
103
|
+
label: "Score",
|
|
104
|
+
value: _jsx(Text, { color: colors.info, children: run.score }),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if (basicFields.length > 0) {
|
|
108
|
+
detailSections.push({
|
|
109
|
+
title: "Details",
|
|
110
|
+
icon: figures.squareSmallFilled,
|
|
111
|
+
color: colors.warning,
|
|
112
|
+
fields: basicFields,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
// Metadata section
|
|
116
|
+
if (run.metadata && Object.keys(run.metadata).length > 0) {
|
|
117
|
+
const metadataFields = Object.entries(run.metadata).map(([key, value]) => ({
|
|
118
|
+
label: key,
|
|
119
|
+
value: value,
|
|
120
|
+
}));
|
|
121
|
+
detailSections.push({
|
|
122
|
+
title: "Metadata",
|
|
123
|
+
icon: figures.identical,
|
|
124
|
+
color: colors.secondary,
|
|
125
|
+
fields: metadataFields,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
// Operations available for benchmark runs
|
|
129
|
+
const operations = [
|
|
130
|
+
{
|
|
131
|
+
key: "view-scenarios",
|
|
132
|
+
label: "View Scenario Runs",
|
|
133
|
+
color: colors.info,
|
|
134
|
+
icon: figures.arrowRight,
|
|
135
|
+
shortcut: "s",
|
|
136
|
+
},
|
|
137
|
+
];
|
|
138
|
+
// Handle operation selection
|
|
139
|
+
const handleOperation = async (operation, resource) => {
|
|
140
|
+
switch (operation) {
|
|
141
|
+
case "view-scenarios":
|
|
142
|
+
navigate("scenario-run-list", { benchmarkRunId: resource.id });
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
// Build detailed info lines for full details view
|
|
147
|
+
const buildDetailLines = (r) => {
|
|
148
|
+
const lines = [];
|
|
149
|
+
// Core Information
|
|
150
|
+
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Benchmark Run Details" }, "core-title"));
|
|
151
|
+
lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "ID: ", r.id] }, "core-id"));
|
|
152
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Name: ", r.name || "(none)"] }, "core-name"));
|
|
153
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Status: ", r.state] }, "core-status"));
|
|
154
|
+
if (r.benchmark_id) {
|
|
155
|
+
lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "Benchmark ID: ", r.benchmark_id] }, "core-benchmark"));
|
|
156
|
+
}
|
|
157
|
+
if (r.start_time_ms) {
|
|
158
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Started: ", new Date(r.start_time_ms).toLocaleString()] }, "core-created"));
|
|
159
|
+
}
|
|
160
|
+
const detailEndTimeMs = r.start_time_ms && r.duration_ms
|
|
161
|
+
? r.start_time_ms + r.duration_ms
|
|
162
|
+
: undefined;
|
|
163
|
+
if (detailEndTimeMs) {
|
|
164
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Ended: ", new Date(detailEndTimeMs).toLocaleString()] }, "core-ended"));
|
|
165
|
+
}
|
|
166
|
+
if (r.score !== undefined && r.score !== null) {
|
|
167
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Score: ", r.score] }, "core-score"));
|
|
168
|
+
}
|
|
169
|
+
lines.push(_jsx(Text, { children: " " }, "core-space"));
|
|
170
|
+
// Metadata
|
|
171
|
+
if (r.metadata && Object.keys(r.metadata).length > 0) {
|
|
172
|
+
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Metadata" }, "meta-title"));
|
|
173
|
+
Object.entries(r.metadata).forEach(([key, value], idx) => {
|
|
174
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", key, ": ", value] }, `meta-${idx}`));
|
|
175
|
+
});
|
|
176
|
+
lines.push(_jsx(Text, { children: " " }, "meta-space"));
|
|
177
|
+
}
|
|
178
|
+
// Raw JSON
|
|
179
|
+
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Raw JSON" }, "json-title"));
|
|
180
|
+
const jsonLines = JSON.stringify(r, null, 2).split("\n");
|
|
181
|
+
jsonLines.forEach((line, idx) => {
|
|
182
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", line] }, `json-${idx}`));
|
|
183
|
+
});
|
|
184
|
+
return lines;
|
|
185
|
+
};
|
|
186
|
+
// Check if run is still in progress for polling
|
|
187
|
+
const isRunning = run.state === "running";
|
|
188
|
+
return (_jsx(ResourceDetailPage, { resource: run, resourceType: "Benchmark Runs", getDisplayName: (r) => r.name || r.id, getId: (r) => r.id, getStatus: (r) => r.state, detailSections: detailSections, operations: operations, onOperation: handleOperation, onBack: goBack, buildDetailLines: buildDetailLines, pollResource: isRunning ? pollRun : undefined, breadcrumbPrefix: [{ label: "Home" }, { label: "Benchmarks" }] }));
|
|
189
|
+
}
|