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.
- package/README.md +194 -40
- package/bin/openk8s.js +2 -0
- package/package.json +52 -6
- package/src/app/app-state.ts +461 -0
- package/src/app/app.tsx +708 -0
- package/src/app/components/inspector.tsx +449 -0
- package/src/app/components/kind-rows.tsx +66 -0
- package/src/app/components/notification-tray.tsx +59 -0
- package/src/app/components/overlays/delete-confirm-overlay.tsx +79 -0
- package/src/app/components/overlays/helm-rollback-overlay.tsx +86 -0
- package/src/app/components/overlays/index.ts +12 -0
- package/src/app/components/overlays/logs-dialog.tsx +303 -0
- package/src/app/components/overlays/port-forward-overlay.tsx +184 -0
- package/src/app/components/overlays/scale-dialog.tsx +96 -0
- package/src/app/components/overlays/select-overlay.tsx +68 -0
- package/src/app/components/overlays/shared.tsx +18 -0
- package/src/app/components/port-forwards-tray.tsx +57 -0
- package/src/app/components/resource-rows.tsx +120 -0
- package/src/app/hooks/use-app-keyboard.ts +723 -0
- package/src/app/hooks/use-app-side-effects.ts +39 -0
- package/src/app/hooks/use-clipboard.ts +54 -0
- package/src/app/hooks/use-data-fetching.ts +366 -0
- package/src/app/hooks/use-log-stream.ts +113 -0
- package/src/app/hooks/use-port-forward.ts +149 -0
- package/src/app/persistence.ts +44 -0
- package/src/app/theme.ts +95 -0
- package/src/app/use-polling-tick.ts +27 -0
- package/src/app/utils.ts +274 -0
- package/src/index.tsx +8 -0
- package/src/lib/k8s/k8s-format.ts +42 -0
- package/src/lib/k8s/resource-detail-builder.ts +545 -0
- package/src/lib/k8s/resource-parser.ts +308 -0
- package/src/lib/k8s/types.ts +164 -0
- package/src/lib/kubectl/kubectl-service.ts +1116 -0
- 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
|
+
}
|