@runloop/rl-cli 1.2.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 +28 -8
- 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 +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 +47 -0
- package/package.json +2 -2
|
@@ -4,6 +4,7 @@ import { Box, Text, useInput, useApp } from "ink";
|
|
|
4
4
|
import TextInput from "ink-text-input";
|
|
5
5
|
import figures from "figures";
|
|
6
6
|
import { Breadcrumb } from "./Breadcrumb.js";
|
|
7
|
+
import { NavigationTips } from "./NavigationTips.js";
|
|
7
8
|
import { SpinnerComponent } from "./Spinner.js";
|
|
8
9
|
import { ErrorMessage } from "./ErrorMessage.js";
|
|
9
10
|
import { Table } from "./Table.js";
|
|
@@ -233,6 +234,22 @@ export function ResourceListView({ config }) {
|
|
|
233
234
|
setSearchMode(false);
|
|
234
235
|
setCurrentPage(0);
|
|
235
236
|
setSelectedIndex(0);
|
|
236
|
-
} }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Esc to cancel]"] })] })), !searchMode && searchQuery && (_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: colors.primary, children: [figures.info, " Searching for: "] }), _jsx(Text, { color: colors.warning, bold: true, children: searchQuery }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "(", currentResources.length, " results) [/ to edit, Esc to clear]"] })] }))] })), _jsx(Table, { data: currentResources, keyExtractor: config.keyExtractor, selectedIndex: selectedIndex, title: `${config.resourceNamePlural.toLowerCase()}[${searchQuery ? currentResources.length : resources.length}]`, columns: config.columns }, `table-${searchQuery}-${currentPage}`), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", resources.length] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _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 ", filteredResources.length] }), _jsx(Text, { children: " " }), _jsx(Text, { color: colors.success, children: figures.circleFilled })] }),
|
|
237
|
-
|
|
237
|
+
} }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Esc to cancel]"] })] })), !searchMode && searchQuery && (_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: colors.primary, children: [figures.info, " Searching for: "] }), _jsx(Text, { color: colors.warning, bold: true, children: searchQuery }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "(", currentResources.length, " results) [/ to edit, Esc to clear]"] })] }))] })), _jsx(Table, { data: currentResources, keyExtractor: config.keyExtractor, selectedIndex: selectedIndex, title: `${config.resourceNamePlural.toLowerCase()}[${searchQuery ? currentResources.length : resources.length}]`, columns: config.columns }, `table-${searchQuery}-${currentPage}`), _jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", resources.length] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _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 ", filteredResources.length] }), _jsx(Text, { children: " " }), _jsx(Text, { color: colors.success, children: figures.circleFilled })] }), _jsx(NavigationTips, { showArrows: true, tips: [
|
|
238
|
+
{
|
|
239
|
+
icon: `${figures.arrowLeft}${figures.arrowRight}`,
|
|
240
|
+
label: "Page",
|
|
241
|
+
condition: totalPages > 1,
|
|
242
|
+
},
|
|
243
|
+
{ key: "Enter", label: "Details", condition: !!config.onSelect },
|
|
244
|
+
{
|
|
245
|
+
key: "/",
|
|
246
|
+
label: "Search",
|
|
247
|
+
condition: config.searchConfig?.enabled,
|
|
248
|
+
},
|
|
249
|
+
...(config.additionalShortcuts || []).map((shortcut) => ({
|
|
250
|
+
key: shortcut.key,
|
|
251
|
+
label: shortcut.label,
|
|
252
|
+
})),
|
|
253
|
+
{ key: "Esc", label: "Back" },
|
|
254
|
+
] })] }));
|
|
238
255
|
}
|
|
@@ -68,7 +68,7 @@ export const getStatusDisplay = (status) => {
|
|
|
68
68
|
// === ERROR STATES ===
|
|
69
69
|
case "failure":
|
|
70
70
|
return {
|
|
71
|
-
icon: figures.
|
|
71
|
+
icon: figures.warning,
|
|
72
72
|
color: colors.error,
|
|
73
73
|
text: "FAILED ",
|
|
74
74
|
label: "Failed",
|
|
@@ -76,7 +76,7 @@ export const getStatusDisplay = (status) => {
|
|
|
76
76
|
case "build_failed":
|
|
77
77
|
case "failed":
|
|
78
78
|
return {
|
|
79
|
-
icon: figures.
|
|
79
|
+
icon: figures.warning,
|
|
80
80
|
color: colors.error,
|
|
81
81
|
text: "FAILED ",
|
|
82
82
|
label: "Failed",
|
package/dist/components/Table.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { Box, Text } from "ink";
|
|
4
4
|
import figures from "figures";
|
|
@@ -10,21 +10,19 @@ import { colors, sanitizeWidth } from "../utils/theme.js";
|
|
|
10
10
|
export function Table({ data, columns, selectedIndex = -1, showSelection = true, emptyState, keyExtractor, title, }) {
|
|
11
11
|
// Safety: Handle null/undefined data
|
|
12
12
|
if (!data || !Array.isArray(data)) {
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
if (data.length === 0 && emptyState) {
|
|
16
|
-
return _jsx(_Fragment, { children: emptyState });
|
|
13
|
+
data = [];
|
|
17
14
|
}
|
|
15
|
+
const isEmpty = data.length === 0;
|
|
18
16
|
// Filter visible columns
|
|
19
17
|
const visibleColumns = columns.filter((col) => col.visible !== false);
|
|
20
|
-
return (_jsxs(Box, { flexDirection: "column", children: [title && (_jsx(Box, { paddingX: 1, marginBottom: 0, children: _jsxs(Text, { color: colors.primary, bold: true, children: ["\u256D\u2500 ", title.length > 50 ? title.substring(0, 50) + "..." : title, " ", "─".repeat(10), "\u256E"] }) })), _jsxs(Box, { flexDirection: "column", borderStyle: title ? "single" : "round", borderColor: colors.border, paddingX: 1, children: [_jsxs(Box, { children: [showSelection && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { children: " " })] })), visibleColumns.map((column) => {
|
|
18
|
+
return (_jsxs(Box, { flexDirection: "column", width: "100%", children: [title && (_jsx(Box, { paddingX: 1, marginBottom: 0, children: _jsxs(Text, { color: colors.primary, bold: true, children: ["\u256D\u2500 ", title.length > 50 ? title.substring(0, 50) + "..." : title, " ", "─".repeat(10), "\u256E"] }) })), _jsxs(Box, { flexDirection: "column", width: "100%", borderStyle: title ? "single" : "round", borderColor: colors.border, paddingX: 1, children: [_jsxs(Box, { width: "100%", children: [showSelection && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { children: " " })] })), visibleColumns.map((column) => {
|
|
21
19
|
// Cap column width to prevent Yoga crashes from padEnd creating massive strings
|
|
22
20
|
const safeWidth = sanitizeWidth(column.width, 1, 100);
|
|
23
21
|
return (_jsx(Text, { bold: true, dimColor: true, children: column.label.slice(0, safeWidth).padEnd(safeWidth, " ") }, `header-${column.key}`));
|
|
24
|
-
})] }), data.map((row, index) => {
|
|
22
|
+
}), _jsx(Box, { flexGrow: 1 })] }), isEmpty && (_jsxs(Box, { paddingY: 1, width: "100%", children: [showSelection && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { children: " " })] })), emptyState || (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.info, " No items found"] })), _jsx(Box, { flexGrow: 1 })] })), data.map((row, index) => {
|
|
25
23
|
const isSelected = index === selectedIndex;
|
|
26
24
|
const rowKey = keyExtractor(row);
|
|
27
|
-
return (_jsxs(Box, { children: [showSelection && (_jsxs(_Fragment, { children: [_jsx(Text, { color: isSelected ? colors.primary : colors.textDim, children: isSelected ? figures.pointer : " " }), _jsx(Text, { children: " " })] })), visibleColumns.map((column, colIndex) => (_jsx(React.Fragment, { children: column.render(row, index, isSelected) }, `${rowKey}-${column.key}-${colIndex}`)))] }, rowKey));
|
|
25
|
+
return (_jsxs(Box, { width: "100%", children: [showSelection && (_jsxs(_Fragment, { children: [_jsx(Text, { color: isSelected ? colors.primary : colors.textDim, children: isSelected ? figures.pointer : " " }), _jsx(Text, { children: " " })] })), visibleColumns.map((column, colIndex) => (_jsx(React.Fragment, { children: column.render(row, index, isSelected) }, `${rowKey}-${column.key}-${colIndex}`))), _jsx(Box, { flexGrow: 1 })] }, rowKey));
|
|
28
26
|
})] })] }));
|
|
29
27
|
}
|
|
30
28
|
/**
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import figures from "figures";
|
|
4
|
+
import { colors } from "../../utils/theme.js";
|
|
5
|
+
export const FormActionButton = ({ label, isActive, hint = "[Enter to execute]", }) => {
|
|
6
|
+
return (_jsxs(Box, { marginBottom: 0, children: [_jsxs(Text, { color: isActive ? colors.success : colors.textDim, bold: isActive, children: [isActive ? figures.pointer : " ", " ", label] }), isActive && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", hint] }))] }));
|
|
7
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import figures from "figures";
|
|
4
|
+
import { colors } from "../../utils/theme.js";
|
|
5
|
+
export const FormField = ({ label, isActive, children, hint, error, }) => {
|
|
6
|
+
return (_jsxs(Box, { marginBottom: 0, children: [_jsxs(Text, { color: error ? colors.error : isActive ? colors.primary : colors.textDim, children: [isActive ? figures.pointer : " ", " ", label, ":", " "] }), children, error && (_jsxs(Text, { color: colors.error, children: [" ", figures.cross, " ", error] })), hint && isActive && !error && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", hint] }))] }));
|
|
7
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* FormListManager - List management component for forms (add/edit/delete items)
|
|
4
|
+
*/
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { Box, Text, useInput } from "ink";
|
|
7
|
+
import TextInput from "ink-text-input";
|
|
8
|
+
import figures from "figures";
|
|
9
|
+
import { colors } from "../../utils/theme.js";
|
|
10
|
+
export const FormListManager = ({ title, items, onItemsChange, isActive, isExpanded, onExpandedChange, itemPlaceholder = "item", addLabel = "+ Add new item", collapsedLabel = "items", }) => {
|
|
11
|
+
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
12
|
+
const [inputMode, setInputMode] = React.useState(null);
|
|
13
|
+
const [inputValue, setInputValue] = React.useState("");
|
|
14
|
+
const [editingIndex, setEditingIndex] = React.useState(-1);
|
|
15
|
+
// Selection model: 0 = "Add new", 1..n = Existing items, n+1 = "Done"
|
|
16
|
+
const maxIndex = items.length + 1;
|
|
17
|
+
useInput((input, key) => {
|
|
18
|
+
if (!isExpanded)
|
|
19
|
+
return;
|
|
20
|
+
// Handle input mode (typing)
|
|
21
|
+
if (inputMode) {
|
|
22
|
+
if (key.return && inputValue.trim()) {
|
|
23
|
+
if (inputMode === "add") {
|
|
24
|
+
onItemsChange([...items, inputValue.trim()]);
|
|
25
|
+
}
|
|
26
|
+
else if (inputMode === "edit" && editingIndex >= 0) {
|
|
27
|
+
const newItems = [...items];
|
|
28
|
+
newItems[editingIndex] = inputValue.trim();
|
|
29
|
+
onItemsChange(newItems);
|
|
30
|
+
}
|
|
31
|
+
setInputValue("");
|
|
32
|
+
setInputMode(null);
|
|
33
|
+
setEditingIndex(-1);
|
|
34
|
+
setSelectedIndex(0);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
else if (key.escape) {
|
|
38
|
+
// Cancel input - restore item if editing
|
|
39
|
+
if (inputMode === "edit" && editingIndex >= 0) {
|
|
40
|
+
// Item was already removed, add it back
|
|
41
|
+
const newItems = [...items];
|
|
42
|
+
newItems.splice(editingIndex, 0, inputValue);
|
|
43
|
+
onItemsChange(newItems);
|
|
44
|
+
}
|
|
45
|
+
setInputValue("");
|
|
46
|
+
setInputMode(null);
|
|
47
|
+
setEditingIndex(-1);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// Navigation mode
|
|
53
|
+
if (key.upArrow && selectedIndex > 0) {
|
|
54
|
+
setSelectedIndex(selectedIndex - 1);
|
|
55
|
+
}
|
|
56
|
+
else if (key.downArrow && selectedIndex < maxIndex) {
|
|
57
|
+
setSelectedIndex(selectedIndex + 1);
|
|
58
|
+
}
|
|
59
|
+
else if (key.return) {
|
|
60
|
+
if (selectedIndex === 0) {
|
|
61
|
+
// Add new
|
|
62
|
+
setInputValue("");
|
|
63
|
+
setInputMode("add");
|
|
64
|
+
}
|
|
65
|
+
else if (selectedIndex === maxIndex) {
|
|
66
|
+
// Done - exit expanded mode
|
|
67
|
+
onExpandedChange(false);
|
|
68
|
+
setSelectedIndex(0);
|
|
69
|
+
}
|
|
70
|
+
else if (selectedIndex >= 1 && selectedIndex <= items.length) {
|
|
71
|
+
// Edit existing (selectedIndex - 1 gives array index)
|
|
72
|
+
const itemIndex = selectedIndex - 1;
|
|
73
|
+
const itemToEdit = items[itemIndex];
|
|
74
|
+
setInputValue(itemToEdit);
|
|
75
|
+
setEditingIndex(itemIndex);
|
|
76
|
+
// Remove the item while editing
|
|
77
|
+
const newItems = items.filter((_, i) => i !== itemIndex);
|
|
78
|
+
onItemsChange(newItems);
|
|
79
|
+
setInputMode("edit");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else if ((input === "d" || key.delete) &&
|
|
83
|
+
selectedIndex >= 1 &&
|
|
84
|
+
selectedIndex <= items.length) {
|
|
85
|
+
// Delete selected item
|
|
86
|
+
const itemIndex = selectedIndex - 1;
|
|
87
|
+
const newItems = items.filter((_, i) => i !== itemIndex);
|
|
88
|
+
onItemsChange(newItems);
|
|
89
|
+
// Adjust selection
|
|
90
|
+
if (selectedIndex > newItems.length) {
|
|
91
|
+
setSelectedIndex(Math.max(0, newItems.length));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else if (key.escape || input === "q") {
|
|
95
|
+
// Exit expanded mode
|
|
96
|
+
onExpandedChange(false);
|
|
97
|
+
setSelectedIndex(0);
|
|
98
|
+
}
|
|
99
|
+
}, { isActive: isExpanded });
|
|
100
|
+
// Collapsed view
|
|
101
|
+
if (!isExpanded) {
|
|
102
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 0, children: [_jsxs(Box, { children: [_jsxs(Text, { color: isActive ? colors.primary : colors.textDim, children: [isActive ? figures.pointer : " ", " ", title, ":", " "] }), _jsxs(Text, { color: colors.text, children: [items.length, " ", collapsedLabel] }), isActive && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Enter to manage]"] }))] }), items.length > 0 && (_jsxs(Box, { marginLeft: 3, flexDirection: "column", children: [items.slice(0, 3).map((item, idx) => (_jsxs(Text, { color: colors.textDim, dimColor: true, children: [figures.pointer, " ", item] }, idx))), items.length > 3 && (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["... and ", items.length - 3, " more"] }))] }))] }));
|
|
103
|
+
}
|
|
104
|
+
// Expanded view
|
|
105
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: colors.primary, paddingX: 1, paddingY: 1, marginBottom: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", title] }), inputMode && (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: inputMode === "add" ? colors.success : colors.warning, paddingX: 1, children: [_jsx(Text, { color: inputMode === "add" ? colors.success : colors.warning, bold: true, children: inputMode === "add" ? "Adding New" : "Editing" }), _jsxs(Box, { children: [_jsx(Text, { color: colors.primary, children: "Value: " }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, placeholder: itemPlaceholder })] })] })), !inputMode && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: selectedIndex === 0 ? colors.primary : colors.textDim, children: [selectedIndex === 0 ? figures.pointer : " ", " "] }), _jsx(Text, { color: selectedIndex === 0 ? colors.success : colors.textDim, bold: selectedIndex === 0, children: addLabel })] }), items.length > 0 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: items.map((item, index) => {
|
|
106
|
+
const itemIndex = index + 1;
|
|
107
|
+
const isSelected = selectedIndex === itemIndex;
|
|
108
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? colors.primary : colors.textDim, children: [isSelected ? figures.pointer : " ", " "] }), _jsx(Text, { color: isSelected ? colors.primary : colors.textDim, bold: isSelected, children: item })] }, index));
|
|
109
|
+
}) })), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: selectedIndex === maxIndex ? colors.primary : colors.textDim, children: [selectedIndex === maxIndex ? figures.pointer : " ", " "] }), _jsxs(Text, { color: selectedIndex === maxIndex ? colors.success : colors.textDim, bold: selectedIndex === maxIndex, children: [figures.tick, " Done"] })] })] })), _jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: colors.border, paddingX: 1, children: _jsx(Text, { color: colors.textDim, dimColor: true, children: inputMode
|
|
110
|
+
? "[Enter] Save • [esc] Cancel"
|
|
111
|
+
: `${figures.arrowUp}${figures.arrowDown} Navigate • [Enter] ${selectedIndex === 0 ? "Add" : selectedIndex === maxIndex ? "Done" : "Edit"} • [d] Delete • [esc] Back` }) })] }));
|
|
112
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* FormSelect - Left/right arrow selection field for forms
|
|
4
|
+
*/
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { Text } from "ink";
|
|
7
|
+
import figures from "figures";
|
|
8
|
+
import { FormField } from "./FormField.js";
|
|
9
|
+
import { colors } from "../../utils/theme.js";
|
|
10
|
+
export function FormSelect({ label, value, options, onChange, isActive, getDisplayLabel, }) {
|
|
11
|
+
const displayValue = getDisplayLabel ? getDisplayLabel(value) : value;
|
|
12
|
+
return (_jsx(FormField, { label: label, isActive: isActive, hint: `[${figures.arrowLeft}${figures.arrowRight} to change]`, children: _jsx(Text, { color: isActive ? colors.primary : colors.text, bold: isActive, children: displayValue || "(none)" }) }));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Helper hook to handle left/right arrow navigation for select fields
|
|
16
|
+
*/
|
|
17
|
+
export function useFormSelectNavigation(value, options, onChange, isActive) {
|
|
18
|
+
const handleInput = React.useCallback((input, key) => {
|
|
19
|
+
if (!isActive)
|
|
20
|
+
return false;
|
|
21
|
+
if (key.leftArrow || key.rightArrow) {
|
|
22
|
+
const currentIndex = options.indexOf(value);
|
|
23
|
+
const safeIndex = currentIndex === -1 ? 0 : currentIndex;
|
|
24
|
+
// Wrap around: left from first goes to last, right from last goes to first
|
|
25
|
+
const newIndex = key.leftArrow
|
|
26
|
+
? (safeIndex - 1 + options.length) % options.length
|
|
27
|
+
: (safeIndex + 1) % options.length;
|
|
28
|
+
onChange(options[newIndex]);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}, [value, options, onChange, isActive]);
|
|
33
|
+
return handleInput;
|
|
34
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from "ink";
|
|
3
|
+
import TextInput from "ink-text-input";
|
|
4
|
+
import { FormField } from "./FormField.js";
|
|
5
|
+
import { colors } from "../../utils/theme.js";
|
|
6
|
+
export const FormTextInput = ({ label, value, onChange, isActive, placeholder, error, }) => {
|
|
7
|
+
return (_jsx(FormField, { label: label, isActive: isActive, error: error, children: isActive ? (_jsx(TextInput, { value: value, onChange: onChange, placeholder: placeholder })) : (_jsx(Text, { color: error ? colors.error : colors.text, children: value || "(empty)" })) }));
|
|
8
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form Components - Reusable form primitives for create/edit pages
|
|
3
|
+
*/
|
|
4
|
+
export { FormField } from "./FormField.js";
|
|
5
|
+
export { FormTextInput } from "./FormTextInput.js";
|
|
6
|
+
export { FormSelect, useFormSelectNavigation, } from "./FormSelect.js";
|
|
7
|
+
export { FormActionButton, } from "./FormActionButton.js";
|
|
8
|
+
export { FormListManager, } from "./FormListManager.js";
|
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { useStdout } from "ink";
|
|
3
|
+
/**
|
|
4
|
+
* Get safe terminal dimensions with bounds checking
|
|
5
|
+
*/
|
|
6
|
+
function getSafeDimensions(stdout) {
|
|
7
|
+
const sampledWidth = stdout?.columns && stdout.columns > 0 ? stdout.columns : 120;
|
|
8
|
+
const sampledHeight = stdout?.rows && stdout.rows > 0 ? stdout.rows : 30;
|
|
9
|
+
return {
|
|
10
|
+
width: Math.max(80, Math.min(300, sampledWidth)),
|
|
11
|
+
height: Math.max(20, Math.min(100, sampledHeight)),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
3
14
|
/**
|
|
4
15
|
* Custom hook to calculate available viewport height for content rendering.
|
|
5
16
|
* Ensures consistent layout calculations across all CLI screens and prevents overflow.
|
|
17
|
+
* Responds to terminal resize events to update layout dynamically.
|
|
6
18
|
*
|
|
7
19
|
* @param options Configuration for viewport calculation
|
|
8
20
|
* @returns Viewport dimensions including available height for content
|
|
@@ -16,29 +28,35 @@ import { useStdout } from "ink";
|
|
|
16
28
|
export function useViewportHeight(options = {}) {
|
|
17
29
|
const { overhead = 0, minHeight = 5, maxHeight = 100 } = options;
|
|
18
30
|
const { stdout } = useStdout();
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
// Use state to track dimensions so we can respond to resize events
|
|
32
|
+
const [dimensions, setDimensions] = React.useState(() => getSafeDimensions(stdout));
|
|
33
|
+
// Listen for terminal resize events
|
|
34
|
+
React.useEffect(() => {
|
|
35
|
+
if (!stdout)
|
|
36
|
+
return;
|
|
37
|
+
const handleResize = () => {
|
|
38
|
+
const newDimensions = getSafeDimensions(stdout);
|
|
39
|
+
setDimensions((prev) => {
|
|
40
|
+
// Only update if dimensions actually changed
|
|
41
|
+
if (prev.width !== newDimensions.width ||
|
|
42
|
+
prev.height !== newDimensions.height) {
|
|
43
|
+
return newDimensions;
|
|
44
|
+
}
|
|
45
|
+
return prev;
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
// Listen for resize events
|
|
49
|
+
stdout.on("resize", handleResize);
|
|
50
|
+
// Also check dimensions on mount in case they differ from initial
|
|
51
|
+
handleResize();
|
|
52
|
+
return () => {
|
|
53
|
+
stdout.off("resize", handleResize);
|
|
35
54
|
};
|
|
36
|
-
}
|
|
37
|
-
const terminalHeight = dimensions.
|
|
38
|
-
const terminalWidth = dimensions.
|
|
55
|
+
}, [stdout]);
|
|
56
|
+
const terminalHeight = dimensions.height;
|
|
57
|
+
const terminalWidth = dimensions.width;
|
|
39
58
|
// Calculate viewport height with bounds
|
|
40
59
|
const viewportHeight = Math.max(minHeight, Math.min(maxHeight, terminalHeight - overhead));
|
|
41
|
-
// Removed console.logs to prevent rendering interference
|
|
42
60
|
return {
|
|
43
61
|
viewportHeight,
|
|
44
62
|
terminalHeight,
|
package/dist/router/Router.js
CHANGED
|
@@ -8,6 +8,8 @@ import { useNavigation } from "../store/navigationStore.js";
|
|
|
8
8
|
import { useDevboxStore } from "../store/devboxStore.js";
|
|
9
9
|
import { useBlueprintStore } from "../store/blueprintStore.js";
|
|
10
10
|
import { useSnapshotStore } from "../store/snapshotStore.js";
|
|
11
|
+
import { useNetworkPolicyStore } from "../store/networkPolicyStore.js";
|
|
12
|
+
import { useObjectStore } from "../store/objectStore.js";
|
|
11
13
|
import { ErrorBoundary } from "../components/ErrorBoundary.js";
|
|
12
14
|
// Import screen components
|
|
13
15
|
import { MenuScreen } from "../screens/MenuScreen.js";
|
|
@@ -16,8 +18,15 @@ import { DevboxDetailScreen } from "../screens/DevboxDetailScreen.js";
|
|
|
16
18
|
import { DevboxActionsScreen } from "../screens/DevboxActionsScreen.js";
|
|
17
19
|
import { DevboxCreateScreen } from "../screens/DevboxCreateScreen.js";
|
|
18
20
|
import { BlueprintListScreen } from "../screens/BlueprintListScreen.js";
|
|
21
|
+
import { BlueprintDetailScreen } from "../screens/BlueprintDetailScreen.js";
|
|
19
22
|
import { BlueprintLogsScreen } from "../screens/BlueprintLogsScreen.js";
|
|
20
23
|
import { SnapshotListScreen } from "../screens/SnapshotListScreen.js";
|
|
24
|
+
import { SnapshotDetailScreen } from "../screens/SnapshotDetailScreen.js";
|
|
25
|
+
import { NetworkPolicyListScreen } from "../screens/NetworkPolicyListScreen.js";
|
|
26
|
+
import { NetworkPolicyDetailScreen } from "../screens/NetworkPolicyDetailScreen.js";
|
|
27
|
+
import { NetworkPolicyCreateScreen } from "../screens/NetworkPolicyCreateScreen.js";
|
|
28
|
+
import { ObjectListScreen } from "../screens/ObjectListScreen.js";
|
|
29
|
+
import { ObjectDetailScreen } from "../screens/ObjectDetailScreen.js";
|
|
21
30
|
import { SSHSessionScreen } from "../screens/SSHSessionScreen.js";
|
|
22
31
|
/**
|
|
23
32
|
* Router component that renders the current screen
|
|
@@ -58,6 +67,19 @@ export function Router() {
|
|
|
58
67
|
useSnapshotStore.getState().clearAll();
|
|
59
68
|
}
|
|
60
69
|
break;
|
|
70
|
+
case "network-policy-list":
|
|
71
|
+
case "network-policy-detail":
|
|
72
|
+
case "network-policy-create":
|
|
73
|
+
if (!currentScreen.startsWith("network-policy")) {
|
|
74
|
+
useNetworkPolicyStore.getState().clearAll();
|
|
75
|
+
}
|
|
76
|
+
break;
|
|
77
|
+
case "object-list":
|
|
78
|
+
case "object-detail":
|
|
79
|
+
if (!currentScreen.startsWith("object")) {
|
|
80
|
+
useObjectStore.getState().clearAll();
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
61
83
|
}
|
|
62
84
|
}
|
|
63
85
|
prevScreenRef.current = currentScreen;
|
|
@@ -66,5 +88,5 @@ export function Router() {
|
|
|
66
88
|
// and mount new component, preventing race conditions during screen transitions.
|
|
67
89
|
// The key ensures React treats this as a completely new component tree.
|
|
68
90
|
// Wrap in ErrorBoundary to catch any Yoga WASM errors gracefully.
|
|
69
|
-
return (_jsxs(ErrorBoundary, { children: [currentScreen === "menu" && (_jsx(MenuScreen, { ...params }, currentScreen)), currentScreen === "devbox-list" && (_jsx(DevboxListScreen, { ...params }, currentScreen)), currentScreen === "devbox-detail" && (_jsx(DevboxDetailScreen, { ...params }, currentScreen)), currentScreen === "devbox-actions" && (_jsx(DevboxActionsScreen, { ...params }, currentScreen)), currentScreen === "devbox-create" && (_jsx(DevboxCreateScreen, { ...params }, currentScreen)), currentScreen === "blueprint-list" && (_jsx(BlueprintListScreen, { ...params }, currentScreen)), currentScreen === "blueprint-detail" && (_jsx(
|
|
91
|
+
return (_jsxs(ErrorBoundary, { children: [currentScreen === "menu" && (_jsx(MenuScreen, { ...params }, currentScreen)), currentScreen === "devbox-list" && (_jsx(DevboxListScreen, { ...params }, currentScreen)), currentScreen === "devbox-detail" && (_jsx(DevboxDetailScreen, { ...params }, currentScreen)), currentScreen === "devbox-actions" && (_jsx(DevboxActionsScreen, { ...params }, currentScreen)), currentScreen === "devbox-create" && (_jsx(DevboxCreateScreen, { ...params }, currentScreen)), currentScreen === "blueprint-list" && (_jsx(BlueprintListScreen, { ...params }, currentScreen)), currentScreen === "blueprint-detail" && (_jsx(BlueprintDetailScreen, { ...params }, currentScreen)), currentScreen === "blueprint-logs" && (_jsx(BlueprintLogsScreen, { ...params }, currentScreen)), currentScreen === "snapshot-list" && (_jsx(SnapshotListScreen, { ...params }, currentScreen)), currentScreen === "snapshot-detail" && (_jsx(SnapshotDetailScreen, { ...params }, currentScreen)), currentScreen === "network-policy-list" && (_jsx(NetworkPolicyListScreen, { ...params }, currentScreen)), currentScreen === "network-policy-detail" && (_jsx(NetworkPolicyDetailScreen, { ...params }, currentScreen)), currentScreen === "network-policy-create" && (_jsx(NetworkPolicyCreateScreen, { ...params }, currentScreen)), currentScreen === "object-list" && (_jsx(ObjectListScreen, { ...params }, currentScreen)), currentScreen === "object-detail" && (_jsx(ObjectDetailScreen, { ...params }, currentScreen)), currentScreen === "ssh-session" && (_jsx(SSHSessionScreen, { ...params }, currentScreen))] }, `boundary-${currentScreen}`));
|
|
70
92
|
}
|