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
|
@@ -17,7 +17,7 @@ export function KindRows({ resourceKinds, selectedKind, onSelect, onActivate }:
|
|
|
17
17
|
|
|
18
18
|
useEffect(() => {
|
|
19
19
|
scrollRef.current?.scrollChildIntoView(`kind:${selectedKind}`);
|
|
20
|
-
}, [selectedKind]);
|
|
20
|
+
}, [selectedKind, resourceKinds]);
|
|
21
21
|
|
|
22
22
|
if (resourceKinds.length === 0) {
|
|
23
23
|
return (
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { TextAttributes } from "@opentui/core";
|
|
3
|
+
|
|
4
|
+
import { GLYPHS, KEY_HINT, OVERLAY_SURFACE, TEXT_PRIMARY, TEXT_SUBTLE, toneStyles } from "../theme";
|
|
5
|
+
import {
|
|
6
|
+
NOTIFICATION_PROGRESS_INTERVAL_MS,
|
|
7
|
+
NOTIFICATION_SLIDE_FRAMES,
|
|
8
|
+
NOTIFICATION_SLIDE_INTERVAL_MS,
|
|
9
|
+
TOAST_DURATION_MS,
|
|
10
|
+
} from "../constants";
|
|
11
|
+
import type { Notification } from "../../lib/k8s/types";
|
|
12
|
+
|
|
13
|
+
export interface NotificationCardProps {
|
|
14
|
+
notification: Notification;
|
|
15
|
+
onDismiss: (id: string) => void;
|
|
16
|
+
onAction: (notificationId: string, actionId: string) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const TONE_ICONS: Record<Notification["tone"], string> = {
|
|
20
|
+
info: GLYPHS.dotEmpty,
|
|
21
|
+
success: GLYPHS.dot,
|
|
22
|
+
warning: GLYPHS.warn,
|
|
23
|
+
danger: GLYPHS.cross,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const SLIDE_DISTANCE = 15;
|
|
27
|
+
|
|
28
|
+
function easeOut(t: number): number {
|
|
29
|
+
return 1 - Math.pow(1 - t, 2);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function NotificationCard({ notification, onDismiss, onAction }: NotificationCardProps) {
|
|
33
|
+
const palette = toneStyles(notification.tone);
|
|
34
|
+
const icon = TONE_ICONS[notification.tone];
|
|
35
|
+
|
|
36
|
+
const [phase, setPhase] = useState<"entering" | "visible" | "exiting">("entering");
|
|
37
|
+
const [slideOffset, setSlideOffset] = useState(SLIDE_DISTANCE);
|
|
38
|
+
const [progress, setProgress] = useState(1);
|
|
39
|
+
|
|
40
|
+
const slideFrameRef = useRef(0);
|
|
41
|
+
const progressRef = useRef<ReturnType<typeof setInterval> | undefined>(undefined);
|
|
42
|
+
|
|
43
|
+
// Slide-in animation
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
const interval = setInterval(() => {
|
|
46
|
+
slideFrameRef.current += 1;
|
|
47
|
+
const frame = slideFrameRef.current;
|
|
48
|
+
if (frame >= NOTIFICATION_SLIDE_FRAMES) {
|
|
49
|
+
setSlideOffset(0);
|
|
50
|
+
setPhase("visible");
|
|
51
|
+
clearInterval(interval);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const t = frame / NOTIFICATION_SLIDE_FRAMES;
|
|
55
|
+
setSlideOffset(Math.round(SLIDE_DISTANCE * (1 - easeOut(t))));
|
|
56
|
+
}, NOTIFICATION_SLIDE_INTERVAL_MS);
|
|
57
|
+
return () => clearInterval(interval);
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
60
|
+
// Progress bar (non-persistent only)
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (notification.persistent || phase !== "visible") return;
|
|
63
|
+
|
|
64
|
+
const startTime = Date.now();
|
|
65
|
+
progressRef.current = setInterval(() => {
|
|
66
|
+
const elapsed = Date.now() - startTime;
|
|
67
|
+
const remaining = Math.max(0, 1 - elapsed / TOAST_DURATION_MS);
|
|
68
|
+
setProgress(remaining);
|
|
69
|
+
if (remaining <= 0) {
|
|
70
|
+
if (progressRef.current) clearInterval(progressRef.current);
|
|
71
|
+
}
|
|
72
|
+
}, NOTIFICATION_PROGRESS_INTERVAL_MS);
|
|
73
|
+
|
|
74
|
+
return () => {
|
|
75
|
+
if (progressRef.current) clearInterval(progressRef.current);
|
|
76
|
+
};
|
|
77
|
+
}, [notification.persistent, phase]);
|
|
78
|
+
|
|
79
|
+
const handleDismiss = (): void => {
|
|
80
|
+
if (phase === "exiting") return;
|
|
81
|
+
if (progressRef.current) clearInterval(progressRef.current);
|
|
82
|
+
setPhase("exiting");
|
|
83
|
+
slideFrameRef.current = 0;
|
|
84
|
+
const interval = setInterval(() => {
|
|
85
|
+
slideFrameRef.current += 1;
|
|
86
|
+
const frame = slideFrameRef.current;
|
|
87
|
+
if (frame >= NOTIFICATION_SLIDE_FRAMES) {
|
|
88
|
+
clearInterval(interval);
|
|
89
|
+
onDismiss(notification.id);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const t = frame / NOTIFICATION_SLIDE_FRAMES;
|
|
93
|
+
setSlideOffset(Math.round(SLIDE_DISTANCE * easeOut(t)));
|
|
94
|
+
}, NOTIFICATION_SLIDE_INTERVAL_MS);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const barWidth = Math.round(progress * 20);
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<box
|
|
101
|
+
style={{
|
|
102
|
+
border: true,
|
|
103
|
+
borderColor: palette.border,
|
|
104
|
+
borderStyle: "rounded",
|
|
105
|
+
backgroundColor: OVERLAY_SURFACE,
|
|
106
|
+
paddingLeft: 1,
|
|
107
|
+
paddingRight: 1,
|
|
108
|
+
flexDirection: "column",
|
|
109
|
+
marginLeft: slideOffset,
|
|
110
|
+
}}
|
|
111
|
+
onMouseDown={handleDismiss}
|
|
112
|
+
>
|
|
113
|
+
<box style={{ flexDirection: "row", alignItems: "center" }}>
|
|
114
|
+
<text fg={palette.fg}>
|
|
115
|
+
{icon}
|
|
116
|
+
</text>
|
|
117
|
+
<text fg={TEXT_PRIMARY} wrapMode="word" style={{ flexGrow: 1, marginLeft: 1 }}>
|
|
118
|
+
{notification.message}
|
|
119
|
+
</text>
|
|
120
|
+
</box>
|
|
121
|
+
{notification.actions.length > 0 ? (
|
|
122
|
+
<box style={{ flexDirection: "row", columnGap: 2, marginTop: 0 }}>
|
|
123
|
+
{notification.actions.map((action) => (
|
|
124
|
+
<text
|
|
125
|
+
key={action.id}
|
|
126
|
+
fg={KEY_HINT}
|
|
127
|
+
attributes={TextAttributes.BOLD}
|
|
128
|
+
onMouseDown={(e: unknown) => {
|
|
129
|
+
(e as { stopPropagation?: () => void })?.stopPropagation?.();
|
|
130
|
+
onAction(notification.id, action.id);
|
|
131
|
+
}}
|
|
132
|
+
>
|
|
133
|
+
{`[${action.label}]`}
|
|
134
|
+
</text>
|
|
135
|
+
))}
|
|
136
|
+
{!notification.persistent && (
|
|
137
|
+
<text fg={TEXT_SUBTLE} attributes={TextAttributes.DIM}>
|
|
138
|
+
{`${"█".repeat(barWidth)}${"░".repeat(20 - barWidth)}`}
|
|
139
|
+
</text>
|
|
140
|
+
)}
|
|
141
|
+
</box>
|
|
142
|
+
) : null}
|
|
143
|
+
</box>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -1,59 +1,40 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { MAX_VISIBLE_NOTIFICATIONS, NOTIFICATION_WIDTH_PCT } from "../constants";
|
|
2
2
|
import type { Notification } from "../../lib/k8s/types";
|
|
3
|
+
import { NotificationCard } from "./notification-card";
|
|
3
4
|
|
|
4
5
|
export interface NotificationTrayProps {
|
|
5
6
|
notifications: Notification[];
|
|
6
7
|
onDismiss: (id: string) => void;
|
|
8
|
+
onAction: (notificationId: string, actionId: string) => void;
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
export function NotificationTray({ notifications, onDismiss }: NotificationTrayProps) {
|
|
11
|
+
export function NotificationTray({ notifications, onDismiss, onAction }: NotificationTrayProps) {
|
|
10
12
|
if (notifications.length === 0) {
|
|
11
|
-
return
|
|
13
|
+
return null;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
// Show at most
|
|
15
|
-
const visible = notifications.slice(-
|
|
16
|
+
// Show at most MAX_VISIBLE_NOTIFICATIONS, most recent last (rendered bottom-to-top)
|
|
17
|
+
const visible = notifications.slice(-MAX_VISIBLE_NOTIFICATIONS);
|
|
16
18
|
|
|
17
19
|
return (
|
|
18
20
|
<box
|
|
19
21
|
style={{
|
|
20
22
|
position: "absolute",
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
width:
|
|
23
|
+
bottom: 5,
|
|
24
|
+
right: 1,
|
|
25
|
+
width: NOTIFICATION_WIDTH_PCT,
|
|
24
26
|
flexDirection: "column",
|
|
25
|
-
rowGap:
|
|
27
|
+
rowGap: 0,
|
|
26
28
|
}}
|
|
27
29
|
>
|
|
28
|
-
{visible.map((notification) =>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
borderColor: palette.border,
|
|
37
|
-
borderStyle: "rounded",
|
|
38
|
-
backgroundColor: OVERLAY_SURFACE,
|
|
39
|
-
paddingLeft: 1,
|
|
40
|
-
paddingRight: 1,
|
|
41
|
-
paddingTop: 0,
|
|
42
|
-
paddingBottom: 0,
|
|
43
|
-
flexDirection: "row",
|
|
44
|
-
justifyContent: "space-between",
|
|
45
|
-
alignItems: "center",
|
|
46
|
-
}}
|
|
47
|
-
>
|
|
48
|
-
<text fg={palette.fg} wrapMode="word" style={{ flexShrink: 1, flexGrow: 1 }}>
|
|
49
|
-
{notification.message}
|
|
50
|
-
</text>
|
|
51
|
-
<text fg={TEXT_SUBTLE} onMouseDown={() => onDismiss(notification.id)}>
|
|
52
|
-
{` ${GLYPHS.stop}`}
|
|
53
|
-
</text>
|
|
54
|
-
</box>
|
|
55
|
-
);
|
|
56
|
-
})}
|
|
30
|
+
{visible.map((notification) => (
|
|
31
|
+
<NotificationCard
|
|
32
|
+
key={notification.id}
|
|
33
|
+
notification={notification}
|
|
34
|
+
onDismiss={onDismiss}
|
|
35
|
+
onAction={onAction}
|
|
36
|
+
/>
|
|
37
|
+
))}
|
|
57
38
|
</box>
|
|
58
39
|
);
|
|
59
40
|
}
|
|
@@ -3,13 +3,12 @@ import { TextAttributes } from "@opentui/core";
|
|
|
3
3
|
import {
|
|
4
4
|
DANGER,
|
|
5
5
|
GLYPHS,
|
|
6
|
-
KEY_HINT,
|
|
7
6
|
OVERLAY_SURFACE,
|
|
8
|
-
|
|
7
|
+
ATTR_DIM,
|
|
9
8
|
TEXT_SUBTLE,
|
|
10
9
|
toneStyles,
|
|
11
10
|
} from "../../theme";
|
|
12
|
-
import { DIALOG_LEFT, DIALOG_TOP, DIALOG_WIDTH } from "./shared";
|
|
11
|
+
import { DIALOG_LEFT, DIALOG_TOP, DIALOG_WIDTH, KeyHintRow } from "./shared";
|
|
13
12
|
import { statusLine } from "../../utils";
|
|
14
13
|
import type { ResourceRef } from "../../../lib/k8s/types";
|
|
15
14
|
|
|
@@ -20,7 +19,11 @@ export interface DeleteConfirmOverlayProps {
|
|
|
20
19
|
export function DeleteConfirmOverlay({ targets }: DeleteConfirmOverlayProps) {
|
|
21
20
|
const palette = toneStyles("danger");
|
|
22
21
|
const title =
|
|
23
|
-
targets.length
|
|
22
|
+
targets.length === 0
|
|
23
|
+
? "No selection"
|
|
24
|
+
: targets.length === 1
|
|
25
|
+
? `Delete ${statusLine({ ref: targets[0] })}?`
|
|
26
|
+
: `Delete ${targets.length} selected resources?`;
|
|
24
27
|
const overlayHeight = Math.min(22, Math.max(9, targets.length + 8));
|
|
25
28
|
|
|
26
29
|
return (
|
|
@@ -43,8 +46,12 @@ export function DeleteConfirmOverlay({ targets }: DeleteConfirmOverlayProps) {
|
|
|
43
46
|
<text fg={palette.fg} attributes={TextAttributes.BOLD}>
|
|
44
47
|
{title}
|
|
45
48
|
</text>
|
|
46
|
-
{targets.length
|
|
47
|
-
<text fg={TEXT_SUBTLE} attributes={
|
|
49
|
+
{targets.length === 0 ? (
|
|
50
|
+
<text fg={TEXT_SUBTLE} attributes={ATTR_DIM}>
|
|
51
|
+
Nothing selected for deletion.
|
|
52
|
+
</text>
|
|
53
|
+
) : targets.length === 1 ? (
|
|
54
|
+
<text fg={TEXT_SUBTLE} attributes={ATTR_DIM}>
|
|
48
55
|
Runs kubectl delete on the current selection.
|
|
49
56
|
</text>
|
|
50
57
|
) : (
|
|
@@ -54,7 +61,7 @@ export function DeleteConfirmOverlay({ targets }: DeleteConfirmOverlayProps) {
|
|
|
54
61
|
<text
|
|
55
62
|
key={`${target.kind}:${target.namespace ?? "cluster"}:${target.name}`}
|
|
56
63
|
fg={TEXT_SUBTLE}
|
|
57
|
-
attributes={
|
|
64
|
+
attributes={ATTR_DIM}
|
|
58
65
|
>
|
|
59
66
|
<span fg={DANGER}>{GLYPHS.cross}</span>
|
|
60
67
|
{" "}
|
|
@@ -64,16 +71,7 @@ export function DeleteConfirmOverlay({ targets }: DeleteConfirmOverlayProps) {
|
|
|
64
71
|
</box>
|
|
65
72
|
</scrollbox>
|
|
66
73
|
)}
|
|
67
|
-
<
|
|
68
|
-
<text fg={TEXT_SUBTLE} attributes={TEXT_MUTED}>
|
|
69
|
-
<span fg={DANGER}>{GLYPHS.enter}</span>
|
|
70
|
-
{" confirm"}
|
|
71
|
-
</text>
|
|
72
|
-
<text fg={TEXT_SUBTLE} attributes={TEXT_MUTED}>
|
|
73
|
-
<span fg={KEY_HINT}>{GLYPHS.esc}</span>
|
|
74
|
-
{" cancel"}
|
|
75
|
-
</text>
|
|
76
|
-
</box>
|
|
74
|
+
<KeyHintRow hints={[[GLYPHS.enter, "confirm"], [GLYPHS.esc, "cancel"]]} />
|
|
77
75
|
</box>
|
|
78
76
|
);
|
|
79
77
|
}
|
|
@@ -1,16 +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_SUBTLE,
|
|
11
|
-
toneStyles,
|
|
12
|
-
} from "../../theme";
|
|
13
|
-
import { DIALOG_LEFT, DIALOG_TOP, DIALOG_WIDTH } from "./shared";
|
|
3
|
+
import { GLYPHS, ATTR_DIM, TEXT_SUBTLE, toneStyles } from "../../theme";
|
|
4
|
+
import { DialogFrame, KeyHintRow, TextInput } from "./shared";
|
|
14
5
|
|
|
15
6
|
export interface HelmRollbackOverlayProps {
|
|
16
7
|
releaseName: string;
|
|
@@ -22,65 +13,20 @@ export function HelmRollbackOverlay({ releaseName, revisionValue, onChange }: He
|
|
|
22
13
|
const palette = toneStyles("warning");
|
|
23
14
|
|
|
24
15
|
return (
|
|
25
|
-
<
|
|
26
|
-
style={{
|
|
27
|
-
position: "absolute",
|
|
28
|
-
left: DIALOG_LEFT,
|
|
29
|
-
top: DIALOG_TOP,
|
|
30
|
-
width: DIALOG_WIDTH,
|
|
31
|
-
height: 12,
|
|
32
|
-
border: true,
|
|
33
|
-
borderStyle: "rounded",
|
|
34
|
-
borderColor: palette.border,
|
|
35
|
-
backgroundColor: OVERLAY_SURFACE,
|
|
36
|
-
padding: 1,
|
|
37
|
-
flexDirection: "column",
|
|
38
|
-
}}
|
|
39
|
-
title="Helm Rollback"
|
|
40
|
-
>
|
|
16
|
+
<DialogFrame title="Helm Rollback" tone="warning" height={12}>
|
|
41
17
|
<text fg={palette.fg} attributes={TextAttributes.BOLD}>
|
|
42
18
|
{releaseName}
|
|
43
19
|
</text>
|
|
44
|
-
<text fg={TEXT_SUBTLE} attributes={
|
|
20
|
+
<text fg={TEXT_SUBTLE} attributes={ATTR_DIM}>
|
|
45
21
|
{"Leave revision empty to roll back to previous version"}
|
|
46
22
|
</text>
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
marginTop: 1,
|
|
56
|
-
height: 3,
|
|
57
|
-
justifyContent: "center",
|
|
58
|
-
alignItems: "center",
|
|
59
|
-
flexDirection: "row",
|
|
60
|
-
}}
|
|
61
|
-
>
|
|
62
|
-
<text fg={TEXT_SUBTLE} attributes={TextAttributes.DIM}>
|
|
63
|
-
Revision:
|
|
64
|
-
</text>
|
|
65
|
-
<input
|
|
66
|
-
focused
|
|
67
|
-
value={revisionValue}
|
|
68
|
-
placeholder="(previous)"
|
|
69
|
-
style={{ flexGrow: 1, marginLeft: 1 }}
|
|
70
|
-
onInput={onChange}
|
|
71
|
-
onChange={onChange}
|
|
72
|
-
/>
|
|
73
|
-
</box>
|
|
74
|
-
<box style={{ flexDirection: "row", columnGap: 2, marginTop: 1, justifyContent: "flex-end" }}>
|
|
75
|
-
<text fg={TEXT_SUBTLE} attributes={TEXT_MUTED}>
|
|
76
|
-
<span fg={KEY_HINT}>{GLYPHS.enter}</span>
|
|
77
|
-
{" rollback"}
|
|
78
|
-
</text>
|
|
79
|
-
<text fg={TEXT_SUBTLE} attributes={TEXT_MUTED}>
|
|
80
|
-
<span fg={KEY_HINT}>{GLYPHS.esc}</span>
|
|
81
|
-
{" cancel"}
|
|
82
|
-
</text>
|
|
83
|
-
</box>
|
|
84
|
-
</box>
|
|
23
|
+
<TextInput
|
|
24
|
+
label="Revision:"
|
|
25
|
+
value={revisionValue}
|
|
26
|
+
placeholder="(previous)"
|
|
27
|
+
onChange={onChange}
|
|
28
|
+
/>
|
|
29
|
+
<KeyHintRow hints={[[GLYPHS.enter, "rollback"], [GLYPHS.esc, "cancel"]]} />
|
|
30
|
+
</DialogFrame>
|
|
85
31
|
);
|
|
86
32
|
}
|
|
@@ -0,0 +1,173 @@
|
|
|
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"],
|
|
27
|
+
["Y", "yaml tab (press again to copy)"],
|
|
28
|
+
["V", "events tab"],
|
|
29
|
+
["I", "describe tab"],
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
title: "Resource Actions",
|
|
34
|
+
rows: [
|
|
35
|
+
["L", "logs"],
|
|
36
|
+
["E", "edit (or edit helm values)"],
|
|
37
|
+
["X", "shell (kubectl exec)"],
|
|
38
|
+
["F", "port forward (per-resource)"],
|
|
39
|
+
["D", "delete"],
|
|
40
|
+
["Shift+S", "scale"],
|
|
41
|
+
["Shift+R", "rollout restart"],
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
title: "Helm Actions",
|
|
46
|
+
rows: [
|
|
47
|
+
["B", "rollback"],
|
|
48
|
+
["U", "upgrade --reuse-values"],
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const RIGHT_SECTIONS: HelpSection[] = [
|
|
54
|
+
{
|
|
55
|
+
title: "Overlays",
|
|
56
|
+
rows: [
|
|
57
|
+
["C", "cluster switcher"],
|
|
58
|
+
["N", "namespace switcher"],
|
|
59
|
+
["Shift+N", "notification history"],
|
|
60
|
+
["Shift+F", "active port forwards"],
|
|
61
|
+
["?", "this help"],
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
title: "Filter",
|
|
66
|
+
rows: [
|
|
67
|
+
["/", "enter filter mode"],
|
|
68
|
+
["Enter", "confirm filter"],
|
|
69
|
+
["Escape", "cancel filter"],
|
|
70
|
+
["Ctrl+U", "clear filter"],
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
title: "Logs Overlay",
|
|
75
|
+
rows: [
|
|
76
|
+
["/", "search"],
|
|
77
|
+
["N / Shift+N", "next / prev match"],
|
|
78
|
+
["C", "switch container"],
|
|
79
|
+
["P", "toggle previous logs"],
|
|
80
|
+
["T", "set tail lines"],
|
|
81
|
+
["S", "set since duration"],
|
|
82
|
+
["Up/Down", "scroll logs"],
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
title: "Port Forward List",
|
|
87
|
+
rows: [
|
|
88
|
+
["X", "stop selected forward"],
|
|
89
|
+
["C", "stop all forwards"],
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
title: "Global",
|
|
94
|
+
rows: [
|
|
95
|
+
["R", "refresh current view"],
|
|
96
|
+
["Escape", "close overlay"],
|
|
97
|
+
["Ctrl+C", "quit"],
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
const ALL_SECTIONS = [...LEFT_SECTIONS, ...RIGHT_SECTIONS];
|
|
103
|
+
|
|
104
|
+
const TWO_COLUMN_THRESHOLD = 90;
|
|
105
|
+
|
|
106
|
+
function renderSection(section: HelpSection, index: number, keyWidth: number): React.ReactNode {
|
|
107
|
+
return (
|
|
108
|
+
<box key={section.title} style={{ flexDirection: "column", width: "100%" }}>
|
|
109
|
+
{index > 0 && <box style={{ height: 1 }} />}
|
|
110
|
+
<text fg={toneStyles("accent").fg} attributes={TextAttributes.BOLD}>
|
|
111
|
+
{section.title}
|
|
112
|
+
</text>
|
|
113
|
+
{section.rows.map(([key, label], rowIdx) => (
|
|
114
|
+
<box key={`${index}:${rowIdx}`} style={{ flexDirection: "row", width: "100%", paddingLeft: 2 }}>
|
|
115
|
+
<box style={{ width: keyWidth }}>
|
|
116
|
+
<text fg={KEY_HINT}>{key}</text>
|
|
117
|
+
</box>
|
|
118
|
+
<text fg={TEXT_SUBTLE} attributes={ATTR_DIM}>
|
|
119
|
+
{label}
|
|
120
|
+
</text>
|
|
121
|
+
</box>
|
|
122
|
+
))}
|
|
123
|
+
</box>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function renderColumn(sections: HelpSection[], keyWidth: number): React.ReactNode {
|
|
128
|
+
return (
|
|
129
|
+
<box style={{ flexDirection: "column", flexGrow: 1, width: "100%" }}>
|
|
130
|
+
{sections.map((section, index) => renderSection(section, index, keyWidth))}
|
|
131
|
+
</box>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function HelpOverlay() {
|
|
136
|
+
const dims = useTerminalDimensions();
|
|
137
|
+
const twoColumns = dims.width >= TWO_COLUMN_THRESHOLD;
|
|
138
|
+
|
|
139
|
+
const left = twoColumns ? "8%" : "16%";
|
|
140
|
+
const width = twoColumns ? "84%" : "68%";
|
|
141
|
+
const keyWidth = twoColumns ? 20 : 24;
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<box
|
|
145
|
+
style={{
|
|
146
|
+
position: "absolute",
|
|
147
|
+
left,
|
|
148
|
+
top: "6%",
|
|
149
|
+
width,
|
|
150
|
+
height: "86%",
|
|
151
|
+
border: true,
|
|
152
|
+
borderColor: PANEL_BORDER_ACTIVE,
|
|
153
|
+
borderStyle: "rounded",
|
|
154
|
+
backgroundColor: OVERLAY_SURFACE,
|
|
155
|
+
padding: 1,
|
|
156
|
+
flexDirection: "column",
|
|
157
|
+
}}
|
|
158
|
+
title="Keyboard Shortcuts"
|
|
159
|
+
>
|
|
160
|
+
<scrollbox style={{ flexGrow: 1 }} focused>
|
|
161
|
+
{twoColumns ? (
|
|
162
|
+
<box style={{ flexDirection: "row", width: "100%", columnGap: 2 }}>
|
|
163
|
+
{renderColumn(LEFT_SECTIONS, keyWidth)}
|
|
164
|
+
{renderColumn(RIGHT_SECTIONS, keyWidth)}
|
|
165
|
+
</box>
|
|
166
|
+
) : (
|
|
167
|
+
renderColumn(ALL_SECTIONS, keyWidth)
|
|
168
|
+
)}
|
|
169
|
+
</scrollbox>
|
|
170
|
+
<KeyHintRow hints={[[GLYPHS.esc, "close"]]} />
|
|
171
|
+
</box>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
@@ -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";
|