openk8s 1.0.4 → 1.1.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 +1 -1
- package/package.json +11 -10
- package/src/app/__tests__/app-state.test.ts +47 -5
- package/src/app/__tests__/utils.test.ts +5 -14
- package/src/app/app-actions.ts +5 -0
- package/src/app/app-state.ts +11 -2
- package/src/app/app.tsx +57 -46
- package/src/app/components/detail-sections.tsx +2 -2
- package/src/app/components/footer.tsx +23 -30
- package/src/app/components/inspector.tsx +28 -43
- package/src/app/components/kind-rows.tsx +1 -1
- package/src/app/components/notification-card.tsx +145 -0
- package/src/app/components/notification-tray.tsx +19 -38
- package/src/app/components/overlays/delete-confirm-overlay.tsx +15 -17
- package/src/app/components/overlays/helm-rollback-overlay.tsx +12 -66
- package/src/app/components/overlays/help-overlay.tsx +173 -0
- package/src/app/components/overlays/index.ts +5 -2
- package/src/app/components/overlays/logs-dialog.tsx +50 -80
- package/src/app/components/overlays/notification-history-overlay.tsx +79 -0
- package/src/app/components/overlays/port-forward-list-overlay.tsx +85 -0
- package/src/app/components/overlays/port-forward-overlay.tsx +16 -56
- package/src/app/components/overlays/scale-dialog.tsx +12 -67
- package/src/app/components/overlays/select-overlay.tsx +5 -14
- package/src/app/components/overlays/shared.tsx +85 -6
- package/src/app/components/resource-rows.tsx +1 -1
- package/src/app/constants.ts +24 -0
- package/src/app/hooks/keyboard/filter-handlers.ts +2 -1
- package/src/app/hooks/keyboard/global-handlers.ts +32 -11
- package/src/app/hooks/keyboard/helm-handlers.ts +14 -9
- package/src/app/hooks/keyboard/keys.ts +18 -0
- package/src/app/hooks/keyboard/logs-handlers.ts +3 -1
- package/src/app/hooks/keyboard/navigation-handlers.ts +5 -4
- package/src/app/hooks/keyboard/overlay-handlers.ts +11 -7
- package/src/app/hooks/keyboard/port-forward-handlers.ts +19 -14
- package/src/app/hooks/keyboard/shell-edit-handlers.ts +45 -17
- package/src/app/hooks/use-app-keyboard.ts +22 -2
- package/src/app/hooks/use-app-side-effects.ts +10 -7
- package/src/app/hooks/use-clipboard.ts +8 -10
- package/src/app/hooks/use-data-fetching.ts +11 -5
- package/src/app/hooks/use-log-stream.ts +8 -4
- package/src/app/hooks/use-notifications.ts +92 -0
- package/src/app/hooks/use-port-forward.ts +19 -4
- package/src/app/persistence.ts +7 -3
- package/src/app/syntax-theme.ts +31 -0
- package/src/app/theme.ts +2 -3
- package/src/app/use-footer-hints.ts +21 -16
- package/src/app/utils.ts +1 -9
- package/src/assets/tree-sitter/yaml/highlights.scm +79 -0
- package/src/assets/tree-sitter/yaml/tree-sitter-yaml.wasm +0 -0
- package/src/index.tsx +22 -2
- package/src/lib/k8s/__tests__/k8s-format.test.ts +17 -0
- package/src/lib/k8s/__tests__/resource-parser.test.ts +40 -2
- package/src/lib/k8s/detail-builders/event-builder.ts +17 -11
- package/src/lib/k8s/detail-builders/hpa-cronjob-builder.ts +34 -31
- package/src/lib/k8s/detail-builders/node-builder.ts +22 -11
- package/src/lib/k8s/detail-builders/overview-builder.ts +4 -17
- package/src/lib/k8s/detail-builders/pod-builder.ts +52 -24
- package/src/lib/k8s/detail-builders/rbac-builder.ts +20 -29
- package/src/lib/k8s/k8s-format.ts +14 -9
- package/src/lib/k8s/resource-detail-builder.ts +1 -1
- package/src/lib/k8s/resource-parser.ts +17 -2
- package/src/lib/k8s/types.ts +10 -1
- package/src/lib/kubectl/__tests__/metrics-utils.test.ts +9 -0
- package/src/lib/kubectl/kubectl-helpers.ts +16 -11
- package/src/lib/kubectl/kubectl-service.ts +39 -39
- package/src/lib/kubectl/kubectl-types.ts +10 -1
- package/src/lib/kubectl/metrics-utils.ts +21 -7
- package/src/lib/kubectl/spawn-utils.ts +50 -11
- package/src/app/__tests__/components/inspector-tokens.test.ts +0 -101
- package/src/app/components/inspector-tokens.ts +0 -93
- package/src/app/components/port-forwards-tray.tsx +0 -57
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect, useMemo, useState } from "react";
|
|
2
|
+
import type { MutableRefObject } from "react";
|
|
2
3
|
import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core";
|
|
3
4
|
|
|
4
5
|
import {
|
|
@@ -7,22 +8,13 @@ import {
|
|
|
7
8
|
KEY_HINT,
|
|
8
9
|
OVERLAY_SURFACE,
|
|
9
10
|
PANEL_BORDER_ACTIVE,
|
|
10
|
-
|
|
11
|
+
ATTR_DIM,
|
|
11
12
|
TEXT_SUBTLE,
|
|
12
13
|
YAML_COMMENT,
|
|
13
14
|
toneStyles,
|
|
14
15
|
} from "../../theme";
|
|
15
16
|
import type { LoadStatus, LogOptions } from "../../../lib/k8s/types";
|
|
16
|
-
|
|
17
|
-
function logLineColor(line: string): string {
|
|
18
|
-
const lower = line.toLowerCase();
|
|
19
|
-
if (/\b(error|err|fatal|panic|critical)\b/.test(lower)) return toneStyles("danger").fg;
|
|
20
|
-
if (/\bwarn(?:ing)?\b/.test(lower)) return toneStyles("warning").fg;
|
|
21
|
-
if (/\binfo(?:rmation)?\b/.test(lower)) return toneStyles("info").fg;
|
|
22
|
-
if (/\bdebug\b/.test(lower)) return TEXT_SUBTLE;
|
|
23
|
-
if (/\btrace\b/.test(lower)) return YAML_COMMENT;
|
|
24
|
-
return TEXT_SUBTLE;
|
|
25
|
-
}
|
|
17
|
+
import { KeyHintRow, TextInput } from "./shared";
|
|
26
18
|
|
|
27
19
|
export interface LogsDialogActions {
|
|
28
20
|
nextMatch: () => void;
|
|
@@ -43,7 +35,7 @@ export interface LogsDialogProps {
|
|
|
43
35
|
searchMode?: boolean | undefined;
|
|
44
36
|
searchText?: string | undefined;
|
|
45
37
|
onSearchChange?: ((value: string) => void) | undefined;
|
|
46
|
-
actionsRef?:
|
|
38
|
+
actionsRef?: MutableRefObject<LogsDialogActions | null> | undefined;
|
|
47
39
|
}
|
|
48
40
|
|
|
49
41
|
export function LogsDialog({
|
|
@@ -62,7 +54,7 @@ export function LogsDialog({
|
|
|
62
54
|
onSearchChange,
|
|
63
55
|
actionsRef,
|
|
64
56
|
}: LogsDialogProps) {
|
|
65
|
-
const lines = logsText ? logsText.split("\n") : [];
|
|
57
|
+
const lines = useMemo(() => (logsText ? logsText.split("\n") : []), [logsText]);
|
|
66
58
|
const palette = toneStyles("info");
|
|
67
59
|
const [currentMatchIdx, setCurrentMatchIdx] = useState(0);
|
|
68
60
|
|
|
@@ -73,18 +65,31 @@ export function LogsDialog({
|
|
|
73
65
|
if (line.toLowerCase().includes(lower)) acc.push(i);
|
|
74
66
|
return acc;
|
|
75
67
|
}, []);
|
|
76
|
-
|
|
77
|
-
}, [logsText, searchText]);
|
|
68
|
+
}, [lines, searchText]);
|
|
78
69
|
|
|
70
|
+
// Reset to first match when search text changes, and clamp when matches shrink (e.g. new logs stream in)
|
|
79
71
|
useEffect(() => {
|
|
80
|
-
if (!searchText)
|
|
72
|
+
if (!searchText) {
|
|
73
|
+
setCurrentMatchIdx(0);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
81
76
|
setCurrentMatchIdx(0);
|
|
82
77
|
const firstLine = matchIndices[0];
|
|
83
78
|
if (firstLine !== undefined) {
|
|
84
79
|
scrollRef.current?.scrollTo(firstLine);
|
|
85
80
|
}
|
|
86
|
-
|
|
87
|
-
|
|
81
|
+
}, [searchText, matchIndices]);
|
|
82
|
+
|
|
83
|
+
// Keep currentMatchIdx in range when matchIndices shrinks
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (matchIndices.length === 0) {
|
|
86
|
+
if (currentMatchIdx !== 0) setCurrentMatchIdx(0);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (currentMatchIdx >= matchIndices.length) {
|
|
90
|
+
setCurrentMatchIdx(matchIndices.length - 1);
|
|
91
|
+
}
|
|
92
|
+
}, [matchIndices, currentMatchIdx]);
|
|
88
93
|
|
|
89
94
|
const nextMatch = () => {
|
|
90
95
|
if (matchIndices.length === 0) return;
|
|
@@ -102,9 +107,11 @@ export function LogsDialog({
|
|
|
102
107
|
if (lineIdx !== undefined) scrollRef.current?.scrollTo(lineIdx);
|
|
103
108
|
};
|
|
104
109
|
|
|
105
|
-
|
|
106
|
-
actionsRef
|
|
107
|
-
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (actionsRef) {
|
|
112
|
+
actionsRef.current = { nextMatch, prevMatch };
|
|
113
|
+
}
|
|
114
|
+
});
|
|
108
115
|
|
|
109
116
|
const warningFg = toneStyles("warning").fg;
|
|
110
117
|
const multiContainer = (containers?.length ?? 0) > 1;
|
|
@@ -131,22 +138,22 @@ export function LogsDialog({
|
|
|
131
138
|
>
|
|
132
139
|
{/* Options status bar */}
|
|
133
140
|
<box style={{ flexDirection: "row", columnGap: 2, marginBottom: 0 }}>
|
|
134
|
-
<text fg={logsOptions.previous ? warningFg : TEXT_SUBTLE} attributes={
|
|
141
|
+
<text fg={logsOptions.previous ? warningFg : TEXT_SUBTLE} attributes={ATTR_DIM}>
|
|
135
142
|
<span fg={KEY_HINT}>P</span>
|
|
136
143
|
{` prev:${logsOptions.previous ? "on" : "off"}`}
|
|
137
144
|
</text>
|
|
138
|
-
<text fg={TEXT_SUBTLE} attributes={
|
|
145
|
+
<text fg={TEXT_SUBTLE} attributes={ATTR_DIM}>
|
|
139
146
|
<span fg={KEY_HINT}>T</span>
|
|
140
147
|
{` tail:${logsOptions.tail}`}
|
|
141
148
|
</text>
|
|
142
149
|
{logsOptions.since ? (
|
|
143
|
-
<text fg={TEXT_SUBTLE} attributes={
|
|
150
|
+
<text fg={TEXT_SUBTLE} attributes={ATTR_DIM}>
|
|
144
151
|
<span fg={KEY_HINT}>S</span>
|
|
145
152
|
{` since:${logsOptions.since}`}
|
|
146
153
|
</text>
|
|
147
154
|
) : undefined}
|
|
148
155
|
{multiContainer ? (
|
|
149
|
-
<text fg={TEXT_SUBTLE} attributes={
|
|
156
|
+
<text fg={TEXT_SUBTLE} attributes={ATTR_DIM}>
|
|
150
157
|
<span fg={KEY_HINT}>C</span>
|
|
151
158
|
{logsContainer ? ` ctr:${logsContainer}` : " all-ctrs"}
|
|
152
159
|
</text>
|
|
@@ -184,7 +191,7 @@ export function LogsDialog({
|
|
|
184
191
|
const matched = line.slice(matchStart, matchStart + searchText.length);
|
|
185
192
|
const after = line.slice(matchStart + searchText.length);
|
|
186
193
|
return (
|
|
187
|
-
<text key={`${index}:${line}`} fg={
|
|
194
|
+
<text key={`${index}:${line}`} fg={TEXT_SUBTLE}>
|
|
188
195
|
{before}
|
|
189
196
|
<span fg={warningFg} attributes={isCurrentMatch ? TextAttributes.BOLD : TextAttributes.DIM}>
|
|
190
197
|
{matched}
|
|
@@ -194,7 +201,7 @@ export function LogsDialog({
|
|
|
194
201
|
);
|
|
195
202
|
}
|
|
196
203
|
return (
|
|
197
|
-
<text key={`${index}:${line}`} fg={
|
|
204
|
+
<text key={`${index}:${line}`} fg={TEXT_SUBTLE}>
|
|
198
205
|
{line}
|
|
199
206
|
</text>
|
|
200
207
|
);
|
|
@@ -205,33 +212,12 @@ export function LogsDialog({
|
|
|
205
212
|
|
|
206
213
|
{/* Option input row */}
|
|
207
214
|
{inputMode ? (
|
|
208
|
-
<
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
paddingLeft: 1,
|
|
215
|
-
paddingRight: 1,
|
|
216
|
-
marginTop: 1,
|
|
217
|
-
height: 3,
|
|
218
|
-
justifyContent: "center",
|
|
219
|
-
alignItems: "center",
|
|
220
|
-
flexDirection: "row",
|
|
221
|
-
}}
|
|
222
|
-
>
|
|
223
|
-
<text fg={TEXT_SUBTLE} attributes={TextAttributes.DIM}>
|
|
224
|
-
{inputMode === "tail" ? "Tail lines:" : "Since (e.g. 1h, 30m):"}
|
|
225
|
-
</text>
|
|
226
|
-
<input
|
|
227
|
-
focused
|
|
228
|
-
value={inputValue ?? ""}
|
|
229
|
-
placeholder={inputMode === "tail" ? "120" : "1h"}
|
|
230
|
-
style={{ flexGrow: 1, marginLeft: 1 }}
|
|
231
|
-
onInput={onOptionInputChange}
|
|
232
|
-
onChange={onOptionInputChange}
|
|
233
|
-
/>
|
|
234
|
-
</box>
|
|
215
|
+
<TextInput
|
|
216
|
+
label={inputMode === "tail" ? "Tail lines:" : "Since (e.g. 1h, 30m):"}
|
|
217
|
+
value={inputValue ?? ""}
|
|
218
|
+
placeholder={inputMode === "tail" ? "120" : "1h"}
|
|
219
|
+
onChange={onOptionInputChange ?? (() => undefined)}
|
|
220
|
+
/>
|
|
235
221
|
) : undefined}
|
|
236
222
|
|
|
237
223
|
{/* Search input row */}
|
|
@@ -261,11 +247,11 @@ export function LogsDialog({
|
|
|
261
247
|
onChange={onSearchChange}
|
|
262
248
|
/>
|
|
263
249
|
{matchIndices.length > 0 ? (
|
|
264
|
-
<text fg={TEXT_SUBTLE} attributes={
|
|
250
|
+
<text fg={TEXT_SUBTLE} attributes={ATTR_DIM}>
|
|
265
251
|
{` ${currentMatchIdx + 1}/${matchIndices.length}`}
|
|
266
252
|
</text>
|
|
267
253
|
) : searchText ? (
|
|
268
|
-
<text fg={toneStyles("danger").fg} attributes={
|
|
254
|
+
<text fg={toneStyles("danger").fg} attributes={ATTR_DIM}>
|
|
269
255
|
{" no match"}
|
|
270
256
|
</text>
|
|
271
257
|
) : undefined}
|
|
@@ -273,31 +259,15 @@ export function LogsDialog({
|
|
|
273
259
|
) : undefined}
|
|
274
260
|
|
|
275
261
|
{/* Hints */}
|
|
276
|
-
<
|
|
277
|
-
{
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
<>
|
|
284
|
-
<text fg={TEXT_SUBTLE} attributes={TEXT_MUTED}>
|
|
285
|
-
<span fg={KEY_HINT}>{"/"}</span>
|
|
286
|
-
{" search"}
|
|
287
|
-
</text>
|
|
288
|
-
<text fg={TEXT_SUBTLE} attributes={TEXT_MUTED}>
|
|
289
|
-
<span fg={KEY_HINT}>{"↑↓"}</span>
|
|
290
|
-
{" scroll"}
|
|
291
|
-
</text>
|
|
292
|
-
</>
|
|
293
|
-
)}
|
|
294
|
-
<text fg={TEXT_SUBTLE} attributes={TEXT_MUTED}>
|
|
295
|
-
<span fg={KEY_HINT}>{GLYPHS.esc}</span>
|
|
296
|
-
{" close"}
|
|
297
|
-
</text>
|
|
298
|
-
</box>
|
|
262
|
+
<KeyHintRow
|
|
263
|
+
hints={
|
|
264
|
+
inputMode || searchMode
|
|
265
|
+
? [[GLYPHS.enter, "confirm"], [GLYPHS.esc, "close"]]
|
|
266
|
+
: [["/", "search"], ["↑↓", "scroll"], [GLYPHS.esc, "close"]]
|
|
267
|
+
}
|
|
268
|
+
/>
|
|
299
269
|
</box>
|
|
300
270
|
);
|
|
301
271
|
}
|
|
302
272
|
|
|
303
|
-
|
|
273
|
+
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { TextAttributes } from "@opentui/core";
|
|
2
|
+
|
|
3
|
+
import { GLYPHS, OVERLAY_SURFACE, PANEL_BORDER_ACTIVE, TEXT_PRIMARY, TEXT_SUBTLE, ATTR_DIM, toneStyles } from "../../theme";
|
|
4
|
+
import { DIALOG_LEFT, DIALOG_TOP, DIALOG_WIDTH, KeyHintRow } from "./shared";
|
|
5
|
+
import type { Notification } from "../../../lib/k8s/types";
|
|
6
|
+
|
|
7
|
+
export interface NotificationHistoryOverlayProps {
|
|
8
|
+
history: Notification[];
|
|
9
|
+
onClear: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const TONE_ICONS: Record<Notification["tone"], string> = {
|
|
13
|
+
info: GLYPHS.dotEmpty,
|
|
14
|
+
success: GLYPHS.dot,
|
|
15
|
+
warning: GLYPHS.warn,
|
|
16
|
+
danger: GLYPHS.cross,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function formatTimeAgo(timestamp: number): string {
|
|
20
|
+
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
|
21
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
22
|
+
const minutes = Math.floor(seconds / 60);
|
|
23
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
24
|
+
const hours = Math.floor(minutes / 60);
|
|
25
|
+
if (hours < 24) return `${hours}h ago`;
|
|
26
|
+
const days = Math.floor(hours / 24);
|
|
27
|
+
return `${days}d ago`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function NotificationHistoryOverlay({ history, onClear }: NotificationHistoryOverlayProps) {
|
|
31
|
+
return (
|
|
32
|
+
<box
|
|
33
|
+
style={{
|
|
34
|
+
position: "absolute",
|
|
35
|
+
left: DIALOG_LEFT,
|
|
36
|
+
top: DIALOG_TOP,
|
|
37
|
+
width: DIALOG_WIDTH,
|
|
38
|
+
height: "60%",
|
|
39
|
+
border: true,
|
|
40
|
+
borderColor: PANEL_BORDER_ACTIVE,
|
|
41
|
+
borderStyle: "rounded",
|
|
42
|
+
backgroundColor: OVERLAY_SURFACE,
|
|
43
|
+
padding: 1,
|
|
44
|
+
flexDirection: "column",
|
|
45
|
+
}}
|
|
46
|
+
title="Notification History"
|
|
47
|
+
>
|
|
48
|
+
{history.length === 0 ? (
|
|
49
|
+
<text fg={TEXT_SUBTLE} attributes={TextAttributes.DIM}>
|
|
50
|
+
No notification history
|
|
51
|
+
</text>
|
|
52
|
+
) : (
|
|
53
|
+
<scrollbox style={{ flexGrow: 1 }} focused>
|
|
54
|
+
<box style={{ flexDirection: "column", width: "100%" }}>
|
|
55
|
+
{history.map((notification, index) => {
|
|
56
|
+
const palette = toneStyles(notification.tone);
|
|
57
|
+
const icon = TONE_ICONS[notification.tone];
|
|
58
|
+
return (
|
|
59
|
+
<box
|
|
60
|
+
key={`${index}:${notification.id}`}
|
|
61
|
+
style={{ flexDirection: "row", marginBottom: 0, width: "100%" }}
|
|
62
|
+
>
|
|
63
|
+
<text fg={palette.fg}>{icon}</text>
|
|
64
|
+
<text fg={TEXT_PRIMARY} wrapMode="word" style={{ flexGrow: 1, marginLeft: 1 }}>
|
|
65
|
+
{notification.message}
|
|
66
|
+
</text>
|
|
67
|
+
<text fg={TEXT_SUBTLE} attributes={ATTR_DIM}>
|
|
68
|
+
{formatTimeAgo(notification.createdAt)}
|
|
69
|
+
</text>
|
|
70
|
+
</box>
|
|
71
|
+
);
|
|
72
|
+
})}
|
|
73
|
+
</box>
|
|
74
|
+
</scrollbox>
|
|
75
|
+
)}
|
|
76
|
+
<KeyHintRow hints={[["C", "clear history"], [GLYPHS.esc, "close"]]} />
|
|
77
|
+
</box>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { TextAttributes } from "@opentui/core";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
GLYPHS,
|
|
5
|
+
OVERLAY_SURFACE,
|
|
6
|
+
PANEL_BORDER_ACTIVE,
|
|
7
|
+
ROW_SELECTED,
|
|
8
|
+
TEXT_PRIMARY,
|
|
9
|
+
TEXT_SUBTLE,
|
|
10
|
+
} from "../../theme";
|
|
11
|
+
import { DIALOG_LEFT, DIALOG_TOP, DIALOG_WIDTH, KeyHintRow } from "./shared";
|
|
12
|
+
import { statusLine } from "../../utils";
|
|
13
|
+
import type { ActivePortForward } from "../../../lib/k8s/types";
|
|
14
|
+
|
|
15
|
+
export interface PortForwardListOverlayProps {
|
|
16
|
+
forwards: ActivePortForward[];
|
|
17
|
+
selectedIndex: number;
|
|
18
|
+
onChange: (index: number) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function PortForwardListOverlay({ forwards, selectedIndex, onChange }: PortForwardListOverlayProps) {
|
|
22
|
+
const clampedIndex = forwards.length > 0 ? Math.min(selectedIndex, forwards.length - 1) : 0;
|
|
23
|
+
|
|
24
|
+
const items = forwards.map((f) => {
|
|
25
|
+
const isReady = f.status === "ready";
|
|
26
|
+
const dot = isReady ? GLYPHS.dot : GLYPHS.dotEmpty;
|
|
27
|
+
return {
|
|
28
|
+
name: `${dot} ${statusLine({ ref: f.ref })} localhost:${f.localPort} \u2192 :${f.remotePort}`,
|
|
29
|
+
description: f.status,
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const hints: [string, string][] = [];
|
|
34
|
+
if (forwards.length > 0) {
|
|
35
|
+
hints.push(["X", "stop"]);
|
|
36
|
+
hints.push(["C", "stop all"]);
|
|
37
|
+
}
|
|
38
|
+
hints.push([GLYPHS.esc, "close"]);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<box
|
|
42
|
+
style={{
|
|
43
|
+
position: "absolute",
|
|
44
|
+
left: DIALOG_LEFT,
|
|
45
|
+
top: DIALOG_TOP,
|
|
46
|
+
width: DIALOG_WIDTH,
|
|
47
|
+
height: "60%",
|
|
48
|
+
border: true,
|
|
49
|
+
borderColor: PANEL_BORDER_ACTIVE,
|
|
50
|
+
borderStyle: "rounded",
|
|
51
|
+
backgroundColor: OVERLAY_SURFACE,
|
|
52
|
+
padding: 1,
|
|
53
|
+
flexDirection: "column",
|
|
54
|
+
}}
|
|
55
|
+
title="Port Forwards"
|
|
56
|
+
>
|
|
57
|
+
{forwards.length === 0 ? (
|
|
58
|
+
<text fg={TEXT_SUBTLE} attributes={TextAttributes.DIM}>
|
|
59
|
+
No active port forwards
|
|
60
|
+
</text>
|
|
61
|
+
) : (
|
|
62
|
+
<box style={{ flexGrow: 1 }}>
|
|
63
|
+
<select
|
|
64
|
+
focused
|
|
65
|
+
style={{ flexGrow: 1 }}
|
|
66
|
+
backgroundColor={OVERLAY_SURFACE}
|
|
67
|
+
focusedBackgroundColor={OVERLAY_SURFACE}
|
|
68
|
+
textColor={TEXT_PRIMARY}
|
|
69
|
+
focusedTextColor={TEXT_PRIMARY}
|
|
70
|
+
selectedBackgroundColor={ROW_SELECTED}
|
|
71
|
+
selectedTextColor={TEXT_PRIMARY}
|
|
72
|
+
descriptionColor={TEXT_SUBTLE}
|
|
73
|
+
selectedDescriptionColor={TEXT_PRIMARY}
|
|
74
|
+
showDescription
|
|
75
|
+
showScrollIndicator
|
|
76
|
+
selectedIndex={clampedIndex}
|
|
77
|
+
options={items}
|
|
78
|
+
onChange={onChange}
|
|
79
|
+
/>
|
|
80
|
+
</box>
|
|
81
|
+
)}
|
|
82
|
+
<KeyHintRow hints={hints} />
|
|
83
|
+
</box>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import { TextAttributes } from "@opentui/core";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
FILTER_BACKGROUND,
|
|
5
4
|
GLYPHS,
|
|
6
5
|
KEY_HINT,
|
|
7
6
|
OVERLAY_SURFACE,
|
|
8
|
-
PANEL_BORDER_ACTIVE,
|
|
9
|
-
TEXT_MUTED,
|
|
10
7
|
TEXT_PRIMARY,
|
|
11
8
|
TEXT_SUBTLE,
|
|
12
9
|
toneStyles,
|
|
13
10
|
} from "../../theme";
|
|
14
|
-
import { DIALOG_LEFT, DIALOG_TOP, DIALOG_WIDTH } from "./shared";
|
|
11
|
+
import { DIALOG_LEFT, DIALOG_TOP, DIALOG_WIDTH, KeyHintRow, TextInput } from "./shared";
|
|
15
12
|
import type { ActivePortForward } from "../../../lib/k8s/types";
|
|
16
13
|
|
|
17
14
|
export interface PortForwardEntry {
|
|
@@ -101,7 +98,7 @@ export function PortForwardOverlay({
|
|
|
101
98
|
|
|
102
99
|
return (
|
|
103
100
|
<text
|
|
104
|
-
key={entry.remotePort}
|
|
101
|
+
key={`${entry.remotePort}:${entry.suggestedLocalPort}`}
|
|
105
102
|
fg={selected ? TEXT_PRIMARY : TEXT_SUBTLE}
|
|
106
103
|
attributes={selected ? undefined : TextAttributes.DIM}
|
|
107
104
|
>
|
|
@@ -126,59 +123,22 @@ export function PortForwardOverlay({
|
|
|
126
123
|
</box>
|
|
127
124
|
|
|
128
125
|
{selectedEntry && !selectedIsActive && (
|
|
129
|
-
<
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
paddingLeft: 1,
|
|
136
|
-
paddingRight: 1,
|
|
137
|
-
marginTop: 1,
|
|
138
|
-
height: 3,
|
|
139
|
-
justifyContent: "center",
|
|
140
|
-
alignItems: "center",
|
|
141
|
-
flexDirection: "row",
|
|
142
|
-
}}
|
|
143
|
-
>
|
|
144
|
-
<text fg={TEXT_SUBTLE} attributes={TextAttributes.DIM}>
|
|
145
|
-
Local:
|
|
146
|
-
</text>
|
|
147
|
-
<input
|
|
148
|
-
focused
|
|
149
|
-
value={localPortValue}
|
|
150
|
-
placeholder={String(selectedEntry.suggestedLocalPort)}
|
|
151
|
-
style={{ flexGrow: 1, marginLeft: 1 }}
|
|
152
|
-
onInput={onChange}
|
|
153
|
-
onChange={onChange}
|
|
154
|
-
/>
|
|
155
|
-
</box>
|
|
126
|
+
<TextInput
|
|
127
|
+
label="Local:"
|
|
128
|
+
value={localPortValue}
|
|
129
|
+
placeholder={String(selectedEntry.suggestedLocalPort)}
|
|
130
|
+
onChange={onChange}
|
|
131
|
+
/>
|
|
156
132
|
)}
|
|
157
133
|
|
|
158
|
-
<
|
|
159
|
-
{
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
<text fg={TEXT_SUBTLE} attributes={TEXT_MUTED}>
|
|
167
|
-
<span fg={KEY_HINT}>{"X"}</span>
|
|
168
|
-
{" stop"}
|
|
169
|
-
</text>
|
|
170
|
-
)}
|
|
171
|
-
{entries.length > 1 && (
|
|
172
|
-
<text fg={TEXT_SUBTLE} attributes={TEXT_MUTED}>
|
|
173
|
-
<span fg={KEY_HINT}>{"↑↓"}</span>
|
|
174
|
-
{" navigate"}
|
|
175
|
-
</text>
|
|
176
|
-
)}
|
|
177
|
-
<text fg={TEXT_SUBTLE} attributes={TEXT_MUTED}>
|
|
178
|
-
<span fg={KEY_HINT}>{GLYPHS.esc}</span>
|
|
179
|
-
{" close"}
|
|
180
|
-
</text>
|
|
181
|
-
</box>
|
|
134
|
+
<KeyHintRow
|
|
135
|
+
hints={[
|
|
136
|
+
...(anyInactive ? [[GLYPHS.enter, "start"] as [string, string]] : []),
|
|
137
|
+
...(anyActive ? [["X", "stop"] as [string, string]] : []),
|
|
138
|
+
...(entries.length > 1 ? [["↑↓", "navigate"] as [string, string]] : []),
|
|
139
|
+
[GLYPHS.esc, "close"],
|
|
140
|
+
]}
|
|
141
|
+
/>
|
|
182
142
|
</box>
|
|
183
143
|
);
|
|
184
144
|
}
|
|
@@ -1,17 +1,7 @@
|
|
|
1
1
|
import { TextAttributes } from "@opentui/core";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
GLYPHS,
|
|
6
|
-
KEY_HINT,
|
|
7
|
-
OVERLAY_SURFACE,
|
|
8
|
-
PANEL_BORDER_ACTIVE,
|
|
9
|
-
TEXT_MUTED,
|
|
10
|
-
TEXT_PRIMARY,
|
|
11
|
-
TEXT_SUBTLE,
|
|
12
|
-
toneStyles,
|
|
13
|
-
} from "../../theme";
|
|
14
|
-
import { DIALOG_LEFT, DIALOG_TOP, DIALOG_WIDTH } from "./shared";
|
|
3
|
+
import { GLYPHS, TEXT_PRIMARY, TEXT_SUBTLE, ATTR_DIM, toneStyles } from "../../theme";
|
|
4
|
+
import { DialogFrame, KeyHintRow, TextInput } from "./shared";
|
|
15
5
|
|
|
16
6
|
export interface ScaleDialogProps {
|
|
17
7
|
target: string;
|
|
@@ -24,26 +14,11 @@ export function ScaleDialog({ target, currentReplicas, replicasValue, onChange }
|
|
|
24
14
|
const palette = toneStyles("accent");
|
|
25
15
|
|
|
26
16
|
return (
|
|
27
|
-
<
|
|
28
|
-
style={{
|
|
29
|
-
position: "absolute",
|
|
30
|
-
left: DIALOG_LEFT,
|
|
31
|
-
top: DIALOG_TOP,
|
|
32
|
-
width: DIALOG_WIDTH,
|
|
33
|
-
height: 12,
|
|
34
|
-
border: true,
|
|
35
|
-
borderStyle: "rounded",
|
|
36
|
-
borderColor: palette.border,
|
|
37
|
-
backgroundColor: OVERLAY_SURFACE,
|
|
38
|
-
padding: 1,
|
|
39
|
-
flexDirection: "column",
|
|
40
|
-
}}
|
|
41
|
-
title="Scale Replicas"
|
|
42
|
-
>
|
|
17
|
+
<DialogFrame title="Scale Replicas" tone="accent" height={12}>
|
|
43
18
|
<text fg={palette.fg} attributes={TextAttributes.BOLD}>
|
|
44
19
|
{target}
|
|
45
20
|
</text>
|
|
46
|
-
<text fg={TEXT_SUBTLE} attributes={
|
|
21
|
+
<text fg={TEXT_SUBTLE} attributes={ATTR_DIM}>
|
|
47
22
|
{currentReplicas !== undefined ? (
|
|
48
23
|
<>
|
|
49
24
|
{"Current "}
|
|
@@ -54,43 +29,13 @@ export function ScaleDialog({ target, currentReplicas, replicasValue, onChange }
|
|
|
54
29
|
"Replicas unknown"
|
|
55
30
|
)}
|
|
56
31
|
</text>
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
marginTop: 1,
|
|
66
|
-
height: 3,
|
|
67
|
-
justifyContent: "center",
|
|
68
|
-
alignItems: "center",
|
|
69
|
-
flexDirection: "row",
|
|
70
|
-
}}
|
|
71
|
-
>
|
|
72
|
-
<text fg={TEXT_SUBTLE} attributes={TextAttributes.DIM}>
|
|
73
|
-
Target:
|
|
74
|
-
</text>
|
|
75
|
-
<input
|
|
76
|
-
focused
|
|
77
|
-
value={replicasValue}
|
|
78
|
-
placeholder={currentReplicas !== undefined ? String(currentReplicas) : "0"}
|
|
79
|
-
style={{ flexGrow: 1, marginLeft: 1 }}
|
|
80
|
-
onInput={onChange}
|
|
81
|
-
onChange={onChange}
|
|
82
|
-
/>
|
|
83
|
-
</box>
|
|
84
|
-
<box style={{ flexDirection: "row", columnGap: 2, marginTop: 1, justifyContent: "flex-end" }}>
|
|
85
|
-
<text fg={TEXT_SUBTLE} attributes={TEXT_MUTED}>
|
|
86
|
-
<span fg={KEY_HINT}>{GLYPHS.enter}</span>
|
|
87
|
-
{" scale"}
|
|
88
|
-
</text>
|
|
89
|
-
<text fg={TEXT_SUBTLE} attributes={TEXT_MUTED}>
|
|
90
|
-
<span fg={KEY_HINT}>{GLYPHS.esc}</span>
|
|
91
|
-
{" cancel"}
|
|
92
|
-
</text>
|
|
93
|
-
</box>
|
|
94
|
-
</box>
|
|
32
|
+
<TextInput
|
|
33
|
+
label="Target:"
|
|
34
|
+
value={replicasValue}
|
|
35
|
+
placeholder={currentReplicas !== undefined ? String(currentReplicas) : "0"}
|
|
36
|
+
onChange={onChange}
|
|
37
|
+
/>
|
|
38
|
+
<KeyHintRow hints={[[GLYPHS.enter, "scale"], [GLYPHS.esc, "cancel"]]} />
|
|
39
|
+
</DialogFrame>
|
|
95
40
|
);
|
|
96
41
|
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
GLYPHS,
|
|
3
|
-
KEY_HINT,
|
|
4
3
|
OVERLAY_SURFACE,
|
|
5
4
|
PANEL_BORDER_ACTIVE,
|
|
6
5
|
ROW_SELECTED,
|
|
7
|
-
TEXT_MUTED,
|
|
8
6
|
TEXT_PRIMARY,
|
|
9
7
|
TEXT_SUBTLE,
|
|
10
8
|
} from "../../theme";
|
|
9
|
+
import { KeyHintRow } from "./shared";
|
|
11
10
|
|
|
12
11
|
export interface SelectOverlayProps {
|
|
13
12
|
title: string;
|
|
@@ -17,6 +16,7 @@ export interface SelectOverlayProps {
|
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
export function SelectOverlay({ title, items, selectedIndex, onChange }: SelectOverlayProps) {
|
|
19
|
+
const clampedIndex = items.length > 0 ? Math.min(selectedIndex, items.length - 1) : 0;
|
|
20
20
|
return (
|
|
21
21
|
<box
|
|
22
22
|
style={{
|
|
@@ -48,21 +48,12 @@ export function SelectOverlay({ title, items, selectedIndex, onChange }: SelectO
|
|
|
48
48
|
selectedDescriptionColor={TEXT_PRIMARY}
|
|
49
49
|
showDescription
|
|
50
50
|
showScrollIndicator
|
|
51
|
-
selectedIndex={
|
|
51
|
+
selectedIndex={clampedIndex}
|
|
52
52
|
options={items}
|
|
53
|
-
onChange={
|
|
53
|
+
onChange={onChange}
|
|
54
54
|
/>
|
|
55
55
|
</box>
|
|
56
|
-
<
|
|
57
|
-
<text fg={TEXT_SUBTLE} attributes={TEXT_MUTED}>
|
|
58
|
-
<span fg={KEY_HINT}>{GLYPHS.enter}</span>
|
|
59
|
-
{" select"}
|
|
60
|
-
</text>
|
|
61
|
-
<text fg={TEXT_SUBTLE} attributes={TEXT_MUTED}>
|
|
62
|
-
<span fg={KEY_HINT}>{GLYPHS.esc}</span>
|
|
63
|
-
{" close"}
|
|
64
|
-
</text>
|
|
65
|
-
</box>
|
|
56
|
+
<KeyHintRow hints={[[GLYPHS.enter, "select"], [GLYPHS.esc, "close"]]} />
|
|
66
57
|
</box>
|
|
67
58
|
);
|
|
68
59
|
}
|