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.
- package/README.md +1 -1
- package/package.json +5 -2
- package/src/app/__tests__/app-state.test.ts +376 -0
- package/src/app/__tests__/components/inspector-tokens.test.ts +101 -0
- package/src/app/__tests__/utils.test.ts +358 -0
- package/src/app/app-actions.ts +262 -0
- package/src/app/app-state.ts +16 -263
- package/src/app/app.tsx +22 -170
- package/src/app/components/detail-sections.tsx +131 -0
- package/src/app/components/footer.tsx +52 -0
- package/src/app/components/header.tsx +37 -0
- package/src/app/components/inspector-tokens.ts +93 -0
- package/src/app/components/inspector.tsx +3 -239
- package/src/app/components/resource-rows.tsx +5 -1
- package/src/app/hooks/keyboard/filter-handlers.ts +40 -0
- package/src/app/hooks/keyboard/global-handlers.ts +134 -0
- package/src/app/hooks/keyboard/helm-handlers.ts +104 -0
- package/src/app/hooks/keyboard/logs-handlers.ts +80 -0
- package/src/app/hooks/keyboard/navigation-handlers.ts +103 -0
- package/src/app/hooks/keyboard/overlay-handlers.ts +138 -0
- package/src/app/hooks/keyboard/port-forward-handlers.ts +71 -0
- package/src/app/hooks/keyboard/shell-edit-handlers.ts +253 -0
- package/src/app/hooks/use-app-keyboard.ts +56 -621
- package/src/app/hooks/use-app-side-effects.ts +1 -1
- package/src/app/hooks/use-clipboard.ts +1 -1
- package/src/app/hooks/use-data-fetching.ts +2 -11
- package/src/app/hooks/use-log-stream.ts +1 -1
- package/src/app/hooks/use-port-forward.ts +1 -1
- package/src/app/use-footer-hints.ts +107 -0
- package/src/index.tsx +4 -0
- package/src/lib/k8s/__tests__/k8s-format.test.ts +42 -0
- package/src/lib/k8s/__tests__/resource-detail-builder.test.ts +215 -0
- package/src/lib/k8s/__tests__/resource-parser.test.ts +455 -0
- package/src/lib/k8s/detail-builders/event-builder.ts +21 -0
- package/src/lib/k8s/detail-builders/hpa-cronjob-builder.ts +63 -0
- package/src/lib/k8s/detail-builders/node-builder.ts +41 -0
- package/src/lib/k8s/detail-builders/overview-builder.ts +103 -0
- package/src/lib/k8s/detail-builders/pod-builder.ts +140 -0
- package/src/lib/k8s/detail-builders/rbac-builder.ts +57 -0
- package/src/lib/k8s/resource-detail-builder.ts +22 -502
- package/src/lib/kubectl/__tests__/kubectl-helpers.test.ts +343 -0
- package/src/lib/kubectl/__tests__/metrics-utils.test.ts +84 -0
- package/src/lib/kubectl/__tests__/spawn-utils.test.ts +56 -0
- package/src/lib/kubectl/kubectl-helpers.ts +246 -0
- package/src/lib/kubectl/kubectl-service.ts +77 -565
- package/src/lib/kubectl/kubectl-types.ts +248 -0
- package/src/lib/kubectl/metrics-utils.ts +33 -0
- package/src/lib/kubectl/spawn-utils.ts +27 -0
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ openk8s is a keyboard-driven TUI for browsing and managing Kubernetes clusters,
|
|
|
12
12
|
|
|
13
13
|
| Tool | Required | Notes |
|
|
14
14
|
|------|----------|-------|
|
|
15
|
-
| [kubectl](https://kubernetes.io/docs/tasks/tools/) | yes |
|
|
15
|
+
| [kubectl](https://kubernetes.io/docs/tasks/tools/) | yes | >= 1.36.1, must be configured with a valid kubeconfig |
|
|
16
16
|
| [helm](https://helm.sh/) | no | Enables HelmRelease management |
|
|
17
17
|
| [bun](https://bun.sh/) | yes | Required at runtime to execute the app |
|
|
18
18
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openk8s",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "A terminal UI for Kubernetes",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -32,7 +32,9 @@
|
|
|
32
32
|
],
|
|
33
33
|
"scripts": {
|
|
34
34
|
"dev": "bun run --watch src/index.tsx",
|
|
35
|
-
"prepare": "husky || exit 0"
|
|
35
|
+
"prepare": "husky || exit 0",
|
|
36
|
+
"test": "bun test",
|
|
37
|
+
"test:watch": "bun test --watch"
|
|
36
38
|
},
|
|
37
39
|
"release": {
|
|
38
40
|
"branches": [
|
|
@@ -43,6 +45,7 @@
|
|
|
43
45
|
"@commitlint/cli": "^21.0.1",
|
|
44
46
|
"@commitlint/config-conventional": "^21.0.1",
|
|
45
47
|
"@types/bun": "latest",
|
|
48
|
+
"@types/node": "^25.8.0",
|
|
46
49
|
"@types/react": "^19.2.14",
|
|
47
50
|
"husky": "^9.1.7",
|
|
48
51
|
"semantic-release": "^25.0.3",
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { reducer, initialState, DEFAULT_NAMESPACE } from "../app-state";
|
|
3
|
+
import type { AppAction } from "../app-actions";
|
|
4
|
+
import type { AppState, ResourceListItem, ActivePortForward, Notification } from "../../lib/k8s/types";
|
|
5
|
+
|
|
6
|
+
function reduce(action: AppAction, state?: Partial<AppState>): AppState {
|
|
7
|
+
return reducer({ ...initialState, ...state }, action);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe("reducer", () => {
|
|
11
|
+
test("initial state is well-formed", () => {
|
|
12
|
+
expect(initialState.activePane).toBe("resources");
|
|
13
|
+
expect(initialState.overlay).toBeUndefined();
|
|
14
|
+
expect(initialState.activeNamespace).toBe(DEFAULT_NAMESPACE);
|
|
15
|
+
expect(initialState.selectedKind).toBe("pods");
|
|
16
|
+
expect(initialState.kubectlAvailable).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// ── Pane management ──────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
describe("setActivePane", () => {
|
|
22
|
+
test("changes active pane", () => {
|
|
23
|
+
const state = reduce({ type: "setActivePane", pane: "inspector" });
|
|
24
|
+
expect(state.activePane).toBe("inspector");
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("cyclePane", () => {
|
|
29
|
+
test("cycles forward", () => {
|
|
30
|
+
const state = reduce({ type: "cyclePane", direction: 1 });
|
|
31
|
+
expect(state.activePane).toBe("inspector");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("cycles backward", () => {
|
|
35
|
+
const state = reduce({ type: "cyclePane", direction: -1 });
|
|
36
|
+
expect(state.activePane).toBe("clusters");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("wraps around forward", () => {
|
|
40
|
+
const s1 = reduce({ type: "cyclePane", direction: 1 });
|
|
41
|
+
const s2 = reducer(s1, { type: "cyclePane", direction: 1 });
|
|
42
|
+
const s3 = reducer(s2, { type: "cyclePane", direction: 1 });
|
|
43
|
+
expect(s3.activePane).toBe("resources");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// ── Overlays ─────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
describe("setOverlay", () => {
|
|
50
|
+
test("sets overlay", () => {
|
|
51
|
+
const state = reduce({ type: "setOverlay", overlay: "logs" });
|
|
52
|
+
expect(state.overlay).toBe("logs");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("clears overlay", () => {
|
|
56
|
+
const state = reduce({ type: "setOverlay", overlay: undefined }, { overlay: "logs" as const });
|
|
57
|
+
expect(state.overlay).toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// ── Contexts ─────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
describe("setContexts", () => {
|
|
64
|
+
test("sets contexts and active context", () => {
|
|
65
|
+
const contexts = [{ name: "prod", isCurrent: true }];
|
|
66
|
+
const state = reduce({ type: "setContexts", contexts, activeContext: "prod" });
|
|
67
|
+
expect(state.contexts).toEqual(contexts);
|
|
68
|
+
expect(state.activeContext).toBe("prod");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("setActiveContext", () => {
|
|
73
|
+
test("changes active context", () => {
|
|
74
|
+
const state = reduce({ type: "setActiveContext", activeContext: "staging" });
|
|
75
|
+
expect(state.activeContext).toBe("staging");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ── Namespaces ───────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
describe("setNamespaces", () => {
|
|
82
|
+
test("sets namespaces and active namespace", () => {
|
|
83
|
+
const namespaces = [{ name: "kube-system" }, { name: "default" }];
|
|
84
|
+
const state = reduce({ type: "setNamespaces", namespaces, activeNamespace: "kube-system" });
|
|
85
|
+
expect(state.namespaces).toEqual(namespaces);
|
|
86
|
+
expect(state.activeNamespace).toBe("kube-system");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("setActiveNamespace", () => {
|
|
91
|
+
test("changes namespace", () => {
|
|
92
|
+
const state = reduce({ type: "setActiveNamespace", namespace: "production" });
|
|
93
|
+
expect(state.activeNamespace).toBe("production");
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ── Resource kinds ───────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
describe("setResourceKinds", () => {
|
|
100
|
+
test("sets kinds and selected kind", () => {
|
|
101
|
+
const kinds = [{ name: "pods", namespaced: true, shortNames: ["po"] }];
|
|
102
|
+
const state = reduce({ type: "setResourceKinds", resourceKinds: kinds, selectedKind: "pods" });
|
|
103
|
+
expect(state.resourceKinds).toEqual(kinds);
|
|
104
|
+
expect(state.selectedKind).toBe("pods");
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("setSelectedKind", () => {
|
|
109
|
+
test("changes selected kind and clears filter", () => {
|
|
110
|
+
const state = reduce({ type: "setSelectedKind", kind: "services" }, { resourceFilter: "nginx" });
|
|
111
|
+
expect(state.selectedKind).toBe("services");
|
|
112
|
+
expect(state.resourceFilter).toBe("");
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// ── Resources ────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
describe("setResources", () => {
|
|
119
|
+
test("sets resources and selected name", () => {
|
|
120
|
+
const resources: ResourceListItem[] = [
|
|
121
|
+
{ ref: { kind: "Pod", name: "nginx-1" }, status: "Running", age: "5m", summary: "nginx" },
|
|
122
|
+
{ ref: { kind: "Pod", name: "nginx-2" }, status: "Running", age: "2m", summary: "nginx" },
|
|
123
|
+
];
|
|
124
|
+
const state = reduce({ type: "setResources", resources, selectedResourceName: "nginx-1" });
|
|
125
|
+
expect(state.resources).toEqual(resources);
|
|
126
|
+
expect(state.selectedResourceName).toBe("nginx-1");
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("toggleSelectedResource", () => {
|
|
131
|
+
test("adds to selection", () => {
|
|
132
|
+
const state = reduce({ type: "toggleSelectedResource", name: "pod-1" });
|
|
133
|
+
expect(state.selectedResourceNames).toEqual(["pod-1"]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("removes from selection", () => {
|
|
137
|
+
const state = reduce({ type: "toggleSelectedResource", name: "pod-1" }, { selectedResourceNames: ["pod-1", "pod-2"] });
|
|
138
|
+
expect(state.selectedResourceNames).toEqual(["pod-2"]);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("clearSelectedResources", () => {
|
|
143
|
+
test("clears all selections", () => {
|
|
144
|
+
const state = reduce({ type: "clearSelectedResources" }, { selectedResourceNames: ["pod-1", "pod-2"] });
|
|
145
|
+
expect(state.selectedResourceNames).toEqual([]);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// ── Filter ───────────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
describe("setResourceFilter", () => {
|
|
152
|
+
test("sets filter value", () => {
|
|
153
|
+
const state = reduce({ type: "setResourceFilter", value: "nginx" });
|
|
154
|
+
expect(state.resourceFilter).toBe("nginx");
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// ── Selected resource name ───────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
describe("setSelectedResourceName", () => {
|
|
161
|
+
test("sets selected resource name", () => {
|
|
162
|
+
const state = reduce({ type: "setSelectedResourceName", name: "web-1" });
|
|
163
|
+
expect(state.selectedResourceName).toBe("web-1");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("clears selected resource name", () => {
|
|
167
|
+
const state = reduce({ type: "setSelectedResourceName", name: undefined });
|
|
168
|
+
expect(state.selectedResourceName).toBeUndefined();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ── Detail ───────────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
describe("setSelectedResourceDetail", () => {
|
|
175
|
+
test("sets detail", () => {
|
|
176
|
+
const detail: any = { ref: { kind: "Pod", name: "test" }, summaryLines: [], summarySections: [], yaml: "" };
|
|
177
|
+
const state = reduce({ type: "setSelectedResourceDetail", detail });
|
|
178
|
+
expect(state.selectedResourceDetail).toBe(detail);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("clears detail", () => {
|
|
182
|
+
const state = reduce({ type: "setSelectedResourceDetail", detail: undefined });
|
|
183
|
+
expect(state.selectedResourceDetail).toBeUndefined();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ── Events ───────────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
describe("setEvents", () => {
|
|
190
|
+
test("sets events", () => {
|
|
191
|
+
const events = [{ type: "Normal", reason: "Pulled", message: "Pulled image", age: "1m", source: "kubelet" }];
|
|
192
|
+
const state = reduce({ type: "setEvents", events });
|
|
193
|
+
expect(state.events).toEqual(events);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// ── Reveal / Secret ──────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
describe("toggleRevealDetailLine", () => {
|
|
200
|
+
test("adds id to revealed list", () => {
|
|
201
|
+
const state = reduce({ type: "toggleRevealDetailLine", id: "line-1" });
|
|
202
|
+
expect(state.revealedDetailLineIds).toEqual(["line-1"]);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("removes id from revealed list", () => {
|
|
206
|
+
const state = reduce({ type: "toggleRevealDetailLine", id: "line-1" }, { revealedDetailLineIds: ["line-1"] });
|
|
207
|
+
expect(state.revealedDetailLineIds).toEqual([]);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("setRevealedSecretValue", () => {
|
|
212
|
+
test("sets revealed secret value", () => {
|
|
213
|
+
const state = reduce({ type: "setRevealedSecretValue", id: "secret-1", value: "password123" });
|
|
214
|
+
expect(state.revealedSecretValues["secret-1"]).toBe("password123");
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// ── Port forwarding ──────────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
describe("addActivePortForward", () => {
|
|
221
|
+
test("adds a port forward entry", () => {
|
|
222
|
+
const forward: ActivePortForward = {
|
|
223
|
+
id: "pf-1", context: "prod", namespace: "default", namespaced: true,
|
|
224
|
+
ref: { kind: "Pod", name: "web" }, localPort: 8080, remotePort: 80, status: "starting",
|
|
225
|
+
};
|
|
226
|
+
const state = reduce({ type: "addActivePortForward", forward });
|
|
227
|
+
expect(state.activePortForwards).toHaveLength(1);
|
|
228
|
+
expect(state.activePortForwards[0]?.id).toBe("pf-1");
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe("updateActivePortForward", () => {
|
|
233
|
+
test("updates an existing forward", () => {
|
|
234
|
+
const forward: ActivePortForward = {
|
|
235
|
+
id: "pf-1", context: "prod", namespace: "default", namespaced: true,
|
|
236
|
+
ref: { kind: "Pod", name: "web" }, localPort: 8080, remotePort: 80, status: "starting",
|
|
237
|
+
};
|
|
238
|
+
const state = reduce({ type: "updateActivePortForward", id: "pf-1", patch: { status: "ready" } }, { activePortForwards: [forward] });
|
|
239
|
+
expect(state.activePortForwards[0]?.status).toBe("ready");
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe("removeActivePortForward", () => {
|
|
244
|
+
test("removes a port forward", () => {
|
|
245
|
+
const forward: ActivePortForward = {
|
|
246
|
+
id: "pf-1", context: "prod", namespace: "default", namespaced: true,
|
|
247
|
+
ref: { kind: "Pod", name: "web" }, localPort: 8080, remotePort: 80, status: "ready",
|
|
248
|
+
};
|
|
249
|
+
const state = reduce({ type: "removeActivePortForward", id: "pf-1" }, { activePortForwards: [forward] });
|
|
250
|
+
expect(state.activePortForwards).toHaveLength(0);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// ── Notifications ────────────────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
describe("pushNotification", () => {
|
|
257
|
+
test("adds a notification", () => {
|
|
258
|
+
const notification: Notification = { id: "n-1", tone: "info", message: "Hello" };
|
|
259
|
+
const state = reduce({ type: "pushNotification", notification });
|
|
260
|
+
expect(state.notifications).toHaveLength(1);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe("dismissNotification", () => {
|
|
265
|
+
test("removes a notification", () => {
|
|
266
|
+
const n1: Notification = { id: "n-1", tone: "info", message: "Hello" };
|
|
267
|
+
const n2: Notification = { id: "n-2", tone: "success", message: "Done" };
|
|
268
|
+
const state = reduce({ type: "dismissNotification", id: "n-1" }, { notifications: [n1, n2] });
|
|
269
|
+
expect(state.notifications).toHaveLength(1);
|
|
270
|
+
expect(state.notifications[0]?.id).toBe("n-2");
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// ── Logs ─────────────────────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
describe("setLogsData", () => {
|
|
277
|
+
test("sets logs data", () => {
|
|
278
|
+
const state = reduce({ type: "setLogsData", logsTarget: "pod/web", logsText: "line 1", logsStatus: "ready" });
|
|
279
|
+
expect(state.logsTarget).toBe("pod/web");
|
|
280
|
+
expect(state.logsText).toBe("line 1");
|
|
281
|
+
expect(state.logsStatus).toBe("ready");
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe("setLogsContainer", () => {
|
|
286
|
+
test("sets container", () => {
|
|
287
|
+
const state = reduce({ type: "setLogsContainer", container: "nginx" });
|
|
288
|
+
expect(state.logsContainer).toBe("nginx");
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe("setLogsOptions", () => {
|
|
293
|
+
test("sets log options", () => {
|
|
294
|
+
const state = reduce({ type: "setLogsOptions", options: { tail: 50, previous: true } });
|
|
295
|
+
expect(state.logsOptions.tail).toBe(50);
|
|
296
|
+
expect(state.logsOptions.previous).toBe(true);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// ── Metrics ──────────────────────────────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
describe("setPodMetrics", () => {
|
|
303
|
+
test("sets pod metrics", () => {
|
|
304
|
+
const metrics = { "pod-1": { cpu: "100m", memory: "256Mi" } };
|
|
305
|
+
const state = reduce({ type: "setPodMetrics", metrics });
|
|
306
|
+
expect(state.podMetrics).toEqual(metrics);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
describe("setNodeMetrics", () => {
|
|
311
|
+
test("sets node metrics", () => {
|
|
312
|
+
const metrics = { "node-1": { cpu: "500m", memory: "1Gi", cpuPct: 25, memPct: 30 } };
|
|
313
|
+
const state = reduce({ type: "setNodeMetrics", metrics });
|
|
314
|
+
expect(state.nodeMetrics).toEqual(metrics);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// ── Misc ─────────────────────────────────────────────────────────────────
|
|
319
|
+
|
|
320
|
+
describe("setStatusMessage", () => {
|
|
321
|
+
test("sets status message", () => {
|
|
322
|
+
const state = reduce({ type: "setStatusMessage", message: "Connected" });
|
|
323
|
+
expect(state.statusMessage).toBe("Connected");
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
describe("setKubectlAvailable", () => {
|
|
328
|
+
test("sets availability", () => {
|
|
329
|
+
const state = reduce({ type: "setKubectlAvailable", available: false });
|
|
330
|
+
expect(state.kubectlAvailable).toBe(false);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe("clearTransientViews", () => {
|
|
335
|
+
test("clears events, logs, reveals, and selections", () => {
|
|
336
|
+
const state = reduce(
|
|
337
|
+
{ type: "clearTransientViews" },
|
|
338
|
+
{
|
|
339
|
+
events: [{ type: "Normal", reason: "test", message: "", age: "1m", source: "k" }],
|
|
340
|
+
eventsStatus: "ready",
|
|
341
|
+
logsTarget: "pod/web",
|
|
342
|
+
logsText: "output",
|
|
343
|
+
logsStatus: "ready",
|
|
344
|
+
logsContainer: "nginx",
|
|
345
|
+
revealedDetailLineIds: ["line-1"],
|
|
346
|
+
revealedSecretValues: { "line-1": "value" },
|
|
347
|
+
selectedResourceNames: ["pod-1"],
|
|
348
|
+
},
|
|
349
|
+
);
|
|
350
|
+
expect(state.events).toHaveLength(0);
|
|
351
|
+
expect(state.eventsStatus).toBe("idle");
|
|
352
|
+
expect(state.logsTarget).toBeUndefined();
|
|
353
|
+
expect(state.logsText).toBe("");
|
|
354
|
+
expect(state.logsStatus).toBe("idle");
|
|
355
|
+
expect(state.logsContainer).toBeUndefined();
|
|
356
|
+
expect(state.revealedDetailLineIds).toHaveLength(0);
|
|
357
|
+
expect(state.revealedSecretValues).toEqual({});
|
|
358
|
+
expect(state.selectedResourceNames).toHaveLength(0);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
describe("setHelmRollbackRevision", () => {
|
|
363
|
+
test("sets the revision value", () => {
|
|
364
|
+
const state = reduce({ type: "setHelmRollbackRevision", value: "42" });
|
|
365
|
+
expect(state.helmRollbackRevision).toBe("42");
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
describe("default case", () => {
|
|
370
|
+
test("returns unchanged state for unknown action", () => {
|
|
371
|
+
const state = reduce({ type: "setStatusMessage", message: "hi" });
|
|
372
|
+
const result = reducer(state, { type: "unknown" } as any);
|
|
373
|
+
expect(result).toEqual(state);
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { tokenizeYamlLine, tokenizeDescribeLine } from "../../components/inspector-tokens";
|
|
3
|
+
import { YAML_COMMENT, YAML_KEY, TEXT_SUBTLE, YAML_VALUE } from "../../theme";
|
|
4
|
+
|
|
5
|
+
describe("tokenizeYamlLine", () => {
|
|
6
|
+
test("empty line", () => {
|
|
7
|
+
const tokens = tokenizeYamlLine("");
|
|
8
|
+
expect(tokens[0]?.text).toBe(" ");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("whitespace line", () => {
|
|
12
|
+
const tokens = tokenizeYamlLine(" ");
|
|
13
|
+
expect(tokens[0]?.text).toBe(" ");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("comment line", () => {
|
|
17
|
+
const tokens = tokenizeYamlLine("# this is a comment");
|
|
18
|
+
expect(tokens).toHaveLength(1);
|
|
19
|
+
expect(tokens[0]?.fg).toBe(YAML_COMMENT);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("key: value line", () => {
|
|
23
|
+
const tokens = tokenizeYamlLine("name: nginx");
|
|
24
|
+
expect(tokens).toHaveLength(4);
|
|
25
|
+
expect(tokens[0]?.text).toBe("");
|
|
26
|
+
expect(tokens[1]?.text).toBe("name");
|
|
27
|
+
expect(tokens[1]?.fg).toBe(YAML_KEY);
|
|
28
|
+
expect(tokens[3]?.text).toBe("nginx");
|
|
29
|
+
expect(tokens[3]?.fg).toBe(YAML_VALUE);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("indented key: value", () => {
|
|
33
|
+
const tokens = tokenizeYamlLine(" name: nginx");
|
|
34
|
+
expect(tokens[0]?.text).toBe(" ");
|
|
35
|
+
expect(tokens[1]?.text).toBe("name");
|
|
36
|
+
expect(tokens[1]?.fg).toBe(YAML_KEY);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("list item with value", () => {
|
|
40
|
+
const tokens = tokenizeYamlLine("- name: nginx");
|
|
41
|
+
expect(tokens[0]?.text).toBe("- ");
|
|
42
|
+
expect(tokens[1]?.text).toBe("name");
|
|
43
|
+
expect(tokens[1]?.fg).toBe(YAML_KEY);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("bare list marker", () => {
|
|
47
|
+
const tokens = tokenizeYamlLine("-");
|
|
48
|
+
expect(tokens).toHaveLength(1);
|
|
49
|
+
expect(tokens[0]?.fg).toBe(TEXT_SUBTLE);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("key only (ends with colon)", () => {
|
|
53
|
+
const tokens = tokenizeYamlLine("metadata:");
|
|
54
|
+
expect(tokens).toHaveLength(3);
|
|
55
|
+
expect(tokens[1]?.text).toBe("metadata");
|
|
56
|
+
expect(tokens[1]?.fg).toBe(YAML_KEY);
|
|
57
|
+
expect(tokens[2]?.text).toBe(":");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("plain text fallthrough", () => {
|
|
61
|
+
const tokens = tokenizeYamlLine("some random text");
|
|
62
|
+
expect(tokens).toHaveLength(1);
|
|
63
|
+
expect(tokens[0]?.fg).toBe(TEXT_SUBTLE);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("tokenizeDescribeLine", () => {
|
|
68
|
+
test("empty line", () => {
|
|
69
|
+
const tokens = tokenizeDescribeLine("");
|
|
70
|
+
expect(tokens[0]?.text).toBe(" ");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("section header", () => {
|
|
74
|
+
const tokens = tokenizeDescribeLine("Name: test-pod");
|
|
75
|
+
expect(tokens).toHaveLength(3);
|
|
76
|
+
expect(tokens[0]?.text).toBe("Name");
|
|
77
|
+
expect(tokens[0]?.fg).toBe(YAML_KEY);
|
|
78
|
+
expect(tokens[2]?.fg).toBe(YAML_VALUE);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("indented key: value (double space after colon)", () => {
|
|
82
|
+
const tokens = tokenizeDescribeLine(" Namespace: default");
|
|
83
|
+
expect(tokens).toHaveLength(2);
|
|
84
|
+
expect(tokens[0]?.fg).toBe(TEXT_SUBTLE);
|
|
85
|
+
expect(tokens[1]?.fg).toBe(YAML_VALUE);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("key: single value", () => {
|
|
89
|
+
const tokens = tokenizeDescribeLine("key:value");
|
|
90
|
+
expect(tokens).toHaveLength(3);
|
|
91
|
+
expect(tokens[0]?.fg).toBe(TEXT_SUBTLE);
|
|
92
|
+
expect(tokens[1]?.text).toBe(":");
|
|
93
|
+
expect(tokens[2]?.text).toBe("value");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("plain text", () => {
|
|
97
|
+
const tokens = tokenizeDescribeLine("Some plain description text");
|
|
98
|
+
expect(tokens).toHaveLength(1);
|
|
99
|
+
expect(tokens[0]?.fg).toBe(TEXT_SUBTLE);
|
|
100
|
+
});
|
|
101
|
+
});
|