@runloop/rl-cli 1.7.1 → 1.9.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 +19 -5
- package/dist/cli.js +0 -0
- package/dist/commands/blueprint/delete.js +21 -0
- package/dist/commands/blueprint/list.js +226 -174
- package/dist/commands/blueprint/prune.js +13 -28
- package/dist/commands/devbox/create.js +41 -0
- package/dist/commands/devbox/list.js +125 -109
- package/dist/commands/devbox/tunnel.js +4 -19
- package/dist/commands/gateway-config/create.js +44 -0
- package/dist/commands/gateway-config/delete.js +21 -0
- package/dist/commands/gateway-config/get.js +15 -0
- package/dist/commands/gateway-config/list.js +493 -0
- package/dist/commands/gateway-config/update.js +60 -0
- package/dist/commands/menu.js +2 -1
- package/dist/commands/secret/list.js +379 -4
- package/dist/commands/snapshot/list.js +11 -2
- package/dist/commands/snapshot/prune.js +265 -0
- package/dist/components/BenchmarkMenu.js +108 -0
- package/dist/components/DetailedInfoView.js +20 -0
- package/dist/components/DevboxActionsMenu.js +9 -61
- package/dist/components/DevboxCreatePage.js +531 -14
- package/dist/components/DevboxDetailPage.js +27 -22
- package/dist/components/GatewayConfigCreatePage.js +265 -0
- package/dist/components/LogsViewer.js +6 -40
- package/dist/components/MainMenu.js +63 -22
- package/dist/components/ResourceDetailPage.js +143 -160
- package/dist/components/ResourceListView.js +3 -33
- package/dist/components/ResourcePicker.js +220 -0
- package/dist/components/SecretCreatePage.js +183 -0
- package/dist/components/SettingsMenu.js +95 -0
- package/dist/components/StateHistory.js +1 -20
- package/dist/components/StatusBadge.js +80 -0
- package/dist/components/StreamingLogsViewer.js +8 -42
- package/dist/components/form/FormTextInput.js +4 -2
- package/dist/components/resourceDetailTypes.js +18 -0
- package/dist/hooks/useInputHandler.js +103 -0
- package/dist/router/Router.js +99 -2
- package/dist/screens/BenchmarkDetailScreen.js +163 -0
- package/dist/screens/BenchmarkJobCreateScreen.js +524 -0
- package/dist/screens/BenchmarkJobDetailScreen.js +614 -0
- package/dist/screens/BenchmarkJobListScreen.js +479 -0
- package/dist/screens/BenchmarkListScreen.js +266 -0
- package/dist/screens/BenchmarkMenuScreen.js +29 -0
- package/dist/screens/BenchmarkRunDetailScreen.js +425 -0
- package/dist/screens/BenchmarkRunListScreen.js +275 -0
- package/dist/screens/BlueprintDetailScreen.js +5 -1
- package/dist/screens/DevboxCreateScreen.js +2 -2
- package/dist/screens/GatewayConfigDetailScreen.js +236 -0
- package/dist/screens/GatewayConfigListScreen.js +7 -0
- package/dist/screens/MenuScreen.js +5 -2
- package/dist/screens/ScenarioRunDetailScreen.js +226 -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 +26 -0
- package/dist/screens/SnapshotDetailScreen.js +6 -0
- package/dist/services/agentService.js +42 -0
- package/dist/services/benchmarkJobService.js +122 -0
- package/dist/services/benchmarkService.js +120 -0
- package/dist/services/gatewayConfigService.js +114 -0
- package/dist/services/scenarioService.js +34 -0
- package/dist/store/benchmarkJobStore.js +66 -0
- package/dist/store/benchmarkStore.js +183 -0
- package/dist/store/betaFeatureStore.js +47 -0
- package/dist/store/gatewayConfigStore.js +83 -0
- package/dist/store/index.js +1 -0
- package/dist/utils/browser.js +22 -0
- package/dist/utils/clipboard.js +41 -0
- package/dist/utils/commands.js +80 -0
- package/dist/utils/config.js +8 -0
- package/dist/utils/time.js +121 -0
- package/package.json +42 -43
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* BenchmarkRunListScreen - List view for benchmark runs
|
|
4
|
+
*/
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { Box, Text, useInput, useApp } from "ink";
|
|
7
|
+
import figures from "figures";
|
|
8
|
+
import { useNavigation } from "../store/navigationStore.js";
|
|
9
|
+
import { SpinnerComponent } from "../components/Spinner.js";
|
|
10
|
+
import { ErrorMessage } from "../components/ErrorMessage.js";
|
|
11
|
+
import { Breadcrumb } from "../components/Breadcrumb.js";
|
|
12
|
+
import { NavigationTips } from "../components/NavigationTips.js";
|
|
13
|
+
import { Table, createTextColumn, createComponentColumn, } from "../components/Table.js";
|
|
14
|
+
import { ActionsPopup } from "../components/ActionsPopup.js";
|
|
15
|
+
import { formatTimeAgo } from "../components/ResourceListView.js";
|
|
16
|
+
import { SearchBar } from "../components/SearchBar.js";
|
|
17
|
+
import { getStatusDisplay } from "../components/StatusBadge.js";
|
|
18
|
+
import { colors } from "../utils/theme.js";
|
|
19
|
+
import { useViewportHeight } from "../hooks/useViewportHeight.js";
|
|
20
|
+
import { useExitOnCtrlC } from "../hooks/useExitOnCtrlC.js";
|
|
21
|
+
import { useCursorPagination } from "../hooks/useCursorPagination.js";
|
|
22
|
+
import { useListSearch } from "../hooks/useListSearch.js";
|
|
23
|
+
import { listBenchmarkRuns } from "../services/benchmarkService.js";
|
|
24
|
+
export function BenchmarkRunListScreen() {
|
|
25
|
+
const { exit: inkExit } = useApp();
|
|
26
|
+
const { navigate, goBack } = useNavigation();
|
|
27
|
+
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
28
|
+
const [showPopup, setShowPopup] = React.useState(false);
|
|
29
|
+
const [selectedOperation, setSelectedOperation] = React.useState(0);
|
|
30
|
+
// Search state
|
|
31
|
+
const search = useListSearch({
|
|
32
|
+
onSearchSubmit: () => setSelectedIndex(0),
|
|
33
|
+
onSearchClear: () => setSelectedIndex(0),
|
|
34
|
+
});
|
|
35
|
+
// Calculate overhead for viewport height
|
|
36
|
+
const overhead = 13 + search.getSearchOverhead();
|
|
37
|
+
const { viewportHeight, terminalWidth } = useViewportHeight({
|
|
38
|
+
overhead,
|
|
39
|
+
minHeight: 5,
|
|
40
|
+
});
|
|
41
|
+
const PAGE_SIZE = viewportHeight;
|
|
42
|
+
// Column widths
|
|
43
|
+
const fixedWidth = 6;
|
|
44
|
+
const idWidth = 25;
|
|
45
|
+
const statusWidth = 12;
|
|
46
|
+
const timeWidth = 18;
|
|
47
|
+
const baseWidth = fixedWidth + idWidth + statusWidth + timeWidth;
|
|
48
|
+
const remainingWidth = terminalWidth - baseWidth;
|
|
49
|
+
const nameWidth = Math.min(80, Math.max(15, remainingWidth));
|
|
50
|
+
// Fetch function for pagination hook
|
|
51
|
+
const fetchPage = React.useCallback(async (params) => {
|
|
52
|
+
const result = await listBenchmarkRuns({
|
|
53
|
+
limit: params.limit,
|
|
54
|
+
startingAfter: params.startingAt,
|
|
55
|
+
});
|
|
56
|
+
return {
|
|
57
|
+
items: result.benchmarkRuns,
|
|
58
|
+
hasMore: result.hasMore,
|
|
59
|
+
totalCount: result.totalCount,
|
|
60
|
+
};
|
|
61
|
+
}, []);
|
|
62
|
+
// Use the shared pagination hook
|
|
63
|
+
const { items: benchmarkRuns, loading, navigating, error, currentPage, hasMore, hasPrev, totalCount, nextPage, prevPage, refresh, } = useCursorPagination({
|
|
64
|
+
fetchPage,
|
|
65
|
+
pageSize: PAGE_SIZE,
|
|
66
|
+
getItemId: (run) => run.id,
|
|
67
|
+
pollInterval: 5000,
|
|
68
|
+
pollingEnabled: !showPopup && !search.searchMode,
|
|
69
|
+
deps: [PAGE_SIZE],
|
|
70
|
+
});
|
|
71
|
+
// Operations for benchmark runs
|
|
72
|
+
const operations = React.useMemo(() => [
|
|
73
|
+
{
|
|
74
|
+
key: "view_details",
|
|
75
|
+
label: "View Details",
|
|
76
|
+
color: colors.primary,
|
|
77
|
+
icon: figures.pointer,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
key: "view_scenarios",
|
|
81
|
+
label: "View Scenario Runs",
|
|
82
|
+
color: colors.info,
|
|
83
|
+
icon: figures.arrowRight,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
key: "create_job",
|
|
87
|
+
label: "Create Job",
|
|
88
|
+
color: colors.success,
|
|
89
|
+
icon: figures.play,
|
|
90
|
+
},
|
|
91
|
+
], []);
|
|
92
|
+
// Build columns
|
|
93
|
+
const columns = React.useMemo(() => [
|
|
94
|
+
createTextColumn("id", "ID", (run) => run.id, {
|
|
95
|
+
width: idWidth + 1,
|
|
96
|
+
color: colors.idColor,
|
|
97
|
+
dimColor: false,
|
|
98
|
+
bold: false,
|
|
99
|
+
}),
|
|
100
|
+
createTextColumn("name", "Name", (run) => run.name || "", {
|
|
101
|
+
width: nameWidth,
|
|
102
|
+
}),
|
|
103
|
+
createComponentColumn("status", "Status", (run, _index, isSelected) => {
|
|
104
|
+
const statusDisplay = getStatusDisplay(run.state);
|
|
105
|
+
const text = statusDisplay.text
|
|
106
|
+
.slice(0, statusWidth)
|
|
107
|
+
.padEnd(statusWidth, " ");
|
|
108
|
+
return (_jsx(Text, { color: isSelected ? colors.text : statusDisplay.color, bold: isSelected, inverse: isSelected, children: text }));
|
|
109
|
+
}, { width: statusWidth }),
|
|
110
|
+
createTextColumn("created", "Created", (run) => run.start_time_ms ? formatTimeAgo(run.start_time_ms) : "", {
|
|
111
|
+
width: timeWidth,
|
|
112
|
+
color: colors.textDim,
|
|
113
|
+
dimColor: false,
|
|
114
|
+
bold: false,
|
|
115
|
+
}),
|
|
116
|
+
], [idWidth, nameWidth, statusWidth, timeWidth]);
|
|
117
|
+
// Handle Ctrl+C to exit
|
|
118
|
+
useExitOnCtrlC();
|
|
119
|
+
// Ensure selected index is within bounds
|
|
120
|
+
React.useEffect(() => {
|
|
121
|
+
if (benchmarkRuns.length > 0 && selectedIndex >= benchmarkRuns.length) {
|
|
122
|
+
setSelectedIndex(Math.max(0, benchmarkRuns.length - 1));
|
|
123
|
+
}
|
|
124
|
+
}, [benchmarkRuns.length, selectedIndex]);
|
|
125
|
+
const selectedRun = benchmarkRuns[selectedIndex];
|
|
126
|
+
// Calculate pagination info for display
|
|
127
|
+
const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE));
|
|
128
|
+
const startIndex = currentPage * PAGE_SIZE;
|
|
129
|
+
const endIndex = startIndex + benchmarkRuns.length;
|
|
130
|
+
useInput((input, key) => {
|
|
131
|
+
// Handle search mode input
|
|
132
|
+
if (search.searchMode) {
|
|
133
|
+
if (key.escape) {
|
|
134
|
+
search.cancelSearch();
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// Handle popup navigation
|
|
139
|
+
if (showPopup) {
|
|
140
|
+
if (key.upArrow && selectedOperation > 0) {
|
|
141
|
+
setSelectedOperation(selectedOperation - 1);
|
|
142
|
+
}
|
|
143
|
+
else if (key.downArrow && selectedOperation < operations.length - 1) {
|
|
144
|
+
setSelectedOperation(selectedOperation + 1);
|
|
145
|
+
}
|
|
146
|
+
else if (key.return) {
|
|
147
|
+
setShowPopup(false);
|
|
148
|
+
const operationKey = operations[selectedOperation].key;
|
|
149
|
+
if (operationKey === "view_details") {
|
|
150
|
+
navigate("benchmark-run-detail", {
|
|
151
|
+
benchmarkRunId: selectedRun.id,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
else if (operationKey === "view_scenarios") {
|
|
155
|
+
navigate("scenario-run-list", {
|
|
156
|
+
benchmarkRunId: selectedRun.id,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
else if (operationKey === "create_job") {
|
|
160
|
+
navigate("benchmark-job-create");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
else if (input === "v" && selectedRun) {
|
|
164
|
+
setShowPopup(false);
|
|
165
|
+
navigate("benchmark-run-detail", {
|
|
166
|
+
benchmarkRunId: selectedRun.id,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
else if (input === "s" && selectedRun) {
|
|
170
|
+
setShowPopup(false);
|
|
171
|
+
navigate("scenario-run-list", {
|
|
172
|
+
benchmarkRunId: selectedRun.id,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
else if (input === "j") {
|
|
176
|
+
setShowPopup(false);
|
|
177
|
+
navigate("benchmark-job-create");
|
|
178
|
+
}
|
|
179
|
+
else if (key.escape || input === "q") {
|
|
180
|
+
setShowPopup(false);
|
|
181
|
+
setSelectedOperation(0);
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const pageRuns = benchmarkRuns.length;
|
|
186
|
+
// Handle list view navigation
|
|
187
|
+
if (key.upArrow && selectedIndex > 0) {
|
|
188
|
+
setSelectedIndex(selectedIndex - 1);
|
|
189
|
+
}
|
|
190
|
+
else if (key.downArrow && selectedIndex < pageRuns - 1) {
|
|
191
|
+
setSelectedIndex(selectedIndex + 1);
|
|
192
|
+
}
|
|
193
|
+
else if ((input === "n" || key.rightArrow) &&
|
|
194
|
+
!loading &&
|
|
195
|
+
!navigating &&
|
|
196
|
+
hasMore) {
|
|
197
|
+
nextPage();
|
|
198
|
+
setSelectedIndex(0);
|
|
199
|
+
}
|
|
200
|
+
else if ((input === "p" || key.leftArrow) &&
|
|
201
|
+
!loading &&
|
|
202
|
+
!navigating &&
|
|
203
|
+
hasPrev) {
|
|
204
|
+
prevPage();
|
|
205
|
+
setSelectedIndex(0);
|
|
206
|
+
}
|
|
207
|
+
else if (key.return && selectedRun) {
|
|
208
|
+
navigate("benchmark-run-detail", {
|
|
209
|
+
benchmarkRunId: selectedRun.id,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
else if (input === "a" && selectedRun) {
|
|
213
|
+
setShowPopup(true);
|
|
214
|
+
setSelectedOperation(0);
|
|
215
|
+
}
|
|
216
|
+
else if (input === "j") {
|
|
217
|
+
// Quick shortcut to create a new job
|
|
218
|
+
navigate("benchmark-job-create");
|
|
219
|
+
}
|
|
220
|
+
else if (input === "/") {
|
|
221
|
+
search.enterSearchMode();
|
|
222
|
+
}
|
|
223
|
+
else if (key.escape) {
|
|
224
|
+
if (search.handleEscape()) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
goBack();
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
// Loading state
|
|
231
|
+
if (loading && benchmarkRuns.length === 0) {
|
|
232
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
233
|
+
{ label: "Home" },
|
|
234
|
+
{ label: "Benchmarks" },
|
|
235
|
+
{ label: "Benchmark Runs", active: true },
|
|
236
|
+
] }), _jsx(SpinnerComponent, { message: "Loading benchmark runs..." })] }));
|
|
237
|
+
}
|
|
238
|
+
// Error state
|
|
239
|
+
if (error) {
|
|
240
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
241
|
+
{ label: "Home" },
|
|
242
|
+
{ label: "Benchmarks" },
|
|
243
|
+
{ label: "Benchmark Runs", active: true },
|
|
244
|
+
] }), _jsx(ErrorMessage, { message: "Failed to list benchmark runs", error: error })] }));
|
|
245
|
+
}
|
|
246
|
+
// Main list view
|
|
247
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
248
|
+
{ label: "Home" },
|
|
249
|
+
{ label: "Benchmarks" },
|
|
250
|
+
{ label: "Benchmark Runs", active: true },
|
|
251
|
+
] }), _jsx(SearchBar, { searchMode: search.searchMode, searchQuery: search.searchQuery, submittedSearchQuery: search.submittedSearchQuery, resultCount: totalCount, onSearchChange: search.setSearchQuery, onSearchSubmit: search.submitSearch, placeholder: "Search benchmark runs..." }), !showPopup && (_jsx(Table, { data: benchmarkRuns, keyExtractor: (run) => run.id, selectedIndex: selectedIndex, title: `benchmark_runs[${totalCount}]`, columns: columns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No benchmark runs found"] }) })), !showPopup && (_jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", totalCount] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), navigating ? (_jsxs(Text, { color: colors.warning, children: [figures.pointer, " Loading page ", currentPage + 1, "..."] })) : (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Page ", currentPage + 1, " of ", totalPages] }))] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Showing ", startIndex + 1, "-", endIndex, " of ", totalCount] })] })), showPopup && selectedRun && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedRun, operations: operations.map((op) => ({
|
|
252
|
+
key: op.key,
|
|
253
|
+
label: op.label,
|
|
254
|
+
color: op.color,
|
|
255
|
+
icon: op.icon,
|
|
256
|
+
shortcut: op.key === "view_details"
|
|
257
|
+
? "v"
|
|
258
|
+
: op.key === "view_scenarios"
|
|
259
|
+
? "s"
|
|
260
|
+
: op.key === "create_job"
|
|
261
|
+
? "j"
|
|
262
|
+
: "",
|
|
263
|
+
})), selectedOperation: selectedOperation, onClose: () => setShowPopup(false) }) })), _jsx(NavigationTips, { showArrows: true, tips: [
|
|
264
|
+
{
|
|
265
|
+
icon: `${figures.arrowLeft}${figures.arrowRight}`,
|
|
266
|
+
label: "Page",
|
|
267
|
+
condition: hasMore || hasPrev,
|
|
268
|
+
},
|
|
269
|
+
{ key: "Enter", label: "Details" },
|
|
270
|
+
{ key: "j", label: "New Job" },
|
|
271
|
+
{ key: "a", label: "Actions" },
|
|
272
|
+
{ key: "/", label: "Search" },
|
|
273
|
+
{ key: "Esc", label: "Back" },
|
|
274
|
+
] })] }));
|
|
275
|
+
}
|
|
@@ -351,5 +351,9 @@ export function BlueprintDetailScreen({ blueprintId, }) {
|
|
|
351
351
|
});
|
|
352
352
|
return lines;
|
|
353
353
|
};
|
|
354
|
-
return (_jsx(ResourceDetailPage, { resource: blueprint, resourceType: "Blueprints", getDisplayName: (bp) => bp.name || bp.id, getId: (bp) => bp.id, getStatus: (bp) => bp.status, detailSections: detailSections, operations: operations, onOperation: handleOperation, onBack: goBack, buildDetailLines: buildDetailLines, pollResource: blueprint.status === "
|
|
354
|
+
return (_jsx(ResourceDetailPage, { resource: blueprint, resourceType: "Blueprints", getDisplayName: (bp) => bp.name || bp.id, getId: (bp) => bp.id, getStatus: (bp) => bp.status, detailSections: detailSections, operations: operations, onOperation: handleOperation, onBack: goBack, buildDetailLines: buildDetailLines, pollResource: blueprint.status === "queued" ||
|
|
355
|
+
blueprint.status === "provisioning" ||
|
|
356
|
+
blueprint.status === "building"
|
|
357
|
+
? pollBlueprint
|
|
358
|
+
: undefined }));
|
|
355
359
|
}
|
|
@@ -2,10 +2,10 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { useNavigation } from "../store/navigationStore.js";
|
|
3
3
|
import { DevboxCreatePage } from "../components/DevboxCreatePage.js";
|
|
4
4
|
export function DevboxCreateScreen() {
|
|
5
|
-
const { goBack, navigate } = useNavigation();
|
|
5
|
+
const { goBack, navigate, params } = useNavigation();
|
|
6
6
|
const handleCreate = (devbox) => {
|
|
7
7
|
// After creation, navigate to the devbox detail page
|
|
8
8
|
navigate("devbox-detail", { devboxId: devbox.id });
|
|
9
9
|
};
|
|
10
|
-
return _jsx(DevboxCreatePage, { onBack: goBack, onCreate: handleCreate });
|
|
10
|
+
return (_jsx(DevboxCreatePage, { onBack: goBack, onCreate: handleCreate, initialBlueprintId: params.blueprintId, initialSnapshotId: params.snapshotId }));
|
|
11
11
|
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* GatewayConfigDetailScreen - Detail page for gateway configs
|
|
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 { useGatewayConfigStore, } from "../store/gatewayConfigStore.js";
|
|
11
|
+
import { ResourceDetailPage, formatTimestamp, } from "../components/ResourceDetailPage.js";
|
|
12
|
+
import { getGatewayConfig, deleteGatewayConfig, } from "../services/gatewayConfigService.js";
|
|
13
|
+
import { SpinnerComponent } from "../components/Spinner.js";
|
|
14
|
+
import { ErrorMessage } from "../components/ErrorMessage.js";
|
|
15
|
+
import { Breadcrumb } from "../components/Breadcrumb.js";
|
|
16
|
+
import { ConfirmationPrompt } from "../components/ConfirmationPrompt.js";
|
|
17
|
+
import { GatewayConfigCreatePage } from "../components/GatewayConfigCreatePage.js";
|
|
18
|
+
import { colors } from "../utils/theme.js";
|
|
19
|
+
/**
|
|
20
|
+
* Get a display label for the auth mechanism type
|
|
21
|
+
*/
|
|
22
|
+
function getAuthTypeLabel(authMechanism) {
|
|
23
|
+
if (authMechanism.type === "bearer") {
|
|
24
|
+
return "Bearer Token";
|
|
25
|
+
}
|
|
26
|
+
if (authMechanism.type === "header") {
|
|
27
|
+
return authMechanism.key ? `Header: ${authMechanism.key}` : "Header";
|
|
28
|
+
}
|
|
29
|
+
return authMechanism.type;
|
|
30
|
+
}
|
|
31
|
+
export function GatewayConfigDetailScreen({ gatewayConfigId, }) {
|
|
32
|
+
const { goBack } = useNavigation();
|
|
33
|
+
const gatewayConfigs = useGatewayConfigStore((state) => state.gatewayConfigs);
|
|
34
|
+
const [loading, setLoading] = React.useState(false);
|
|
35
|
+
const [error, setError] = React.useState(null);
|
|
36
|
+
const [fetchedConfig, setFetchedConfig] = React.useState(null);
|
|
37
|
+
const [deleting, setDeleting] = React.useState(false);
|
|
38
|
+
const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false);
|
|
39
|
+
const [showEditForm, setShowEditForm] = React.useState(false);
|
|
40
|
+
// Find config in store first
|
|
41
|
+
const configFromStore = gatewayConfigs.find((c) => c.id === gatewayConfigId);
|
|
42
|
+
// Fetch config from API if not in store or missing full details
|
|
43
|
+
React.useEffect(() => {
|
|
44
|
+
if (gatewayConfigId && !loading && !fetchedConfig) {
|
|
45
|
+
// Always fetch full details since store may only have basic info
|
|
46
|
+
setLoading(true);
|
|
47
|
+
setError(null);
|
|
48
|
+
getGatewayConfig(gatewayConfigId)
|
|
49
|
+
.then((config) => {
|
|
50
|
+
setFetchedConfig(config);
|
|
51
|
+
setLoading(false);
|
|
52
|
+
})
|
|
53
|
+
.catch((err) => {
|
|
54
|
+
setError(err);
|
|
55
|
+
setLoading(false);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}, [gatewayConfigId, loading, fetchedConfig]);
|
|
59
|
+
// Use fetched config for full details, fall back to store for basic display
|
|
60
|
+
const config = fetchedConfig || configFromStore;
|
|
61
|
+
// Show loading state while fetching or before fetch starts
|
|
62
|
+
if (!config && gatewayConfigId && !error) {
|
|
63
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
64
|
+
{ label: "Gateway Configs" },
|
|
65
|
+
{ label: "Loading...", active: true },
|
|
66
|
+
] }), _jsx(SpinnerComponent, { message: "Loading gateway config details..." })] }));
|
|
67
|
+
}
|
|
68
|
+
// Show error state if fetch failed
|
|
69
|
+
if (error && !config) {
|
|
70
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
71
|
+
{ label: "Gateway Configs" },
|
|
72
|
+
{ label: "Error", active: true },
|
|
73
|
+
] }), _jsx(ErrorMessage, { message: "Failed to load gateway config details", error: error })] }));
|
|
74
|
+
}
|
|
75
|
+
// Show error if no config found
|
|
76
|
+
if (!config) {
|
|
77
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
78
|
+
{ label: "Gateway Configs" },
|
|
79
|
+
{ label: "Not Found", active: true },
|
|
80
|
+
] }), _jsx(ErrorMessage, { message: `Gateway config ${gatewayConfigId || "unknown"} not found`, error: new Error("Gateway config not found") })] }));
|
|
81
|
+
}
|
|
82
|
+
// Build detail sections
|
|
83
|
+
const detailSections = [];
|
|
84
|
+
// Basic details section
|
|
85
|
+
const basicFields = [];
|
|
86
|
+
if (config.description) {
|
|
87
|
+
basicFields.push({
|
|
88
|
+
label: "Description",
|
|
89
|
+
value: config.description,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
basicFields.push({
|
|
93
|
+
label: "Endpoint",
|
|
94
|
+
value: config.endpoint,
|
|
95
|
+
});
|
|
96
|
+
if (config.create_time_ms) {
|
|
97
|
+
basicFields.push({
|
|
98
|
+
label: "Created",
|
|
99
|
+
value: formatTimestamp(config.create_time_ms),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (config.account_id) {
|
|
103
|
+
basicFields.push({
|
|
104
|
+
label: "Account ID",
|
|
105
|
+
value: config.account_id,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (basicFields.length > 0) {
|
|
109
|
+
detailSections.push({
|
|
110
|
+
title: "Details",
|
|
111
|
+
icon: figures.squareSmallFilled,
|
|
112
|
+
color: colors.warning,
|
|
113
|
+
fields: basicFields,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
// Auth mechanism section
|
|
117
|
+
const authFields = [];
|
|
118
|
+
authFields.push({
|
|
119
|
+
label: "Auth Type",
|
|
120
|
+
value: (_jsx(Text, { color: colors.info, bold: true, children: getAuthTypeLabel(config.auth_mechanism) })),
|
|
121
|
+
});
|
|
122
|
+
if (config.auth_mechanism.type === "header" && config.auth_mechanism.key) {
|
|
123
|
+
authFields.push({
|
|
124
|
+
label: "Header Key",
|
|
125
|
+
value: config.auth_mechanism.key,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
detailSections.push({
|
|
129
|
+
title: "Authentication",
|
|
130
|
+
icon: figures.arrowRight,
|
|
131
|
+
color: colors.info,
|
|
132
|
+
fields: authFields,
|
|
133
|
+
});
|
|
134
|
+
// Operations available for gateway configs
|
|
135
|
+
const operations = [
|
|
136
|
+
{
|
|
137
|
+
key: "edit",
|
|
138
|
+
label: "Edit Gateway Config",
|
|
139
|
+
color: colors.warning,
|
|
140
|
+
icon: figures.pointer,
|
|
141
|
+
shortcut: "e",
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
key: "delete",
|
|
145
|
+
label: "Delete Gateway Config",
|
|
146
|
+
color: colors.error,
|
|
147
|
+
icon: figures.cross,
|
|
148
|
+
shortcut: "d",
|
|
149
|
+
},
|
|
150
|
+
];
|
|
151
|
+
// Handle operation selection
|
|
152
|
+
const handleOperation = async (operation, _resource) => {
|
|
153
|
+
switch (operation) {
|
|
154
|
+
case "edit":
|
|
155
|
+
setShowEditForm(true);
|
|
156
|
+
break;
|
|
157
|
+
case "delete":
|
|
158
|
+
// Show confirmation dialog
|
|
159
|
+
setShowDeleteConfirm(true);
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
// Execute delete after confirmation
|
|
164
|
+
const executeDelete = async () => {
|
|
165
|
+
if (!config)
|
|
166
|
+
return;
|
|
167
|
+
setShowDeleteConfirm(false);
|
|
168
|
+
setDeleting(true);
|
|
169
|
+
try {
|
|
170
|
+
await deleteGatewayConfig(config.id);
|
|
171
|
+
goBack();
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
setError(err);
|
|
175
|
+
setDeleting(false);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
// Build detailed info lines for full details view
|
|
179
|
+
const buildDetailLines = (gc) => {
|
|
180
|
+
const lines = [];
|
|
181
|
+
// Core Information
|
|
182
|
+
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Gateway Config Details" }, "core-title"));
|
|
183
|
+
lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "ID: ", gc.id] }, "core-id"));
|
|
184
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Name: ", gc.name] }, "core-name"));
|
|
185
|
+
if (gc.description) {
|
|
186
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Description: ", gc.description] }, "core-desc"));
|
|
187
|
+
}
|
|
188
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Endpoint: ", gc.endpoint] }, "core-endpoint"));
|
|
189
|
+
if (gc.create_time_ms) {
|
|
190
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Created: ", new Date(gc.create_time_ms).toLocaleString()] }, "core-created"));
|
|
191
|
+
}
|
|
192
|
+
if (gc.account_id) {
|
|
193
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Account ID: ", gc.account_id] }, "core-account"));
|
|
194
|
+
}
|
|
195
|
+
lines.push(_jsx(Text, { children: " " }, "core-space"));
|
|
196
|
+
// Auth Mechanism
|
|
197
|
+
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Authentication" }, "auth-title"));
|
|
198
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Type: ", getAuthTypeLabel(gc.auth_mechanism)] }, "auth-type"));
|
|
199
|
+
if (gc.auth_mechanism.type === "header" && gc.auth_mechanism.key) {
|
|
200
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Header Key: ", gc.auth_mechanism.key] }, "auth-key"));
|
|
201
|
+
}
|
|
202
|
+
lines.push(_jsx(Text, { children: " " }, "auth-space"));
|
|
203
|
+
// Raw JSON
|
|
204
|
+
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Raw JSON" }, "json-title"));
|
|
205
|
+
const jsonLines = JSON.stringify(gc, null, 2).split("\n");
|
|
206
|
+
jsonLines.forEach((line, idx) => {
|
|
207
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", line] }, `json-${idx}`));
|
|
208
|
+
});
|
|
209
|
+
return lines;
|
|
210
|
+
};
|
|
211
|
+
// Show edit form
|
|
212
|
+
if (showEditForm && config) {
|
|
213
|
+
return (_jsx(GatewayConfigCreatePage, { onBack: () => setShowEditForm(false), onCreate: (updatedConfig) => {
|
|
214
|
+
// Update the fetched config with the new data
|
|
215
|
+
setFetchedConfig(updatedConfig);
|
|
216
|
+
setShowEditForm(false);
|
|
217
|
+
}, initialConfig: config }));
|
|
218
|
+
}
|
|
219
|
+
// Show delete confirmation
|
|
220
|
+
if (showDeleteConfirm && config) {
|
|
221
|
+
return (_jsx(ConfirmationPrompt, { title: "Delete Gateway Config", message: `Are you sure you want to delete "${config.name || config.id}"?`, details: "This action cannot be undone. Any devboxes using this gateway config will no longer have access to it.", breadcrumbItems: [
|
|
222
|
+
{ label: "Gateway Configs" },
|
|
223
|
+
{ label: config.name || config.id },
|
|
224
|
+
{ label: "Delete", active: true },
|
|
225
|
+
], onConfirm: executeDelete, onCancel: () => setShowDeleteConfirm(false) }));
|
|
226
|
+
}
|
|
227
|
+
// Show deleting state
|
|
228
|
+
if (deleting) {
|
|
229
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
230
|
+
{ label: "Gateway Configs" },
|
|
231
|
+
{ label: config.name || config.id },
|
|
232
|
+
{ label: "Deleting...", active: true },
|
|
233
|
+
] }), _jsx(SpinnerComponent, { message: "Deleting gateway config..." })] }));
|
|
234
|
+
}
|
|
235
|
+
return (_jsx(ResourceDetailPage, { resource: config, resourceType: "Gateway Configs", getDisplayName: (gc) => gc.name || gc.id, getId: (gc) => gc.id, getStatus: () => "active", detailSections: detailSections, operations: operations, onOperation: handleOperation, onBack: goBack, buildDetailLines: buildDetailLines }));
|
|
236
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useNavigation } from "../store/navigationStore.js";
|
|
3
|
+
import { ListGatewayConfigsUI } from "../commands/gateway-config/list.js";
|
|
4
|
+
export function GatewayConfigListScreen() {
|
|
5
|
+
const { goBack } = useNavigation();
|
|
6
|
+
return _jsx(ListGatewayConfigsUI, { onBack: goBack });
|
|
7
|
+
}
|
|
@@ -14,12 +14,15 @@ export function MenuScreen() {
|
|
|
14
14
|
case "snapshots":
|
|
15
15
|
navigate("snapshot-list");
|
|
16
16
|
break;
|
|
17
|
-
case "
|
|
18
|
-
navigate("
|
|
17
|
+
case "settings":
|
|
18
|
+
navigate("settings-menu");
|
|
19
19
|
break;
|
|
20
20
|
case "objects":
|
|
21
21
|
navigate("object-list");
|
|
22
22
|
break;
|
|
23
|
+
case "benchmarks":
|
|
24
|
+
navigate("benchmark-menu");
|
|
25
|
+
break;
|
|
23
26
|
default:
|
|
24
27
|
// Fallback for any other screen names
|
|
25
28
|
navigate(key);
|