openk8s 1.0.4 → 1.1.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 +1 -1
- package/package.json +11 -10
- package/src/app/__tests__/app-state.test.ts +47 -5
- package/src/app/__tests__/utils.test.ts +5 -14
- package/src/app/app-actions.ts +5 -0
- package/src/app/app-state.ts +11 -2
- package/src/app/app.tsx +57 -46
- package/src/app/components/detail-sections.tsx +2 -2
- package/src/app/components/footer.tsx +23 -30
- package/src/app/components/inspector.tsx +28 -43
- package/src/app/components/kind-rows.tsx +1 -1
- package/src/app/components/notification-card.tsx +145 -0
- package/src/app/components/notification-tray.tsx +19 -38
- package/src/app/components/overlays/delete-confirm-overlay.tsx +15 -17
- package/src/app/components/overlays/helm-rollback-overlay.tsx +12 -66
- package/src/app/components/overlays/help-overlay.tsx +173 -0
- package/src/app/components/overlays/index.ts +5 -2
- package/src/app/components/overlays/logs-dialog.tsx +50 -80
- package/src/app/components/overlays/notification-history-overlay.tsx +79 -0
- package/src/app/components/overlays/port-forward-list-overlay.tsx +85 -0
- package/src/app/components/overlays/port-forward-overlay.tsx +16 -56
- package/src/app/components/overlays/scale-dialog.tsx +12 -67
- package/src/app/components/overlays/select-overlay.tsx +5 -14
- package/src/app/components/overlays/shared.tsx +85 -6
- package/src/app/components/resource-rows.tsx +1 -1
- package/src/app/constants.ts +24 -0
- package/src/app/hooks/keyboard/filter-handlers.ts +2 -1
- package/src/app/hooks/keyboard/global-handlers.ts +32 -11
- package/src/app/hooks/keyboard/helm-handlers.ts +14 -9
- package/src/app/hooks/keyboard/keys.ts +18 -0
- package/src/app/hooks/keyboard/logs-handlers.ts +3 -1
- package/src/app/hooks/keyboard/navigation-handlers.ts +5 -4
- package/src/app/hooks/keyboard/overlay-handlers.ts +11 -7
- package/src/app/hooks/keyboard/port-forward-handlers.ts +19 -14
- package/src/app/hooks/keyboard/shell-edit-handlers.ts +45 -17
- package/src/app/hooks/use-app-keyboard.ts +22 -2
- package/src/app/hooks/use-app-side-effects.ts +10 -7
- package/src/app/hooks/use-clipboard.ts +8 -10
- package/src/app/hooks/use-data-fetching.ts +11 -5
- package/src/app/hooks/use-log-stream.ts +8 -4
- package/src/app/hooks/use-notifications.ts +92 -0
- package/src/app/hooks/use-port-forward.ts +19 -4
- package/src/app/persistence.ts +7 -3
- package/src/app/syntax-theme.ts +31 -0
- package/src/app/theme.ts +2 -3
- package/src/app/use-footer-hints.ts +21 -16
- package/src/app/utils.ts +1 -9
- package/src/assets/tree-sitter/yaml/highlights.scm +79 -0
- package/src/assets/tree-sitter/yaml/tree-sitter-yaml.wasm +0 -0
- package/src/index.tsx +22 -2
- package/src/lib/k8s/__tests__/k8s-format.test.ts +17 -0
- package/src/lib/k8s/__tests__/resource-parser.test.ts +40 -2
- package/src/lib/k8s/detail-builders/event-builder.ts +17 -11
- package/src/lib/k8s/detail-builders/hpa-cronjob-builder.ts +34 -31
- package/src/lib/k8s/detail-builders/node-builder.ts +22 -11
- package/src/lib/k8s/detail-builders/overview-builder.ts +4 -17
- package/src/lib/k8s/detail-builders/pod-builder.ts +52 -24
- package/src/lib/k8s/detail-builders/rbac-builder.ts +20 -29
- package/src/lib/k8s/k8s-format.ts +14 -9
- package/src/lib/k8s/resource-detail-builder.ts +1 -1
- package/src/lib/k8s/resource-parser.ts +17 -2
- package/src/lib/k8s/types.ts +10 -1
- package/src/lib/kubectl/__tests__/metrics-utils.test.ts +9 -0
- package/src/lib/kubectl/kubectl-helpers.ts +16 -11
- package/src/lib/kubectl/kubectl-service.ts +39 -39
- package/src/lib/kubectl/kubectl-types.ts +10 -1
- package/src/lib/kubectl/metrics-utils.ts +21 -7
- package/src/lib/kubectl/spawn-utils.ts +50 -11
- package/src/app/__tests__/components/inspector-tokens.test.ts +0 -101
- package/src/app/components/inspector-tokens.ts +0 -93
- package/src/app/components/port-forwards-tray.tsx +0 -57
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
|
|
3
|
-
import { GLYPHS
|
|
4
|
-
import
|
|
3
|
+
import { GLYPHS } from "./theme";
|
|
4
|
+
import { FOOTER_PADDING, HINT_CELL_OVERHEAD } from "./constants";
|
|
5
|
+
import type { ActivePortForward, PaneId, ResourceDetail, ResourceListItem, ResourceRef } from "../lib/k8s/types";
|
|
6
|
+
import { isHelmRelease } from "../lib/kubectl/kubectl-helpers";
|
|
5
7
|
import { KubectlService } from "../lib/kubectl/kubectl-service";
|
|
6
8
|
|
|
7
9
|
export function useFooterHints(
|
|
8
10
|
filterMode: boolean,
|
|
9
|
-
|
|
11
|
+
activePane: PaneId,
|
|
10
12
|
activeResourceRef: ResourceRef | undefined,
|
|
11
13
|
activeDetail: ResourceDetail | undefined,
|
|
12
14
|
selectedResourcePortForwards: ActivePortForward[],
|
|
13
|
-
|
|
15
|
+
activePortForwardsCount: number,
|
|
16
|
+
deleteTargets: ReadonlyArray<ResourceListItem>,
|
|
14
17
|
kubectl: KubectlService,
|
|
15
18
|
): [string, string][] {
|
|
16
19
|
return useMemo((): [string, string][] => {
|
|
@@ -22,12 +25,12 @@ export function useFooterHints(
|
|
|
22
25
|
];
|
|
23
26
|
}
|
|
24
27
|
|
|
25
|
-
const isHelm = activeResourceRef
|
|
28
|
+
const isHelm = isHelmRelease(activeResourceRef);
|
|
26
29
|
const hasResource = Boolean(activeResourceRef);
|
|
27
30
|
const hasPortForwards =
|
|
28
31
|
(activeDetail?.portForwards?.length ?? 0) > 0 || selectedResourcePortForwards.length > 0;
|
|
29
32
|
|
|
30
|
-
if (
|
|
33
|
+
if (activePane === "clusters") {
|
|
31
34
|
return [
|
|
32
35
|
[GLYPHS.tab, "panes"],
|
|
33
36
|
["↑↓", "kinds"],
|
|
@@ -35,7 +38,7 @@ export function useFooterHints(
|
|
|
35
38
|
];
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
if (
|
|
41
|
+
if (activePane === "inspector") {
|
|
39
42
|
return [
|
|
40
43
|
[GLYPHS.tab, "panes"],
|
|
41
44
|
["←", "back"],
|
|
@@ -48,17 +51,17 @@ export function useFooterHints(
|
|
|
48
51
|
["↑↓", "navigate"],
|
|
49
52
|
["/", "filter"],
|
|
50
53
|
["R", "refresh"],
|
|
54
|
+
["⇧N", "history"],
|
|
55
|
+
...(activePortForwardsCount > 0 ? [["⇧F", "forwards"] as [string, string]] : []),
|
|
56
|
+
["?", "help"],
|
|
51
57
|
];
|
|
52
58
|
|
|
53
59
|
if (!hasResource) return hints;
|
|
54
60
|
|
|
55
61
|
if (isHelm) {
|
|
56
|
-
hints.push(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
["B", "rollback"],
|
|
60
|
-
["U", "upgrade"],
|
|
61
|
-
);
|
|
62
|
+
hints.push(["E", "edit values"]);
|
|
63
|
+
if (deleteTargets.length > 0) hints.push(["D", "delete"]);
|
|
64
|
+
hints.push(["B", "rollback"], ["U", "upgrade"]);
|
|
62
65
|
} else {
|
|
63
66
|
hints.push(["L", "logs"], ["E", "edit"], ["X", "shell"]);
|
|
64
67
|
if (hasPortForwards) hints.push(["F", "forward"]);
|
|
@@ -70,11 +73,13 @@ export function useFooterHints(
|
|
|
70
73
|
return hints;
|
|
71
74
|
}, [
|
|
72
75
|
filterMode,
|
|
73
|
-
|
|
76
|
+
activePane,
|
|
74
77
|
activeResourceRef,
|
|
75
78
|
activeDetail,
|
|
76
79
|
selectedResourcePortForwards,
|
|
80
|
+
activePortForwardsCount,
|
|
77
81
|
deleteTargets,
|
|
82
|
+
kubectl,
|
|
78
83
|
]);
|
|
79
84
|
}
|
|
80
85
|
|
|
@@ -83,14 +88,14 @@ export function useFooterHintRows(
|
|
|
83
88
|
terminalWidth: number,
|
|
84
89
|
): { footerHintsRow1: [string, string][]; footerHintsRow2: [string, string][] } {
|
|
85
90
|
return useMemo(() => {
|
|
86
|
-
const innerWidth = terminalWidth -
|
|
91
|
+
const innerWidth = terminalWidth - FOOTER_PADDING;
|
|
87
92
|
const row1: [string, string][] = [];
|
|
88
93
|
const row2: [string, string][] = [];
|
|
89
94
|
let used1 = 0;
|
|
90
95
|
let used2 = 0;
|
|
91
96
|
|
|
92
97
|
for (const [key, label] of footerHints) {
|
|
93
|
-
const w = key.length + label.length +
|
|
98
|
+
const w = key.length + label.length + HINT_CELL_OVERHEAD;
|
|
94
99
|
const gap1 = row1.length === 0 ? 0 : 2;
|
|
95
100
|
const gap2 = row2.length === 0 ? 0 : 2;
|
|
96
101
|
if (used1 + gap1 + w <= innerWidth) {
|
package/src/app/utils.ts
CHANGED
|
@@ -52,11 +52,6 @@ export interface ResourcePreviewOptions {
|
|
|
52
52
|
resource?: ResourceListItem | undefined;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
export interface CurrentKindLabelOptions {
|
|
56
|
-
resourceKinds: ResourceKind[];
|
|
57
|
-
selectedKind: string;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
55
|
export interface PortForwardIdOptions {
|
|
61
56
|
context: string;
|
|
62
57
|
namespace: string;
|
|
@@ -185,10 +180,6 @@ export function kindDescription(kind: ResourceKind): string {
|
|
|
185
180
|
return `${scope}${shortNames}`;
|
|
186
181
|
}
|
|
187
182
|
|
|
188
|
-
export function currentKindLabel(options: CurrentKindLabelOptions): string {
|
|
189
|
-
return options.resourceKinds.find((kind) => kind.name === options.selectedKind)?.name ?? options.selectedKind;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
183
|
export function resourcePreview(options: ResourcePreviewOptions): string[] {
|
|
193
184
|
if (!options.resource) {
|
|
194
185
|
return ["Select a resource to inspect it."];
|
|
@@ -263,6 +254,7 @@ export function selectedRef(options: SelectedRefOptions): ResourceRef | undefine
|
|
|
263
254
|
const EKS_ARN_RE = /^arn:aws:eks:[^:]+:\d{12}:cluster\/(.+)$/;
|
|
264
255
|
|
|
265
256
|
export function truncate(s: string, maxLen: number): string {
|
|
257
|
+
if (maxLen <= 0) return s.length > 0 ? "\u2026" : s;
|
|
266
258
|
if (s.length <= maxLen) return s;
|
|
267
259
|
return s.slice(0, maxLen - 1) + "\u2026";
|
|
268
260
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
(boolean_scalar) @boolean
|
|
2
|
+
|
|
3
|
+
(null_scalar) @constant.builtin
|
|
4
|
+
|
|
5
|
+
[
|
|
6
|
+
(double_quote_scalar)
|
|
7
|
+
(single_quote_scalar)
|
|
8
|
+
(block_scalar)
|
|
9
|
+
(string_scalar)
|
|
10
|
+
] @string
|
|
11
|
+
|
|
12
|
+
[
|
|
13
|
+
(integer_scalar)
|
|
14
|
+
(float_scalar)
|
|
15
|
+
] @number
|
|
16
|
+
|
|
17
|
+
(comment) @comment
|
|
18
|
+
|
|
19
|
+
[
|
|
20
|
+
(anchor_name)
|
|
21
|
+
(alias_name)
|
|
22
|
+
] @label
|
|
23
|
+
|
|
24
|
+
(tag) @type
|
|
25
|
+
|
|
26
|
+
[
|
|
27
|
+
(yaml_directive)
|
|
28
|
+
(tag_directive)
|
|
29
|
+
(reserved_directive)
|
|
30
|
+
] @attribute
|
|
31
|
+
|
|
32
|
+
(block_mapping_pair
|
|
33
|
+
key: (flow_node
|
|
34
|
+
[
|
|
35
|
+
(double_quote_scalar)
|
|
36
|
+
(single_quote_scalar)
|
|
37
|
+
] @property))
|
|
38
|
+
|
|
39
|
+
(block_mapping_pair
|
|
40
|
+
key: (flow_node
|
|
41
|
+
(plain_scalar
|
|
42
|
+
(string_scalar) @property)))
|
|
43
|
+
|
|
44
|
+
(flow_mapping
|
|
45
|
+
(_
|
|
46
|
+
key: (flow_node
|
|
47
|
+
[
|
|
48
|
+
(double_quote_scalar)
|
|
49
|
+
(single_quote_scalar)
|
|
50
|
+
] @property)))
|
|
51
|
+
|
|
52
|
+
(flow_mapping
|
|
53
|
+
(_
|
|
54
|
+
key: (flow_node
|
|
55
|
+
(plain_scalar
|
|
56
|
+
(string_scalar) @property))))
|
|
57
|
+
|
|
58
|
+
[
|
|
59
|
+
","
|
|
60
|
+
"-"
|
|
61
|
+
":"
|
|
62
|
+
">"
|
|
63
|
+
"?"
|
|
64
|
+
"|"
|
|
65
|
+
] @punctuation.delimiter
|
|
66
|
+
|
|
67
|
+
[
|
|
68
|
+
"["
|
|
69
|
+
"]"
|
|
70
|
+
"{"
|
|
71
|
+
"}"
|
|
72
|
+
] @punctuation.bracket
|
|
73
|
+
|
|
74
|
+
[
|
|
75
|
+
"*"
|
|
76
|
+
"&"
|
|
77
|
+
"---"
|
|
78
|
+
"..."
|
|
79
|
+
] @punctuation.special
|
|
Binary file
|
package/src/index.tsx
CHANGED
|
@@ -1,11 +1,31 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { createCliRenderer, addDefaultParsers } from "@opentui/core";
|
|
2
3
|
import { createRoot } from "@opentui/react";
|
|
3
|
-
import { setAbortController } from "./lib/kubectl/spawn-utils";
|
|
4
|
+
import { setAbortController, abortRunningProcesses } from "./lib/kubectl/spawn-utils";
|
|
4
5
|
import { App } from "./app/app";
|
|
5
6
|
|
|
7
|
+
const yamlAssetsDir = join(import.meta.dirname, "assets", "tree-sitter", "yaml");
|
|
8
|
+
addDefaultParsers([
|
|
9
|
+
{
|
|
10
|
+
filetype: "yaml",
|
|
11
|
+
aliases: ["yml"],
|
|
12
|
+
queries: {
|
|
13
|
+
highlights: [join(yamlAssetsDir, "highlights.scm")],
|
|
14
|
+
},
|
|
15
|
+
wasm: join(yamlAssetsDir, "tree-sitter-yaml.wasm"),
|
|
16
|
+
},
|
|
17
|
+
]);
|
|
18
|
+
|
|
6
19
|
const abortController = new AbortController();
|
|
7
20
|
setAbortController(abortController);
|
|
8
21
|
|
|
22
|
+
for (const signal of ["SIGINT", "SIGTERM"] as const) {
|
|
23
|
+
process.on(signal, () => {
|
|
24
|
+
abortRunningProcesses();
|
|
25
|
+
process.exit(0);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
9
29
|
const renderer = await createCliRenderer({
|
|
10
30
|
useMouse: true,
|
|
11
31
|
});
|
|
@@ -39,4 +39,21 @@ describe("formatAge", () => {
|
|
|
39
39
|
test("returns '-' for invalid date", () => {
|
|
40
40
|
expect(formatAge("not-a-date")).toBe("-");
|
|
41
41
|
});
|
|
42
|
+
|
|
43
|
+
test("parses helm-style timestamp with offset as UTC", () => {
|
|
44
|
+
const result = formatAge("2024-01-15 10:30:00.000000000 +0000 UTC");
|
|
45
|
+
expect(result).not.toBe("-");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("parses helm-style timestamp without offset as UTC (not local)", () => {
|
|
49
|
+
// Regression: helm values without +0000 used to be parsed in local time.
|
|
50
|
+
// Using a far-past date so the result is well-formed regardless of timezone.
|
|
51
|
+
const result = formatAge("2000-01-01 00:00:00.000 UTC");
|
|
52
|
+
expect(result).toMatch(/^\d+d$/);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("does not double-append Z to ISO strings", () => {
|
|
56
|
+
const result = formatAge("2024-01-15T10:30:00Z");
|
|
57
|
+
expect(result).not.toBe("-");
|
|
58
|
+
});
|
|
42
59
|
});
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
formatEnvFromSource,
|
|
22
22
|
formatProbe,
|
|
23
23
|
formatVolumeSource,
|
|
24
|
+
recordsOf,
|
|
24
25
|
} from "../resource-parser";
|
|
25
26
|
|
|
26
27
|
// ── Type guards ──────────────────────────────────────────────────────────────
|
|
@@ -77,6 +78,18 @@ describe("asNumber", () => {
|
|
|
77
78
|
test("returns zero for zero", () => {
|
|
78
79
|
expect(asNumber(0)).toBe(0);
|
|
79
80
|
});
|
|
81
|
+
|
|
82
|
+
test("returns undefined for NaN", () => {
|
|
83
|
+
expect(asNumber(NaN)).toBeUndefined();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("returns undefined for Infinity", () => {
|
|
87
|
+
expect(asNumber(Infinity)).toBeUndefined();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("does not let NaN leak through nullish-coalesce fallbacks", () => {
|
|
91
|
+
expect(asNumber(NaN) ?? 0).toBe(0);
|
|
92
|
+
});
|
|
80
93
|
});
|
|
81
94
|
|
|
82
95
|
describe("asBoolean", () => {
|
|
@@ -277,6 +290,31 @@ describe("extractContainerPorts", () => {
|
|
|
277
290
|
expect(extractContainerPorts({})).toEqual([]);
|
|
278
291
|
expect(extractContainerPorts(null)).toEqual([]);
|
|
279
292
|
});
|
|
293
|
+
|
|
294
|
+
test("extracts ports from pod-template spec (used by overview-builder)", () => {
|
|
295
|
+
const template = {
|
|
296
|
+
spec: {
|
|
297
|
+
containers: [
|
|
298
|
+
{ ports: [{ containerPort: 8080 }] },
|
|
299
|
+
{ ports: [{ containerPort: 8080 }] },
|
|
300
|
+
],
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
const ports = extractContainerPorts((template as Record<string, unknown>).spec);
|
|
304
|
+
expect(ports).toEqual([8080]);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe("recordsOf", () => {
|
|
309
|
+
test("returns records from an array", () => {
|
|
310
|
+
expect(recordsOf([{ a: 1 }, "x", { b: 2 }])).toEqual([{ a: 1 }, { b: 2 }]);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("returns empty for non-array", () => {
|
|
314
|
+
expect(recordsOf(undefined)).toEqual([]);
|
|
315
|
+
expect(recordsOf(null)).toEqual([]);
|
|
316
|
+
expect(recordsOf({})).toEqual([]);
|
|
317
|
+
});
|
|
280
318
|
});
|
|
281
319
|
|
|
282
320
|
describe("extractContainerNames", () => {
|
|
@@ -313,11 +351,11 @@ describe("formatContainerState", () => {
|
|
|
313
351
|
});
|
|
314
352
|
|
|
315
353
|
test("undefined state", () => {
|
|
316
|
-
expect(formatContainerState(undefined)).toBe("
|
|
354
|
+
expect(formatContainerState(undefined)).toBe("unknown");
|
|
317
355
|
});
|
|
318
356
|
|
|
319
357
|
test("null state", () => {
|
|
320
|
-
expect(formatContainerState(null)).toBe("
|
|
358
|
+
expect(formatContainerState(null)).toBe("unknown");
|
|
321
359
|
});
|
|
322
360
|
});
|
|
323
361
|
|
|
@@ -1,20 +1,26 @@
|
|
|
1
|
+
import { asRecord, asString } from "../resource-parser";
|
|
2
|
+
import { formatAge } from "../k8s-format";
|
|
1
3
|
import type { ResourceDetailSection } from "../types";
|
|
2
4
|
import type { KubernetesResource } from "../resource-parser";
|
|
3
5
|
|
|
6
|
+
function asStringOrDash(value: unknown): string {
|
|
7
|
+
return asString(value) ?? "-";
|
|
8
|
+
}
|
|
9
|
+
|
|
4
10
|
export function buildEventSections(resource: KubernetesResource): ResourceDetailSection[] {
|
|
5
|
-
const raw = resource
|
|
6
|
-
const involvedObject = (raw.involvedObject ?? {}
|
|
7
|
-
const source = (raw.source ?? {}
|
|
11
|
+
const raw = asRecord(resource) ?? {};
|
|
12
|
+
const involvedObject = asRecord(raw.involvedObject) ?? {};
|
|
13
|
+
const source = asRecord(raw.source) ?? {};
|
|
8
14
|
|
|
9
15
|
const lines = [
|
|
10
|
-
`type: ${
|
|
11
|
-
`reason: ${
|
|
12
|
-
`message: ${
|
|
13
|
-
`count: ${
|
|
14
|
-
`firstTimestamp: ${
|
|
15
|
-
`lastTimestamp: ${
|
|
16
|
-
`source: ${
|
|
17
|
-
`involvedObject: ${
|
|
16
|
+
`type: ${asStringOrDash(raw.type)}`,
|
|
17
|
+
`reason: ${asStringOrDash(raw.reason)}`,
|
|
18
|
+
`message: ${asStringOrDash(raw.message)}`,
|
|
19
|
+
`count: ${asStringOrDash(raw.count)}`,
|
|
20
|
+
`firstTimestamp: ${formatAge(asString(raw.firstTimestamp))}`,
|
|
21
|
+
`lastTimestamp: ${formatAge(asString(raw.lastTimestamp))}`,
|
|
22
|
+
`source: ${asStringOrDash(source.component ?? raw.reportingComponent)}`,
|
|
23
|
+
`involvedObject: ${asStringOrDash(involvedObject.kind)}/${asStringOrDash(involvedObject.name)} ns: ${asStringOrDash(involvedObject.namespace)}`,
|
|
18
24
|
];
|
|
19
25
|
|
|
20
26
|
return [{ title: "Event", lines }];
|
|
@@ -1,37 +1,40 @@
|
|
|
1
|
-
import { asArray, asBoolean, asNumber, asRecord, asString } from "../resource-parser";
|
|
1
|
+
import { asArray, asBoolean, asNumber, asRecord, asString, recordsOf } from "../resource-parser";
|
|
2
2
|
import type { ResourceDetailSection } from "../types";
|
|
3
3
|
import type { KubernetesResource } from "../resource-parser";
|
|
4
4
|
|
|
5
|
+
function asStringOrDash(value: unknown): string {
|
|
6
|
+
return asString(value) ?? "-";
|
|
7
|
+
}
|
|
8
|
+
|
|
5
9
|
export function buildHpaSections(resource: KubernetesResource): ResourceDetailSection[] {
|
|
6
10
|
const spec = resource.spec ?? {};
|
|
7
11
|
const status = resource.status ?? {};
|
|
8
12
|
|
|
9
|
-
const scaleTargetRef = (spec.scaleTargetRef ?? {}
|
|
13
|
+
const scaleTargetRef = asRecord(spec.scaleTargetRef) ?? {};
|
|
10
14
|
const lines = [
|
|
11
|
-
`scaleTargetRef: ${
|
|
12
|
-
`minReplicas: ${
|
|
13
|
-
`maxReplicas: ${
|
|
14
|
-
`currentReplicas: ${
|
|
15
|
-
`desiredReplicas: ${
|
|
15
|
+
`scaleTargetRef: ${asStringOrDash(scaleTargetRef.kind)}/${asStringOrDash(scaleTargetRef.name)}`,
|
|
16
|
+
`minReplicas: ${asNumber(spec.minReplicas) ?? "-"}`,
|
|
17
|
+
`maxReplicas: ${asNumber(spec.maxReplicas) ?? "-"}`,
|
|
18
|
+
`currentReplicas: ${asNumber(status.currentReplicas) ?? "-"}`,
|
|
19
|
+
`desiredReplicas: ${asNumber(status.desiredReplicas) ?? "-"}`,
|
|
16
20
|
];
|
|
17
21
|
|
|
18
|
-
const conditions =
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const metrics =
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
});
|
|
22
|
+
const conditions = recordsOf(status.conditions).map((c) => {
|
|
23
|
+
const message = asString(c.message);
|
|
24
|
+
return `${asStringOrDash(c.type)}: ${asStringOrDash(c.status)}${message ? ` — ${message}` : ""}`;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const metrics = recordsOf(status.currentMetrics).map((m) => {
|
|
28
|
+
const type = asString(m.type) ?? "metric";
|
|
29
|
+
const resource = asRecord(m.resource);
|
|
30
|
+
if (resource) {
|
|
31
|
+
const current = asRecord(resource.current);
|
|
32
|
+
const value = current?.averageUtilization ?? current?.averageValue;
|
|
33
|
+
const label = current?.averageUtilization !== undefined ? `${value}%` : asStringOrDash(value);
|
|
34
|
+
return `${type}/${asStringOrDash(resource.name)}: ${label}`;
|
|
35
|
+
}
|
|
36
|
+
return type;
|
|
37
|
+
});
|
|
35
38
|
|
|
36
39
|
return [
|
|
37
40
|
{ title: "HPA", lines },
|
|
@@ -46,16 +49,16 @@ export function buildCronJobSections(resource: KubernetesResource): ResourceDeta
|
|
|
46
49
|
|
|
47
50
|
const activeJobs = asArray(status.active).map((j) => {
|
|
48
51
|
const ref = asRecord(j);
|
|
49
|
-
return ref ? `${
|
|
52
|
+
return ref ? `${asStringOrDash(ref.kind) ?? "Job"}/${asStringOrDash(ref.name)}` : "-";
|
|
50
53
|
});
|
|
51
54
|
|
|
52
55
|
const lines = [
|
|
53
|
-
`schedule: ${
|
|
54
|
-
`suspend: ${
|
|
55
|
-
`lastScheduleTime: ${
|
|
56
|
-
`concurrencyPolicy: ${
|
|
57
|
-
`successfulJobsHistoryLimit: ${
|
|
58
|
-
`failedJobsHistoryLimit: ${
|
|
56
|
+
`schedule: ${asStringOrDash(spec.schedule)}`,
|
|
57
|
+
`suspend: ${asBoolean(spec.suspend) ?? false}`,
|
|
58
|
+
`lastScheduleTime: ${asStringOrDash(status.lastScheduleTime)}`,
|
|
59
|
+
`concurrencyPolicy: ${asStringOrDash(spec.concurrencyPolicy) ?? "Allow"}`,
|
|
60
|
+
`successfulJobsHistoryLimit: ${asNumber(spec.successfulJobsHistoryLimit) ?? "-"}`,
|
|
61
|
+
`failedJobsHistoryLimit: ${asNumber(spec.failedJobsHistoryLimit) ?? "-"}`,
|
|
59
62
|
...(activeJobs.length > 0 ? [`active: ${activeJobs.join(", ")}`] : []),
|
|
60
63
|
];
|
|
61
64
|
|
|
@@ -4,17 +4,17 @@ import type { KubernetesResource } from "../resource-parser";
|
|
|
4
4
|
|
|
5
5
|
export function buildNodeSections(resource: KubernetesResource): ResourceDetailSection[] {
|
|
6
6
|
const status = resource.status ?? {};
|
|
7
|
-
const
|
|
7
|
+
const spec = asRecord(resource.spec) ?? {};
|
|
8
8
|
|
|
9
|
-
const nodeInfo = (status.nodeInfo ?? {}
|
|
10
|
-
const capacity = (status.capacity ?? {}
|
|
11
|
-
const allocatable = (status.allocatable ?? {}
|
|
9
|
+
const nodeInfo = asRecord(status.nodeInfo) ?? {};
|
|
10
|
+
const capacity = asRecord(status.capacity) ?? {};
|
|
11
|
+
const allocatable = asRecord(status.allocatable) ?? {};
|
|
12
12
|
|
|
13
13
|
const infoLines = [
|
|
14
|
-
`kubeletVersion: ${
|
|
15
|
-
`containerRuntime: ${
|
|
16
|
-
`osImage: ${
|
|
17
|
-
`architecture: ${
|
|
14
|
+
`kubeletVersion: ${asString(nodeInfo.kubeletVersion) ?? "-"}`,
|
|
15
|
+
`containerRuntime: ${asString(nodeInfo.containerRuntimeVersion) ?? "-"}`,
|
|
16
|
+
`osImage: ${asString(nodeInfo.osImage) ?? "-"}`,
|
|
17
|
+
`architecture: ${asString(nodeInfo.architecture) ?? "-"}`,
|
|
18
18
|
];
|
|
19
19
|
|
|
20
20
|
const capacityLines = Object.entries(capacity).map(([k, v]) => {
|
|
@@ -25,12 +25,23 @@ export function buildNodeSections(resource: KubernetesResource): ResourceDetailS
|
|
|
25
25
|
const conditions = asArray(status.conditions)
|
|
26
26
|
.map((c) => asRecord(c))
|
|
27
27
|
.filter((c): c is Record<string, unknown> => c !== undefined)
|
|
28
|
-
.map((c) =>
|
|
28
|
+
.map((c) => {
|
|
29
|
+
const type = asString(c.type) ?? "Condition";
|
|
30
|
+
const conditionStatus = asString(c.status) ?? "-";
|
|
31
|
+
const reason = asString(c.reason);
|
|
32
|
+
return reason ? `${type}: ${conditionStatus} ${reason}` : `${type}: ${conditionStatus}`;
|
|
33
|
+
});
|
|
29
34
|
|
|
30
|
-
|
|
35
|
+
// kubectl convention: key=value:effect (or key:effect when value-less)
|
|
36
|
+
const taints = asArray(spec.taints)
|
|
31
37
|
.map((t) => asRecord(t))
|
|
32
38
|
.filter((t): t is Record<string, unknown> => t !== undefined)
|
|
33
|
-
.map((t) =>
|
|
39
|
+
.map((t) => {
|
|
40
|
+
const key = asString(t.key) ?? "key";
|
|
41
|
+
const effect = asString(t.effect) ?? "NoSchedule";
|
|
42
|
+
const value = asString(t.value);
|
|
43
|
+
return value ? `${key}=${value}:${effect}` : `${key}:${effect}`;
|
|
44
|
+
});
|
|
34
45
|
|
|
35
46
|
return [
|
|
36
47
|
{ title: "Node Info", lines: infoLines },
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
asArray, asNumber, asPort, asRecord, asString,
|
|
3
|
-
extractContainerNames, isPodKind, normalizeKind,
|
|
3
|
+
extractContainerNames, extractContainerPorts, isPodKind, normalizeKind,
|
|
4
4
|
suggestedLocalPort, summarizeIngressHosts, summarizeLabels,
|
|
5
|
+
supportsDirectPortForward,
|
|
5
6
|
} from "../resource-parser";
|
|
6
7
|
import type { ResourceDetailSection, ResourceKind, ResourcePortForward } from "../types";
|
|
7
8
|
import type { KubernetesResource } from "../resource-parser";
|
|
@@ -24,9 +25,9 @@ export function buildPortForward(
|
|
|
24
25
|
.map((port) => asPort(asRecord(port)?.port))
|
|
25
26
|
.filter((port): port is number => port !== undefined);
|
|
26
27
|
} else if (isPodKind(kindName)) {
|
|
27
|
-
remotePorts =
|
|
28
|
+
remotePorts = extractContainerPorts(spec);
|
|
28
29
|
} else {
|
|
29
|
-
remotePorts =
|
|
30
|
+
remotePorts = extractContainerPorts(asRecord(spec.template)?.spec);
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
return remotePorts.map((remotePort) => ({
|
|
@@ -35,20 +36,6 @@ export function buildPortForward(
|
|
|
35
36
|
}));
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
function extractContainerPortsFromSpec(specValue: unknown): number[] {
|
|
39
|
-
const spec = asRecord(specValue);
|
|
40
|
-
if (!spec) return [];
|
|
41
|
-
return asArray(spec.containers)
|
|
42
|
-
.map((container) => asRecord(container))
|
|
43
|
-
.filter((c): c is Record<string, unknown> => c !== undefined)
|
|
44
|
-
.flatMap((c) => asArray(c.ports).map((p) => asPort(asRecord(p)?.containerPort)).filter((p): p is number => p !== undefined));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function supportsDirectPortForward(kind: string): boolean {
|
|
48
|
-
const normalized = normalizeKind(kind);
|
|
49
|
-
return ["pod", "pods", "service", "services", "deployment", "deployments", "replicaset", "replicasets", "statefulset", "statefulsets"].includes(normalized);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
39
|
export function buildOverviewSection(
|
|
53
40
|
resourceKind: ResourceKind | undefined,
|
|
54
41
|
resource: KubernetesResource,
|