openk8s 1.0.1 → 1.0.3

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.
Files changed (48) hide show
  1. package/README.md +1 -1
  2. package/package.json +5 -2
  3. package/src/app/__tests__/app-state.test.ts +376 -0
  4. package/src/app/__tests__/components/inspector-tokens.test.ts +101 -0
  5. package/src/app/__tests__/utils.test.ts +358 -0
  6. package/src/app/app-actions.ts +262 -0
  7. package/src/app/app-state.ts +16 -263
  8. package/src/app/app.tsx +22 -170
  9. package/src/app/components/detail-sections.tsx +131 -0
  10. package/src/app/components/footer.tsx +52 -0
  11. package/src/app/components/header.tsx +37 -0
  12. package/src/app/components/inspector-tokens.ts +93 -0
  13. package/src/app/components/inspector.tsx +3 -239
  14. package/src/app/components/resource-rows.tsx +5 -1
  15. package/src/app/hooks/keyboard/filter-handlers.ts +40 -0
  16. package/src/app/hooks/keyboard/global-handlers.ts +134 -0
  17. package/src/app/hooks/keyboard/helm-handlers.ts +104 -0
  18. package/src/app/hooks/keyboard/logs-handlers.ts +80 -0
  19. package/src/app/hooks/keyboard/navigation-handlers.ts +103 -0
  20. package/src/app/hooks/keyboard/overlay-handlers.ts +138 -0
  21. package/src/app/hooks/keyboard/port-forward-handlers.ts +71 -0
  22. package/src/app/hooks/keyboard/shell-edit-handlers.ts +253 -0
  23. package/src/app/hooks/use-app-keyboard.ts +56 -621
  24. package/src/app/hooks/use-app-side-effects.ts +1 -1
  25. package/src/app/hooks/use-clipboard.ts +1 -1
  26. package/src/app/hooks/use-data-fetching.ts +2 -11
  27. package/src/app/hooks/use-log-stream.ts +1 -1
  28. package/src/app/hooks/use-port-forward.ts +1 -1
  29. package/src/app/use-footer-hints.ts +107 -0
  30. package/src/index.tsx +4 -0
  31. package/src/lib/k8s/__tests__/k8s-format.test.ts +42 -0
  32. package/src/lib/k8s/__tests__/resource-detail-builder.test.ts +215 -0
  33. package/src/lib/k8s/__tests__/resource-parser.test.ts +455 -0
  34. package/src/lib/k8s/detail-builders/event-builder.ts +21 -0
  35. package/src/lib/k8s/detail-builders/hpa-cronjob-builder.ts +63 -0
  36. package/src/lib/k8s/detail-builders/node-builder.ts +41 -0
  37. package/src/lib/k8s/detail-builders/overview-builder.ts +103 -0
  38. package/src/lib/k8s/detail-builders/pod-builder.ts +140 -0
  39. package/src/lib/k8s/detail-builders/rbac-builder.ts +57 -0
  40. package/src/lib/k8s/resource-detail-builder.ts +22 -502
  41. package/src/lib/kubectl/__tests__/kubectl-helpers.test.ts +343 -0
  42. package/src/lib/kubectl/__tests__/metrics-utils.test.ts +84 -0
  43. package/src/lib/kubectl/__tests__/spawn-utils.test.ts +56 -0
  44. package/src/lib/kubectl/kubectl-helpers.ts +246 -0
  45. package/src/lib/kubectl/kubectl-service.ts +77 -565
  46. package/src/lib/kubectl/kubectl-types.ts +248 -0
  47. package/src/lib/kubectl/metrics-utils.ts +33 -0
  48. package/src/lib/kubectl/spawn-utils.ts +27 -0
@@ -0,0 +1,253 @@
1
+ import { unlinkSync, writeFileSync } from "node:fs";
2
+ import { spawn } from "node:child_process";
3
+
4
+ import type { AppAction } from "../../app-actions";
5
+ import type { AppState, ResourceRef, ResourceKind, ResourceDetail, NotificationTone } from "../../../lib/k8s/types";
6
+ import { statusLine } from "../../utils";
7
+ import { KubectlService } from "../../../lib/kubectl/kubectl-service";
8
+
9
+ export function handleShellKeys(
10
+ key: { name: string; shift?: boolean },
11
+ state: AppState,
12
+ dispatch: React.Dispatch<AppAction>,
13
+ renderer: { suspend: () => void; resume: () => void },
14
+ activeResourceRef: ResourceRef | undefined,
15
+ selectedKind: ResourceKind | undefined,
16
+ kubectl: KubectlService,
17
+ toastError: (error: unknown) => void,
18
+ setManualRefreshNonce: React.Dispatch<React.SetStateAction<number>>,
19
+ ): boolean {
20
+ if (state.overlay) return false;
21
+
22
+ if (key.name === "x" && activeResourceRef && selectedKind && state.activeContext) {
23
+ if (activeResourceRef.kind.toLowerCase() === "helmreleases") {
24
+ dispatch({ type: "setStatusMessage", message: "Shell not supported for Helm releases" });
25
+ return true;
26
+ }
27
+
28
+ dispatch({ type: "setStatusMessage", message: `Opening shell in ${statusLine({ ref: activeResourceRef })}` });
29
+ dispatch({ type: "setError", error: undefined });
30
+ renderer.suspend();
31
+
32
+ const context = state.activeContext;
33
+ const namespace = state.activeNamespace;
34
+ const namespaced = selectedKind.namespaced;
35
+ const target = activeResourceRef;
36
+
37
+ const child = kubectl.startShell({ context, namespace, resourceRef: target, namespaced });
38
+ child.on("close", () => {
39
+ renderer.resume();
40
+ dispatch({ type: "setError", error: undefined });
41
+ dispatch({ type: "setStatusMessage", message: `Returned from shell in ${statusLine({ ref: target })}` });
42
+ setManualRefreshNonce((current) => current + 1);
43
+ });
44
+ child.on("error", (error) => {
45
+ renderer.resume();
46
+ toastError(error);
47
+ dispatch({ type: "setStatusMessage", message: `Shell failed for ${statusLine({ ref: target })}` });
48
+ });
49
+ return true;
50
+ }
51
+
52
+ return false;
53
+ }
54
+
55
+ export function handleEditKeys(
56
+ key: { name: string; shift?: boolean },
57
+ state: AppState,
58
+ dispatch: React.Dispatch<AppAction>,
59
+ renderer: { suspend: () => void; resume: () => void },
60
+ activeResourceRef: ResourceRef | undefined,
61
+ selectedKind: ResourceKind | undefined,
62
+ activeDetail: ResourceDetail | undefined,
63
+ kubectl: KubectlService,
64
+ toast: (tone: NotificationTone, message: string) => void,
65
+ toastError: (error: unknown) => void,
66
+ setManualRefreshNonce: React.Dispatch<React.SetStateAction<number>>,
67
+ ): boolean {
68
+ if (state.overlay) return false;
69
+
70
+ if (key.name === "e" && !key.shift && activeResourceRef && selectedKind && state.activeContext) {
71
+ const context = state.activeContext;
72
+ const namespace = state.activeNamespace;
73
+ const namespaced = selectedKind.namespaced;
74
+ const target = activeResourceRef;
75
+ const isHelm = target.kind.toLowerCase() === "helmreleases";
76
+
77
+ if (isHelm) {
78
+ const chartName = activeDetail?.helmChart ?? "";
79
+ const values = activeDetail?.yaml ?? "";
80
+ const tmpPath = `/tmp/openk8s-${target.name}-${Date.now()}.yaml`;
81
+ const cleanupTmp = (): void => { try { unlinkSync(tmpPath); } catch { /* ignore */ } };
82
+
83
+ if (!chartName) {
84
+ dispatch({ type: "setStatusMessage", message: "Chart name unavailable — load the detail pane first" });
85
+ return true;
86
+ }
87
+
88
+ try {
89
+ writeFileSync(tmpPath, values, "utf8");
90
+ } catch (err) {
91
+ toastError(err);
92
+ return true;
93
+ }
94
+
95
+ dispatch({ type: "setStatusMessage", message: `Editing values for ${target.name}` });
96
+ dispatch({ type: "setError", error: undefined });
97
+ renderer.suspend();
98
+
99
+ const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
100
+ const editorChild = spawn(editor, [tmpPath], { env: process.env, stdio: "inherit" });
101
+
102
+ editorChild.on("close", (code) => {
103
+ if (code === 0) {
104
+ kubectl
105
+ .helmUpgradeValues({ context, namespace, name: target.name, chart: chartName, valuesFile: tmpPath })
106
+ .then(() => {
107
+ renderer.resume();
108
+ toast("success", `${target.name} upgraded`);
109
+ setManualRefreshNonce((current) => current + 1);
110
+ })
111
+ .catch((err: unknown) => {
112
+ renderer.resume();
113
+ toastError(err);
114
+ })
115
+ .finally(cleanupTmp);
116
+ } else {
117
+ renderer.resume();
118
+ dispatch({ type: "setStatusMessage", message: "Edit cancelled" });
119
+ cleanupTmp();
120
+ }
121
+ });
122
+
123
+ editorChild.on("error", (err) => {
124
+ renderer.resume();
125
+ toastError(err);
126
+ cleanupTmp();
127
+ });
128
+ } else {
129
+ dispatch({ type: "setStatusMessage", message: `Editing ${statusLine({ ref: target })}` });
130
+ dispatch({ type: "setError", error: undefined });
131
+ renderer.suspend();
132
+
133
+ const editChild = kubectl.editResource({ context, namespace, resourceRef: target, namespaced });
134
+
135
+ editChild.on("close", (code) => {
136
+ renderer.resume();
137
+ if (code === 0) {
138
+ dispatch({ type: "setStatusMessage", message: `Saved ${statusLine({ ref: target })}` });
139
+ setManualRefreshNonce((current) => current + 1);
140
+ } else if (code !== null) {
141
+ dispatch({ type: "setStatusMessage", message: `Edit cancelled for ${statusLine({ ref: target })}` });
142
+ }
143
+ });
144
+
145
+ editChild.on("error", (err) => {
146
+ renderer.resume();
147
+ toastError(err);
148
+ dispatch({ type: "setStatusMessage", message: `Edit failed for ${statusLine({ ref: target })}` });
149
+ });
150
+ }
151
+ return true;
152
+ }
153
+
154
+ // Shift+S → scale dialog
155
+ if (key.shift && key.name === "s" && activeResourceRef && selectedKind && state.activeContext && kubectl.canScale(activeResourceRef)) {
156
+ return false; // handled by scale handler
157
+ }
158
+
159
+ return false;
160
+ }
161
+
162
+ export function handleScaleKeys(
163
+ key: { name: string },
164
+ state: AppState,
165
+ dispatch: React.Dispatch<AppAction>,
166
+ activeResourceRef: ResourceRef | undefined,
167
+ selectedKind: ResourceKind | undefined,
168
+ scaleReplicasInput: string,
169
+ kubectl: KubectlService,
170
+ toastError: (error: unknown) => void,
171
+ setManualRefreshNonce: React.Dispatch<React.SetStateAction<number>>,
172
+ ): boolean {
173
+ if (state.overlay !== "scale") return false;
174
+
175
+ if (key.name === "return" && activeResourceRef && selectedKind && state.activeContext) {
176
+ const replicas = Number.parseInt(scaleReplicasInput, 10);
177
+
178
+ if (!Number.isNaN(replicas) && replicas >= 0) {
179
+ const context = state.activeContext;
180
+ const namespace = state.activeNamespace;
181
+ const namespaced = selectedKind.namespaced;
182
+ const target = activeResourceRef;
183
+
184
+ dispatch({ type: "setStatusMessage", message: `Scaling ${statusLine({ ref: target })} to ${replicas}` });
185
+ dispatch({ type: "setError", error: undefined });
186
+ dispatch({ type: "setOverlay", overlay: undefined });
187
+
188
+ void kubectl
189
+ .scaleResource({ context, namespace, resourceRef: target, namespaced, replicas })
190
+ .then(() => {
191
+ dispatch({
192
+ type: "setStatusMessage",
193
+ message: `Scaled ${statusLine({ ref: target })} to ${replicas}`,
194
+ });
195
+ setManualRefreshNonce((current) => current + 1);
196
+ })
197
+ .catch((error: unknown) => {
198
+ toastError(error);
199
+ dispatch({
200
+ type: "setStatusMessage",
201
+ message: `Scale failed for ${statusLine({ ref: target })}`,
202
+ });
203
+ });
204
+ }
205
+ return true;
206
+ }
207
+
208
+ return true;
209
+ }
210
+
211
+ export function handleRolloutRestartKeys(
212
+ key: { name: string; shift?: boolean },
213
+ state: AppState,
214
+ dispatch: React.Dispatch<AppAction>,
215
+ activeResourceRef: ResourceRef | undefined,
216
+ selectedKind: ResourceKind | undefined,
217
+ kubectl: KubectlService,
218
+ toastError: (error: unknown) => void,
219
+ setManualRefreshNonce: React.Dispatch<React.SetStateAction<number>>,
220
+ ): boolean {
221
+ if (state.overlay) return false;
222
+
223
+ if (key.shift && key.name === "r") {
224
+ if (activeResourceRef && selectedKind && state.activeContext && kubectl.canRolloutRestart(activeResourceRef)) {
225
+ const context = state.activeContext;
226
+ const namespace = state.activeNamespace;
227
+ const namespaced = selectedKind.namespaced;
228
+ const target = activeResourceRef;
229
+
230
+ dispatch({ type: "setStatusMessage", message: `Restarting ${statusLine({ ref: target })}` });
231
+ dispatch({ type: "setError", error: undefined });
232
+
233
+ void kubectl
234
+ .rolloutRestart({ context, namespace, resourceRef: target, namespaced })
235
+ .then(() => {
236
+ dispatch({ type: "setStatusMessage", message: `Restarted ${statusLine({ ref: target })}` });
237
+ setManualRefreshNonce((current) => current + 1);
238
+ })
239
+ .catch((error: unknown) => {
240
+ toastError(error);
241
+ dispatch({
242
+ type: "setStatusMessage",
243
+ message: `Rollout restart failed for ${statusLine({ ref: target })}`,
244
+ });
245
+ });
246
+ } else {
247
+ dispatch({ type: "setStatusMessage", message: "Rollout restart not supported for this resource" });
248
+ }
249
+ return true;
250
+ }
251
+
252
+ return false;
253
+ }