openk8s 1.0.4 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +11 -10
- package/src/app/__tests__/app-state.test.ts +47 -5
- package/src/app/__tests__/utils.test.ts +5 -14
- package/src/app/app-actions.ts +5 -0
- package/src/app/app-state.ts +11 -2
- package/src/app/app.tsx +57 -46
- package/src/app/components/detail-sections.tsx +2 -2
- package/src/app/components/footer.tsx +23 -30
- package/src/app/components/inspector.tsx +28 -43
- package/src/app/components/kind-rows.tsx +1 -1
- package/src/app/components/notification-card.tsx +145 -0
- package/src/app/components/notification-tray.tsx +19 -38
- package/src/app/components/overlays/delete-confirm-overlay.tsx +15 -17
- package/src/app/components/overlays/helm-rollback-overlay.tsx +12 -66
- package/src/app/components/overlays/help-overlay.tsx +173 -0
- package/src/app/components/overlays/index.ts +5 -2
- package/src/app/components/overlays/logs-dialog.tsx +50 -80
- package/src/app/components/overlays/notification-history-overlay.tsx +79 -0
- package/src/app/components/overlays/port-forward-list-overlay.tsx +85 -0
- package/src/app/components/overlays/port-forward-overlay.tsx +16 -56
- package/src/app/components/overlays/scale-dialog.tsx +12 -67
- package/src/app/components/overlays/select-overlay.tsx +5 -14
- package/src/app/components/overlays/shared.tsx +85 -6
- package/src/app/components/resource-rows.tsx +1 -1
- package/src/app/constants.ts +24 -0
- package/src/app/hooks/keyboard/filter-handlers.ts +2 -1
- package/src/app/hooks/keyboard/global-handlers.ts +32 -11
- package/src/app/hooks/keyboard/helm-handlers.ts +14 -9
- package/src/app/hooks/keyboard/keys.ts +18 -0
- package/src/app/hooks/keyboard/logs-handlers.ts +3 -1
- package/src/app/hooks/keyboard/navigation-handlers.ts +5 -4
- package/src/app/hooks/keyboard/overlay-handlers.ts +11 -7
- package/src/app/hooks/keyboard/port-forward-handlers.ts +19 -14
- package/src/app/hooks/keyboard/shell-edit-handlers.ts +45 -17
- package/src/app/hooks/use-app-keyboard.ts +22 -2
- package/src/app/hooks/use-app-side-effects.ts +10 -7
- package/src/app/hooks/use-clipboard.ts +8 -10
- package/src/app/hooks/use-data-fetching.ts +11 -5
- package/src/app/hooks/use-log-stream.ts +8 -4
- package/src/app/hooks/use-notifications.ts +92 -0
- package/src/app/hooks/use-port-forward.ts +19 -4
- package/src/app/persistence.ts +7 -3
- package/src/app/syntax-theme.ts +31 -0
- package/src/app/theme.ts +2 -3
- package/src/app/use-footer-hints.ts +21 -16
- package/src/app/utils.ts +1 -9
- package/src/assets/tree-sitter/yaml/highlights.scm +79 -0
- package/src/assets/tree-sitter/yaml/tree-sitter-yaml.wasm +0 -0
- package/src/index.tsx +22 -2
- package/src/lib/k8s/__tests__/k8s-format.test.ts +17 -0
- package/src/lib/k8s/__tests__/resource-parser.test.ts +40 -2
- package/src/lib/k8s/detail-builders/event-builder.ts +17 -11
- package/src/lib/k8s/detail-builders/hpa-cronjob-builder.ts +34 -31
- package/src/lib/k8s/detail-builders/node-builder.ts +22 -11
- package/src/lib/k8s/detail-builders/overview-builder.ts +4 -17
- package/src/lib/k8s/detail-builders/pod-builder.ts +52 -24
- package/src/lib/k8s/detail-builders/rbac-builder.ts +20 -29
- package/src/lib/k8s/k8s-format.ts +14 -9
- package/src/lib/k8s/resource-detail-builder.ts +1 -1
- package/src/lib/k8s/resource-parser.ts +17 -2
- package/src/lib/k8s/types.ts +10 -1
- package/src/lib/kubectl/__tests__/metrics-utils.test.ts +9 -0
- package/src/lib/kubectl/kubectl-helpers.ts +16 -11
- package/src/lib/kubectl/kubectl-service.ts +39 -39
- package/src/lib/kubectl/kubectl-types.ts +10 -1
- package/src/lib/kubectl/metrics-utils.ts +21 -7
- package/src/lib/kubectl/spawn-utils.ts +50 -11
- package/src/app/__tests__/components/inspector-tokens.test.ts +0 -101
- package/src/app/components/inspector-tokens.ts +0 -93
- package/src/app/components/port-forwards-tray.tsx +0 -57
|
@@ -1,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 {
|
|
@@ -72,21 +74,21 @@ export function handleGlobalHotkeys(
|
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
// V → events tab
|
|
75
|
-
if (key.name === "v") {
|
|
77
|
+
if (key.name === "v" && !key.shift) {
|
|
76
78
|
dispatch({ type: "setInspectorTab", tab: "events" });
|
|
77
79
|
return true;
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
// I → describe tab
|
|
81
|
-
if (key.name === "i") {
|
|
83
|
+
if (key.name === "i" && !key.shift) {
|
|
82
84
|
dispatch({ type: "setInspectorTab", tab: "describe" });
|
|
83
85
|
return true;
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
// L → logs
|
|
87
|
-
if (key.name === "l") {
|
|
89
|
+
if (key.name === "l" && !key.shift) {
|
|
88
90
|
if (activeResourceRef) {
|
|
89
|
-
if (activeResourceRef
|
|
91
|
+
if (isHelmRelease(activeResourceRef)) {
|
|
90
92
|
dispatch({ type: "setStatusMessage", message: "Logs not supported for Helm releases" });
|
|
91
93
|
return true;
|
|
92
94
|
}
|
|
@@ -106,13 +108,13 @@ export function handleGlobalHotkeys(
|
|
|
106
108
|
}
|
|
107
109
|
|
|
108
110
|
// F → port-forward overlay
|
|
109
|
-
if (key.name === "f" && activeResourceRef && selectedKind && state.activeContext && ((activeDetail?.portForwards?.length ?? 0) > 0 || selectedResourcePortForwards.length > 0)) {
|
|
111
|
+
if (key.name === "f" && !key.shift && activeResourceRef && selectedKind && state.activeContext && ((activeDetail?.portForwards?.length ?? 0) > 0 || selectedResourcePortForwards.length > 0)) {
|
|
110
112
|
openPortForwardOverlay();
|
|
111
113
|
return true;
|
|
112
114
|
}
|
|
113
115
|
|
|
114
116
|
// D → delete confirm
|
|
115
|
-
if (key.name === "d" && selectedKind && state.activeContext && deleteTargets.length > 0) {
|
|
117
|
+
if (key.name === "d" && !key.shift && selectedKind && state.activeContext && deleteTargets.length > 0) {
|
|
116
118
|
dispatch({ type: "setOverlay", overlay: "delete-confirm" });
|
|
117
119
|
return true;
|
|
118
120
|
}
|
|
@@ -130,5 +132,24 @@ export function handleGlobalHotkeys(
|
|
|
130
132
|
return true;
|
|
131
133
|
}
|
|
132
134
|
|
|
135
|
+
// Shift+N → notification history
|
|
136
|
+
if (key.shift && key.name === "n") {
|
|
137
|
+
dispatch({ type: "setOverlay", overlay: "notification-history" });
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Shift+F → port forward list
|
|
142
|
+
if (key.shift && key.name === "f") {
|
|
143
|
+
dispatch({ type: "setOverlay", overlay: "port-forward-list" });
|
|
144
|
+
setOverlayIndex(0);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ? → help
|
|
149
|
+
if (key.sequence === "?" || (key.name === "slash" && key.shift)) {
|
|
150
|
+
dispatch({ type: "setOverlay", overlay: "help" });
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
133
154
|
return false;
|
|
134
155
|
}
|
|
@@ -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;
|
|
@@ -3,6 +3,7 @@ import type { AppState, ActivePortForward, ResourceRef, ResourceKind, ResourceDe
|
|
|
3
3
|
import type { StartPortForwardLifecycleOptions } from "../../utils";
|
|
4
4
|
import { parseLocalPort } from "../../utils";
|
|
5
5
|
import { buildPortForwardEntries } from "../../components/overlays";
|
|
6
|
+
import { isDown, isUp } from "./keys";
|
|
6
7
|
|
|
7
8
|
export function handlePortForwardKeys(
|
|
8
9
|
key: { name: string },
|
|
@@ -28,23 +29,27 @@ export function handlePortForwardKeys(
|
|
|
28
29
|
|
|
29
30
|
const pfEntries = buildPortForwardEntries(activeDetail?.portForwards ?? [], selectedResourcePortForwards);
|
|
30
31
|
|
|
31
|
-
if (key
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
if (isUp(key)) {
|
|
33
|
+
setOverlayIndex((current) => {
|
|
34
|
+
const newIndex = Math.max(0, current - 1);
|
|
35
|
+
const entry = pfEntries[newIndex];
|
|
36
|
+
if (entry && !entry.activeForward) {
|
|
37
|
+
setPortForwardLocalPort(String(entry.suggestedLocalPort));
|
|
38
|
+
}
|
|
39
|
+
return newIndex;
|
|
40
|
+
});
|
|
38
41
|
return true;
|
|
39
42
|
}
|
|
40
43
|
|
|
41
|
-
if (key
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
if (isDown(key)) {
|
|
45
|
+
setOverlayIndex((current) => {
|
|
46
|
+
const newIndex = Math.min(Math.max(0, pfEntries.length - 1), current + 1);
|
|
47
|
+
const entry = pfEntries[newIndex];
|
|
48
|
+
if (entry && !entry.activeForward) {
|
|
49
|
+
setPortForwardLocalPort(String(entry.suggestedLocalPort));
|
|
50
|
+
}
|
|
51
|
+
return newIndex;
|
|
52
|
+
});
|
|
48
53
|
return true;
|
|
49
54
|
}
|
|
50
55
|
|