@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.
Files changed (57) hide show
  1. package/README.md +29 -8
  2. package/dist/commands/blueprint/from-dockerfile.js +182 -0
  3. package/dist/commands/blueprint/list.js +97 -28
  4. package/dist/commands/blueprint/prune.js +7 -19
  5. package/dist/commands/devbox/create.js +3 -0
  6. package/dist/commands/devbox/list.js +44 -65
  7. package/dist/commands/menu.js +2 -1
  8. package/dist/commands/network-policy/create.js +27 -0
  9. package/dist/commands/network-policy/delete.js +21 -0
  10. package/dist/commands/network-policy/get.js +15 -0
  11. package/dist/commands/network-policy/list.js +494 -0
  12. package/dist/commands/object/list.js +516 -24
  13. package/dist/commands/snapshot/list.js +90 -29
  14. package/dist/components/Banner.js +109 -8
  15. package/dist/components/ConfirmationPrompt.js +45 -0
  16. package/dist/components/DevboxActionsMenu.js +42 -6
  17. package/dist/components/DevboxCard.js +1 -1
  18. package/dist/components/DevboxCreatePage.js +174 -168
  19. package/dist/components/DevboxDetailPage.js +218 -272
  20. package/dist/components/LogsViewer.js +8 -1
  21. package/dist/components/MainMenu.js +35 -4
  22. package/dist/components/NavigationTips.js +24 -0
  23. package/dist/components/NetworkPolicyCreatePage.js +263 -0
  24. package/dist/components/OperationsMenu.js +9 -1
  25. package/dist/components/ResourceActionsMenu.js +5 -1
  26. package/dist/components/ResourceDetailPage.js +204 -0
  27. package/dist/components/ResourceListView.js +19 -2
  28. package/dist/components/StatusBadge.js +2 -2
  29. package/dist/components/Table.js +6 -8
  30. package/dist/components/form/FormActionButton.js +7 -0
  31. package/dist/components/form/FormField.js +7 -0
  32. package/dist/components/form/FormListManager.js +112 -0
  33. package/dist/components/form/FormSelect.js +34 -0
  34. package/dist/components/form/FormTextInput.js +8 -0
  35. package/dist/components/form/index.js +8 -0
  36. package/dist/hooks/useViewportHeight.js +38 -20
  37. package/dist/router/Router.js +23 -1
  38. package/dist/screens/BlueprintDetailScreen.js +355 -0
  39. package/dist/screens/DevboxDetailScreen.js +4 -4
  40. package/dist/screens/MenuScreen.js +6 -0
  41. package/dist/screens/NetworkPolicyCreateScreen.js +7 -0
  42. package/dist/screens/NetworkPolicyDetailScreen.js +247 -0
  43. package/dist/screens/NetworkPolicyListScreen.js +7 -0
  44. package/dist/screens/ObjectDetailScreen.js +377 -0
  45. package/dist/screens/ObjectListScreen.js +7 -0
  46. package/dist/screens/SnapshotDetailScreen.js +208 -0
  47. package/dist/services/blueprintService.js +30 -11
  48. package/dist/services/networkPolicyService.js +108 -0
  49. package/dist/services/objectService.js +101 -0
  50. package/dist/services/snapshotService.js +39 -3
  51. package/dist/store/blueprintStore.js +4 -10
  52. package/dist/store/index.js +1 -0
  53. package/dist/store/networkPolicyStore.js +83 -0
  54. package/dist/store/objectStore.js +92 -0
  55. package/dist/store/snapshotStore.js +4 -8
  56. package/dist/utils/commands.js +65 -0
  57. 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 (loading) {
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 and not loading
52
- if (!devbox && !loading) {
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
+ }