@runloop/rl-cli 1.1.0 → 1.3.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/list.js +97 -28
- package/dist/commands/blueprint/prune.js +258 -0
- 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 +95 -81
- 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 +264 -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 +337 -0
- 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 +58 -0
- package/package.json +2 -2
|
@@ -1,34 +1,526 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Box, Text, useInput, useApp } from "ink";
|
|
4
|
+
import TextInput from "ink-text-input";
|
|
5
|
+
import figures from "figures";
|
|
6
|
+
import { writeFile } from "fs/promises";
|
|
4
7
|
import { getClient } from "../../utils/client.js";
|
|
8
|
+
import { Header } from "../../components/Header.js";
|
|
9
|
+
import { SpinnerComponent } from "../../components/Spinner.js";
|
|
10
|
+
import { ErrorMessage } from "../../components/ErrorMessage.js";
|
|
11
|
+
import { SuccessMessage } from "../../components/SuccessMessage.js";
|
|
12
|
+
import { Breadcrumb } from "../../components/Breadcrumb.js";
|
|
13
|
+
import { NavigationTips } from "../../components/NavigationTips.js";
|
|
14
|
+
import { Table, createTextColumn } from "../../components/Table.js";
|
|
15
|
+
import { ActionsPopup } from "../../components/ActionsPopup.js";
|
|
16
|
+
import { formatTimeAgo } from "../../components/ResourceListView.js";
|
|
5
17
|
import { output, outputError } from "../../utils/output.js";
|
|
6
|
-
|
|
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 { useNavigation } from "../../store/navigationStore.js";
|
|
23
|
+
import { formatFileSize } from "../../services/objectService.js";
|
|
24
|
+
import { ConfirmationPrompt } from "../../components/ConfirmationPrompt.js";
|
|
25
|
+
const DEFAULT_PAGE_SIZE = 10;
|
|
26
|
+
const ListObjectsUI = ({ onBack, onExit, }) => {
|
|
27
|
+
const { exit: inkExit } = useApp();
|
|
28
|
+
const { navigate } = useNavigation();
|
|
29
|
+
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
30
|
+
const [showPopup, setShowPopup] = React.useState(false);
|
|
31
|
+
const [selectedOperation, setSelectedOperation] = React.useState(0);
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
33
|
+
const [selectedObject, setSelectedObject] = React.useState(null);
|
|
34
|
+
const [executingOperation, setExecutingOperation] = React.useState(null);
|
|
35
|
+
const [operationResult, setOperationResult] = React.useState(null);
|
|
36
|
+
const [operationError, setOperationError] = React.useState(null);
|
|
37
|
+
const [operationLoading, setOperationLoading] = React.useState(false);
|
|
38
|
+
const [showDownloadPrompt, setShowDownloadPrompt] = React.useState(false);
|
|
39
|
+
const [downloadPath, setDownloadPath] = React.useState("");
|
|
40
|
+
const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false);
|
|
41
|
+
// Calculate overhead for viewport height
|
|
42
|
+
const overhead = 13;
|
|
43
|
+
const { viewportHeight, terminalWidth } = useViewportHeight({
|
|
44
|
+
overhead,
|
|
45
|
+
minHeight: 5,
|
|
46
|
+
});
|
|
47
|
+
const PAGE_SIZE = viewportHeight;
|
|
48
|
+
// All width constants
|
|
49
|
+
const fixedWidth = 6; // border + padding
|
|
50
|
+
const idWidth = 25;
|
|
51
|
+
const stateWidth = 12;
|
|
52
|
+
const typeWidth = 15;
|
|
53
|
+
const sizeWidth = 12;
|
|
54
|
+
const timeWidth = 20;
|
|
55
|
+
const ttlWidth = 14;
|
|
56
|
+
const showTypeColumn = terminalWidth >= 100;
|
|
57
|
+
const showSizeColumn = terminalWidth >= 90;
|
|
58
|
+
const showTtlColumn = terminalWidth >= 80;
|
|
59
|
+
// Name width uses remaining space after fixed columns
|
|
60
|
+
const baseWidth = fixedWidth + idWidth + stateWidth + timeWidth;
|
|
61
|
+
const optionalWidth = (showTypeColumn ? typeWidth : 0) +
|
|
62
|
+
(showSizeColumn ? sizeWidth : 0) +
|
|
63
|
+
(showTtlColumn ? ttlWidth : 0);
|
|
64
|
+
const remainingWidth = terminalWidth - baseWidth - optionalWidth;
|
|
65
|
+
const nameWidth = Math.min(80, Math.max(15, remainingWidth));
|
|
66
|
+
// Helper to format TTL remaining time
|
|
67
|
+
const formatTtl = (deleteAfterMs) => {
|
|
68
|
+
if (!deleteAfterMs)
|
|
69
|
+
return "";
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
const remainingMs = deleteAfterMs - now;
|
|
72
|
+
if (remainingMs <= 0)
|
|
73
|
+
return "Expired";
|
|
74
|
+
const remainingMinutes = Math.floor(remainingMs / 60000);
|
|
75
|
+
if (remainingMinutes < 60) {
|
|
76
|
+
return `${remainingMinutes}m left`;
|
|
77
|
+
}
|
|
78
|
+
const hours = Math.floor(remainingMinutes / 60);
|
|
79
|
+
const mins = remainingMinutes % 60;
|
|
80
|
+
if (hours < 24) {
|
|
81
|
+
return `${hours}h ${mins}m left`;
|
|
82
|
+
}
|
|
83
|
+
const days = Math.floor(hours / 24);
|
|
84
|
+
const remainingHours = hours % 24;
|
|
85
|
+
return `${days}d ${remainingHours}h left`;
|
|
86
|
+
};
|
|
87
|
+
// Fetch function for pagination hook
|
|
88
|
+
const fetchPage = React.useCallback(async (params) => {
|
|
89
|
+
const client = getClient();
|
|
90
|
+
const pageObjects = [];
|
|
91
|
+
// Build query params
|
|
92
|
+
const queryParams = {
|
|
93
|
+
limit: params.limit,
|
|
94
|
+
};
|
|
95
|
+
if (params.startingAt) {
|
|
96
|
+
queryParams.starting_after = params.startingAt;
|
|
97
|
+
}
|
|
98
|
+
// Fetch ONE page only
|
|
99
|
+
const result = await client.objects.list(queryParams);
|
|
100
|
+
// Extract data and create defensive copies
|
|
101
|
+
if (result.objects && Array.isArray(result.objects)) {
|
|
102
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
103
|
+
result.objects.forEach((obj) => {
|
|
104
|
+
pageObjects.push({
|
|
105
|
+
id: obj.id,
|
|
106
|
+
name: obj.name,
|
|
107
|
+
content_type: obj.content_type,
|
|
108
|
+
size_bytes: obj.size_bytes,
|
|
109
|
+
state: obj.state,
|
|
110
|
+
is_public: obj.is_public,
|
|
111
|
+
create_time_ms: obj.create_time_ms,
|
|
112
|
+
delete_after_time_ms: obj.delete_after_time_ms,
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
// Access pagination properties from the result
|
|
117
|
+
const pageResult = result;
|
|
118
|
+
return {
|
|
119
|
+
items: pageObjects,
|
|
120
|
+
hasMore: pageResult.has_more || false,
|
|
121
|
+
totalCount: pageResult.total_count || pageObjects.length,
|
|
122
|
+
};
|
|
123
|
+
}, []);
|
|
124
|
+
// Use the shared pagination hook
|
|
125
|
+
const { items: objects, loading, navigating, error, currentPage, hasMore, hasPrev, totalCount, nextPage, prevPage, refresh, } = useCursorPagination({
|
|
126
|
+
fetchPage,
|
|
127
|
+
pageSize: PAGE_SIZE,
|
|
128
|
+
getItemId: (obj) => obj.id,
|
|
129
|
+
pollInterval: 2000,
|
|
130
|
+
pollingEnabled: !showPopup &&
|
|
131
|
+
!executingOperation &&
|
|
132
|
+
!showDownloadPrompt &&
|
|
133
|
+
!showDeleteConfirm,
|
|
134
|
+
deps: [PAGE_SIZE],
|
|
135
|
+
});
|
|
136
|
+
// Operations for objects
|
|
137
|
+
const operations = React.useMemo(() => [
|
|
138
|
+
{
|
|
139
|
+
key: "view_details",
|
|
140
|
+
label: "View Details",
|
|
141
|
+
color: colors.primary,
|
|
142
|
+
icon: figures.pointer,
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
key: "download",
|
|
146
|
+
label: "Download",
|
|
147
|
+
color: colors.success,
|
|
148
|
+
icon: figures.arrowDown,
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
key: "delete",
|
|
152
|
+
label: "Delete",
|
|
153
|
+
color: colors.error,
|
|
154
|
+
icon: figures.cross,
|
|
155
|
+
},
|
|
156
|
+
], []);
|
|
157
|
+
// Build columns
|
|
158
|
+
const columns = React.useMemo(() => [
|
|
159
|
+
createTextColumn("id", "ID", (obj) => obj.id, {
|
|
160
|
+
width: idWidth + 1,
|
|
161
|
+
color: colors.idColor,
|
|
162
|
+
dimColor: false,
|
|
163
|
+
bold: false,
|
|
164
|
+
}),
|
|
165
|
+
createTextColumn("name", "Name", (obj) => obj.name || "", {
|
|
166
|
+
width: nameWidth,
|
|
167
|
+
}),
|
|
168
|
+
createTextColumn("state", "State", (obj) => obj.state || "", {
|
|
169
|
+
width: stateWidth,
|
|
170
|
+
color: colors.warning,
|
|
171
|
+
dimColor: false,
|
|
172
|
+
bold: false,
|
|
173
|
+
}),
|
|
174
|
+
createTextColumn("type", "Type", (obj) => obj.content_type || "", {
|
|
175
|
+
width: typeWidth,
|
|
176
|
+
color: colors.textDim,
|
|
177
|
+
dimColor: false,
|
|
178
|
+
bold: false,
|
|
179
|
+
visible: showTypeColumn,
|
|
180
|
+
}),
|
|
181
|
+
createTextColumn("size", "Size", (obj) => formatFileSize(obj.size_bytes), {
|
|
182
|
+
width: sizeWidth,
|
|
183
|
+
color: colors.textDim,
|
|
184
|
+
dimColor: false,
|
|
185
|
+
bold: false,
|
|
186
|
+
visible: showSizeColumn,
|
|
187
|
+
}),
|
|
188
|
+
createTextColumn("created", "Created", (obj) => obj.create_time_ms ? formatTimeAgo(obj.create_time_ms) : "", {
|
|
189
|
+
width: timeWidth,
|
|
190
|
+
color: colors.textDim,
|
|
191
|
+
dimColor: false,
|
|
192
|
+
bold: false,
|
|
193
|
+
}),
|
|
194
|
+
createTextColumn("ttl", "Expires", (obj) => formatTtl(obj.delete_after_time_ms), {
|
|
195
|
+
width: ttlWidth,
|
|
196
|
+
color: colors.warning,
|
|
197
|
+
dimColor: false,
|
|
198
|
+
bold: false,
|
|
199
|
+
visible: showTtlColumn,
|
|
200
|
+
}),
|
|
201
|
+
], [
|
|
202
|
+
idWidth,
|
|
203
|
+
nameWidth,
|
|
204
|
+
stateWidth,
|
|
205
|
+
typeWidth,
|
|
206
|
+
sizeWidth,
|
|
207
|
+
timeWidth,
|
|
208
|
+
ttlWidth,
|
|
209
|
+
showTypeColumn,
|
|
210
|
+
showSizeColumn,
|
|
211
|
+
showTtlColumn,
|
|
212
|
+
formatTtl,
|
|
213
|
+
]);
|
|
214
|
+
// Handle Ctrl+C to exit
|
|
215
|
+
useExitOnCtrlC();
|
|
216
|
+
// Ensure selected index is within bounds
|
|
217
|
+
React.useEffect(() => {
|
|
218
|
+
if (objects.length > 0 && selectedIndex >= objects.length) {
|
|
219
|
+
setSelectedIndex(Math.max(0, objects.length - 1));
|
|
220
|
+
}
|
|
221
|
+
}, [objects.length, selectedIndex]);
|
|
222
|
+
const selectedObjectItem = objects[selectedIndex];
|
|
223
|
+
// Calculate pagination info for display
|
|
224
|
+
const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE));
|
|
225
|
+
const startIndex = currentPage * PAGE_SIZE;
|
|
226
|
+
const endIndex = startIndex + objects.length;
|
|
227
|
+
const executeOperation = async (obj, operationKey, targetPath) => {
|
|
228
|
+
const client = getClient();
|
|
229
|
+
if (!obj)
|
|
230
|
+
return;
|
|
231
|
+
try {
|
|
232
|
+
setOperationLoading(true);
|
|
233
|
+
switch (operationKey) {
|
|
234
|
+
case "delete":
|
|
235
|
+
await client.objects.delete(obj.id);
|
|
236
|
+
setOperationResult(`Storage object ${obj.id} deleted successfully`);
|
|
237
|
+
break;
|
|
238
|
+
case "download": {
|
|
239
|
+
if (!targetPath) {
|
|
240
|
+
setOperationError(new Error("No download path specified"));
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
// Get download URL
|
|
244
|
+
const downloadUrlResponse = await client.objects.download(obj.id, {
|
|
245
|
+
duration_seconds: 3600,
|
|
246
|
+
});
|
|
247
|
+
// Download the file
|
|
248
|
+
const response = await fetch(downloadUrlResponse.download_url);
|
|
249
|
+
if (!response.ok) {
|
|
250
|
+
throw new Error(`Download failed: HTTP ${response.status}`);
|
|
251
|
+
}
|
|
252
|
+
// Save the file
|
|
253
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
254
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
255
|
+
await writeFile(targetPath, buffer);
|
|
256
|
+
setOperationResult(`Downloaded to ${targetPath}`);
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
setOperationError(err);
|
|
263
|
+
}
|
|
264
|
+
finally {
|
|
265
|
+
setOperationLoading(false);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
// Handle download submission
|
|
269
|
+
const handleDownloadSubmit = () => {
|
|
270
|
+
if (downloadPath.trim() && selectedObject) {
|
|
271
|
+
setShowDownloadPrompt(false);
|
|
272
|
+
setExecutingOperation("download");
|
|
273
|
+
executeOperation(selectedObject, "download", downloadPath.trim());
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
useInput((input, key) => {
|
|
277
|
+
// Handle operation result display
|
|
278
|
+
if (operationResult || operationError) {
|
|
279
|
+
if (input === "q" || key.escape || key.return) {
|
|
280
|
+
const wasDelete = executingOperation === "delete";
|
|
281
|
+
const hadError = operationError !== null;
|
|
282
|
+
setOperationResult(null);
|
|
283
|
+
setOperationError(null);
|
|
284
|
+
setExecutingOperation(null);
|
|
285
|
+
setSelectedObject(null);
|
|
286
|
+
// Refresh the list after delete to show updated data
|
|
287
|
+
// Use setTimeout to ensure state updates are applied first
|
|
288
|
+
if (wasDelete && !hadError) {
|
|
289
|
+
setTimeout(() => refresh(), 0);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
// Handle download prompt
|
|
295
|
+
if (showDownloadPrompt) {
|
|
296
|
+
if (key.escape) {
|
|
297
|
+
setShowDownloadPrompt(false);
|
|
298
|
+
setDownloadPath("");
|
|
299
|
+
setSelectedObject(null);
|
|
300
|
+
}
|
|
301
|
+
else if (key.return) {
|
|
302
|
+
handleDownloadSubmit();
|
|
303
|
+
}
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
// Handle popup navigation
|
|
307
|
+
if (showPopup) {
|
|
308
|
+
if (key.upArrow && selectedOperation > 0) {
|
|
309
|
+
setSelectedOperation(selectedOperation - 1);
|
|
310
|
+
}
|
|
311
|
+
else if (key.downArrow && selectedOperation < operations.length - 1) {
|
|
312
|
+
setSelectedOperation(selectedOperation + 1);
|
|
313
|
+
}
|
|
314
|
+
else if (key.return) {
|
|
315
|
+
setShowPopup(false);
|
|
316
|
+
const operationKey = operations[selectedOperation].key;
|
|
317
|
+
if (operationKey === "view_details") {
|
|
318
|
+
navigate("object-detail", {
|
|
319
|
+
objectId: selectedObjectItem.id,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
else if (operationKey === "download") {
|
|
323
|
+
// Show download prompt
|
|
324
|
+
setSelectedObject(selectedObjectItem);
|
|
325
|
+
const defaultName = selectedObjectItem.name || selectedObjectItem.id;
|
|
326
|
+
setDownloadPath(`./${defaultName}`);
|
|
327
|
+
setShowDownloadPrompt(true);
|
|
328
|
+
}
|
|
329
|
+
else if (operationKey === "delete") {
|
|
330
|
+
// Show delete confirmation
|
|
331
|
+
setSelectedObject(selectedObjectItem);
|
|
332
|
+
setShowDeleteConfirm(true);
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
setSelectedObject(selectedObjectItem);
|
|
336
|
+
setExecutingOperation(operationKey);
|
|
337
|
+
// Execute immediately with the object and operation passed directly
|
|
338
|
+
executeOperation(selectedObjectItem, operationKey);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
else if (input === "v" && selectedObjectItem) {
|
|
342
|
+
// View details hotkey
|
|
343
|
+
setShowPopup(false);
|
|
344
|
+
navigate("object-detail", {
|
|
345
|
+
objectId: selectedObjectItem.id,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
else if (key.escape || input === "q") {
|
|
349
|
+
setShowPopup(false);
|
|
350
|
+
setSelectedOperation(0);
|
|
351
|
+
}
|
|
352
|
+
else if (input === "w") {
|
|
353
|
+
// Download hotkey - show prompt
|
|
354
|
+
setShowPopup(false);
|
|
355
|
+
setSelectedObject(selectedObjectItem);
|
|
356
|
+
const defaultName = selectedObjectItem.name || selectedObjectItem.id;
|
|
357
|
+
setDownloadPath(`./${defaultName}`);
|
|
358
|
+
setShowDownloadPrompt(true);
|
|
359
|
+
}
|
|
360
|
+
else if (input === "d") {
|
|
361
|
+
// Delete hotkey - show confirmation
|
|
362
|
+
setShowPopup(false);
|
|
363
|
+
setSelectedObject(selectedObjectItem);
|
|
364
|
+
setShowDeleteConfirm(true);
|
|
365
|
+
}
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const pageObjects = objects.length;
|
|
369
|
+
// Handle list view navigation
|
|
370
|
+
if (key.upArrow && selectedIndex > 0) {
|
|
371
|
+
setSelectedIndex(selectedIndex - 1);
|
|
372
|
+
}
|
|
373
|
+
else if (key.downArrow && selectedIndex < pageObjects - 1) {
|
|
374
|
+
setSelectedIndex(selectedIndex + 1);
|
|
375
|
+
}
|
|
376
|
+
else if ((input === "n" || key.rightArrow) &&
|
|
377
|
+
!loading &&
|
|
378
|
+
!navigating &&
|
|
379
|
+
hasMore) {
|
|
380
|
+
nextPage();
|
|
381
|
+
setSelectedIndex(0);
|
|
382
|
+
}
|
|
383
|
+
else if ((input === "p" || key.leftArrow) &&
|
|
384
|
+
!loading &&
|
|
385
|
+
!navigating &&
|
|
386
|
+
hasPrev) {
|
|
387
|
+
prevPage();
|
|
388
|
+
setSelectedIndex(0);
|
|
389
|
+
}
|
|
390
|
+
else if (key.return && selectedObjectItem) {
|
|
391
|
+
// Enter key navigates to detail view
|
|
392
|
+
navigate("object-detail", {
|
|
393
|
+
objectId: selectedObjectItem.id,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
else if (input === "a" && selectedObjectItem) {
|
|
397
|
+
setShowPopup(true);
|
|
398
|
+
setSelectedOperation(0);
|
|
399
|
+
}
|
|
400
|
+
else if (key.escape) {
|
|
401
|
+
if (onBack) {
|
|
402
|
+
onBack();
|
|
403
|
+
}
|
|
404
|
+
else if (onExit) {
|
|
405
|
+
onExit();
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
inkExit();
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
// Operation result display
|
|
413
|
+
if (operationResult || operationError) {
|
|
414
|
+
const operationLabel = operations.find((o) => o.key === executingOperation)?.label ||
|
|
415
|
+
"Operation";
|
|
416
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
417
|
+
{ label: "Storage Objects" },
|
|
418
|
+
{
|
|
419
|
+
label: selectedObject?.name || selectedObject?.id || "Object",
|
|
420
|
+
},
|
|
421
|
+
{ label: operationLabel, active: true },
|
|
422
|
+
] }), _jsx(Header, { title: "Operation Result" }), operationResult && _jsx(SuccessMessage, { message: operationResult }), operationError && (_jsx(ErrorMessage, { message: "Operation failed", error: operationError })), _jsx(NavigationTips, { tips: [{ key: "Enter/q/esc", label: "Continue" }] })] }));
|
|
423
|
+
}
|
|
424
|
+
// Download prompt
|
|
425
|
+
if (showDownloadPrompt && selectedObject) {
|
|
426
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
427
|
+
{ label: "Storage Objects" },
|
|
428
|
+
{ label: selectedObject.name || selectedObject.id },
|
|
429
|
+
{ label: "Download", active: true },
|
|
430
|
+
] }), _jsx(Header, { title: "Download Storage Object" }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: colors.text, children: [figures.arrowRight, " Downloading:", " ", _jsx(Text, { color: colors.primary, children: selectedObject.name || selectedObject.id })] }), selectedObject.size_bytes && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.info, " Size: ", formatFileSize(selectedObject.size_bytes)] }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: colors.text, children: "Save to path:" }), _jsxs(Box, { marginTop: 0, children: [_jsxs(Text, { color: colors.primary, children: [figures.pointer, " "] }), _jsx(TextInput, { value: downloadPath, onChange: setDownloadPath, placeholder: "./filename" })] })] }), _jsx(NavigationTips, { tips: [
|
|
431
|
+
{ key: "Enter", label: "Download" },
|
|
432
|
+
{ key: "Esc", label: "Cancel" },
|
|
433
|
+
] })] }));
|
|
434
|
+
}
|
|
435
|
+
// Delete confirmation
|
|
436
|
+
if (showDeleteConfirm && selectedObject) {
|
|
437
|
+
return (_jsx(ConfirmationPrompt, { title: "Delete Storage Object", message: `Are you sure you want to delete "${selectedObject.name || selectedObject.id}"?`, details: "This action cannot be undone.", breadcrumbItems: [
|
|
438
|
+
{ label: "Storage Objects" },
|
|
439
|
+
{ label: selectedObject.name || selectedObject.id },
|
|
440
|
+
{ label: "Delete", active: true },
|
|
441
|
+
], onConfirm: () => {
|
|
442
|
+
setShowDeleteConfirm(false);
|
|
443
|
+
setExecutingOperation("delete");
|
|
444
|
+
executeOperation(selectedObject, "delete");
|
|
445
|
+
}, onCancel: () => {
|
|
446
|
+
setShowDeleteConfirm(false);
|
|
447
|
+
setSelectedObject(null);
|
|
448
|
+
} }));
|
|
449
|
+
}
|
|
450
|
+
// Operation loading state
|
|
451
|
+
if (operationLoading && selectedObject) {
|
|
452
|
+
const operationLabel = operations.find((o) => o.key === executingOperation)?.label ||
|
|
453
|
+
"Operation";
|
|
454
|
+
const messages = {
|
|
455
|
+
delete: "Deleting storage object...",
|
|
456
|
+
download: "Downloading...",
|
|
457
|
+
};
|
|
458
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
|
|
459
|
+
{ label: "Storage Objects" },
|
|
460
|
+
{ label: selectedObject.name || selectedObject.id },
|
|
461
|
+
{ label: operationLabel, active: true },
|
|
462
|
+
] }), _jsx(Header, { title: "Executing Operation" }), _jsx(SpinnerComponent, { message: messages[executingOperation] || "Please wait..." })] }));
|
|
463
|
+
}
|
|
464
|
+
// Loading state
|
|
465
|
+
if (loading && objects.length === 0) {
|
|
466
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Storage Objects", active: true }] }), _jsx(SpinnerComponent, { message: "Loading storage objects..." })] }));
|
|
467
|
+
}
|
|
468
|
+
// Error state
|
|
469
|
+
if (error) {
|
|
470
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Storage Objects", active: true }] }), _jsx(ErrorMessage, { message: "Failed to list storage objects", error: error })] }));
|
|
471
|
+
}
|
|
472
|
+
// Main list view
|
|
473
|
+
return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Storage Objects", active: true }] }), !showPopup && (_jsx(Table, { data: objects, keyExtractor: (obj) => obj.id, selectedIndex: selectedIndex, title: `storage_objects[${totalCount}]`, columns: columns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No storage objects found. Try: rli object upload", " ", "<file>"] }) })), !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 && selectedObjectItem && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedObjectItem, operations: operations.map((op) => ({
|
|
474
|
+
key: op.key,
|
|
475
|
+
label: op.label,
|
|
476
|
+
color: op.color,
|
|
477
|
+
icon: op.icon,
|
|
478
|
+
shortcut: op.key === "view_details"
|
|
479
|
+
? "v"
|
|
480
|
+
: op.key === "download"
|
|
481
|
+
? "w"
|
|
482
|
+
: op.key === "delete"
|
|
483
|
+
? "d"
|
|
484
|
+
: "",
|
|
485
|
+
})), selectedOperation: selectedOperation, onClose: () => setShowPopup(false) }) })), _jsx(NavigationTips, { showArrows: true, tips: [
|
|
486
|
+
{
|
|
487
|
+
icon: `${figures.arrowLeft}${figures.arrowRight}`,
|
|
488
|
+
label: "Page",
|
|
489
|
+
condition: hasMore || hasPrev,
|
|
490
|
+
},
|
|
491
|
+
{ key: "Enter", label: "Details" },
|
|
492
|
+
{ key: "a", label: "Actions" },
|
|
493
|
+
{ key: "Esc", label: "Back" },
|
|
494
|
+
] })] }));
|
|
495
|
+
};
|
|
496
|
+
// Export the UI component for use in the main menu
|
|
497
|
+
export { ListObjectsUI };
|
|
498
|
+
export async function listObjects(options) {
|
|
7
499
|
try {
|
|
8
500
|
const client = getClient();
|
|
9
|
-
// Build params
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (options.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (options.state)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
501
|
+
// Build query params
|
|
502
|
+
const queryParams = {
|
|
503
|
+
limit: DEFAULT_PAGE_SIZE,
|
|
504
|
+
};
|
|
505
|
+
if (options.name) {
|
|
506
|
+
queryParams.name = options.name;
|
|
507
|
+
}
|
|
508
|
+
if (options.contentType) {
|
|
509
|
+
queryParams.content_type = options.contentType;
|
|
510
|
+
}
|
|
511
|
+
if (options.state) {
|
|
512
|
+
queryParams.state = options.state;
|
|
513
|
+
}
|
|
514
|
+
if (options.public !== undefined) {
|
|
515
|
+
queryParams.is_public = options.public;
|
|
516
|
+
}
|
|
517
|
+
// Fetch objects
|
|
518
|
+
const result = await client.objects.list(queryParams);
|
|
519
|
+
// Extract objects array
|
|
28
520
|
const objects = result.objects || [];
|
|
29
521
|
output(objects, { format: options.output, defaultFormat: "json" });
|
|
30
522
|
}
|
|
31
523
|
catch (error) {
|
|
32
|
-
outputError("Failed to list objects", error);
|
|
524
|
+
outputError("Failed to list storage objects", error);
|
|
33
525
|
}
|
|
34
526
|
}
|