openk8s 0.0.1 → 1.0.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.
Files changed (35) hide show
  1. package/README.md +194 -40
  2. package/bin/openk8s.js +2 -0
  3. package/package.json +52 -6
  4. package/src/app/app-state.ts +461 -0
  5. package/src/app/app.tsx +708 -0
  6. package/src/app/components/inspector.tsx +449 -0
  7. package/src/app/components/kind-rows.tsx +66 -0
  8. package/src/app/components/notification-tray.tsx +59 -0
  9. package/src/app/components/overlays/delete-confirm-overlay.tsx +79 -0
  10. package/src/app/components/overlays/helm-rollback-overlay.tsx +86 -0
  11. package/src/app/components/overlays/index.ts +12 -0
  12. package/src/app/components/overlays/logs-dialog.tsx +303 -0
  13. package/src/app/components/overlays/port-forward-overlay.tsx +184 -0
  14. package/src/app/components/overlays/scale-dialog.tsx +96 -0
  15. package/src/app/components/overlays/select-overlay.tsx +68 -0
  16. package/src/app/components/overlays/shared.tsx +18 -0
  17. package/src/app/components/port-forwards-tray.tsx +57 -0
  18. package/src/app/components/resource-rows.tsx +120 -0
  19. package/src/app/hooks/use-app-keyboard.ts +723 -0
  20. package/src/app/hooks/use-app-side-effects.ts +39 -0
  21. package/src/app/hooks/use-clipboard.ts +54 -0
  22. package/src/app/hooks/use-data-fetching.ts +366 -0
  23. package/src/app/hooks/use-log-stream.ts +113 -0
  24. package/src/app/hooks/use-port-forward.ts +149 -0
  25. package/src/app/persistence.ts +44 -0
  26. package/src/app/theme.ts +95 -0
  27. package/src/app/use-polling-tick.ts +27 -0
  28. package/src/app/utils.ts +274 -0
  29. package/src/index.tsx +8 -0
  30. package/src/lib/k8s/k8s-format.ts +42 -0
  31. package/src/lib/k8s/resource-detail-builder.ts +545 -0
  32. package/src/lib/k8s/resource-parser.ts +308 -0
  33. package/src/lib/k8s/types.ts +164 -0
  34. package/src/lib/kubectl/kubectl-service.ts +1116 -0
  35. package/src/lib/kubectl/spawn-utils.ts +81 -0
@@ -0,0 +1,68 @@
1
+ import {
2
+ GLYPHS,
3
+ KEY_HINT,
4
+ OVERLAY_SURFACE,
5
+ PANEL_BORDER_ACTIVE,
6
+ ROW_SELECTED,
7
+ TEXT_MUTED,
8
+ TEXT_PRIMARY,
9
+ TEXT_SUBTLE,
10
+ } from "../../theme";
11
+
12
+ export interface SelectOverlayProps {
13
+ title: string;
14
+ items: Array<{ name: string; description: string }>;
15
+ selectedIndex: number;
16
+ onChange: (index: number) => void;
17
+ }
18
+
19
+ export function SelectOverlay({ title, items, selectedIndex, onChange }: SelectOverlayProps) {
20
+ return (
21
+ <box
22
+ style={{
23
+ position: "absolute",
24
+ left: "20%",
25
+ top: "15%",
26
+ width: "60%",
27
+ height: "70%",
28
+ border: true,
29
+ borderColor: PANEL_BORDER_ACTIVE,
30
+ borderStyle: "rounded",
31
+ backgroundColor: OVERLAY_SURFACE,
32
+ padding: 1,
33
+ flexDirection: "column",
34
+ }}
35
+ title={title}
36
+ >
37
+ <box style={{ flexGrow: 1 }}>
38
+ <select
39
+ focused
40
+ style={{ flexGrow: 1 }}
41
+ backgroundColor={OVERLAY_SURFACE}
42
+ focusedBackgroundColor={OVERLAY_SURFACE}
43
+ textColor={TEXT_PRIMARY}
44
+ focusedTextColor={TEXT_PRIMARY}
45
+ selectedBackgroundColor={ROW_SELECTED}
46
+ selectedTextColor={TEXT_PRIMARY}
47
+ descriptionColor={TEXT_SUBTLE}
48
+ selectedDescriptionColor={TEXT_PRIMARY}
49
+ showDescription
50
+ showScrollIndicator
51
+ selectedIndex={selectedIndex}
52
+ options={items}
53
+ onChange={(index) => onChange(index)}
54
+ />
55
+ </box>
56
+ <box style={{ flexDirection: "row", columnGap: 2, marginTop: 1, justifyContent: "flex-end" }}>
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>
66
+ </box>
67
+ );
68
+ }
@@ -0,0 +1,18 @@
1
+ import { GLYPHS, KEY_HINT, TEXT_MUTED, TEXT_SUBTLE } from "../../theme";
2
+
3
+ export const DIALOG_LEFT = "28%";
4
+ export const DIALOG_TOP = "20%";
5
+ export const DIALOG_WIDTH = "44%";
6
+
7
+ export function KeyHintRow({ hints }: { hints: [string, string][] }) {
8
+ return (
9
+ <box style={{ flexDirection: "row", columnGap: 2, marginTop: 1 }}>
10
+ {hints.map(([key, label]) => (
11
+ <text key={key} fg={TEXT_SUBTLE} attributes={TEXT_MUTED}>
12
+ <span fg={KEY_HINT}>{GLYPHS.enter === key || GLYPHS.esc === key ? key : key}</span>
13
+ {` ${label}`}
14
+ </text>
15
+ ))}
16
+ </box>
17
+ );
18
+ }
@@ -0,0 +1,57 @@
1
+ import { TextAttributes } from "@opentui/core";
2
+
3
+ import { DANGER, GLYPHS, KEY_HINT, OVERLAY_SURFACE, PANEL_BORDER, TEXT_PRIMARY, TEXT_SUBTLE } from "../theme";
4
+ import { activePortForwardLabel } from "../utils";
5
+ import type { ActivePortForward } from "../../lib/k8s/types";
6
+
7
+ export interface PortForwardsTrayProps {
8
+ forwards: ActivePortForward[];
9
+ onStop: (id: string) => void;
10
+ }
11
+
12
+ export function PortForwardsTray({ forwards, onStop }: PortForwardsTrayProps) {
13
+ if (forwards.length === 0) {
14
+ return undefined;
15
+ }
16
+
17
+ return (
18
+ <box
19
+ style={{
20
+ border: true,
21
+ borderColor: PANEL_BORDER,
22
+ borderStyle: "rounded",
23
+ backgroundColor: OVERLAY_SURFACE,
24
+ paddingLeft: 1,
25
+ paddingRight: 1,
26
+ flexDirection: "column",
27
+ }}
28
+ >
29
+ {/* Tray heading */}
30
+ <text fg={KEY_HINT}>
31
+ {GLYPHS.forward}
32
+ <span fg={TEXT_SUBTLE} attributes={TextAttributes.DIM}>{" Port Forwards"}</span>
33
+ </text>
34
+ <box style={{ flexDirection: "column", width: "100%" }}>
35
+ {forwards.map((forward) => {
36
+ const isReady = forward.status === "ready";
37
+ const statusDot = isReady ? GLYPHS.dot : GLYPHS.dotEmpty;
38
+ const statusFg = isReady ? KEY_HINT : TEXT_SUBTLE;
39
+
40
+ return (
41
+ <box key={forward.id} style={{ flexDirection: "row", justifyContent: "space-between", width: "100%" }}>
42
+ <text fg={isReady ? TEXT_PRIMARY : TEXT_SUBTLE}>
43
+ <span fg={statusFg}>{statusDot}</span>
44
+ {` ${activePortForwardLabel(forward)} `}
45
+ <span fg={TEXT_SUBTLE} attributes={TextAttributes.DIM}>{forward.status}</span>
46
+ </text>
47
+ <text fg={DANGER} onMouseDown={() => onStop(forward.id)}>
48
+ <span fg={DANGER}>{GLYPHS.stop}</span>
49
+ {" stop"}
50
+ </text>
51
+ </box>
52
+ );
53
+ })}
54
+ </box>
55
+ </box>
56
+ );
57
+ }
@@ -0,0 +1,120 @@
1
+ import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core";
2
+ import { useEffect, useRef } from "react";
3
+
4
+ import { GLYPHS, KEY_HINT, ROW_SELECTED, ROW_SELECTED_ALT, TEXT_PRIMARY, TEXT_SUBTLE, toneStyles } from "../theme";
5
+ import { resourceEmptyState, statusTone } from "../utils";
6
+ import type { LoadStatus, NodeMetricEntry, PodMetricEntry, ResourceListItem } from "../../lib/k8s/types";
7
+
8
+ export interface ResourceRowsProps {
9
+ resources: ResourceListItem[];
10
+ selectedName?: string | undefined;
11
+ selectedNames: string[];
12
+ status: LoadStatus;
13
+ filter: string;
14
+ /** Set of "kind:name" keys for resources with active port-forwards */
15
+ activeForwardKeys: ReadonlySet<string>;
16
+ podMetrics?: Record<string, PodMetricEntry> | undefined;
17
+ nodeMetrics?: Record<string, NodeMetricEntry> | undefined;
18
+ onSelect: (name?: string | undefined) => void;
19
+ onToggleSelected: (name: string) => void;
20
+ onActivate: () => void;
21
+ }
22
+
23
+ export function ResourceRows({
24
+ resources,
25
+ selectedName,
26
+ selectedNames,
27
+ status,
28
+ filter,
29
+ activeForwardKeys,
30
+ podMetrics,
31
+ nodeMetrics,
32
+ onSelect,
33
+ onToggleSelected,
34
+ onActivate,
35
+ }: ResourceRowsProps) {
36
+ const scrollRef = useRef<ScrollBoxRenderable | null>(null);
37
+
38
+ useEffect(() => {
39
+ if (selectedName) {
40
+ scrollRef.current?.scrollChildIntoView(`resource:${selectedName}`);
41
+ }
42
+ }, [selectedName, resources]);
43
+
44
+ return (
45
+ <scrollbox ref={scrollRef} onMouseDown={onActivate} style={{ flexGrow: 1 }}>
46
+ {resources.length === 0 ? (
47
+ <text fg={TEXT_SUBTLE} attributes={TextAttributes.DIM}>
48
+ {resourceEmptyState(status, filter)}
49
+ </text>
50
+ ) : (
51
+ <box style={{ flexDirection: "column", width: "100%" }}>
52
+ {resources.map((resource) => {
53
+ const selected = resource.ref.name === selectedName;
54
+ const marked = selectedNames.includes(resource.ref.name);
55
+ const palette = toneStyles(statusTone(resource.status));
56
+
57
+ // Determine status glyph
58
+ const tone = statusTone(resource.status);
59
+ const statusGlyph =
60
+ tone === "danger" ? GLYPHS.cross :
61
+ tone === "warning" ? GLYPHS.warn :
62
+ tone === "success" ? GLYPHS.dot :
63
+ GLYPHS.dotEmpty;
64
+
65
+ const hasActiveForward = activeForwardKeys.has(
66
+ `${resource.ref.kind.toLowerCase()}:${resource.ref.name}`
67
+ );
68
+
69
+ // Metrics suffix
70
+ const pm = podMetrics?.[resource.ref.name];
71
+ const nm = nodeMetrics?.[resource.ref.name];
72
+ let metricsSuffix = "";
73
+ if (pm) {
74
+ metricsSuffix = ` cpu ${pm.cpu} mem ${pm.memory}`;
75
+ } else if (nm) {
76
+ metricsSuffix = ` cpu ${nm.cpu} (${nm.cpuPct}%) mem ${nm.memory} (${nm.memPct}%)`;
77
+ }
78
+
79
+ return (
80
+ <box
81
+ id={`resource:${resource.ref.name}`}
82
+ key={`${resource.ref.kind}:${resource.ref.namespace ?? "cluster"}:${resource.ref.name}`}
83
+ onMouseDown={() => {
84
+ onActivate();
85
+ onSelect(resource.ref.name);
86
+ }}
87
+ style={{
88
+ width: "100%",
89
+ flexDirection: "column",
90
+ paddingLeft: 1,
91
+ paddingRight: 1,
92
+ backgroundColor: selected ? ROW_SELECTED : marked ? ROW_SELECTED_ALT : undefined,
93
+ }}
94
+ >
95
+ {/* Name row: cursor ▶ + mark ◆/◇ + name + optional forward icon */}
96
+ <text fg={TEXT_PRIMARY} attributes={selected ? TextAttributes.BOLD : undefined}>
97
+ <span fg={selected ? palette.fg : "transparent"}>{GLYPHS.cursor}</span>
98
+ {" "}
99
+ <span fg={marked ? palette.fg : TEXT_SUBTLE}>
100
+ {marked ? GLYPHS.marked : GLYPHS.unmarked}
101
+ </span>
102
+ {` ${resource.ref.name}`}
103
+ {hasActiveForward ? (
104
+ <span fg={KEY_HINT}>{` ${GLYPHS.forward}`}</span>
105
+ ) : undefined}
106
+ </text>
107
+ {/* Status row: dot + status text + age + summary */}
108
+ <text fg={palette.fg}>
109
+ {" "}
110
+ <span fg={palette.fg}>{statusGlyph}</span>
111
+ {` ${resource.status} ${resource.age}${resource.summary ? ` ${resource.summary}` : ""}${metricsSuffix}`}
112
+ </text>
113
+ </box>
114
+ );
115
+ })}
116
+ </box>
117
+ )}
118
+ </scrollbox>
119
+ );
120
+ }