openk8s 1.0.5 → 1.2.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 +13 -10
- package/src/app/__tests__/app-state.test.ts +48 -6
- package/src/app/__tests__/utils.test.ts +6 -15
- 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 +40 -83
- 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 +171 -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 +30 -21
- 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 +32 -16
- 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 +10 -6
- 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-detail-builder.test.ts +4 -6
- package/src/lib/k8s/__tests__/resource-parser.test.ts +40 -2
- package/src/lib/k8s/detail-builders/event-builder.ts +18 -12
- 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 +4 -7
- package/src/lib/k8s/resource-parser.ts +17 -2
- package/src/lib/k8s/types.ts +12 -4
- 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 -54
- 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
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { TextAttributes } from "@opentui/core";
|
|
2
|
+
import { useTerminalDimensions } from "@opentui/react";
|
|
3
|
+
|
|
4
|
+
import { GLYPHS, OVERLAY_SURFACE, PANEL_BORDER_ACTIVE, KEY_HINT, TEXT_SUBTLE, ATTR_DIM, toneStyles } from "../../theme";
|
|
5
|
+
import { KeyHintRow } from "./shared";
|
|
6
|
+
|
|
7
|
+
interface HelpSection {
|
|
8
|
+
title: string;
|
|
9
|
+
rows: [string, string][];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const LEFT_SECTIONS: HelpSection[] = [
|
|
13
|
+
{
|
|
14
|
+
title: "Navigation",
|
|
15
|
+
rows: [
|
|
16
|
+
["Tab / Shift+Tab", "cycle panes"],
|
|
17
|
+
["Up/Down or k/j", "navigate list"],
|
|
18
|
+
["Left or h", "back / prev tab"],
|
|
19
|
+
["Right", "forward / next tab"],
|
|
20
|
+
["Space", "multi-select resource"],
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
title: "Inspector Tabs",
|
|
25
|
+
rows: [
|
|
26
|
+
["S", "summary tab (incl. recent events)"],
|
|
27
|
+
["Y", "yaml tab (press again to copy)"],
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
title: "Resource Actions",
|
|
32
|
+
rows: [
|
|
33
|
+
["L", "logs"],
|
|
34
|
+
["E", "edit (or edit helm values)"],
|
|
35
|
+
["X", "shell (kubectl exec)"],
|
|
36
|
+
["F", "port forward (per-resource)"],
|
|
37
|
+
["D", "delete"],
|
|
38
|
+
["Shift+S", "scale"],
|
|
39
|
+
["Shift+R", "rollout restart"],
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
title: "Helm Actions",
|
|
44
|
+
rows: [
|
|
45
|
+
["B", "rollback"],
|
|
46
|
+
["U", "upgrade --reuse-values"],
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const RIGHT_SECTIONS: HelpSection[] = [
|
|
52
|
+
{
|
|
53
|
+
title: "Overlays",
|
|
54
|
+
rows: [
|
|
55
|
+
["C", "cluster switcher"],
|
|
56
|
+
["N", "namespace switcher"],
|
|
57
|
+
["Shift+N", "notification history"],
|
|
58
|
+
["Shift+F", "active port forwards"],
|
|
59
|
+
["?", "this help"],
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
title: "Filter",
|
|
64
|
+
rows: [
|
|
65
|
+
["/", "enter filter mode"],
|
|
66
|
+
["Enter", "confirm filter"],
|
|
67
|
+
["Escape", "cancel filter"],
|
|
68
|
+
["Ctrl+U", "clear filter"],
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
title: "Logs Overlay",
|
|
73
|
+
rows: [
|
|
74
|
+
["/", "search"],
|
|
75
|
+
["N / Shift+N", "next / prev match"],
|
|
76
|
+
["C", "switch container"],
|
|
77
|
+
["P", "toggle previous logs"],
|
|
78
|
+
["T", "set tail lines"],
|
|
79
|
+
["S", "set since duration"],
|
|
80
|
+
["Up/Down", "scroll logs"],
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
title: "Port Forward List",
|
|
85
|
+
rows: [
|
|
86
|
+
["X", "stop selected forward"],
|
|
87
|
+
["C", "stop all forwards"],
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
title: "Global",
|
|
92
|
+
rows: [
|
|
93
|
+
["R", "refresh current view"],
|
|
94
|
+
["Escape", "close overlay"],
|
|
95
|
+
["Ctrl+C", "quit"],
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
const ALL_SECTIONS = [...LEFT_SECTIONS, ...RIGHT_SECTIONS];
|
|
101
|
+
|
|
102
|
+
const TWO_COLUMN_THRESHOLD = 90;
|
|
103
|
+
|
|
104
|
+
function renderSection(section: HelpSection, index: number, keyWidth: number): React.ReactNode {
|
|
105
|
+
return (
|
|
106
|
+
<box key={section.title} style={{ flexDirection: "column", width: "100%" }}>
|
|
107
|
+
{index > 0 && <box style={{ height: 1 }} />}
|
|
108
|
+
<text fg={toneStyles("accent").fg} attributes={TextAttributes.BOLD}>
|
|
109
|
+
{section.title}
|
|
110
|
+
</text>
|
|
111
|
+
{section.rows.map(([key, label], rowIdx) => (
|
|
112
|
+
<box key={`${index}:${rowIdx}`} style={{ flexDirection: "row", width: "100%", paddingLeft: 2 }}>
|
|
113
|
+
<box style={{ width: keyWidth }}>
|
|
114
|
+
<text fg={KEY_HINT}>{key}</text>
|
|
115
|
+
</box>
|
|
116
|
+
<text fg={TEXT_SUBTLE} attributes={ATTR_DIM}>
|
|
117
|
+
{label}
|
|
118
|
+
</text>
|
|
119
|
+
</box>
|
|
120
|
+
))}
|
|
121
|
+
</box>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function renderColumn(sections: HelpSection[], keyWidth: number): React.ReactNode {
|
|
126
|
+
return (
|
|
127
|
+
<box style={{ flexDirection: "column", flexGrow: 1, width: "100%" }}>
|
|
128
|
+
{sections.map((section, index) => renderSection(section, index, keyWidth))}
|
|
129
|
+
</box>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function HelpOverlay() {
|
|
134
|
+
const dims = useTerminalDimensions();
|
|
135
|
+
const twoColumns = dims.width >= TWO_COLUMN_THRESHOLD;
|
|
136
|
+
|
|
137
|
+
const left = twoColumns ? "8%" : "16%";
|
|
138
|
+
const width = twoColumns ? "84%" : "68%";
|
|
139
|
+
const keyWidth = twoColumns ? 20 : 24;
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<box
|
|
143
|
+
style={{
|
|
144
|
+
position: "absolute",
|
|
145
|
+
left,
|
|
146
|
+
top: "6%",
|
|
147
|
+
width,
|
|
148
|
+
height: "86%",
|
|
149
|
+
border: true,
|
|
150
|
+
borderColor: PANEL_BORDER_ACTIVE,
|
|
151
|
+
borderStyle: "rounded",
|
|
152
|
+
backgroundColor: OVERLAY_SURFACE,
|
|
153
|
+
padding: 1,
|
|
154
|
+
flexDirection: "column",
|
|
155
|
+
}}
|
|
156
|
+
title="Keyboard Shortcuts"
|
|
157
|
+
>
|
|
158
|
+
<scrollbox style={{ flexGrow: 1 }} focused>
|
|
159
|
+
{twoColumns ? (
|
|
160
|
+
<box style={{ flexDirection: "row", width: "100%", columnGap: 2 }}>
|
|
161
|
+
{renderColumn(LEFT_SECTIONS, keyWidth)}
|
|
162
|
+
{renderColumn(RIGHT_SECTIONS, keyWidth)}
|
|
163
|
+
</box>
|
|
164
|
+
) : (
|
|
165
|
+
renderColumn(ALL_SECTIONS, keyWidth)
|
|
166
|
+
)}
|
|
167
|
+
</scrollbox>
|
|
168
|
+
<KeyHintRow hints={[[GLYPHS.esc, "close"]]} />
|
|
169
|
+
</box>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { DIALOG_LEFT, DIALOG_TOP, DIALOG_WIDTH, KeyHintRow } from "./shared";
|
|
1
|
+
export { DIALOG_LEFT, DIALOG_TOP, DIALOG_WIDTH, DialogFrame, KeyHintRow, TextInput } from "./shared";
|
|
2
2
|
export { SelectOverlay, type SelectOverlayProps } from "./select-overlay";
|
|
3
3
|
export { DeleteConfirmOverlay, type DeleteConfirmOverlayProps } from "./delete-confirm-overlay";
|
|
4
4
|
export {
|
|
@@ -7,6 +7,9 @@ export {
|
|
|
7
7
|
type PortForwardOverlayProps,
|
|
8
8
|
buildPortForwardEntries,
|
|
9
9
|
} from "./port-forward-overlay";
|
|
10
|
+
export { PortForwardListOverlay, type PortForwardListOverlayProps } from "./port-forward-list-overlay";
|
|
10
11
|
export { LogsDialog, type LogsDialogActions, type LogsDialogProps } from "./logs-dialog";
|
|
11
12
|
export { ScaleDialog, type ScaleDialogProps } from "./scale-dialog";
|
|
12
13
|
export { HelmRollbackOverlay, type HelmRollbackOverlayProps } from "./helm-rollback-overlay";
|
|
14
|
+
export { NotificationHistoryOverlay, type NotificationHistoryOverlayProps } from "./notification-history-overlay";
|
|
15
|
+
export { HelpOverlay } from "./help-overlay";
|
|
@@ -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
|
}
|