@runloop/rl-cli 1.2.0 → 1.4.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 +29 -8
- package/dist/commands/blueprint/from-dockerfile.js +182 -0
- package/dist/commands/blueprint/list.js +97 -28
- package/dist/commands/blueprint/prune.js +7 -19
- package/dist/commands/devbox/create.js +3 -0
- package/dist/commands/devbox/list.js +44 -65
- package/dist/commands/menu.js +2 -1
- package/dist/commands/network-policy/create.js +27 -0
- package/dist/commands/network-policy/delete.js +21 -0
- package/dist/commands/network-policy/get.js +15 -0
- package/dist/commands/network-policy/list.js +494 -0
- package/dist/commands/object/list.js +516 -24
- package/dist/commands/snapshot/list.js +90 -29
- package/dist/components/Banner.js +109 -8
- package/dist/components/ConfirmationPrompt.js +45 -0
- package/dist/components/DevboxActionsMenu.js +42 -6
- package/dist/components/DevboxCard.js +1 -1
- package/dist/components/DevboxCreatePage.js +174 -168
- package/dist/components/DevboxDetailPage.js +218 -272
- package/dist/components/LogsViewer.js +8 -1
- package/dist/components/MainMenu.js +35 -4
- package/dist/components/NavigationTips.js +24 -0
- package/dist/components/NetworkPolicyCreatePage.js +263 -0
- package/dist/components/OperationsMenu.js +9 -1
- package/dist/components/ResourceActionsMenu.js +5 -1
- package/dist/components/ResourceDetailPage.js +204 -0
- package/dist/components/ResourceListView.js +19 -2
- package/dist/components/StatusBadge.js +2 -2
- package/dist/components/Table.js +6 -8
- package/dist/components/form/FormActionButton.js +7 -0
- package/dist/components/form/FormField.js +7 -0
- package/dist/components/form/FormListManager.js +112 -0
- package/dist/components/form/FormSelect.js +34 -0
- package/dist/components/form/FormTextInput.js +8 -0
- package/dist/components/form/index.js +8 -0
- package/dist/hooks/useViewportHeight.js +38 -20
- package/dist/router/Router.js +23 -1
- package/dist/screens/BlueprintDetailScreen.js +355 -0
- package/dist/screens/DevboxDetailScreen.js +4 -4
- package/dist/screens/MenuScreen.js +6 -0
- package/dist/screens/NetworkPolicyCreateScreen.js +7 -0
- package/dist/screens/NetworkPolicyDetailScreen.js +247 -0
- package/dist/screens/NetworkPolicyListScreen.js +7 -0
- package/dist/screens/ObjectDetailScreen.js +377 -0
- package/dist/screens/ObjectListScreen.js +7 -0
- package/dist/screens/SnapshotDetailScreen.js +208 -0
- package/dist/services/blueprintService.js +30 -11
- package/dist/services/networkPolicyService.js +108 -0
- package/dist/services/objectService.js +101 -0
- package/dist/services/snapshotService.js +39 -3
- package/dist/store/blueprintStore.js +4 -10
- package/dist/store/index.js +1 -0
- package/dist/store/networkPolicyStore.js +83 -0
- package/dist/store/objectStore.js +92 -0
- package/dist/store/snapshotStore.js +4 -8
- package/dist/utils/commands.js +65 -0
- package/package.json +2 -2
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* BlueprintDetailScreen - Detail page for blueprints
|
|
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 { useBlueprintStore } from "../store/blueprintStore.js";
|
|
11
|
+
import { ResourceDetailPage, formatTimestamp, } from "../components/ResourceDetailPage.js";
|
|
12
|
+
import { getBlueprint } from "../services/blueprintService.js";
|
|
13
|
+
import { getClient } from "../utils/client.js";
|
|
14
|
+
import { SpinnerComponent } from "../components/Spinner.js";
|
|
15
|
+
import { ErrorMessage } from "../components/ErrorMessage.js";
|
|
16
|
+
import { Breadcrumb } from "../components/Breadcrumb.js";
|
|
17
|
+
import { ConfirmationPrompt } from "../components/ConfirmationPrompt.js";
|
|
18
|
+
import { colors } from "../utils/theme.js";
|
|
19
|
+
export function BlueprintDetailScreen({ blueprintId, }) {
|
|
20
|
+
const { goBack, navigate } = useNavigation();
|
|
21
|
+
const blueprints = useBlueprintStore((state) => state.blueprints);
|
|
22
|
+
const [loading, setLoading] = React.useState(false);
|
|
23
|
+
const [error, setError] = React.useState(null);
|
|
24
|
+
const [fetchedBlueprint, setFetchedBlueprint] = React.useState(null);
|
|
25
|
+
const [deleting, setDeleting] = React.useState(false);
|
|
26
|
+
const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false);
|
|
27
|
+
// Find blueprint in store first
|
|
28
|
+
const blueprintFromStore = blueprints.find((b) => b.id === blueprintId);
|
|
29
|
+
// Polling function - must be defined before any early returns (Rules of Hooks)
|
|
30
|
+
const pollBlueprint = React.useCallback(async () => {
|
|
31
|
+
if (!blueprintId)
|
|
32
|
+
return null;
|
|
33
|
+
return getBlueprint(blueprintId);
|
|
34
|
+
}, [blueprintId]);
|
|
35
|
+
// Fetch blueprint from API if not in store or missing full details
|
|
36
|
+
React.useEffect(() => {
|
|
37
|
+
if (blueprintId && !loading && !fetchedBlueprint) {
|
|
38
|
+
// Always fetch full details since store may only have basic info
|
|
39
|
+
setLoading(true);
|
|
40
|
+
setError(null);
|
|
41
|
+
getBlueprint(blueprintId)
|
|
42
|
+
.then((blueprint) => {
|
|
43
|
+
setFetchedBlueprint(blueprint);
|
|
44
|
+
setLoading(false);
|
|
45
|
+
})
|
|
46
|
+
.catch((err) => {
|
|
47
|
+
setError(err);
|
|
48
|
+
setLoading(false);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}, [blueprintId, loading, fetchedBlueprint]);
|
|
52
|
+
// Use fetched blueprint for full details, fall back to store for basic display
|
|
53
|
+
const blueprint = fetchedBlueprint || blueprintFromStore;
|
|
54
|
+
// Show loading state while fetching or before fetch starts
|
|
55
|
+
if (!blueprint && blueprintId && !error) {
|
|
56
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
57
|
+
{ label: "Blueprints" },
|
|
58
|
+
{ label: "Loading...", active: true },
|
|
59
|
+
] }), _jsx(SpinnerComponent, { message: "Loading blueprint details..." })] }));
|
|
60
|
+
}
|
|
61
|
+
// Show error state if fetch failed
|
|
62
|
+
if (error && !blueprint) {
|
|
63
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints" }, { label: "Error", active: true }] }), _jsx(ErrorMessage, { message: "Failed to load blueprint details", error: error })] }));
|
|
64
|
+
}
|
|
65
|
+
// Show error if no blueprint found
|
|
66
|
+
if (!blueprint) {
|
|
67
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
68
|
+
{ label: "Blueprints" },
|
|
69
|
+
{ label: "Not Found", active: true },
|
|
70
|
+
] }), _jsx(ErrorMessage, { message: `Blueprint ${blueprintId || "unknown"} not found`, error: new Error("Blueprint not found") })] }));
|
|
71
|
+
}
|
|
72
|
+
// Build detail sections
|
|
73
|
+
const detailSections = [];
|
|
74
|
+
// Basic details section
|
|
75
|
+
const basicFields = [];
|
|
76
|
+
if (blueprint.create_time_ms) {
|
|
77
|
+
basicFields.push({
|
|
78
|
+
label: "Created",
|
|
79
|
+
value: formatTimestamp(blueprint.create_time_ms),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (blueprint.architecture) {
|
|
83
|
+
basicFields.push({
|
|
84
|
+
label: "Architecture",
|
|
85
|
+
value: blueprint.architecture,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (blueprint.resources) {
|
|
89
|
+
basicFields.push({
|
|
90
|
+
label: "Resources",
|
|
91
|
+
value: blueprint.resources,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (basicFields.length > 0) {
|
|
95
|
+
detailSections.push({
|
|
96
|
+
title: "Details",
|
|
97
|
+
icon: figures.squareSmallFilled,
|
|
98
|
+
color: colors.warning,
|
|
99
|
+
fields: basicFields,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
// Launch parameters section
|
|
103
|
+
const lp = blueprint.parameters?.launch_parameters;
|
|
104
|
+
if (lp) {
|
|
105
|
+
const lpFields = [];
|
|
106
|
+
if (lp.custom_cpu_cores) {
|
|
107
|
+
lpFields.push({
|
|
108
|
+
label: "CPU Cores",
|
|
109
|
+
value: String(lp.custom_cpu_cores),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (lp.custom_gb_memory) {
|
|
113
|
+
lpFields.push({
|
|
114
|
+
label: "Memory",
|
|
115
|
+
value: `${lp.custom_gb_memory}GB`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if (lp.custom_disk_size) {
|
|
119
|
+
lpFields.push({
|
|
120
|
+
label: "Disk Size",
|
|
121
|
+
value: `${lp.custom_disk_size}GB`,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (lp.keep_alive_time_seconds) {
|
|
125
|
+
const minutes = Math.floor(lp.keep_alive_time_seconds / 60);
|
|
126
|
+
const hours = Math.floor(minutes / 60);
|
|
127
|
+
lpFields.push({
|
|
128
|
+
label: "Keep Alive",
|
|
129
|
+
value: hours > 0 ? `${hours}h ${minutes % 60}m` : `${minutes}m`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (lp.available_ports && lp.available_ports.length > 0) {
|
|
133
|
+
lpFields.push({
|
|
134
|
+
label: "Available Ports",
|
|
135
|
+
value: lp.available_ports.join(", "),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
if (lp.required_services && lp.required_services.length > 0) {
|
|
139
|
+
lpFields.push({
|
|
140
|
+
label: "Required Services",
|
|
141
|
+
value: lp.required_services.join(", "),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
if (lpFields.length > 0) {
|
|
145
|
+
detailSections.push({
|
|
146
|
+
title: "Launch Parameters",
|
|
147
|
+
icon: figures.arrowRight,
|
|
148
|
+
color: colors.secondary,
|
|
149
|
+
fields: lpFields,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Setup section
|
|
154
|
+
const params = blueprint.parameters;
|
|
155
|
+
if (params) {
|
|
156
|
+
const setupFields = [];
|
|
157
|
+
if (params.dockerfile) {
|
|
158
|
+
const lineCount = params.dockerfile.split("\n").length;
|
|
159
|
+
setupFields.push({
|
|
160
|
+
label: "Dockerfile",
|
|
161
|
+
value: _jsxs(Text, { dimColor: true, children: [lineCount, " lines"] }),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
if (params.system_setup_commands &&
|
|
165
|
+
params.system_setup_commands.length > 0) {
|
|
166
|
+
setupFields.push({
|
|
167
|
+
label: "Setup Commands",
|
|
168
|
+
value: `${params.system_setup_commands.length} commands`,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
if (params.file_mounts && Object.keys(params.file_mounts).length > 0) {
|
|
172
|
+
setupFields.push({
|
|
173
|
+
label: "File Mounts",
|
|
174
|
+
value: `${Object.keys(params.file_mounts).length} mounts`,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (setupFields.length > 0) {
|
|
178
|
+
detailSections.push({
|
|
179
|
+
title: "Build Configuration",
|
|
180
|
+
icon: figures.hamburger,
|
|
181
|
+
color: colors.info,
|
|
182
|
+
fields: setupFields,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// Error section - show failure reason if present
|
|
187
|
+
if (blueprint.failure_reason) {
|
|
188
|
+
detailSections.push({
|
|
189
|
+
title: "Error",
|
|
190
|
+
icon: figures.cross,
|
|
191
|
+
color: colors.error,
|
|
192
|
+
fields: [
|
|
193
|
+
{
|
|
194
|
+
label: "Failure Reason",
|
|
195
|
+
value: blueprint.failure_reason,
|
|
196
|
+
color: colors.error,
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
// Operations available for blueprints
|
|
202
|
+
const operations = [
|
|
203
|
+
{
|
|
204
|
+
key: "logs",
|
|
205
|
+
label: "View Build Logs",
|
|
206
|
+
color: colors.info,
|
|
207
|
+
icon: figures.info,
|
|
208
|
+
shortcut: "l",
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
key: "create-devbox",
|
|
212
|
+
label: "Create Devbox from Blueprint",
|
|
213
|
+
color: colors.success,
|
|
214
|
+
icon: figures.play,
|
|
215
|
+
shortcut: "c",
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
key: "delete",
|
|
219
|
+
label: "Delete Blueprint",
|
|
220
|
+
color: colors.error,
|
|
221
|
+
icon: figures.cross,
|
|
222
|
+
shortcut: "d",
|
|
223
|
+
},
|
|
224
|
+
];
|
|
225
|
+
// Handle operation selection
|
|
226
|
+
const handleOperation = async (operation, resource) => {
|
|
227
|
+
switch (operation) {
|
|
228
|
+
case "logs":
|
|
229
|
+
navigate("blueprint-logs", { blueprintId: resource.id });
|
|
230
|
+
break;
|
|
231
|
+
case "create-devbox":
|
|
232
|
+
navigate("devbox-create", { blueprintId: resource.id });
|
|
233
|
+
break;
|
|
234
|
+
case "delete":
|
|
235
|
+
// Show confirmation dialog
|
|
236
|
+
setShowDeleteConfirm(true);
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
// Execute delete after confirmation
|
|
241
|
+
const executeDelete = async () => {
|
|
242
|
+
if (!blueprint)
|
|
243
|
+
return;
|
|
244
|
+
setShowDeleteConfirm(false);
|
|
245
|
+
setDeleting(true);
|
|
246
|
+
try {
|
|
247
|
+
const client = getClient();
|
|
248
|
+
await client.blueprints.delete(blueprint.id);
|
|
249
|
+
goBack();
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
setError(err);
|
|
253
|
+
setDeleting(false);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
// Show delete confirmation
|
|
257
|
+
if (showDeleteConfirm && blueprint) {
|
|
258
|
+
return (_jsx(ConfirmationPrompt, { title: "Delete Blueprint", message: `Are you sure you want to delete "${blueprint.name || blueprint.id}"?`, details: "This action cannot be undone.", breadcrumbItems: [
|
|
259
|
+
{ label: "Blueprints" },
|
|
260
|
+
{ label: blueprint.name || blueprint.id },
|
|
261
|
+
{ label: "Delete", active: true },
|
|
262
|
+
], onConfirm: executeDelete, onCancel: () => setShowDeleteConfirm(false) }));
|
|
263
|
+
}
|
|
264
|
+
// Show deleting state
|
|
265
|
+
if (deleting) {
|
|
266
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
267
|
+
{ label: "Blueprints" },
|
|
268
|
+
{ label: blueprint?.name || blueprint?.id || "Blueprint" },
|
|
269
|
+
{ label: "Deleting...", active: true },
|
|
270
|
+
] }), _jsx(SpinnerComponent, { message: "Deleting blueprint..." })] }));
|
|
271
|
+
}
|
|
272
|
+
// Build detailed info lines for full details view
|
|
273
|
+
const buildDetailLines = (bp) => {
|
|
274
|
+
const lines = [];
|
|
275
|
+
// Core Information
|
|
276
|
+
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Blueprint Details" }, "core-title"));
|
|
277
|
+
lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "ID: ", bp.id] }, "core-id"));
|
|
278
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Name: ", bp.name || "(none)"] }, "core-name"));
|
|
279
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Status: ", bp.status] }, "core-status"));
|
|
280
|
+
if (bp.failure_reason) {
|
|
281
|
+
lines.push(_jsxs(Text, { color: colors.error, children: [" ", "Failure Reason: ", bp.failure_reason] }, "core-failure"));
|
|
282
|
+
}
|
|
283
|
+
if (bp.create_time_ms) {
|
|
284
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Created: ", new Date(bp.create_time_ms).toLocaleString()] }, "core-created"));
|
|
285
|
+
}
|
|
286
|
+
lines.push(_jsx(Text, { children: " " }, "core-space"));
|
|
287
|
+
// Launch Parameters
|
|
288
|
+
const lp = bp.parameters?.launch_parameters;
|
|
289
|
+
if (lp) {
|
|
290
|
+
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Launch Parameters" }, "lp-title"));
|
|
291
|
+
if (lp.architecture) {
|
|
292
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Architecture: ", lp.architecture] }, "lp-arch"));
|
|
293
|
+
}
|
|
294
|
+
if (lp.resource_size_request) {
|
|
295
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Resource Size: ", lp.resource_size_request] }, "lp-resources"));
|
|
296
|
+
}
|
|
297
|
+
if (lp.custom_cpu_cores) {
|
|
298
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "CPU Cores: ", lp.custom_cpu_cores] }, "lp-cpu"));
|
|
299
|
+
}
|
|
300
|
+
if (lp.custom_gb_memory) {
|
|
301
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Memory: ", lp.custom_gb_memory, "GB"] }, "lp-memory"));
|
|
302
|
+
}
|
|
303
|
+
if (lp.custom_disk_size) {
|
|
304
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Disk Size: ", lp.custom_disk_size, "GB"] }, "lp-disk"));
|
|
305
|
+
}
|
|
306
|
+
if (lp.keep_alive_time_seconds) {
|
|
307
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Keep Alive: ", lp.keep_alive_time_seconds, "s"] }, "lp-keepalive"));
|
|
308
|
+
}
|
|
309
|
+
if (lp.available_ports && lp.available_ports.length > 0) {
|
|
310
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Available Ports: ", lp.available_ports.join(", ")] }, "lp-ports"));
|
|
311
|
+
}
|
|
312
|
+
if (lp.launch_commands && lp.launch_commands.length > 0) {
|
|
313
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Launch Commands:"] }, "lp-launch-cmds"));
|
|
314
|
+
lp.launch_commands.forEach((cmd, idx) => {
|
|
315
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", figures.pointer, " ", cmd] }, `lp-cmd-${idx}`));
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
lines.push(_jsx(Text, { children: " " }, "lp-space"));
|
|
319
|
+
}
|
|
320
|
+
// Build Configuration
|
|
321
|
+
const params = bp.parameters;
|
|
322
|
+
if (params) {
|
|
323
|
+
if (params.dockerfile) {
|
|
324
|
+
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Dockerfile" }, "dockerfile-title"));
|
|
325
|
+
params.dockerfile.split("\n").forEach((line, idx) => {
|
|
326
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", line] }, `dockerfile-${idx}`));
|
|
327
|
+
});
|
|
328
|
+
lines.push(_jsx(Text, { children: " " }, "dockerfile-space"));
|
|
329
|
+
}
|
|
330
|
+
if (params.system_setup_commands &&
|
|
331
|
+
params.system_setup_commands.length > 0) {
|
|
332
|
+
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "System Setup Commands" }, "setup-title"));
|
|
333
|
+
params.system_setup_commands.forEach((cmd, idx) => {
|
|
334
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", idx + 1, ". ", cmd] }, `setup-${idx}`));
|
|
335
|
+
});
|
|
336
|
+
lines.push(_jsx(Text, { children: " " }, "setup-space"));
|
|
337
|
+
}
|
|
338
|
+
if (params.file_mounts && Object.keys(params.file_mounts).length > 0) {
|
|
339
|
+
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "File Mounts" }, "mounts-title"));
|
|
340
|
+
Object.entries(params.file_mounts).forEach(([path, _content], idx) => {
|
|
341
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", path] }, `mount-${idx}`));
|
|
342
|
+
});
|
|
343
|
+
lines.push(_jsx(Text, { children: " " }, "mounts-space"));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// Raw JSON
|
|
347
|
+
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Raw JSON" }, "json-title"));
|
|
348
|
+
const jsonLines = JSON.stringify(bp, null, 2).split("\n");
|
|
349
|
+
jsonLines.forEach((line, idx) => {
|
|
350
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", line] }, `json-${idx}`));
|
|
351
|
+
});
|
|
352
|
+
return lines;
|
|
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 === "building" ? pollBlueprint : undefined }));
|
|
355
|
+
}
|
|
@@ -40,16 +40,16 @@ export function DevboxDetailScreen({ devboxId }) {
|
|
|
40
40
|
}, [devboxFromStore, devboxId, loading, fetchedDevbox, setDevboxesInStore]);
|
|
41
41
|
// Use devbox from store or fetched devbox
|
|
42
42
|
const devbox = devboxFromStore || fetchedDevbox;
|
|
43
|
-
// Show loading state while fetching
|
|
44
|
-
if (
|
|
43
|
+
// Show loading state while fetching or before fetch starts
|
|
44
|
+
if (!devbox && devboxId && !error) {
|
|
45
45
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes" }, { label: "Loading...", active: true }] }), _jsx(SpinnerComponent, { message: "Loading devbox details..." })] }));
|
|
46
46
|
}
|
|
47
47
|
// Show error state if fetch failed
|
|
48
48
|
if (error) {
|
|
49
49
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes" }, { label: "Error", active: true }] }), _jsx(ErrorMessage, { message: "Failed to load devbox details", error: error })] }));
|
|
50
50
|
}
|
|
51
|
-
// Show error if no devbox found
|
|
52
|
-
if (!devbox
|
|
51
|
+
// Show error if no devbox found
|
|
52
|
+
if (!devbox) {
|
|
53
53
|
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes" }, { label: "Not Found", active: true }] }), _jsx(ErrorMessage, { message: `Devbox ${devboxId || "unknown"} not found`, error: new Error("Devbox not found in cache and could not be fetched") })] }));
|
|
54
54
|
}
|
|
55
55
|
// At this point devbox is guaranteed to exist (loading check above handles the null case)
|
|
@@ -14,6 +14,12 @@ export function MenuScreen() {
|
|
|
14
14
|
case "snapshots":
|
|
15
15
|
navigate("snapshot-list");
|
|
16
16
|
break;
|
|
17
|
+
case "network-policies":
|
|
18
|
+
navigate("network-policy-list");
|
|
19
|
+
break;
|
|
20
|
+
case "objects":
|
|
21
|
+
navigate("object-list");
|
|
22
|
+
break;
|
|
17
23
|
default:
|
|
18
24
|
// Fallback for any other screen names
|
|
19
25
|
navigate(key);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useNavigation } from "../store/navigationStore.js";
|
|
3
|
+
import { NetworkPolicyCreatePage } from "../components/NetworkPolicyCreatePage.js";
|
|
4
|
+
export function NetworkPolicyCreateScreen() {
|
|
5
|
+
const { goBack, navigate } = useNavigation();
|
|
6
|
+
return (_jsx(NetworkPolicyCreatePage, { onBack: goBack, onCreate: (policy) => navigate("network-policy-detail", { networkPolicyId: policy.id }) }));
|
|
7
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* NetworkPolicyDetailScreen - Detail page for network policies
|
|
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 { useNetworkPolicyStore, } from "../store/networkPolicyStore.js";
|
|
11
|
+
import { ResourceDetailPage, formatTimestamp, } from "../components/ResourceDetailPage.js";
|
|
12
|
+
import { getNetworkPolicy, deleteNetworkPolicy, } from "../services/networkPolicyService.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 { NetworkPolicyCreatePage } from "../components/NetworkPolicyCreatePage.js";
|
|
18
|
+
import { colors } from "../utils/theme.js";
|
|
19
|
+
/**
|
|
20
|
+
* Get a display label for the egress policy type
|
|
21
|
+
*/
|
|
22
|
+
function getEgressTypeLabel(egress) {
|
|
23
|
+
if (egress.allow_all) {
|
|
24
|
+
return "Allow All";
|
|
25
|
+
}
|
|
26
|
+
if (egress.allowed_hostnames.length === 0) {
|
|
27
|
+
return "Deny All";
|
|
28
|
+
}
|
|
29
|
+
return "Custom";
|
|
30
|
+
}
|
|
31
|
+
export function NetworkPolicyDetailScreen({ networkPolicyId, }) {
|
|
32
|
+
const { goBack } = useNavigation();
|
|
33
|
+
const networkPolicies = useNetworkPolicyStore((state) => state.networkPolicies);
|
|
34
|
+
const [loading, setLoading] = React.useState(false);
|
|
35
|
+
const [error, setError] = React.useState(null);
|
|
36
|
+
const [fetchedPolicy, setFetchedPolicy] = 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 policy in store first
|
|
41
|
+
const policyFromStore = networkPolicies.find((p) => p.id === networkPolicyId);
|
|
42
|
+
// Fetch policy from API if not in store or missing full details
|
|
43
|
+
React.useEffect(() => {
|
|
44
|
+
if (networkPolicyId && !loading && !fetchedPolicy) {
|
|
45
|
+
// Always fetch full details since store may only have basic info
|
|
46
|
+
setLoading(true);
|
|
47
|
+
setError(null);
|
|
48
|
+
getNetworkPolicy(networkPolicyId)
|
|
49
|
+
.then((policy) => {
|
|
50
|
+
setFetchedPolicy(policy);
|
|
51
|
+
setLoading(false);
|
|
52
|
+
})
|
|
53
|
+
.catch((err) => {
|
|
54
|
+
setError(err);
|
|
55
|
+
setLoading(false);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}, [networkPolicyId, loading, fetchedPolicy]);
|
|
59
|
+
// Use fetched policy for full details, fall back to store for basic display
|
|
60
|
+
const policy = fetchedPolicy || policyFromStore;
|
|
61
|
+
// Show loading state while fetching or before fetch starts
|
|
62
|
+
if (!policy && networkPolicyId && !error) {
|
|
63
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
64
|
+
{ label: "Network Policies" },
|
|
65
|
+
{ label: "Loading...", active: true },
|
|
66
|
+
] }), _jsx(SpinnerComponent, { message: "Loading network policy details..." })] }));
|
|
67
|
+
}
|
|
68
|
+
// Show error state if fetch failed
|
|
69
|
+
if (error && !policy) {
|
|
70
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
71
|
+
{ label: "Network Policies" },
|
|
72
|
+
{ label: "Error", active: true },
|
|
73
|
+
] }), _jsx(ErrorMessage, { message: "Failed to load network policy details", error: error })] }));
|
|
74
|
+
}
|
|
75
|
+
// Show error if no policy found
|
|
76
|
+
if (!policy) {
|
|
77
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
78
|
+
{ label: "Network Policies" },
|
|
79
|
+
{ label: "Not Found", active: true },
|
|
80
|
+
] }), _jsx(ErrorMessage, { message: `Network policy ${networkPolicyId || "unknown"} not found`, error: new Error("Network policy not found") })] }));
|
|
81
|
+
}
|
|
82
|
+
// Build detail sections
|
|
83
|
+
const detailSections = [];
|
|
84
|
+
// Basic details section
|
|
85
|
+
const basicFields = [];
|
|
86
|
+
if (policy.description) {
|
|
87
|
+
basicFields.push({
|
|
88
|
+
label: "Description",
|
|
89
|
+
value: policy.description,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
if (policy.create_time_ms) {
|
|
93
|
+
basicFields.push({
|
|
94
|
+
label: "Created",
|
|
95
|
+
value: formatTimestamp(policy.create_time_ms),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (policy.update_time_ms) {
|
|
99
|
+
basicFields.push({
|
|
100
|
+
label: "Last Updated",
|
|
101
|
+
value: formatTimestamp(policy.update_time_ms),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
if (basicFields.length > 0) {
|
|
105
|
+
detailSections.push({
|
|
106
|
+
title: "Details",
|
|
107
|
+
icon: figures.squareSmallFilled,
|
|
108
|
+
color: colors.warning,
|
|
109
|
+
fields: basicFields,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
// Egress rules section
|
|
113
|
+
const egressFields = [];
|
|
114
|
+
egressFields.push({
|
|
115
|
+
label: "Policy Type",
|
|
116
|
+
value: (_jsx(Text, { color: policy.egress.allow_all
|
|
117
|
+
? colors.success
|
|
118
|
+
: policy.egress.allowed_hostnames.length === 0
|
|
119
|
+
? colors.error
|
|
120
|
+
: colors.warning, bold: true, children: getEgressTypeLabel(policy.egress) })),
|
|
121
|
+
});
|
|
122
|
+
egressFields.push({
|
|
123
|
+
label: "Allow Devbox-to-Devbox",
|
|
124
|
+
value: policy.egress.allow_devbox_to_devbox ? "Yes" : "No",
|
|
125
|
+
});
|
|
126
|
+
if (policy.egress.allowed_hostnames &&
|
|
127
|
+
policy.egress.allowed_hostnames.length > 0) {
|
|
128
|
+
egressFields.push({
|
|
129
|
+
label: "Allowed Hostnames",
|
|
130
|
+
value: `${policy.egress.allowed_hostnames.length} hostname(s)`,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
detailSections.push({
|
|
134
|
+
title: "Egress Rules",
|
|
135
|
+
icon: figures.arrowRight,
|
|
136
|
+
color: colors.info,
|
|
137
|
+
fields: egressFields,
|
|
138
|
+
});
|
|
139
|
+
// Operations available for network policies
|
|
140
|
+
const operations = [
|
|
141
|
+
{
|
|
142
|
+
key: "edit",
|
|
143
|
+
label: "Edit Network Policy",
|
|
144
|
+
color: colors.warning,
|
|
145
|
+
icon: figures.pointer,
|
|
146
|
+
shortcut: "e",
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
key: "delete",
|
|
150
|
+
label: "Delete Network Policy",
|
|
151
|
+
color: colors.error,
|
|
152
|
+
icon: figures.cross,
|
|
153
|
+
shortcut: "d",
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
// Handle operation selection
|
|
157
|
+
const handleOperation = async (operation, _resource) => {
|
|
158
|
+
switch (operation) {
|
|
159
|
+
case "edit":
|
|
160
|
+
setShowEditForm(true);
|
|
161
|
+
break;
|
|
162
|
+
case "delete":
|
|
163
|
+
// Show confirmation dialog
|
|
164
|
+
setShowDeleteConfirm(true);
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
// Execute delete after confirmation
|
|
169
|
+
const executeDelete = async () => {
|
|
170
|
+
if (!policy)
|
|
171
|
+
return;
|
|
172
|
+
setShowDeleteConfirm(false);
|
|
173
|
+
setDeleting(true);
|
|
174
|
+
try {
|
|
175
|
+
await deleteNetworkPolicy(policy.id);
|
|
176
|
+
goBack();
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
setError(err);
|
|
180
|
+
setDeleting(false);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
// Build detailed info lines for full details view
|
|
184
|
+
const buildDetailLines = (np) => {
|
|
185
|
+
const lines = [];
|
|
186
|
+
// Core Information
|
|
187
|
+
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Network Policy Details" }, "core-title"));
|
|
188
|
+
lines.push(_jsxs(Text, { color: colors.idColor, children: [" ", "ID: ", np.id] }, "core-id"));
|
|
189
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Name: ", np.name] }, "core-name"));
|
|
190
|
+
if (np.description) {
|
|
191
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Description: ", np.description] }, "core-desc"));
|
|
192
|
+
}
|
|
193
|
+
if (np.create_time_ms) {
|
|
194
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Created: ", new Date(np.create_time_ms).toLocaleString()] }, "core-created"));
|
|
195
|
+
}
|
|
196
|
+
if (np.update_time_ms) {
|
|
197
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Last Updated: ", new Date(np.update_time_ms).toLocaleString()] }, "core-updated"));
|
|
198
|
+
}
|
|
199
|
+
lines.push(_jsx(Text, { children: " " }, "core-space"));
|
|
200
|
+
// Egress Rules
|
|
201
|
+
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Egress Rules" }, "egress-title"));
|
|
202
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Policy Type: ", getEgressTypeLabel(np.egress)] }, "egress-type"));
|
|
203
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Allow All: ", np.egress.allow_all ? "Yes" : "No"] }, "egress-allow-all"));
|
|
204
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", "Allow Devbox-to-Devbox:", " ", np.egress.allow_devbox_to_devbox ? "Yes" : "No"] }, "egress-devbox"));
|
|
205
|
+
lines.push(_jsx(Text, { children: " " }, "egress-space"));
|
|
206
|
+
// Allowed Hostnames
|
|
207
|
+
if (np.egress.allowed_hostnames && np.egress.allowed_hostnames.length > 0) {
|
|
208
|
+
lines.push(_jsxs(Text, { color: colors.warning, bold: true, children: ["Allowed Hostnames (", np.egress.allowed_hostnames.length, ")"] }, "hostnames-title"));
|
|
209
|
+
np.egress.allowed_hostnames.forEach((hostname, idx) => {
|
|
210
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", figures.pointer, " ", hostname] }, `hostname-${idx}`));
|
|
211
|
+
});
|
|
212
|
+
lines.push(_jsx(Text, { children: " " }, "hostnames-space"));
|
|
213
|
+
}
|
|
214
|
+
// Raw JSON
|
|
215
|
+
lines.push(_jsx(Text, { color: colors.warning, bold: true, children: "Raw JSON" }, "json-title"));
|
|
216
|
+
const jsonLines = JSON.stringify(np, null, 2).split("\n");
|
|
217
|
+
jsonLines.forEach((line, idx) => {
|
|
218
|
+
lines.push(_jsxs(Text, { dimColor: true, children: [" ", line] }, `json-${idx}`));
|
|
219
|
+
});
|
|
220
|
+
return lines;
|
|
221
|
+
};
|
|
222
|
+
// Show edit form
|
|
223
|
+
if (showEditForm && policy) {
|
|
224
|
+
return (_jsx(NetworkPolicyCreatePage, { onBack: () => setShowEditForm(false), onCreate: (updatedPolicy) => {
|
|
225
|
+
// Update the fetched policy with the new data
|
|
226
|
+
setFetchedPolicy(updatedPolicy);
|
|
227
|
+
setShowEditForm(false);
|
|
228
|
+
}, initialPolicy: policy }));
|
|
229
|
+
}
|
|
230
|
+
// Show delete confirmation
|
|
231
|
+
if (showDeleteConfirm && policy) {
|
|
232
|
+
return (_jsx(ConfirmationPrompt, { title: "Delete Network Policy", message: `Are you sure you want to delete "${policy.name || policy.id}"?`, details: "This action cannot be undone. Any devboxes using this policy will lose their network restrictions.", breadcrumbItems: [
|
|
233
|
+
{ label: "Network Policies" },
|
|
234
|
+
{ label: policy.name || policy.id },
|
|
235
|
+
{ label: "Delete", active: true },
|
|
236
|
+
], onConfirm: executeDelete, onCancel: () => setShowDeleteConfirm(false) }));
|
|
237
|
+
}
|
|
238
|
+
// Show deleting state
|
|
239
|
+
if (deleting) {
|
|
240
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
241
|
+
{ label: "Network Policies" },
|
|
242
|
+
{ label: policy.name || policy.id },
|
|
243
|
+
{ label: "Deleting...", active: true },
|
|
244
|
+
] }), _jsx(SpinnerComponent, { message: "Deleting network policy..." })] }));
|
|
245
|
+
}
|
|
246
|
+
return (_jsx(ResourceDetailPage, { resource: policy, resourceType: "Network Policies", getDisplayName: (np) => np.name || np.id, getId: (np) => np.id, getStatus: () => "active", detailSections: detailSections, operations: operations, onOperation: handleOperation, onBack: goBack, buildDetailLines: buildDetailLines }));
|
|
247
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useNavigation } from "../store/navigationStore.js";
|
|
3
|
+
import { ListNetworkPoliciesUI } from "../commands/network-policy/list.js";
|
|
4
|
+
export function NetworkPolicyListScreen() {
|
|
5
|
+
const { goBack } = useNavigation();
|
|
6
|
+
return _jsx(ListNetworkPoliciesUI, { onBack: goBack });
|
|
7
|
+
}
|