openk8s 1.1.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openk8s",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "A terminal UI for Kubernetes",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -55,6 +55,8 @@
55
55
  "dependencies": {
56
56
  "@opentui/core": "^0.4.1",
57
57
  "@opentui/react": "^0.4.1",
58
+ "@types/js-yaml": "^4.0.9",
59
+ "js-yaml": "^5.1.0",
58
60
  "react": "^19.2.7"
59
61
  }
60
62
  }
@@ -174,7 +174,7 @@ describe("reducer", () => {
174
174
 
175
175
  describe("setSelectedResourceDetail", () => {
176
176
  test("sets detail", () => {
177
- const detail: any = { ref: { kind: "Pod", name: "test" }, summaryLines: [], summarySections: [], yaml: "" };
177
+ const detail: any = { ref: { kind: "Pod", name: "test" }, summarySections: [], yaml: "" };
178
178
  const state = reduce({ type: "setSelectedResourceDetail", detail });
179
179
  expect(state.selectedResourceDetail).toBe(detail);
180
180
  });
@@ -298,7 +298,7 @@ describe("defaultNamespace", () => {
298
298
  describe("selectedRef", () => {
299
299
  const detail: ResourceDetail = {
300
300
  ref: { kind: "Pod", name: "web", namespace: "default" },
301
- summaryLines: [], summarySections: [], yaml: "",
301
+ summarySections: [], yaml: "",
302
302
  };
303
303
 
304
304
  test("returns detail ref when it matches resource", () => {
@@ -16,8 +16,6 @@ import type { EventItem, InspectorTab, PaneId, ResourceDetail } from "../../lib/
16
16
  export const INSPECTOR_TABS: Array<{ id: InspectorTab; label: string; hotkey: string }> = [
17
17
  { id: "summary", label: "Summary", hotkey: "s" },
18
18
  { id: "yaml", label: "YAML", hotkey: "y" },
19
- { id: "events", label: "Events", hotkey: "v" },
20
- { id: "describe", label: "Describe", hotkey: "i" },
21
19
  ];
22
20
 
23
21
  export interface InspectorTabsProps {
@@ -132,54 +130,28 @@ export function InspectorBody({
132
130
  );
133
131
  }
134
132
 
135
- if (activeTab === "describe") {
136
- return (
137
- <box style={{ flexGrow: 1, flexDirection: "column" }}>
138
- <box style={{ height: 1 }} />
139
- <scrollbox
140
- focused={active}
141
- scrollX
142
- horizontalScrollbarOptions={{ showArrows: false }}
143
- onMouseDown={onActivate}
144
- flexGrow={1}
145
- >
146
- <box style={{ flexDirection: "column", paddingRight: 1 }}>
147
- {detail?.describe ? (
148
- detail.describe.split("\n").map((line, index) => (
149
- <text key={`${index}:${line}`} fg={TEXT_SUBTLE} wrapMode="none">
150
- {line || " "}
151
- </text>
152
- ))
153
- ) : detailStatus === "loading" ? (
154
- <text fg={TEXT_SUBTLE} attributes={TextAttributes.DIM}>
155
- Loading describe output...
156
- </text>
157
- ) : (
158
- <text fg={TEXT_SUBTLE} attributes={TextAttributes.DIM}>
159
- No describe output available
160
- </text>
161
- )}
162
- </box>
163
- </scrollbox>
164
- </box>
165
- );
166
- }
167
-
168
133
  return (
169
134
  <box style={{ flexGrow: 1, flexDirection: "column" }}>
170
135
  <box style={{ height: 1 }} />
171
136
  <scrollbox focused={active} onMouseDown={onActivate} flexGrow={1}>
172
137
  <box style={{ flexDirection: "column", width: "100%", paddingRight: 1 }}>
173
- {activeTab === "events" ? (
174
- <EventList events={events} status={eventsStatus} />
175
- ) : detail ? (
176
- <DetailSectionsInternal
177
- sections={detail.summarySections}
178
- revealedIds={revealedDetailLineIds}
179
- revealedSecretValues={revealedSecretValues}
180
- onToggleReveal={onToggleReveal}
181
- onCopyText={onCopyText}
182
- />
138
+ {detail ? (
139
+ <>
140
+ <DetailSectionsInternal
141
+ sections={detail.summarySections}
142
+ revealedIds={revealedDetailLineIds}
143
+ revealedSecretValues={revealedSecretValues}
144
+ onToggleReveal={onToggleReveal}
145
+ onCopyText={onCopyText}
146
+ />
147
+ <box style={{ flexDirection: "column", marginTop: 1, marginBottom: 1 }}>
148
+ <text fg={KEY_HINT}>
149
+ {"─ "}
150
+ <span fg={TEXT_PRIMARY} attributes={TextAttributes.BOLD}>Recent Events</span>
151
+ </text>
152
+ <EventList events={events} status={eventsStatus} />
153
+ </box>
154
+ </>
183
155
  ) : detailStatus === "loading" ? (
184
156
  <text fg={TEXT_SUBTLE} attributes={TextAttributes.DIM}>
185
157
  Loading resource detail...
@@ -23,10 +23,8 @@ const LEFT_SECTIONS: HelpSection[] = [
23
23
  {
24
24
  title: "Inspector Tabs",
25
25
  rows: [
26
- ["S", "summary tab"],
26
+ ["S", "summary tab (incl. recent events)"],
27
27
  ["Y", "yaml tab (press again to copy)"],
28
- ["V", "events tab"],
29
- ["I", "describe tab"],
30
28
  ],
31
29
  },
32
30
  {
@@ -73,18 +73,6 @@ export function handleGlobalHotkeys(
73
73
  return true;
74
74
  }
75
75
 
76
- // V → events tab
77
- if (key.name === "v" && !key.shift) {
78
- dispatch({ type: "setInspectorTab", tab: "events" });
79
- return true;
80
- }
81
-
82
- // I → describe tab
83
- if (key.name === "i" && !key.shift) {
84
- dispatch({ type: "setInspectorTab", tab: "describe" });
85
- return true;
86
- }
87
-
88
76
  // L → logs
89
77
  if (key.name === "l" && !key.shift) {
90
78
  if (activeResourceRef) {
@@ -90,7 +90,7 @@ export function handleEditKeys(
90
90
 
91
91
  if (isHelm) {
92
92
  const chartName = activeDetail?.helmChart ?? "";
93
- const values = activeDetail?.yaml ?? "";
93
+ const values = activeDetail?.helmValues ?? "";
94
94
  const tmpPath = `/tmp/openk8s-${target.name}-${Date.now()}.yaml`;
95
95
  const cleanupTmp = (): void => { try { unlinkSync(tmpPath); } catch { /* ignore */ } };
96
96
 
@@ -289,7 +289,7 @@ export function useDataFetching(
289
289
  useEffect(() => {
290
290
  const context = state.activeContext;
291
291
 
292
- if (!context || !activeResourceRef || !selectedKind || state.inspectorTab !== "events") {
292
+ if (!context || !activeResourceRef || !selectedKind) {
293
293
  return;
294
294
  }
295
295
 
@@ -333,7 +333,7 @@ export function useDataFetching(
333
333
  return () => {
334
334
  cancelled = true;
335
335
  };
336
- }, [activeResourceRef, selectedKind, state.activeContext, state.activeNamespace, state.inspectorTab, viewPollTick, manualRefreshNonce]);
336
+ }, [activeResourceRef, selectedKind, state.activeContext, state.activeNamespace, viewPollTick, manualRefreshNonce]);
337
337
 
338
338
  // Poll pod metrics when viewing pods (silent degradation on error)
339
339
  useEffect(() => {
@@ -12,13 +12,11 @@ describe("buildResourceDetail", () => {
12
12
  resourceKind: defaultKind,
13
13
  resource: { apiVersion: "v1", kind: "Pod", metadata },
14
14
  yaml: "apiVersion: v1\nkind: Pod",
15
- describe: "Name: test-pod",
16
15
  });
17
16
 
18
17
  expect(detail.ref.kind).toBe("Pod");
19
18
  expect(detail.ref.name).toBe("test-pod");
20
19
  expect(detail.yaml).toContain("apiVersion: v1");
21
- expect(detail.describe).toContain("Name: test-pod");
22
20
  expect(detail.summarySections[0]?.title).toBe("Overview");
23
21
  });
24
22
 
@@ -58,8 +56,8 @@ describe("buildResourceDetail", () => {
58
56
  });
59
57
 
60
58
  expect(detail.summarySections.some((s) => s.title === "HPA")).toBe(true);
61
- expect(detail.summaryLines.some((l) => l.includes("minReplicas: 1"))).toBe(true);
62
- expect(detail.summaryLines.some((l) => l.includes("maxReplicas: 10"))).toBe(true);
59
+ expect(detail.summarySections.some((s) => s.lines.some((l) => typeof l === "string" && l.includes("minReplicas: 1")))).toBe(true);
60
+ expect(detail.summarySections.some((s) => s.lines.some((l) => typeof l === "string" && l.includes("maxReplicas: 10")))).toBe(true);
63
61
  });
64
62
 
65
63
  test("builds CronJob sections", () => {
@@ -76,7 +74,7 @@ describe("buildResourceDetail", () => {
76
74
  });
77
75
 
78
76
  expect(detail.summarySections.some((s) => s.title === "CronJob")).toBe(true);
79
- expect(detail.summaryLines.some((l) => l.includes("schedule"))).toBe(true);
77
+ expect(detail.summarySections.some((s) => s.lines.some((l) => typeof l === "string" && l.includes("schedule")))).toBe(true);
80
78
  });
81
79
 
82
80
  test("builds Node sections", () => {
@@ -116,7 +114,7 @@ describe("buildResourceDetail", () => {
116
114
  });
117
115
 
118
116
  expect(detail.summarySections.some((s) => s.title === "Event")).toBe(true);
119
- expect(detail.summaryLines.some((l) => l.includes("BackOff"))).toBe(true);
117
+ expect(detail.summarySections.some((s) => s.lines.some((l) => typeof l === "string" && l.includes("BackOff")))).toBe(true);
120
118
  });
121
119
 
122
120
  test("builds Role sections", () => {
@@ -7,7 +7,7 @@ function asStringOrDash(value: unknown): string {
7
7
  return asString(value) ?? "-";
8
8
  }
9
9
 
10
- export function buildEventSections(resource: KubernetesResource): ResourceDetailSection[] {
10
+ export function buildEventObjectSections(resource: KubernetesResource): ResourceDetailSection[] {
11
11
  const raw = asRecord(resource) ?? {};
12
12
  const involvedObject = asRecord(raw.involvedObject) ?? {};
13
13
  const source = asRecord(raw.source) ?? {};
@@ -8,9 +8,9 @@ import { buildOverviewSection, buildPortForward } from "./detail-builders/overvi
8
8
  import { buildPodSections } from "./detail-builders/pod-builder";
9
9
  import { buildHpaSections, buildCronJobSections } from "./detail-builders/hpa-cronjob-builder";
10
10
  import { buildNodeSections } from "./detail-builders/node-builder";
11
- import { buildEventSections } from "./detail-builders/event-builder";
11
+ import { buildEventObjectSections } from "./detail-builders/event-builder";
12
12
  import { buildRoleSections, buildRoleBindingSections, buildServiceAccountSections } from "./detail-builders/rbac-builder";
13
- import { flattenSections, extractContainerNames } from "./resource-parser";
13
+ import { extractContainerNames } from "./resource-parser";
14
14
 
15
15
  export function buildResourceDetail(options: BuildResourceDetailOptions): ResourceDetail {
16
16
  const kindName = options.resource.kind ?? options.resourceKind?.name ?? "unknown";
@@ -28,7 +28,7 @@ export function buildResourceDetail(options: BuildResourceDetailOptions): Resour
28
28
  } else if (normalizedKind === "node") {
29
29
  additionalSections = buildNodeSections(options.resource);
30
30
  } else if (normalizedKind === "event" || normalizedKind === "events") {
31
- additionalSections = buildEventSections(options.resource);
31
+ additionalSections = buildEventObjectSections(options.resource);
32
32
  } else if (normalizedKind === "role" || normalizedKind === "clusterrole") {
33
33
  additionalSections = buildRoleSections(options.resource);
34
34
  } else if (normalizedKind === "rolebinding" || normalizedKind === "clusterrolebinding") {
@@ -47,10 +47,8 @@ export function buildResourceDetail(options: BuildResourceDetailOptions): Resour
47
47
  name: options.resource.metadata?.name ?? "unknown",
48
48
  namespace: options.resource.metadata?.namespace,
49
49
  },
50
- summaryLines: flattenSections(summarySections),
51
50
  summarySections,
52
51
  yaml: options.yaml,
53
- describe: options.describe,
54
52
  replicas: overview.replicas,
55
53
  portForwards: portForward.length > 0 ? portForward : undefined,
56
54
  containers: containers && containers.length > 0 ? containers : undefined,
@@ -61,5 +59,4 @@ export interface BuildResourceDetailOptions {
61
59
  resourceKind?: ResourceKind | undefined;
62
60
  resource: KubernetesResource;
63
61
  yaml: string;
64
- describe?: string | undefined;
65
62
  }
@@ -2,7 +2,7 @@ export type PaneId = "clusters" | "resources" | "inspector";
2
2
 
3
3
  export type OverlayId = "cluster-switcher" | "namespace-switcher" | "delete-confirm" | "port-forward" | "port-forward-list" | "logs" | "scale" | "container-picker" | "helm-rollback" | "notification-history" | "help";
4
4
 
5
- export type InspectorTab = "summary" | "yaml" | "events" | "describe";
5
+ export type InspectorTab = "summary" | "yaml";
6
6
 
7
7
  export type LoadStatus = "idle" | "loading" | "ready" | "error";
8
8
 
@@ -68,14 +68,13 @@ export interface ResourceDetailSection {
68
68
 
69
69
  export interface ResourceDetail {
70
70
  ref: ResourceRef;
71
- summaryLines: string[];
72
71
  summarySections: ResourceDetailSection[];
73
72
  yaml: string;
74
- describe?: string | undefined;
75
73
  replicas?: number | undefined;
76
74
  portForwards?: ResourcePortForward[] | undefined;
77
75
  containers?: string[] | undefined;
78
76
  helmChart?: string | undefined;
77
+ helmValues?: string | undefined;
79
78
  }
80
79
 
81
80
  export interface EventItem {
@@ -1,5 +1,6 @@
1
1
  import { type ChildProcess } from "node:child_process";
2
2
 
3
+ import { dump as dumpYaml } from "js-yaml";
3
4
  import { formatAge } from "../k8s/k8s-format";
4
5
  import { buildResourceDetail, type KubernetesResource } from "../k8s/resource-detail-builder";
5
6
  import type {
@@ -219,31 +220,17 @@ export class KubectlService {
219
220
  }
220
221
 
221
222
  const jsonArgs = ["--context", options.context, "get", options.resourceKind.name, options.name];
222
- const yamlArgs = ["--context", options.context, "get", options.resourceKind.name, options.name];
223
- const describeArgs = ["--context", options.context, "describe", options.resourceKind.name, options.name];
224
223
 
225
224
  if (options.resourceKind.namespaced) {
226
225
  jsonArgs.push("-n", options.namespace);
227
- yamlArgs.push("-n", options.namespace);
228
- describeArgs.push("-n", options.namespace);
229
226
  }
230
227
 
231
228
  jsonArgs.push("-o", "json");
232
- yamlArgs.push("-o", "yaml");
233
229
 
234
- const [resource, yaml, describeResult] = await Promise.allSettled([
235
- runJson<KubernetesResource>({ command: "kubectl", args: jsonArgs, timeoutMs: KUBECTL_TIMEOUT_MS }),
236
- runText({ command: "kubectl", args: yamlArgs, timeoutMs: KUBECTL_TIMEOUT_MS }),
237
- runText({ command: "kubectl", args: describeArgs, timeoutMs: KUBECTL_TIMEOUT_MS }),
238
- ]);
239
-
240
- if (resource.status !== "fulfilled") {
241
- throw resource.reason;
242
- }
243
- const yamlText = yaml.status === "fulfilled" ? yaml.value : "";
244
- const describe = describeResult.status === "fulfilled" ? describeResult.value : "";
230
+ const resource = await runJson<KubernetesResource>({ command: "kubectl", args: jsonArgs, timeoutMs: KUBECTL_TIMEOUT_MS });
231
+ const yamlText = dumpYaml(resource, { lineWidth: -1, noRefs: true });
245
232
 
246
- return buildResourceDetail({ resourceKind: options.resourceKind, resource: resource.value, yaml: yamlText, describe });
233
+ return buildResourceDetail({ resourceKind: options.resourceKind, resource, yaml: yamlText });
247
234
  }
248
235
 
249
236
  async listResourceEvents(options: ListResourceEventsOptions): Promise<EventItem[]> {
@@ -566,13 +553,15 @@ export class KubectlService {
566
553
  private async getHelmReleaseDetail(options: GetResourceDetailOptions): Promise<ResourceDetail> {
567
554
  const baseArgs = buildHelmBaseArgs(options);
568
555
 
569
- const [statusResult, valuesResult] = await Promise.all([
556
+ const [statusResult, valuesResult, manifestResult] = await Promise.all([
570
557
  runCommand({ command: "helm", args: ["status", options.name, ...baseArgs, "-o", "json"], timeoutMs: KUBECTL_TIMEOUT_MS }),
571
558
  runCommand({ command: "helm", args: ["get", "values", options.name, ...baseArgs], timeoutMs: KUBECTL_TIMEOUT_MS }),
559
+ runCommand({ command: "helm", args: ["get", "manifest", options.name, ...baseArgs], timeoutMs: KUBECTL_TIMEOUT_MS }),
572
560
  ]);
573
561
 
574
562
  const status = JSON.parse(statusResult.stdout) as HelmStatusResponse;
575
- const yaml = valuesResult.stdout;
563
+ const helmValues = valuesResult.stdout;
564
+ const yaml = manifestResult.stdout;
576
565
 
577
566
  const overviewLines: string[] = [
578
567
  `name: ${status.name}`,
@@ -598,23 +587,18 @@ export class KubectlService {
598
587
  ...(notesLines.length > 0 ? [{ title: "Notes", lines: notesLines }] : []),
599
588
  ];
600
589
 
601
- const summaryLines = summarySections.flatMap((section) => [
602
- section.title,
603
- ...section.lines.map((line) => ` ${line}`),
604
- ]);
605
-
606
590
  return {
607
591
  ref: {
608
592
  kind: "helmreleases",
609
593
  name: status.name,
610
594
  namespace: status.namespace,
611
595
  },
612
- summaryLines,
613
596
  summarySections,
614
597
  yaml,
615
598
  replicas: undefined,
616
599
  portForwards: undefined,
617
600
  helmChart: status.chart.metadata.name,
601
+ helmValues,
618
602
  };
619
603
  }
620
604