openk8s 1.0.5 → 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/README.md +1 -1
- package/package.json +13 -10
- package/src/app/__tests__/app-state.test.ts +48 -6
- package/src/app/__tests__/utils.test.ts +6 -15
- 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 +40 -83
- 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 +171 -0
- package/src/app/components/overlays/index.ts +4 -1
- package/src/app/components/overlays/logs-dialog.tsx +47 -67
- 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 +30 -21
- 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 +32 -16
- 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 +10 -6
- 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-detail-builder.test.ts +4 -6
- package/src/lib/k8s/__tests__/resource-parser.test.ts +40 -2
- package/src/lib/k8s/detail-builders/event-builder.ts +18 -12
- 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 +4 -7
- package/src/lib/k8s/resource-parser.ts +17 -2
- package/src/lib/k8s/types.ts +12 -4
- 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 +38 -54
- 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
package/README.md
CHANGED
|
@@ -155,7 +155,7 @@ Three resizable panes: resource kinds (left), resources (center), inspector (rig
|
|
|
155
155
|
|
|
156
156
|
- **Context & namespace switching** — browse any cluster in your kubeconfig, any namespace
|
|
157
157
|
- **Resource browsing** — auto-discovers API resource kinds and lists resources with status, age, and summary; curates a sensible default ordering
|
|
158
|
-
- **Log streaming** — `kubectl logs --follow` with search, container picker, tail count, since duration, and previous logs toggle; Deployments/StatefulSets use `--all-
|
|
158
|
+
- **Log streaming** — `kubectl logs --follow` with search, container picker, tail count, since duration, and previous logs toggle; Deployments/StatefulSets use `--all-containers`
|
|
159
159
|
- **Shell** — drop into a container via `kubectl exec -it sh` (renderer suspends, resumes on exit)
|
|
160
160
|
- **Edit** — `kubectl edit` for resources, temp-file + `helm upgrade --reuse-values` for HelmReleases (opens `$EDITOR`)
|
|
161
161
|
- **Port-forward** — declare ports, start/stop forwards inline with live status
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openk8s",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "A terminal UI for Kubernetes",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"dev": "bun run --watch src/index.tsx",
|
|
35
35
|
"prepare": "husky || exit 0",
|
|
36
36
|
"test": "bun test",
|
|
37
|
-
"test:watch": "bun test --watch"
|
|
37
|
+
"test:watch": "bun test --watch",
|
|
38
|
+
"typecheck": "tsc --noEmit"
|
|
38
39
|
},
|
|
39
40
|
"release": {
|
|
40
41
|
"branches": [
|
|
@@ -42,18 +43,20 @@
|
|
|
42
43
|
]
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
|
-
"@commitlint/cli": "^21.0.
|
|
46
|
-
"@commitlint/config-conventional": "^21.0.
|
|
46
|
+
"@commitlint/cli": "^21.0.2",
|
|
47
|
+
"@commitlint/config-conventional": "^21.0.2",
|
|
47
48
|
"@types/bun": "latest",
|
|
48
|
-
"@types/node": "^25.
|
|
49
|
-
"@types/react": "^19.2.
|
|
49
|
+
"@types/node": "^25.9.3",
|
|
50
|
+
"@types/react": "^19.2.17",
|
|
50
51
|
"husky": "^9.1.7",
|
|
51
|
-
"semantic-release": "^25.0.
|
|
52
|
+
"semantic-release": "^25.0.5",
|
|
52
53
|
"typescript": "^6.0.3"
|
|
53
54
|
},
|
|
54
55
|
"dependencies": {
|
|
55
|
-
"@opentui/core": "^0.
|
|
56
|
-
"@opentui/react": "^0.
|
|
57
|
-
"
|
|
56
|
+
"@opentui/core": "^0.4.1",
|
|
57
|
+
"@opentui/react": "^0.4.1",
|
|
58
|
+
"@types/js-yaml": "^4.0.9",
|
|
59
|
+
"js-yaml": "^5.1.0",
|
|
60
|
+
"react": "^19.2.7"
|
|
58
61
|
}
|
|
59
62
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { reducer, initialState, DEFAULT_NAMESPACE } from "../app-state";
|
|
3
|
+
import { NOTIFICATION_HISTORY_LIMIT } from "../constants";
|
|
3
4
|
import type { AppAction } from "../app-actions";
|
|
4
5
|
import type { AppState, ResourceListItem, ActivePortForward, Notification } from "../../lib/k8s/types";
|
|
5
6
|
|
|
@@ -173,7 +174,7 @@ describe("reducer", () => {
|
|
|
173
174
|
|
|
174
175
|
describe("setSelectedResourceDetail", () => {
|
|
175
176
|
test("sets detail", () => {
|
|
176
|
-
const detail: any = { ref: { kind: "Pod", name: "test" },
|
|
177
|
+
const detail: any = { ref: { kind: "Pod", name: "test" }, summarySections: [], yaml: "" };
|
|
177
178
|
const state = reduce({ type: "setSelectedResourceDetail", detail });
|
|
178
179
|
expect(state.selectedResourceDetail).toBe(detail);
|
|
179
180
|
});
|
|
@@ -253,21 +254,62 @@ describe("reducer", () => {
|
|
|
253
254
|
|
|
254
255
|
// ── Notifications ────────────────────────────────────────────────────────
|
|
255
256
|
|
|
257
|
+
function makeNotification(overrides: Partial<Notification> = {}): Notification {
|
|
258
|
+
return {
|
|
259
|
+
id: "n-1",
|
|
260
|
+
tone: "info",
|
|
261
|
+
message: "Hello",
|
|
262
|
+
persistent: false,
|
|
263
|
+
createdAt: Date.now(),
|
|
264
|
+
actions: [],
|
|
265
|
+
...overrides,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
256
269
|
describe("pushNotification", () => {
|
|
257
270
|
test("adds a notification", () => {
|
|
258
|
-
const notification
|
|
271
|
+
const notification = makeNotification();
|
|
259
272
|
const state = reduce({ type: "pushNotification", notification });
|
|
260
273
|
expect(state.notifications).toHaveLength(1);
|
|
261
274
|
});
|
|
262
275
|
});
|
|
263
276
|
|
|
264
277
|
describe("dismissNotification", () => {
|
|
265
|
-
test("removes a notification", () => {
|
|
266
|
-
const n1
|
|
267
|
-
const n2
|
|
268
|
-
const state = reduce({ type: "dismissNotification", id: "n-1" }, { notifications: [n1, n2] });
|
|
278
|
+
test("removes a notification and adds to history", () => {
|
|
279
|
+
const n1 = makeNotification({ id: "n-1" });
|
|
280
|
+
const n2 = makeNotification({ id: "n-2", tone: "success", message: "Done" });
|
|
281
|
+
const state = reduce({ type: "dismissNotification", id: "n-1" }, { notifications: [n1, n2], notificationHistory: [] });
|
|
269
282
|
expect(state.notifications).toHaveLength(1);
|
|
270
283
|
expect(state.notifications[0]?.id).toBe("n-2");
|
|
284
|
+
expect(state.notificationHistory).toHaveLength(1);
|
|
285
|
+
expect(state.notificationHistory[0]?.id).toBe("n-1");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("does not add to history if notification not found", () => {
|
|
289
|
+
const state = reduce({ type: "dismissNotification", id: "missing" }, { notifications: [], notificationHistory: [] });
|
|
290
|
+
expect(state.notificationHistory).toHaveLength(0);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
describe("clearNotificationHistory", () => {
|
|
295
|
+
test("clears all history", () => {
|
|
296
|
+
const n1 = makeNotification({ id: "n-1" });
|
|
297
|
+
const state = reduce({ type: "clearNotificationHistory" }, { notificationHistory: [n1] });
|
|
298
|
+
expect(state.notificationHistory).toHaveLength(0);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
describe("notification history cap", () => {
|
|
303
|
+
test("caps history at NOTIFICATION_HISTORY_LIMIT", () => {
|
|
304
|
+
const notifications = Array.from({ length: NOTIFICATION_HISTORY_LIMIT + 5 }, (_, i) =>
|
|
305
|
+
makeNotification({ id: `n-${i}` }),
|
|
306
|
+
);
|
|
307
|
+
let state: { notifications: Notification[]; notificationHistory: Notification[] } = { notifications: [], notificationHistory: [] };
|
|
308
|
+
for (const n of notifications) {
|
|
309
|
+
state = reduce({ type: "pushNotification", notification: n }, state);
|
|
310
|
+
state = reduce({ type: "dismissNotification", id: n.id }, state);
|
|
311
|
+
}
|
|
312
|
+
expect(state.notificationHistory.length).toBeLessThanOrEqual(NOTIFICATION_HISTORY_LIMIT);
|
|
271
313
|
});
|
|
272
314
|
});
|
|
273
315
|
|
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
parseLocalPort,
|
|
11
11
|
statusTone,
|
|
12
12
|
kindDescription,
|
|
13
|
-
currentKindLabel,
|
|
14
13
|
resourcePreview,
|
|
15
14
|
normalizeFilter,
|
|
16
15
|
filterResources,
|
|
@@ -187,19 +186,6 @@ describe("kindDescription", () => {
|
|
|
187
186
|
});
|
|
188
187
|
});
|
|
189
188
|
|
|
190
|
-
// ── currentKindLabel ─────────────────────────────────────────────────────────
|
|
191
|
-
|
|
192
|
-
describe("currentKindLabel", () => {
|
|
193
|
-
test("finds kind by name", () => {
|
|
194
|
-
const kinds: ResourceKind[] = [{ name: "pods", namespaced: true, shortNames: ["po"] }];
|
|
195
|
-
expect(currentKindLabel({ resourceKinds: kinds, selectedKind: "pods" })).toBe("pods");
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
test("falls back to selected kind string", () => {
|
|
199
|
-
expect(currentKindLabel({ resourceKinds: [], selectedKind: "pods" })).toBe("pods");
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
|
|
203
189
|
// ── resourcePreview ──────────────────────────────────────────────────────────
|
|
204
190
|
|
|
205
191
|
describe("resourcePreview", () => {
|
|
@@ -312,7 +298,7 @@ describe("defaultNamespace", () => {
|
|
|
312
298
|
describe("selectedRef", () => {
|
|
313
299
|
const detail: ResourceDetail = {
|
|
314
300
|
ref: { kind: "Pod", name: "web", namespace: "default" },
|
|
315
|
-
|
|
301
|
+
summarySections: [], yaml: "",
|
|
316
302
|
};
|
|
317
303
|
|
|
318
304
|
test("returns detail ref when it matches resource", () => {
|
|
@@ -357,6 +343,11 @@ describe("truncate", () => {
|
|
|
357
343
|
test("handles single-char limit", () => {
|
|
358
344
|
expect(truncate("ab", 1)).toBe("\u2026");
|
|
359
345
|
});
|
|
346
|
+
|
|
347
|
+
test("handles zero maxLen", () => {
|
|
348
|
+
expect(truncate("abc", 0)).toBe("\u2026");
|
|
349
|
+
expect(truncate("", 0)).toBe("");
|
|
350
|
+
});
|
|
360
351
|
});
|
|
361
352
|
|
|
362
353
|
// ── formatClusterName ─────────────────────────────────────────────────────────
|
package/src/app/app-actions.ts
CHANGED
|
@@ -198,6 +198,10 @@ export interface DismissNotificationAction {
|
|
|
198
198
|
id: string;
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
export interface ClearNotificationHistoryAction {
|
|
202
|
+
type: "clearNotificationHistory";
|
|
203
|
+
}
|
|
204
|
+
|
|
201
205
|
export interface SetRevealedSecretValueAction {
|
|
202
206
|
type: "setRevealedSecretValue";
|
|
203
207
|
id: string;
|
|
@@ -256,6 +260,7 @@ export type AppAction =
|
|
|
256
260
|
| SetLogsOptionsAction
|
|
257
261
|
| PushNotificationAction
|
|
258
262
|
| DismissNotificationAction
|
|
263
|
+
| ClearNotificationHistoryAction
|
|
259
264
|
| SetRevealedSecretValueAction
|
|
260
265
|
| SetPodMetricsAction
|
|
261
266
|
| SetNodeMetricsAction
|
package/src/app/app-state.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AppAction } from "./app-actions";
|
|
2
|
-
import type { AppState, PaneId } from "../lib/k8s/types";
|
|
2
|
+
import type { AppState, Notification, PaneId } from "../lib/k8s/types";
|
|
3
|
+
import { NOTIFICATION_HISTORY_LIMIT } from "./constants";
|
|
3
4
|
|
|
4
5
|
export { type AppAction } from "./app-actions";
|
|
5
6
|
|
|
@@ -39,6 +40,7 @@ export const initialState: AppState = {
|
|
|
39
40
|
revealedSecretValues: {},
|
|
40
41
|
activePortForwards: [],
|
|
41
42
|
notifications: [],
|
|
43
|
+
notificationHistory: [],
|
|
42
44
|
podMetrics: {},
|
|
43
45
|
nodeMetrics: {},
|
|
44
46
|
helmRollbackRevision: "",
|
|
@@ -189,11 +191,18 @@ export function reducer(state: AppState, action: AppAction): AppState {
|
|
|
189
191
|
...state,
|
|
190
192
|
notifications: [...state.notifications, action.notification],
|
|
191
193
|
};
|
|
192
|
-
case "dismissNotification":
|
|
194
|
+
case "dismissNotification": {
|
|
195
|
+
const dismissed = state.notifications.find((n) => n.id === action.id);
|
|
196
|
+
if (!dismissed) return state;
|
|
197
|
+
const history = [dismissed, ...state.notificationHistory].slice(0, NOTIFICATION_HISTORY_LIMIT);
|
|
193
198
|
return {
|
|
194
199
|
...state,
|
|
195
200
|
notifications: state.notifications.filter((n) => n.id !== action.id),
|
|
201
|
+
notificationHistory: history,
|
|
196
202
|
};
|
|
203
|
+
}
|
|
204
|
+
case "clearNotificationHistory":
|
|
205
|
+
return { ...state, notificationHistory: [] };
|
|
197
206
|
case "setRevealedSecretValue":
|
|
198
207
|
return {
|
|
199
208
|
...state,
|
package/src/app/app.tsx
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { type ScrollBoxRenderable } from "@opentui/core";
|
|
2
2
|
import { useRenderer, useTerminalDimensions } from "@opentui/react";
|
|
3
|
-
import { useCallback, useMemo, useReducer, useRef, useState } from "react";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
|
|
4
4
|
|
|
5
5
|
import { DEFAULT_NAMESPACE, initialState, reducer } from "./app-state";
|
|
6
6
|
import { loadPersistedState } from "./persistence";
|
|
7
|
+
import {
|
|
8
|
+
INSPECTOR_MIN_WIDTH,
|
|
9
|
+
INSPECTOR_PANE_WIDTH,
|
|
10
|
+
KIND_PANE_WIDTH,
|
|
11
|
+
POLL_CLUSTER_MS,
|
|
12
|
+
POLL_METRICS_MS,
|
|
13
|
+
POLL_RESOURCE_MS,
|
|
14
|
+
} from "./constants";
|
|
7
15
|
import {
|
|
8
16
|
FILTER_BACKGROUND,
|
|
9
17
|
GLYPHS,
|
|
@@ -14,13 +22,12 @@ import {
|
|
|
14
22
|
PANEL_BORDER_ACTIVE,
|
|
15
23
|
RESOURCE_SURFACE,
|
|
16
24
|
SURFACE,
|
|
17
|
-
|
|
25
|
+
ATTR_DIM,
|
|
18
26
|
TEXT_PRIMARY,
|
|
19
27
|
TEXT_SUBTLE,
|
|
20
28
|
paneBorder,
|
|
21
29
|
} from "./theme";
|
|
22
30
|
import {
|
|
23
|
-
currentKindLabel,
|
|
24
31
|
filterResources,
|
|
25
32
|
formatClusterName,
|
|
26
33
|
nextVisibleResourceName,
|
|
@@ -32,7 +39,7 @@ import {
|
|
|
32
39
|
} from "./utils";
|
|
33
40
|
import { usePollingTick } from "./use-polling-tick";
|
|
34
41
|
import { useFooterHints, useFooterHintRows } from "./use-footer-hints";
|
|
35
|
-
import { InspectorBody, InspectorTabs
|
|
42
|
+
import { InspectorBody, InspectorTabs } from "./components/inspector";
|
|
36
43
|
import { KindRows } from "./components/kind-rows";
|
|
37
44
|
import { ResourceRows } from "./components/resource-rows";
|
|
38
45
|
import { Header } from "./components/header";
|
|
@@ -42,17 +49,19 @@ import {
|
|
|
42
49
|
HelmRollbackOverlay,
|
|
43
50
|
LogsDialog,
|
|
44
51
|
type LogsDialogActions,
|
|
52
|
+
NotificationHistoryOverlay,
|
|
53
|
+
HelpOverlay,
|
|
45
54
|
PortForwardOverlay,
|
|
55
|
+
PortForwardListOverlay,
|
|
46
56
|
ScaleDialog,
|
|
47
57
|
SelectOverlay,
|
|
48
58
|
buildPortForwardEntries,
|
|
49
59
|
} from "./components/overlays";
|
|
50
|
-
import { PortForwardsTray } from "./components/port-forwards-tray";
|
|
51
60
|
import { NotificationTray } from "./components/notification-tray";
|
|
52
61
|
import { KubectlService } from "../lib/kubectl/kubectl-service";
|
|
53
62
|
import type { ClusterContext, NamespaceItem, OverlayId, PaneId, ResourceDetailLine } from "../lib/k8s/types";
|
|
54
|
-
import type { NotificationTone } from "../lib/k8s/types";
|
|
55
63
|
|
|
64
|
+
import { useNotifications } from "./hooks/use-notifications";
|
|
56
65
|
import { useClipboard } from "./hooks/use-clipboard";
|
|
57
66
|
import { usePortForward } from "./hooks/use-port-forward";
|
|
58
67
|
import { useLogStream } from "./hooks/use-log-stream";
|
|
@@ -73,10 +82,13 @@ const OVERLAY_TITLES: Record<OverlayId, string> = {
|
|
|
73
82
|
"namespace-switcher": "Namespace Switcher",
|
|
74
83
|
"delete-confirm": "Confirm Delete",
|
|
75
84
|
"port-forward": "Port Forward",
|
|
85
|
+
"port-forward-list": "Port Forwards",
|
|
76
86
|
logs: "Logs",
|
|
77
87
|
scale: "Scale",
|
|
78
88
|
"container-picker": "Select Container",
|
|
79
89
|
"helm-rollback": "Helm Rollback",
|
|
90
|
+
"notification-history": "Notification History",
|
|
91
|
+
help: "Keyboard Shortcuts",
|
|
80
92
|
};
|
|
81
93
|
|
|
82
94
|
export function App() {
|
|
@@ -86,8 +98,7 @@ export function App() {
|
|
|
86
98
|
const { activeContext, activeNamespace } = loadPersistedState();
|
|
87
99
|
return {
|
|
88
100
|
...initialState,
|
|
89
|
-
...(activeContext !== undefined ? { activeContext } : {}),
|
|
90
|
-
...(activeNamespace !== undefined ? { activeNamespace } : {}),
|
|
101
|
+
...(activeContext !== undefined ? { activeContext } : {}), ...(activeNamespace !== undefined ? { activeNamespace } : {}),
|
|
91
102
|
};
|
|
92
103
|
});
|
|
93
104
|
const [overlayIndex, setOverlayIndex] = useState(0);
|
|
@@ -102,11 +113,12 @@ export function App() {
|
|
|
102
113
|
|
|
103
114
|
const logsScrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
104
115
|
const logsActionsRef = useRef<LogsDialogActions | null>(null);
|
|
105
|
-
const notificationTimersRef = useRef(new Map<string, ReturnType<typeof setTimeout>>());
|
|
106
116
|
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
const
|
|
117
|
+
const { toast, toastError, dismiss: dismissNotification, executeAction: executeNotificationAction } = useNotifications(dispatch);
|
|
118
|
+
|
|
119
|
+
const clusterPollTick = usePollingTick({ enabled: Boolean(state.activeContext), intervalMs: POLL_CLUSTER_MS });
|
|
120
|
+
const viewPollTick = usePollingTick({ enabled: Boolean(state.activeContext), intervalMs: POLL_RESOURCE_MS });
|
|
121
|
+
const metricsPollTick = usePollingTick({ enabled: Boolean(state.activeContext), intervalMs: POLL_METRICS_MS });
|
|
110
122
|
|
|
111
123
|
const selectedKind = useMemo(
|
|
112
124
|
() => state.resourceKinds.find((kind) => kind.name === state.selectedKind),
|
|
@@ -191,31 +203,18 @@ export function App() {
|
|
|
191
203
|
);
|
|
192
204
|
|
|
193
205
|
const activeForwardKeys = useMemo(
|
|
194
|
-
() => new Set(state.activePortForwards.map((f) => `${f.ref.kind.toLowerCase()}:${f.ref.name}`)),
|
|
206
|
+
() => new Set(state.activePortForwards.map((f) => `${f.ref.kind.toLowerCase()}:${f.ref.namespace ?? "cluster"}:${f.ref.name}`)),
|
|
195
207
|
[state.activePortForwards],
|
|
196
208
|
);
|
|
197
209
|
|
|
198
|
-
const selectedKindLabel =
|
|
199
|
-
() => currentKindLabel({ resourceKinds: state.resourceKinds, selectedKind: state.selectedKind }),
|
|
200
|
-
[state.resourceKinds, state.selectedKind],
|
|
201
|
-
);
|
|
210
|
+
const selectedKindLabel = state.selectedKind;
|
|
202
211
|
|
|
203
|
-
const footerHints = useFooterHints(filterMode, state, activeResourceRef, activeDetail, selectedResourcePortForwards, deleteTargets, kubectl);
|
|
212
|
+
const footerHints = useFooterHints(filterMode, state.activePane, activeResourceRef, activeDetail, selectedResourcePortForwards, state.activePortForwards.length, deleteTargets, kubectl);
|
|
204
213
|
const { footerHintsRow1, footerHintsRow2 } = useFooterHintRows(footerHints, dims.width);
|
|
205
214
|
|
|
206
215
|
// ── Hooks ─────────────────────────────────────────────────────────────────
|
|
207
216
|
|
|
208
|
-
const
|
|
209
|
-
const id = `notif-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
210
|
-
dispatch({ type: "pushNotification", notification: { id, tone, message } });
|
|
211
|
-
const timer = setTimeout(() => {
|
|
212
|
-
notificationTimersRef.current.delete(id);
|
|
213
|
-
dispatch({ type: "dismissNotification", id });
|
|
214
|
-
}, 2_500);
|
|
215
|
-
notificationTimersRef.current.set(id, timer);
|
|
216
|
-
}, []);
|
|
217
|
-
|
|
218
|
-
const { copyToClipboard, toastError } = useClipboard(dispatch, toast);
|
|
217
|
+
const { copyToClipboard } = useClipboard(dispatch, toast);
|
|
219
218
|
|
|
220
219
|
const { portForwardProcessesRef, stopPortForward, startPortForward, openPortForwardOverlay: rawOpenPortForwardOverlay } = usePortForward(dispatch, kubectl, toastError);
|
|
221
220
|
|
|
@@ -235,7 +234,14 @@ export function App() {
|
|
|
235
234
|
setFilterMode,
|
|
236
235
|
);
|
|
237
236
|
|
|
238
|
-
useAppSideEffects(state, dispatch, portForwardProcessesRef,
|
|
237
|
+
useAppSideEffects(state, dispatch, portForwardProcessesRef, activeResourceRef);
|
|
238
|
+
|
|
239
|
+
// If the terminal is too narrow to show the inspector, don't let focus rest on it.
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
if (state.activePane === "inspector" && dims.width < INSPECTOR_MIN_WIDTH) {
|
|
242
|
+
dispatch({ type: "setActivePane", pane: "resources" });
|
|
243
|
+
}
|
|
244
|
+
}, [state.activePane, dims.width]);
|
|
239
245
|
|
|
240
246
|
const openPortForwardOverlay = useCallback(() => {
|
|
241
247
|
rawOpenPortForwardOverlay(activeDetail, selectedResourcePortForwards, setPortForwardLocalPort, setOverlayIndex, dispatch);
|
|
@@ -358,7 +364,7 @@ export function App() {
|
|
|
358
364
|
title={PANE_TITLES.clusters}
|
|
359
365
|
onMouseDown={() => dispatch({ type: "setActivePane", pane: "clusters" })}
|
|
360
366
|
style={{
|
|
361
|
-
width:
|
|
367
|
+
width: KIND_PANE_WIDTH,
|
|
362
368
|
border: true,
|
|
363
369
|
borderColor: paneBorder(state.activePane === "clusters"),
|
|
364
370
|
borderStyle: "rounded",
|
|
@@ -374,7 +380,7 @@ export function App() {
|
|
|
374
380
|
</text>
|
|
375
381
|
</box>
|
|
376
382
|
<box style={{ width: "100%" }}>
|
|
377
|
-
<text fg={TEXT_SUBTLE} attributes={
|
|
383
|
+
<text fg={TEXT_SUBTLE} attributes={ATTR_DIM}>
|
|
378
384
|
<span fg={KEY_HINT}>{GLYPHS.ns}</span>
|
|
379
385
|
{` ${truncate(state.activeNamespace, 26)}`}
|
|
380
386
|
</text>
|
|
@@ -385,7 +391,7 @@ export function App() {
|
|
|
385
391
|
onActivate={() => dispatch({ type: "setActivePane", pane: "clusters" })}
|
|
386
392
|
onSelect={(kind) => dispatch({ type: "setSelectedKind", kind })}
|
|
387
393
|
/>
|
|
388
|
-
<text fg={TEXT_SUBTLE} attributes={
|
|
394
|
+
<text fg={TEXT_SUBTLE} attributes={ATTR_DIM}>
|
|
389
395
|
{"["}
|
|
390
396
|
<span fg={KEY_HINT}>C</span>
|
|
391
397
|
{"] cluster ["}
|
|
@@ -427,7 +433,7 @@ export function App() {
|
|
|
427
433
|
flexDirection: "row",
|
|
428
434
|
}}
|
|
429
435
|
>
|
|
430
|
-
<text fg={TEXT_SUBTLE} attributes={
|
|
436
|
+
<text fg={TEXT_SUBTLE} attributes={ATTR_DIM}>
|
|
431
437
|
Filter:
|
|
432
438
|
</text>
|
|
433
439
|
<input
|
|
@@ -456,12 +462,12 @@ export function App() {
|
|
|
456
462
|
</box>
|
|
457
463
|
|
|
458
464
|
{/* Inspector pane — hidden when terminal is too narrow */}
|
|
459
|
-
{dims.width >=
|
|
465
|
+
{dims.width >= INSPECTOR_MIN_WIDTH && (
|
|
460
466
|
<box
|
|
461
467
|
title={PANE_TITLES.inspector}
|
|
462
468
|
onMouseDown={() => dispatch({ type: "setActivePane", pane: "inspector" })}
|
|
463
469
|
style={{
|
|
464
|
-
width:
|
|
470
|
+
width: INSPECTOR_PANE_WIDTH,
|
|
465
471
|
border: true,
|
|
466
472
|
borderColor: paneBorder(state.activePane === "inspector"),
|
|
467
473
|
borderStyle: "rounded",
|
|
@@ -496,18 +502,10 @@ export function App() {
|
|
|
496
502
|
)}
|
|
497
503
|
</box>
|
|
498
504
|
|
|
499
|
-
<PortForwardsTray forwards={state.activePortForwards} onStop={stopPortForward} />
|
|
500
|
-
|
|
501
505
|
<NotificationTray
|
|
502
506
|
notifications={state.notifications}
|
|
503
|
-
onDismiss={
|
|
504
|
-
|
|
505
|
-
if (timer) {
|
|
506
|
-
clearTimeout(timer);
|
|
507
|
-
notificationTimersRef.current.delete(id);
|
|
508
|
-
}
|
|
509
|
-
dispatch({ type: "dismissNotification", id });
|
|
510
|
-
}}
|
|
507
|
+
onDismiss={dismissNotification}
|
|
508
|
+
onAction={executeNotificationAction}
|
|
511
509
|
/>
|
|
512
510
|
|
|
513
511
|
{/* Overlays */}
|
|
@@ -552,6 +550,19 @@ export function App() {
|
|
|
552
550
|
revisionValue={state.helmRollbackRevision}
|
|
553
551
|
onChange={(value) => dispatch({ type: "setHelmRollbackRevision", value })}
|
|
554
552
|
/>
|
|
553
|
+
) : state.overlay === "port-forward-list" ? (
|
|
554
|
+
<PortForwardListOverlay
|
|
555
|
+
forwards={state.activePortForwards}
|
|
556
|
+
selectedIndex={overlayIndex}
|
|
557
|
+
onChange={setOverlayIndex}
|
|
558
|
+
/>
|
|
559
|
+
) : state.overlay === "notification-history" ? (
|
|
560
|
+
<NotificationHistoryOverlay
|
|
561
|
+
history={state.notificationHistory}
|
|
562
|
+
onClear={() => dispatch({ type: "clearNotificationHistory" })}
|
|
563
|
+
/>
|
|
564
|
+
) : state.overlay === "help" ? (
|
|
565
|
+
<HelpOverlay />
|
|
555
566
|
) : (
|
|
556
567
|
<SelectOverlay
|
|
557
568
|
title={OVERLAY_TITLES[state.overlay]}
|
|
@@ -59,8 +59,8 @@ export interface DetailSectionsInternalProps {
|
|
|
59
59
|
export function DetailSectionsInternal({ sections, revealedIds, revealedSecretValues, onToggleReveal, onCopyText }: DetailSectionsInternalProps) {
|
|
60
60
|
return (
|
|
61
61
|
<box style={{ flexDirection: "column", width: "100%" }}>
|
|
62
|
-
{sections.map((section) => (
|
|
63
|
-
<box key={section.title} style={{ flexDirection: "column", marginBottom: 1 }}>
|
|
62
|
+
{sections.map((section, sectionIndex) => (
|
|
63
|
+
<box key={`${sectionIndex}:${section.title}`} style={{ flexDirection: "column", marginBottom: 1 }}>
|
|
64
64
|
<text fg={KEY_HINT}>
|
|
65
65
|
{"─ "}
|
|
66
66
|
<span fg={TEXT_PRIMARY} attributes={TextAttributes.BOLD}>{section.title}</span>
|
|
@@ -1,16 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import { KEY_HINT, TEXT_MUTED, TEXT_SUBTLE, toneStyles } from "../theme";
|
|
1
|
+
import { KEY_HINT, ATTR_DIM, TEXT_SUBTLE, toneStyles } from "../theme";
|
|
2
|
+
import { FOOTER_PADDING } from "../constants";
|
|
4
3
|
|
|
5
4
|
export interface FooterProps {
|
|
6
5
|
hintRows: [string, string][][];
|
|
7
6
|
}
|
|
8
7
|
|
|
8
|
+
function FooterHintRow({ hints }: { hints: [string, string][] }) {
|
|
9
|
+
return (
|
|
10
|
+
<box style={{ flexDirection: "row", justifyContent: "flex-end", alignItems: "center", flexGrow: 1 }}>
|
|
11
|
+
<box style={{ flexDirection: "row", columnGap: 2 }}>
|
|
12
|
+
{hints.map(([key, label], index) => (
|
|
13
|
+
<text key={`${index}:${key}:${label}`} fg={TEXT_SUBTLE} attributes={ATTR_DIM}>
|
|
14
|
+
{"["}
|
|
15
|
+
<span fg={KEY_HINT}>{key}</span>
|
|
16
|
+
{`] ${label}`}
|
|
17
|
+
</text>
|
|
18
|
+
))}
|
|
19
|
+
</box>
|
|
20
|
+
</box>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
9
24
|
export function Footer({ hintRows }: FooterProps) {
|
|
10
|
-
const [row1, row2] = hintRows;
|
|
11
25
|
const palette = toneStyles("neutral");
|
|
12
|
-
const
|
|
13
|
-
const dimAttr = TEXT_MUTED;
|
|
26
|
+
const [row1, row2] = hintRows;
|
|
14
27
|
|
|
15
28
|
return (
|
|
16
29
|
<box
|
|
@@ -20,33 +33,13 @@ export function Footer({ hintRows }: FooterProps) {
|
|
|
20
33
|
borderColor: palette.border,
|
|
21
34
|
borderStyle: "rounded",
|
|
22
35
|
backgroundColor: palette.bg,
|
|
23
|
-
paddingLeft:
|
|
24
|
-
paddingRight:
|
|
36
|
+
paddingLeft: FOOTER_PADDING,
|
|
37
|
+
paddingRight: FOOTER_PADDING,
|
|
25
38
|
flexDirection: "column",
|
|
26
39
|
}}
|
|
27
40
|
>
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
{(row1 ?? []).map(([key, label]) => (
|
|
31
|
-
<text key={`${key}:${label}`} fg={textFg} attributes={dimAttr}>
|
|
32
|
-
{"["}
|
|
33
|
-
<span fg={KEY_HINT}>{key}</span>
|
|
34
|
-
{`] ${label}`}
|
|
35
|
-
</text>
|
|
36
|
-
))}
|
|
37
|
-
</box>
|
|
38
|
-
</box>
|
|
39
|
-
<box style={{ flexDirection: "row", justifyContent: "flex-end", alignItems: "center", flexGrow: 1 }}>
|
|
40
|
-
<box style={{ flexDirection: "row", columnGap: 2 }}>
|
|
41
|
-
{(row2 ?? []).map(([key, label]) => (
|
|
42
|
-
<text key={`${key}:${label}`} fg={textFg} attributes={dimAttr}>
|
|
43
|
-
{"["}
|
|
44
|
-
<span fg={KEY_HINT}>{key}</span>
|
|
45
|
-
{`] ${label}`}
|
|
46
|
-
</text>
|
|
47
|
-
))}
|
|
48
|
-
</box>
|
|
49
|
-
</box>
|
|
41
|
+
<FooterHintRow hints={row1 ?? []} />
|
|
42
|
+
<FooterHintRow hints={row2 ?? []} />
|
|
50
43
|
</box>
|
|
51
44
|
);
|
|
52
45
|
}
|