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
|
@@ -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
|
}
|
|
@@ -1,18 +1,97 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TextAttributes } from "@opentui/core";
|
|
2
|
+
|
|
3
|
+
import { FILTER_BACKGROUND, GLYPHS, KEY_HINT, OVERLAY_SURFACE, PANEL_BORDER_ACTIVE, ATTR_DIM, TEXT_SUBTLE, toneStyles } from "../../theme";
|
|
2
4
|
|
|
3
5
|
export const DIALOG_LEFT = "28%";
|
|
4
6
|
export const DIALOG_TOP = "20%";
|
|
5
7
|
export const DIALOG_WIDTH = "44%";
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
const GLYPH_VALUES: ReadonlySet<string> = new Set<string>(Object.values(GLYPHS));
|
|
10
|
+
|
|
11
|
+
export function isGlyph(value: string): boolean {
|
|
12
|
+
return GLYPH_VALUES.has(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function KeyHintRow({ hints, justifyContent = "flex-end" }: { hints: [string, string][]; justifyContent?: string }) {
|
|
8
16
|
return (
|
|
9
|
-
<box style={{ flexDirection: "row", columnGap: 2, marginTop: 1 }}>
|
|
10
|
-
{hints.map(([key, label]) => (
|
|
11
|
-
<text key={key} fg={TEXT_SUBTLE} attributes={
|
|
12
|
-
<span fg={KEY_HINT}>{
|
|
17
|
+
<box style={{ flexDirection: "row", columnGap: 2, marginTop: 1, justifyContent: justifyContent as "flex-end" | "flex-start" | "center" }}>
|
|
18
|
+
{hints.map(([key, label], index) => (
|
|
19
|
+
<text key={`${index}:${key}`} fg={TEXT_SUBTLE} attributes={ATTR_DIM}>
|
|
20
|
+
<span fg={KEY_HINT}>{key}</span>
|
|
13
21
|
{` ${label}`}
|
|
14
22
|
</text>
|
|
15
23
|
))}
|
|
16
24
|
</box>
|
|
17
25
|
);
|
|
18
26
|
}
|
|
27
|
+
|
|
28
|
+
export interface TextInputProps {
|
|
29
|
+
label: string;
|
|
30
|
+
value: string;
|
|
31
|
+
placeholder?: string | undefined;
|
|
32
|
+
onChange: (value: string) => void;
|
|
33
|
+
focused?: boolean | undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function TextInput({ label, value, placeholder, onChange, focused = true }: TextInputProps) {
|
|
37
|
+
return (
|
|
38
|
+
<box
|
|
39
|
+
style={{
|
|
40
|
+
border: true,
|
|
41
|
+
borderColor: PANEL_BORDER_ACTIVE,
|
|
42
|
+
borderStyle: "rounded",
|
|
43
|
+
backgroundColor: FILTER_BACKGROUND,
|
|
44
|
+
paddingLeft: 1,
|
|
45
|
+
paddingRight: 1,
|
|
46
|
+
marginTop: 1,
|
|
47
|
+
height: 3,
|
|
48
|
+
justifyContent: "center",
|
|
49
|
+
alignItems: "center",
|
|
50
|
+
flexDirection: "row",
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
<text fg={TEXT_SUBTLE} attributes={TextAttributes.DIM}>
|
|
54
|
+
{label}
|
|
55
|
+
</text>
|
|
56
|
+
<input
|
|
57
|
+
focused={focused}
|
|
58
|
+
value={value}
|
|
59
|
+
placeholder={placeholder ?? ""}
|
|
60
|
+
style={{ flexGrow: 1, marginLeft: 1 }}
|
|
61
|
+
onInput={onChange}
|
|
62
|
+
onChange={onChange}
|
|
63
|
+
/>
|
|
64
|
+
</box>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface DialogFrameProps {
|
|
69
|
+
title: string;
|
|
70
|
+
tone: "accent" | "warning" | "danger" | "info" | "success" | "neutral";
|
|
71
|
+
height: number;
|
|
72
|
+
children: React.ReactNode;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function DialogFrame({ title, tone, height, children }: DialogFrameProps) {
|
|
76
|
+
const palette = toneStyles(tone);
|
|
77
|
+
return (
|
|
78
|
+
<box
|
|
79
|
+
style={{
|
|
80
|
+
position: "absolute",
|
|
81
|
+
left: DIALOG_LEFT,
|
|
82
|
+
top: DIALOG_TOP,
|
|
83
|
+
width: DIALOG_WIDTH,
|
|
84
|
+
height,
|
|
85
|
+
border: true,
|
|
86
|
+
borderStyle: "rounded",
|
|
87
|
+
borderColor: palette.border,
|
|
88
|
+
backgroundColor: OVERLAY_SURFACE,
|
|
89
|
+
padding: 1,
|
|
90
|
+
flexDirection: "column",
|
|
91
|
+
}}
|
|
92
|
+
title={title}
|
|
93
|
+
>
|
|
94
|
+
{children}
|
|
95
|
+
</box>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -67,7 +67,7 @@ export function ResourceRows({
|
|
|
67
67
|
GLYPHS.dotEmpty;
|
|
68
68
|
|
|
69
69
|
const hasActiveForward = activeForwardKeys.has(
|
|
70
|
-
`${resource.ref.kind.toLowerCase()}:${resource.ref.name}`
|
|
70
|
+
`${resource.ref.kind.toLowerCase()}:${resource.ref.namespace ?? "cluster"}:${resource.ref.name}`
|
|
71
71
|
);
|
|
72
72
|
|
|
73
73
|
// Metrics suffix
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Polling intervals (milliseconds)
|
|
2
|
+
export const POLL_CLUSTER_MS = 30_000;
|
|
3
|
+
export const POLL_RESOURCE_MS = 5_000;
|
|
4
|
+
export const POLL_METRICS_MS = 15_000;
|
|
5
|
+
|
|
6
|
+
// UI
|
|
7
|
+
export const TOAST_DURATION_MS = 4_000;
|
|
8
|
+
export const INSPECTOR_MIN_WIDTH = 110;
|
|
9
|
+
export const KIND_PANE_WIDTH = 34;
|
|
10
|
+
export const INSPECTOR_PANE_WIDTH = 52;
|
|
11
|
+
export const TRUNCATE_DEFAULT = 26;
|
|
12
|
+
|
|
13
|
+
// Footer hint layout
|
|
14
|
+
export const FOOTER_PADDING = 4;
|
|
15
|
+
export const HINT_CELL_OVERHEAD = 3;
|
|
16
|
+
|
|
17
|
+
// Notifications
|
|
18
|
+
export const MAX_VISIBLE_NOTIFICATIONS = 3;
|
|
19
|
+
export const NOTIFICATION_HISTORY_LIMIT = 50;
|
|
20
|
+
export const NOTIFICATION_SLIDE_FRAMES = 6;
|
|
21
|
+
export const NOTIFICATION_SLIDE_INTERVAL_MS = 30;
|
|
22
|
+
export const NOTIFICATION_PROGRESS_INTERVAL_MS = 50;
|
|
23
|
+
export const NOTIFICATION_WIDTH_PCT = "40%";
|
|
24
|
+
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AppAction } from "../../app-actions";
|
|
2
|
+
import { isSlash } from "./keys";
|
|
2
3
|
|
|
3
4
|
export function handleFilterKeys(
|
|
4
5
|
key: { name: string; ctrl?: boolean; sequence?: string },
|
|
@@ -12,7 +13,7 @@ export function handleFilterKeys(
|
|
|
12
13
|
return true;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
if (activePane === "resources" && (key
|
|
16
|
+
if (activePane === "resources" && isSlash(key)) {
|
|
16
17
|
setFilterMode(true);
|
|
17
18
|
return true;
|
|
18
19
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { abortRunningProcesses } from "../../../lib/kubectl/spawn-utils";
|
|
2
|
+
import { isHelmRelease } from "../../../lib/kubectl/kubectl-helpers";
|
|
2
3
|
import type { AppAction } from "../../app-actions";
|
|
3
4
|
import type { AppState, ResourceRef, ResourceKind, ResourceDetail, NotificationTone } from "../../../lib/k8s/types";
|
|
4
5
|
import { statusLine } from "../../utils";
|
|
6
|
+
import type { ToastOptions } from "../use-notifications";
|
|
5
7
|
|
|
6
8
|
export function handleCtrlC(
|
|
7
9
|
key: { name: string; ctrl?: boolean },
|
|
@@ -16,7 +18,7 @@ export function handleCtrlC(
|
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export function handleGlobalHotkeys(
|
|
19
|
-
key: { name: string; shift?: boolean },
|
|
21
|
+
key: { name: string; shift?: boolean; sequence?: string },
|
|
20
22
|
state: AppState,
|
|
21
23
|
dispatch: React.Dispatch<AppAction>,
|
|
22
24
|
activeResourceRef: ResourceRef | undefined,
|
|
@@ -27,7 +29,7 @@ export function handleGlobalHotkeys(
|
|
|
27
29
|
visibleSelectedResourceName: string | undefined,
|
|
28
30
|
setOverlayIndex: React.Dispatch<React.SetStateAction<number>>,
|
|
29
31
|
openPortForwardOverlay: () => void,
|
|
30
|
-
toast: (tone: NotificationTone, message: string) => void,
|
|
32
|
+
toast: (tone: NotificationTone, message: string, options?: ToastOptions) => void,
|
|
31
33
|
copyToClipboard: (text: string, label?: string) => void,
|
|
32
34
|
setManualRefreshNonce: React.Dispatch<React.SetStateAction<number>>,
|
|
33
35
|
setScaleReplicasInput: React.Dispatch<React.SetStateAction<string>>,
|
|
@@ -35,14 +37,14 @@ export function handleGlobalHotkeys(
|
|
|
35
37
|
if (state.overlay) return false;
|
|
36
38
|
|
|
37
39
|
// C → cluster switcher
|
|
38
|
-
if (key.name === "c") {
|
|
40
|
+
if (key.name === "c" && !key.shift) {
|
|
39
41
|
dispatch({ type: "setOverlay", overlay: "cluster-switcher" });
|
|
40
42
|
setOverlayIndex(Math.max(0, state.contexts.findIndex((context) => context.name === state.activeContext)));
|
|
41
43
|
return true;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
// N → namespace switcher
|
|
45
|
-
if (key.name === "n") {
|
|
47
|
+
if (key.name === "n" && !key.shift) {
|
|
46
48
|
dispatch({ type: "setOverlay", overlay: "namespace-switcher" });
|
|
47
49
|
setOverlayIndex(Math.max(0, state.namespaces.findIndex((namespace) => namespace.name === state.activeNamespace)));
|
|
48
50
|
return true;
|
|
@@ -62,7 +64,7 @@ export function handleGlobalHotkeys(
|
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
// Y → yaml tab
|
|
65
|
-
if (key.name === "y") {
|
|
67
|
+
if (key.name === "y" && !key.shift) {
|
|
66
68
|
if (state.inspectorTab === "yaml" && activeDetail?.yaml) {
|
|
67
69
|
copyToClipboard(activeDetail.yaml, "YAML");
|
|
68
70
|
} else {
|
|
@@ -71,22 +73,10 @@ export function handleGlobalHotkeys(
|
|
|
71
73
|
return true;
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
// V → events tab
|
|
75
|
-
if (key.name === "v") {
|
|
76
|
-
dispatch({ type: "setInspectorTab", tab: "events" });
|
|
77
|
-
return true;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// I → describe tab
|
|
81
|
-
if (key.name === "i") {
|
|
82
|
-
dispatch({ type: "setInspectorTab", tab: "describe" });
|
|
83
|
-
return true;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
76
|
// L → logs
|
|
87
|
-
if (key.name === "l") {
|
|
77
|
+
if (key.name === "l" && !key.shift) {
|
|
88
78
|
if (activeResourceRef) {
|
|
89
|
-
if (activeResourceRef
|
|
79
|
+
if (isHelmRelease(activeResourceRef)) {
|
|
90
80
|
dispatch({ type: "setStatusMessage", message: "Logs not supported for Helm releases" });
|
|
91
81
|
return true;
|
|
92
82
|
}
|
|
@@ -106,13 +96,13 @@ export function handleGlobalHotkeys(
|
|
|
106
96
|
}
|
|
107
97
|
|
|
108
98
|
// F → port-forward overlay
|
|
109
|
-
if (key.name === "f" && activeResourceRef && selectedKind && state.activeContext && ((activeDetail?.portForwards?.length ?? 0) > 0 || selectedResourcePortForwards.length > 0)) {
|
|
99
|
+
if (key.name === "f" && !key.shift && activeResourceRef && selectedKind && state.activeContext && ((activeDetail?.portForwards?.length ?? 0) > 0 || selectedResourcePortForwards.length > 0)) {
|
|
110
100
|
openPortForwardOverlay();
|
|
111
101
|
return true;
|
|
112
102
|
}
|
|
113
103
|
|
|
114
104
|
// D → delete confirm
|
|
115
|
-
if (key.name === "d" && selectedKind && state.activeContext && deleteTargets.length > 0) {
|
|
105
|
+
if (key.name === "d" && !key.shift && selectedKind && state.activeContext && deleteTargets.length > 0) {
|
|
116
106
|
dispatch({ type: "setOverlay", overlay: "delete-confirm" });
|
|
117
107
|
return true;
|
|
118
108
|
}
|
|
@@ -130,5 +120,24 @@ export function handleGlobalHotkeys(
|
|
|
130
120
|
return true;
|
|
131
121
|
}
|
|
132
122
|
|
|
123
|
+
// Shift+N → notification history
|
|
124
|
+
if (key.shift && key.name === "n") {
|
|
125
|
+
dispatch({ type: "setOverlay", overlay: "notification-history" });
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Shift+F → port forward list
|
|
130
|
+
if (key.shift && key.name === "f") {
|
|
131
|
+
dispatch({ type: "setOverlay", overlay: "port-forward-list" });
|
|
132
|
+
setOverlayIndex(0);
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ? → help
|
|
137
|
+
if (key.sequence === "?" || (key.name === "slash" && key.shift)) {
|
|
138
|
+
dispatch({ type: "setOverlay", overlay: "help" });
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
133
142
|
return false;
|
|
134
143
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { AppAction } from "../../app-actions";
|
|
2
2
|
import type { AppState, ResourceRef, ResourceKind, ResourceDetail, NotificationTone } from "../../../lib/k8s/types";
|
|
3
|
+
import { isHelmRelease } from "../../../lib/kubectl/kubectl-helpers";
|
|
3
4
|
import { KubectlService } from "../../../lib/kubectl/kubectl-service";
|
|
5
|
+
import type { ToastOptions } from "../use-notifications";
|
|
4
6
|
|
|
5
7
|
export function handleHelmRollbackKeys(
|
|
6
8
|
key: { name: string },
|
|
@@ -8,8 +10,8 @@ export function handleHelmRollbackKeys(
|
|
|
8
10
|
dispatch: React.Dispatch<AppAction>,
|
|
9
11
|
activeResourceRef: ResourceRef | undefined,
|
|
10
12
|
kubectl: KubectlService,
|
|
11
|
-
toast: (tone: NotificationTone, message: string) => void,
|
|
12
|
-
toastError: (error: unknown) => void,
|
|
13
|
+
toast: (tone: NotificationTone, message: string, options?: ToastOptions) => void,
|
|
14
|
+
toastError: (error: unknown, options?: ToastOptions) => void,
|
|
13
15
|
setManualRefreshNonce: React.Dispatch<React.SetStateAction<number>>,
|
|
14
16
|
): boolean {
|
|
15
17
|
if (state.overlay !== "helm-rollback") return false;
|
|
@@ -19,7 +21,8 @@ export function handleHelmRollbackKeys(
|
|
|
19
21
|
const namespace = state.activeNamespace;
|
|
20
22
|
const name = activeResourceRef.name;
|
|
21
23
|
const revisionStr = state.helmRollbackRevision.trim();
|
|
22
|
-
const
|
|
24
|
+
const parsedRevision = revisionStr ? Number.parseInt(revisionStr, 10) : undefined;
|
|
25
|
+
const revision = parsedRevision !== undefined && Number.isFinite(parsedRevision) ? parsedRevision : undefined;
|
|
23
26
|
|
|
24
27
|
dispatch({ type: "setOverlay", overlay: undefined });
|
|
25
28
|
dispatch({
|
|
@@ -33,7 +36,9 @@ export function handleHelmRollbackKeys(
|
|
|
33
36
|
toast("success", `${name} rolled back${revision !== undefined ? ` to r${revision}` : ""}`);
|
|
34
37
|
setManualRefreshNonce((current) => current + 1);
|
|
35
38
|
})
|
|
36
|
-
.catch(
|
|
39
|
+
.catch((error: unknown) => {
|
|
40
|
+
toastError(error, { actions: [{ label: "retry", onAction: () => { void kubectl.helmRollback({ context, namespace, name, revision }).then(() => setManualRefreshNonce((c) => c + 1)).catch(toastError); } }] });
|
|
41
|
+
});
|
|
37
42
|
|
|
38
43
|
return true;
|
|
39
44
|
}
|
|
@@ -55,21 +60,21 @@ export function handleHelmUpgradeKeys(
|
|
|
55
60
|
selectedKind: ResourceKind | undefined,
|
|
56
61
|
activeDetail: ResourceDetail | undefined,
|
|
57
62
|
kubectl: KubectlService,
|
|
58
|
-
toast: (tone: NotificationTone, message: string) => void,
|
|
59
|
-
toastError: (error: unknown) => void,
|
|
63
|
+
toast: (tone: NotificationTone, message: string, options?: ToastOptions) => void,
|
|
64
|
+
toastError: (error: unknown, options?: ToastOptions) => void,
|
|
60
65
|
setManualRefreshNonce: React.Dispatch<React.SetStateAction<number>>,
|
|
61
66
|
): boolean {
|
|
62
67
|
if (state.overlay) return false;
|
|
63
68
|
|
|
64
69
|
// B → Helm rollback overlay
|
|
65
|
-
if (key.name === "b" && activeResourceRef
|
|
70
|
+
if (key.name === "b" && isHelmRelease(activeResourceRef) && state.activeContext) {
|
|
66
71
|
dispatch({ type: "setHelmRollbackRevision", value: "" });
|
|
67
72
|
dispatch({ type: "setOverlay", overlay: "helm-rollback" });
|
|
68
73
|
return true;
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
// U → Helm upgrade --reuse-values
|
|
72
|
-
if (key.name === "u" && activeResourceRef
|
|
77
|
+
if (key.name === "u" && isHelmRelease(activeResourceRef) && selectedKind && state.activeContext) {
|
|
73
78
|
const chartName = activeDetail?.helmChart ?? "";
|
|
74
79
|
if (!chartName) {
|
|
75
80
|
dispatch({ type: "setStatusMessage", message: "Chart name unavailable — load the detail pane first" });
|
|
@@ -95,7 +100,7 @@ export function handleHelmUpgradeKeys(
|
|
|
95
100
|
});
|
|
96
101
|
upgradeChild.on("error", (err) => {
|
|
97
102
|
renderer.resume();
|
|
98
|
-
toastError(err);
|
|
103
|
+
toastError(err, { actions: [{ label: "retry", onAction: () => { kubectl.helmUpgrade({ context, namespace, name: target.name, chart: chartName }); } }] });
|
|
99
104
|
});
|
|
100
105
|
return true;
|
|
101
106
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface KeyEvent {
|
|
2
|
+
name: string;
|
|
3
|
+
shift?: boolean;
|
|
4
|
+
ctrl?: boolean;
|
|
5
|
+
sequence?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function isUp(key: KeyEvent): boolean {
|
|
9
|
+
return key.name === "up" || key.name === "k";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isDown(key: KeyEvent): boolean {
|
|
13
|
+
return key.name === "down" || key.name === "j";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isSlash(key: KeyEvent): boolean {
|
|
17
|
+
return key.sequence === "/" || key.name === "slash";
|
|
18
|
+
}
|
|
@@ -3,6 +3,7 @@ import type { ScrollBoxRenderable } from "@opentui/core";
|
|
|
3
3
|
import type { AppAction } from "../../app-actions";
|
|
4
4
|
import type { AppState } from "../../../lib/k8s/types";
|
|
5
5
|
import type { LogsDialogActions } from "../../components/overlays";
|
|
6
|
+
import { isSlash } from "./keys";
|
|
6
7
|
|
|
7
8
|
export function handleLogsKeys(
|
|
8
9
|
key: { name: string; shift?: boolean; sequence?: string },
|
|
@@ -54,7 +55,7 @@ export function handleLogsKeys(
|
|
|
54
55
|
} else if (key.name === "s") {
|
|
55
56
|
setLogsInputMode("since");
|
|
56
57
|
setLogsInputValue(state.logsOptions.since ?? "");
|
|
57
|
-
} else if (key
|
|
58
|
+
} else if (isSlash(key)) {
|
|
58
59
|
setLogsSearchMode(true);
|
|
59
60
|
} else if (key.name === "n" && !key.shift) {
|
|
60
61
|
logsActionsRef.current?.nextMatch();
|
|
@@ -74,6 +75,7 @@ export function handleLogsKeys(
|
|
|
74
75
|
setLogsInputValue("");
|
|
75
76
|
} else if (logsSearchMode && key.name === "return") {
|
|
76
77
|
setLogsSearchMode(false);
|
|
78
|
+
setLogsSearchText("");
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
return true;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AppAction } from "../../app-actions";
|
|
2
2
|
import { INSPECTOR_TABS } from "../../components/inspector";
|
|
3
|
+
import { isDown, isUp } from "./keys";
|
|
3
4
|
import type { AppState, ResourceKind, ResourceListItem } from "../../../lib/k8s/types";
|
|
4
5
|
|
|
5
6
|
export function handlePaneNavigation(
|
|
@@ -18,7 +19,7 @@ export function handlePaneNavigation(
|
|
|
18
19
|
if (state.activePane === "clusters") {
|
|
19
20
|
const currentIndex = Math.max(0, state.resourceKinds.findIndex((kind) => kind.name === state.selectedKind));
|
|
20
21
|
|
|
21
|
-
if ((key
|
|
22
|
+
if (isUp(key) && currentIndex > 0) {
|
|
22
23
|
const nextKind = state.resourceKinds[currentIndex - 1];
|
|
23
24
|
if (nextKind) {
|
|
24
25
|
dispatch({ type: "setSelectedKind", kind: nextKind.name });
|
|
@@ -26,7 +27,7 @@ export function handlePaneNavigation(
|
|
|
26
27
|
return true;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
if ((key
|
|
30
|
+
if (isDown(key) && currentIndex < state.resourceKinds.length - 1) {
|
|
30
31
|
const nextKind = state.resourceKinds[currentIndex + 1];
|
|
31
32
|
if (nextKind) {
|
|
32
33
|
dispatch({ type: "setSelectedKind", kind: nextKind.name });
|
|
@@ -49,7 +50,7 @@ export function handlePaneNavigation(
|
|
|
49
50
|
filteredResources.findIndex((resource) => resource.ref.name === visibleSelectedResourceName),
|
|
50
51
|
);
|
|
51
52
|
|
|
52
|
-
if ((key
|
|
53
|
+
if (isUp(key) && currentIndex > 0) {
|
|
53
54
|
const nextResource = filteredResources[currentIndex - 1];
|
|
54
55
|
if (nextResource) {
|
|
55
56
|
dispatch({ type: "setSelectedResourceName", name: nextResource.ref.name });
|
|
@@ -57,7 +58,7 @@ export function handlePaneNavigation(
|
|
|
57
58
|
return true;
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
if ((key
|
|
61
|
+
if (isDown(key) && currentIndex < filteredResources.length - 1) {
|
|
61
62
|
const nextResource = filteredResources[currentIndex + 1];
|
|
62
63
|
if (nextResource) {
|
|
63
64
|
dispatch({ type: "setSelectedResourceName", name: nextResource.ref.name });
|
|
@@ -2,6 +2,8 @@ import type { AppAction } from "../../app-actions";
|
|
|
2
2
|
import type { AppState, ResourceListItem, ResourceKind, ResourceRef } from "../../../lib/k8s/types";
|
|
3
3
|
import { deleteStatusMessage, statusLine } from "../../utils";
|
|
4
4
|
import { KubectlService } from "../../../lib/kubectl/kubectl-service";
|
|
5
|
+
import { isDown, isUp } from "./keys";
|
|
6
|
+
import type { ToastOptions } from "../use-notifications";
|
|
5
7
|
|
|
6
8
|
export function handleGenericEscape(
|
|
7
9
|
key: { name: string },
|
|
@@ -31,17 +33,19 @@ export function handleOverlayNavigation(
|
|
|
31
33
|
? state.contexts
|
|
32
34
|
: state.overlay === "namespace-switcher"
|
|
33
35
|
? state.namespaces
|
|
34
|
-
: state.overlay === "
|
|
35
|
-
?
|
|
36
|
-
:
|
|
36
|
+
: state.overlay === "port-forward-list"
|
|
37
|
+
? state.activePortForwards
|
|
38
|
+
: state.overlay === "container-picker"
|
|
39
|
+
? []
|
|
40
|
+
: [];
|
|
37
41
|
|
|
38
|
-
if (key
|
|
42
|
+
if (isUp(key)) {
|
|
39
43
|
setOverlayIndex((current) => Math.max(0, current - 1));
|
|
40
44
|
return true;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
|
-
if (key
|
|
44
|
-
setOverlayIndex((current) => Math.min(items.length - 1, current + 1));
|
|
47
|
+
if (isDown(key)) {
|
|
48
|
+
setOverlayIndex((current) => Math.max(0, Math.min(items.length - 1, current + 1)));
|
|
45
49
|
return true;
|
|
46
50
|
}
|
|
47
51
|
|
|
@@ -79,7 +83,7 @@ export function handleDeleteConfirm(
|
|
|
79
83
|
selectedKind: ResourceKind | undefined,
|
|
80
84
|
deleteTargets: ResourceListItem[],
|
|
81
85
|
kubectl: KubectlService,
|
|
82
|
-
toastError: (error: unknown) => void,
|
|
86
|
+
toastError: (error: unknown, options?: ToastOptions) => void,
|
|
83
87
|
setManualRefreshNonce: React.Dispatch<React.SetStateAction<number>>,
|
|
84
88
|
): boolean {
|
|
85
89
|
if (state.overlay !== "delete-confirm") return false;
|