openk8s 1.0.3 → 1.0.5

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.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "A terminal UI for Kubernetes",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -18,6 +18,8 @@ import {
18
18
  defaultNamespace,
19
19
  selectedRef,
20
20
  selectedForwardsForRef,
21
+ truncate,
22
+ formatClusterName,
21
23
  } from "../utils";
22
24
  import type { ResourceListItem, ActivePortForward, ResourceDetail, NamespaceItem, ResourceKind } from "../../lib/k8s/types";
23
25
 
@@ -333,6 +335,58 @@ describe("selectedRef", () => {
333
335
  });
334
336
  });
335
337
 
338
+ // ── truncate ──────────────────────────────────────────────────────────────────
339
+
340
+ describe("truncate", () => {
341
+ test("returns string as-is when within limit", () => {
342
+ expect(truncate("hello", 10)).toBe("hello");
343
+ });
344
+
345
+ test("truncates with ellipsis when over limit", () => {
346
+ expect(truncate("hello world", 5)).toBe("hell\u2026");
347
+ });
348
+
349
+ test("handles empty string", () => {
350
+ expect(truncate("", 5)).toBe("");
351
+ });
352
+
353
+ test("handles exact match", () => {
354
+ expect(truncate("hello", 5)).toBe("hello");
355
+ });
356
+
357
+ test("handles single-char limit", () => {
358
+ expect(truncate("ab", 1)).toBe("\u2026");
359
+ });
360
+ });
361
+
362
+ // ── formatClusterName ─────────────────────────────────────────────────────────
363
+
364
+ describe("formatClusterName", () => {
365
+ test("formats EKS ARN", () => {
366
+ expect(formatClusterName("arn:aws:eks:us-east-1:123456789012:cluster/my-cluster", 30)).toBe("aws/eks/my-cluster");
367
+ });
368
+
369
+ test("truncates long EKS cluster name", () => {
370
+ expect(formatClusterName("arn:aws:eks:us-east-1:123456789012:cluster/this-name-is-way-too-long", 20)).toBe("aws/eks/this-name-i\u2026");
371
+ });
372
+
373
+ test("passes through non-ARN names", () => {
374
+ expect(formatClusterName("minikube", 30)).toBe("minikube");
375
+ });
376
+
377
+ test("truncates long non-ARN names", () => {
378
+ expect(formatClusterName("this-context-name-is-very-long", 15)).toBe("this-context-n\u2026");
379
+ });
380
+
381
+ test("handles empty string", () => {
382
+ expect(formatClusterName("", 10)).toBe("");
383
+ });
384
+
385
+ test("handles EKS name with hyphens and dots", () => {
386
+ expect(formatClusterName("arn:aws:eks:eu-west-2:123456789012:cluster/prod.blue-123", 30)).toBe("aws/eks/prod.blue-123");
387
+ });
388
+ });
389
+
336
390
  // ── selectedForwardsForRef ──────────────────────────────────────────────────
337
391
 
338
392
  describe("selectedForwardsForRef", () => {
package/src/app/app.tsx CHANGED
@@ -22,11 +22,13 @@ import {
22
22
  import {
23
23
  currentKindLabel,
24
24
  filterResources,
25
+ formatClusterName,
25
26
  nextVisibleResourceName,
26
27
  resourcePreview,
27
28
  selectedForwardsForRef,
28
29
  selectedRef,
29
30
  statusLine,
31
+ truncate,
30
32
  } from "./utils";
31
33
  import { usePollingTick } from "./use-polling-tick";
32
34
  import { useFooterHints, useFooterHintRows } from "./use-footer-hints";
@@ -365,14 +367,18 @@ export function App() {
365
367
  padding: 1,
366
368
  }}
367
369
  >
368
- <text fg={TEXT_PRIMARY}>
369
- <span fg={KEY_HINT}>{GLYPHS.cluster}</span>
370
- {` ${state.activeContext ?? "none"}`}
371
- </text>
372
- <text fg={TEXT_SUBTLE} attributes={TEXT_MUTED}>
373
- <span fg={KEY_HINT}>{GLYPHS.ns}</span>
374
- {` ${state.activeNamespace}`}
375
- </text>
370
+ <box style={{ width: "100%" }}>
371
+ <text fg={TEXT_PRIMARY}>
372
+ <span fg={KEY_HINT}>{GLYPHS.cluster}</span>
373
+ {` ${formatClusterName(state.activeContext ?? "none", 26)}`}
374
+ </text>
375
+ </box>
376
+ <box style={{ width: "100%" }}>
377
+ <text fg={TEXT_SUBTLE} attributes={TEXT_MUTED}>
378
+ <span fg={KEY_HINT}>{GLYPHS.ns}</span>
379
+ {` ${truncate(state.activeNamespace, 26)}`}
380
+ </text>
381
+ </box>
376
382
  <KindRows
377
383
  resourceKinds={state.resourceKinds}
378
384
  selectedKind={state.selectedKind}
@@ -7,6 +7,6 @@ export {
7
7
  type PortForwardOverlayProps,
8
8
  buildPortForwardEntries,
9
9
  } from "./port-forward-overlay";
10
- export { LogsDialog, type LogsDialogActions, type LogsDialogProps, logLineColor } from "./logs-dialog";
10
+ export { LogsDialog, type LogsDialogActions, type LogsDialogProps } from "./logs-dialog";
11
11
  export { ScaleDialog, type ScaleDialogProps } from "./scale-dialog";
12
12
  export { HelmRollbackOverlay, type HelmRollbackOverlayProps } from "./helm-rollback-overlay";
@@ -14,16 +14,6 @@ import {
14
14
  } from "../../theme";
15
15
  import type { LoadStatus, LogOptions } from "../../../lib/k8s/types";
16
16
 
17
- function logLineColor(line: string): string {
18
- const lower = line.toLowerCase();
19
- if (/\b(error|err|fatal|panic|critical)\b/.test(lower)) return toneStyles("danger").fg;
20
- if (/\bwarn(?:ing)?\b/.test(lower)) return toneStyles("warning").fg;
21
- if (/\binfo(?:rmation)?\b/.test(lower)) return toneStyles("info").fg;
22
- if (/\bdebug\b/.test(lower)) return TEXT_SUBTLE;
23
- if (/\btrace\b/.test(lower)) return YAML_COMMENT;
24
- return TEXT_SUBTLE;
25
- }
26
-
27
17
  export interface LogsDialogActions {
28
18
  nextMatch: () => void;
29
19
  prevMatch: () => void;
@@ -184,7 +174,7 @@ export function LogsDialog({
184
174
  const matched = line.slice(matchStart, matchStart + searchText.length);
185
175
  const after = line.slice(matchStart + searchText.length);
186
176
  return (
187
- <text key={`${index}:${line}`} fg={logLineColor(line)}>
177
+ <text key={`${index}:${line}`} fg={TEXT_SUBTLE}>
188
178
  {before}
189
179
  <span fg={warningFg} attributes={isCurrentMatch ? TextAttributes.BOLD : TextAttributes.DIM}>
190
180
  {matched}
@@ -194,7 +184,7 @@ export function LogsDialog({
194
184
  );
195
185
  }
196
186
  return (
197
- <text key={`${index}:${line}`} fg={logLineColor(line)}>
187
+ <text key={`${index}:${line}`} fg={TEXT_SUBTLE}>
198
188
  {line}
199
189
  </text>
200
190
  );
@@ -300,4 +290,4 @@ export function LogsDialog({
300
290
  );
301
291
  }
302
292
 
303
- export { logLineColor };
293
+
@@ -35,10 +35,22 @@ export function handleShellKeys(
35
35
  const target = activeResourceRef;
36
36
 
37
37
  const child = kubectl.startShell({ context, namespace, resourceRef: target, namespaced });
38
- child.on("close", () => {
38
+
39
+ let stderrBuffer = "";
40
+ child.stderr?.on("data", (chunk: Buffer) => {
41
+ stderrBuffer += chunk.toString();
42
+ });
43
+
44
+ child.on("close", (code) => {
39
45
  renderer.resume();
40
46
  dispatch({ type: "setError", error: undefined });
41
- dispatch({ type: "setStatusMessage", message: `Returned from shell in ${statusLine({ ref: target })}` });
47
+ if (code === 0) {
48
+ dispatch({ type: "setStatusMessage", message: `Returned from shell in ${statusLine({ ref: target })}` });
49
+ } else {
50
+ const msg = stderrBuffer.trim() || `Shell exited with code ${code}`;
51
+ toastError(new Error(msg));
52
+ dispatch({ type: "setStatusMessage", message: `Shell failed for ${statusLine({ ref: target })}` });
53
+ }
42
54
  setManualRefreshNonce((current) => current + 1);
43
55
  });
44
56
  child.on("error", (error) => {
@@ -183,7 +183,9 @@ export function useDataFetching(
183
183
  let cancelled = false;
184
184
 
185
185
  async function loadResources(): Promise<void> {
186
- dispatch({ type: "setResourcesStatus", status: "loading" });
186
+ if (resourcesLengthRef.current === 0) {
187
+ dispatch({ type: "setResourcesStatus", status: "loading" });
188
+ }
187
189
 
188
190
  try {
189
191
  const resources = await kubectl.listResources({
package/src/app/utils.ts CHANGED
@@ -260,6 +260,22 @@ export function selectedRef(options: SelectedRefOptions): ResourceRef | undefine
260
260
  return options.resource?.ref;
261
261
  }
262
262
 
263
+ const EKS_ARN_RE = /^arn:aws:eks:[^:]+:\d{12}:cluster\/(.+)$/;
264
+
265
+ export function truncate(s: string, maxLen: number): string {
266
+ if (s.length <= maxLen) return s;
267
+ return s.slice(0, maxLen - 1) + "\u2026";
268
+ }
269
+
270
+ export function formatClusterName(name: string, maxLen: number): string {
271
+ const match = name.match(EKS_ARN_RE);
272
+ if (match) {
273
+ const label = `aws/eks/${match[1]}`;
274
+ return truncate(label, maxLen);
275
+ }
276
+ return truncate(name, maxLen);
277
+ }
278
+
263
279
  export function selectedForwardsForRef(options: SelectedForwardsForRefOptions): ActivePortForward[] {
264
280
  if (!options.ref) {
265
281
  return [];
@@ -380,7 +380,7 @@ export class KubectlService {
380
380
 
381
381
  args.push("exec", "-it", target, "--", "sh", "-lc", "exec ${SHELL:-/bin/sh}");
382
382
 
383
- return startPersistentProcess({ command: "kubectl", args, stdio: "inherit" });
383
+ return startPersistentProcess({ command: "kubectl", args, stdio: ["inherit", "inherit", "pipe"] });
384
384
  }
385
385
 
386
386
  editResource(options: EditResourceOptions): ChildProcess {