openk8s 1.0.5 → 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 +4 -1
- package/src/app/components/overlays/logs-dialog.tsx +47 -67
- 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 +31 -15
- 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 +8 -4
- 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 +38 -38
- 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,12 +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";
|
|
17
|
+
import { KeyHintRow, TextInput } from "./shared";
|
|
16
18
|
|
|
17
19
|
export interface LogsDialogActions {
|
|
18
20
|
nextMatch: () => void;
|
|
@@ -33,7 +35,7 @@ export interface LogsDialogProps {
|
|
|
33
35
|
searchMode?: boolean | undefined;
|
|
34
36
|
searchText?: string | undefined;
|
|
35
37
|
onSearchChange?: ((value: string) => void) | undefined;
|
|
36
|
-
actionsRef?:
|
|
38
|
+
actionsRef?: MutableRefObject<LogsDialogActions | null> | undefined;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
export function LogsDialog({
|
|
@@ -52,7 +54,7 @@ export function LogsDialog({
|
|
|
52
54
|
onSearchChange,
|
|
53
55
|
actionsRef,
|
|
54
56
|
}: LogsDialogProps) {
|
|
55
|
-
const lines = logsText ? logsText.split("\n") : [];
|
|
57
|
+
const lines = useMemo(() => (logsText ? logsText.split("\n") : []), [logsText]);
|
|
56
58
|
const palette = toneStyles("info");
|
|
57
59
|
const [currentMatchIdx, setCurrentMatchIdx] = useState(0);
|
|
58
60
|
|
|
@@ -63,18 +65,31 @@ export function LogsDialog({
|
|
|
63
65
|
if (line.toLowerCase().includes(lower)) acc.push(i);
|
|
64
66
|
return acc;
|
|
65
67
|
}, []);
|
|
66
|
-
|
|
67
|
-
}, [logsText, searchText]);
|
|
68
|
+
}, [lines, searchText]);
|
|
68
69
|
|
|
70
|
+
// Reset to first match when search text changes, and clamp when matches shrink (e.g. new logs stream in)
|
|
69
71
|
useEffect(() => {
|
|
70
|
-
if (!searchText)
|
|
72
|
+
if (!searchText) {
|
|
73
|
+
setCurrentMatchIdx(0);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
71
76
|
setCurrentMatchIdx(0);
|
|
72
77
|
const firstLine = matchIndices[0];
|
|
73
78
|
if (firstLine !== undefined) {
|
|
74
79
|
scrollRef.current?.scrollTo(firstLine);
|
|
75
80
|
}
|
|
76
|
-
|
|
77
|
-
|
|
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]);
|
|
78
93
|
|
|
79
94
|
const nextMatch = () => {
|
|
80
95
|
if (matchIndices.length === 0) return;
|
|
@@ -92,9 +107,11 @@ export function LogsDialog({
|
|
|
92
107
|
if (lineIdx !== undefined) scrollRef.current?.scrollTo(lineIdx);
|
|
93
108
|
};
|
|
94
109
|
|
|
95
|
-
|
|
96
|
-
actionsRef
|
|
97
|
-
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (actionsRef) {
|
|
112
|
+
actionsRef.current = { nextMatch, prevMatch };
|
|
113
|
+
}
|
|
114
|
+
});
|
|
98
115
|
|
|
99
116
|
const warningFg = toneStyles("warning").fg;
|
|
100
117
|
const multiContainer = (containers?.length ?? 0) > 1;
|
|
@@ -121,22 +138,22 @@ export function LogsDialog({
|
|
|
121
138
|
>
|
|
122
139
|
{/* Options status bar */}
|
|
123
140
|
<box style={{ flexDirection: "row", columnGap: 2, marginBottom: 0 }}>
|
|
124
|
-
<text fg={logsOptions.previous ? warningFg : TEXT_SUBTLE} attributes={
|
|
141
|
+
<text fg={logsOptions.previous ? warningFg : TEXT_SUBTLE} attributes={ATTR_DIM}>
|
|
125
142
|
<span fg={KEY_HINT}>P</span>
|
|
126
143
|
{` prev:${logsOptions.previous ? "on" : "off"}`}
|
|
127
144
|
</text>
|
|
128
|
-
<text fg={TEXT_SUBTLE} attributes={
|
|
145
|
+
<text fg={TEXT_SUBTLE} attributes={ATTR_DIM}>
|
|
129
146
|
<span fg={KEY_HINT}>T</span>
|
|
130
147
|
{` tail:${logsOptions.tail}`}
|
|
131
148
|
</text>
|
|
132
149
|
{logsOptions.since ? (
|
|
133
|
-
<text fg={TEXT_SUBTLE} attributes={
|
|
150
|
+
<text fg={TEXT_SUBTLE} attributes={ATTR_DIM}>
|
|
134
151
|
<span fg={KEY_HINT}>S</span>
|
|
135
152
|
{` since:${logsOptions.since}`}
|
|
136
153
|
</text>
|
|
137
154
|
) : undefined}
|
|
138
155
|
{multiContainer ? (
|
|
139
|
-
<text fg={TEXT_SUBTLE} attributes={
|
|
156
|
+
<text fg={TEXT_SUBTLE} attributes={ATTR_DIM}>
|
|
140
157
|
<span fg={KEY_HINT}>C</span>
|
|
141
158
|
{logsContainer ? ` ctr:${logsContainer}` : " all-ctrs"}
|
|
142
159
|
</text>
|
|
@@ -195,33 +212,12 @@ export function LogsDialog({
|
|
|
195
212
|
|
|
196
213
|
{/* Option input row */}
|
|
197
214
|
{inputMode ? (
|
|
198
|
-
<
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
paddingLeft: 1,
|
|
205
|
-
paddingRight: 1,
|
|
206
|
-
marginTop: 1,
|
|
207
|
-
height: 3,
|
|
208
|
-
justifyContent: "center",
|
|
209
|
-
alignItems: "center",
|
|
210
|
-
flexDirection: "row",
|
|
211
|
-
}}
|
|
212
|
-
>
|
|
213
|
-
<text fg={TEXT_SUBTLE} attributes={TextAttributes.DIM}>
|
|
214
|
-
{inputMode === "tail" ? "Tail lines:" : "Since (e.g. 1h, 30m):"}
|
|
215
|
-
</text>
|
|
216
|
-
<input
|
|
217
|
-
focused
|
|
218
|
-
value={inputValue ?? ""}
|
|
219
|
-
placeholder={inputMode === "tail" ? "120" : "1h"}
|
|
220
|
-
style={{ flexGrow: 1, marginLeft: 1 }}
|
|
221
|
-
onInput={onOptionInputChange}
|
|
222
|
-
onChange={onOptionInputChange}
|
|
223
|
-
/>
|
|
224
|
-
</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
|
+
/>
|
|
225
221
|
) : undefined}
|
|
226
222
|
|
|
227
223
|
{/* Search input row */}
|
|
@@ -251,11 +247,11 @@ export function LogsDialog({
|
|
|
251
247
|
onChange={onSearchChange}
|
|
252
248
|
/>
|
|
253
249
|
{matchIndices.length > 0 ? (
|
|
254
|
-
<text fg={TEXT_SUBTLE} attributes={
|
|
250
|
+
<text fg={TEXT_SUBTLE} attributes={ATTR_DIM}>
|
|
255
251
|
{` ${currentMatchIdx + 1}/${matchIndices.length}`}
|
|
256
252
|
</text>
|
|
257
253
|
) : searchText ? (
|
|
258
|
-
<text fg={toneStyles("danger").fg} attributes={
|
|
254
|
+
<text fg={toneStyles("danger").fg} attributes={ATTR_DIM}>
|
|
259
255
|
{" no match"}
|
|
260
256
|
</text>
|
|
261
257
|
) : undefined}
|
|
@@ -263,29 +259,13 @@ export function LogsDialog({
|
|
|
263
259
|
) : undefined}
|
|
264
260
|
|
|
265
261
|
{/* Hints */}
|
|
266
|
-
<
|
|
267
|
-
{
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
<>
|
|
274
|
-
<text fg={TEXT_SUBTLE} attributes={TEXT_MUTED}>
|
|
275
|
-
<span fg={KEY_HINT}>{"/"}</span>
|
|
276
|
-
{" search"}
|
|
277
|
-
</text>
|
|
278
|
-
<text fg={TEXT_SUBTLE} attributes={TEXT_MUTED}>
|
|
279
|
-
<span fg={KEY_HINT}>{"↑↓"}</span>
|
|
280
|
-
{" scroll"}
|
|
281
|
-
</text>
|
|
282
|
-
</>
|
|
283
|
-
)}
|
|
284
|
-
<text fg={TEXT_SUBTLE} attributes={TEXT_MUTED}>
|
|
285
|
-
<span fg={KEY_HINT}>{GLYPHS.esc}</span>
|
|
286
|
-
{" close"}
|
|
287
|
-
</text>
|
|
288
|
-
</box>
|
|
262
|
+
<KeyHintRow
|
|
263
|
+
hints={
|
|
264
|
+
inputMode || searchMode
|
|
265
|
+
? [[GLYPHS.enter, "confirm"], [GLYPHS.esc, "close"]]
|
|
266
|
+
: [["/", "search"], ["↑↓", "scroll"], [GLYPHS.esc, "close"]]
|
|
267
|
+
}
|
|
268
|
+
/>
|
|
289
269
|
</box>
|
|
290
270
|
);
|
|
291
271
|
}
|
|
@@ -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
|
}
|