openk8s 1.0.1 → 1.0.3
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 +5 -2
- package/src/app/__tests__/app-state.test.ts +376 -0
- package/src/app/__tests__/components/inspector-tokens.test.ts +101 -0
- package/src/app/__tests__/utils.test.ts +358 -0
- package/src/app/app-actions.ts +262 -0
- package/src/app/app-state.ts +16 -263
- package/src/app/app.tsx +22 -170
- package/src/app/components/detail-sections.tsx +131 -0
- package/src/app/components/footer.tsx +52 -0
- package/src/app/components/header.tsx +37 -0
- package/src/app/components/inspector-tokens.ts +93 -0
- package/src/app/components/inspector.tsx +3 -239
- package/src/app/components/resource-rows.tsx +5 -1
- package/src/app/hooks/keyboard/filter-handlers.ts +40 -0
- package/src/app/hooks/keyboard/global-handlers.ts +134 -0
- package/src/app/hooks/keyboard/helm-handlers.ts +104 -0
- package/src/app/hooks/keyboard/logs-handlers.ts +80 -0
- package/src/app/hooks/keyboard/navigation-handlers.ts +103 -0
- package/src/app/hooks/keyboard/overlay-handlers.ts +138 -0
- package/src/app/hooks/keyboard/port-forward-handlers.ts +71 -0
- package/src/app/hooks/keyboard/shell-edit-handlers.ts +253 -0
- package/src/app/hooks/use-app-keyboard.ts +56 -621
- package/src/app/hooks/use-app-side-effects.ts +1 -1
- package/src/app/hooks/use-clipboard.ts +1 -1
- package/src/app/hooks/use-data-fetching.ts +2 -11
- package/src/app/hooks/use-log-stream.ts +1 -1
- package/src/app/hooks/use-port-forward.ts +1 -1
- package/src/app/use-footer-hints.ts +107 -0
- package/src/index.tsx +4 -0
- package/src/lib/k8s/__tests__/k8s-format.test.ts +42 -0
- package/src/lib/k8s/__tests__/resource-detail-builder.test.ts +215 -0
- package/src/lib/k8s/__tests__/resource-parser.test.ts +455 -0
- package/src/lib/k8s/detail-builders/event-builder.ts +21 -0
- package/src/lib/k8s/detail-builders/hpa-cronjob-builder.ts +63 -0
- package/src/lib/k8s/detail-builders/node-builder.ts +41 -0
- package/src/lib/k8s/detail-builders/overview-builder.ts +103 -0
- package/src/lib/k8s/detail-builders/pod-builder.ts +140 -0
- package/src/lib/k8s/detail-builders/rbac-builder.ts +57 -0
- package/src/lib/k8s/resource-detail-builder.ts +22 -502
- package/src/lib/kubectl/__tests__/kubectl-helpers.test.ts +343 -0
- package/src/lib/kubectl/__tests__/metrics-utils.test.ts +84 -0
- package/src/lib/kubectl/__tests__/spawn-utils.test.ts +56 -0
- package/src/lib/kubectl/kubectl-helpers.ts +246 -0
- package/src/lib/kubectl/kubectl-service.ts +77 -565
- package/src/lib/kubectl/kubectl-types.ts +248 -0
- package/src/lib/kubectl/metrics-utils.ts +33 -0
- package/src/lib/kubectl/spawn-utils.ts +27 -0
package/src/app/app-state.ts
CHANGED
|
@@ -1,17 +1,7 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
EventItem,
|
|
6
|
-
LoadStatus,
|
|
7
|
-
LogOptions,
|
|
8
|
-
Notification,
|
|
9
|
-
OverlayId,
|
|
10
|
-
PaneId,
|
|
11
|
-
ResourceDetail,
|
|
12
|
-
ResourceKind,
|
|
13
|
-
ResourceListItem,
|
|
14
|
-
} from "../lib/k8s/types";
|
|
1
|
+
import type { AppAction } from "./app-actions";
|
|
2
|
+
import type { AppState, PaneId } from "../lib/k8s/types";
|
|
3
|
+
|
|
4
|
+
export { type AppAction } from "./app-actions";
|
|
15
5
|
|
|
16
6
|
export const DEFAULT_NAMESPACE = "default";
|
|
17
7
|
|
|
@@ -54,254 +44,6 @@ export const initialState: AppState = {
|
|
|
54
44
|
helmRollbackRevision: "",
|
|
55
45
|
};
|
|
56
46
|
|
|
57
|
-
export interface SetActivePaneAction {
|
|
58
|
-
type: "setActivePane";
|
|
59
|
-
pane: PaneId;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface CyclePaneAction {
|
|
63
|
-
type: "cyclePane";
|
|
64
|
-
direction: 1 | -1;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export interface SetOverlayAction {
|
|
68
|
-
type: "setOverlay";
|
|
69
|
-
overlay?: OverlayId | undefined;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export interface SetInspectorTabAction {
|
|
73
|
-
type: "setInspectorTab";
|
|
74
|
-
tab: AppState["inspectorTab"];
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export interface SetKubectlAvailableAction {
|
|
78
|
-
type: "setKubectlAvailable";
|
|
79
|
-
available: boolean;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export interface SetStatusMessageAction {
|
|
83
|
-
type: "setStatusMessage";
|
|
84
|
-
message: string;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export interface SetErrorAction {
|
|
88
|
-
type: "setError";
|
|
89
|
-
error?: AppError | undefined;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export interface SetContextsStatusAction {
|
|
93
|
-
type: "setContextsStatus";
|
|
94
|
-
status: LoadStatus;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export interface SetContextsAction {
|
|
98
|
-
type: "setContexts";
|
|
99
|
-
contexts: AppState["contexts"];
|
|
100
|
-
activeContext?: string | undefined;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export interface SetActiveContextAction {
|
|
104
|
-
type: "setActiveContext";
|
|
105
|
-
activeContext?: string | undefined;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export interface SetNamespacesStatusAction {
|
|
109
|
-
type: "setNamespacesStatus";
|
|
110
|
-
status: LoadStatus;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export interface SetNamespacesAction {
|
|
114
|
-
type: "setNamespaces";
|
|
115
|
-
namespaces: AppState["namespaces"];
|
|
116
|
-
activeNamespace: string;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export interface SetActiveNamespaceAction {
|
|
120
|
-
type: "setActiveNamespace";
|
|
121
|
-
namespace: string;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export interface SetResourceKindsStatusAction {
|
|
125
|
-
type: "setResourceKindsStatus";
|
|
126
|
-
status: LoadStatus;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export interface SetResourceKindsAction {
|
|
130
|
-
type: "setResourceKinds";
|
|
131
|
-
resourceKinds: ResourceKind[];
|
|
132
|
-
selectedKind: string;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export interface SetSelectedKindAction {
|
|
136
|
-
type: "setSelectedKind";
|
|
137
|
-
kind: string;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export interface SetResourcesStatusAction {
|
|
141
|
-
type: "setResourcesStatus";
|
|
142
|
-
status: LoadStatus;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export interface SetResourcesAction {
|
|
146
|
-
type: "setResources";
|
|
147
|
-
resources: ResourceListItem[];
|
|
148
|
-
selectedResourceName?: string | undefined;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export interface ToggleSelectedResourceAction {
|
|
152
|
-
type: "toggleSelectedResource";
|
|
153
|
-
name: string;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export interface ClearSelectedResourcesAction {
|
|
157
|
-
type: "clearSelectedResources";
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export interface SetResourceFilterAction {
|
|
161
|
-
type: "setResourceFilter";
|
|
162
|
-
value: string;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export interface SetSelectedResourceNameAction {
|
|
166
|
-
type: "setSelectedResourceName";
|
|
167
|
-
name?: string | undefined;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export interface SetSelectedResourceDetailStatusAction {
|
|
171
|
-
type: "setSelectedResourceDetailStatus";
|
|
172
|
-
status: LoadStatus;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export interface SetSelectedResourceDetailAction {
|
|
176
|
-
type: "setSelectedResourceDetail";
|
|
177
|
-
detail?: ResourceDetail | undefined;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
export interface SetEventsStatusAction {
|
|
181
|
-
type: "setEventsStatus";
|
|
182
|
-
status: LoadStatus;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
export interface SetEventsAction {
|
|
186
|
-
type: "setEvents";
|
|
187
|
-
events: EventItem[];
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
export interface ToggleRevealDetailLineAction {
|
|
191
|
-
type: "toggleRevealDetailLine";
|
|
192
|
-
id: string;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export interface AddActivePortForwardAction {
|
|
196
|
-
type: "addActivePortForward";
|
|
197
|
-
forward: ActivePortForward;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
export interface UpdateActivePortForwardAction {
|
|
201
|
-
type: "updateActivePortForward";
|
|
202
|
-
id: string;
|
|
203
|
-
patch: Partial<ActivePortForward>;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export interface RemoveActivePortForwardAction {
|
|
207
|
-
type: "removeActivePortForward";
|
|
208
|
-
id: string;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
export interface ClearTransientViewsAction {
|
|
212
|
-
type: "clearTransientViews";
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export interface SetLogsDataAction {
|
|
216
|
-
type: "setLogsData";
|
|
217
|
-
logsTarget?: string | undefined;
|
|
218
|
-
logsText: string;
|
|
219
|
-
logsStatus: LoadStatus;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
export interface SetLogsContainerAction {
|
|
223
|
-
type: "setLogsContainer";
|
|
224
|
-
container?: string | undefined;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
export interface SetLogsOptionsAction {
|
|
228
|
-
type: "setLogsOptions";
|
|
229
|
-
options: LogOptions;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
export interface PushNotificationAction {
|
|
233
|
-
type: "pushNotification";
|
|
234
|
-
notification: Notification;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export interface DismissNotificationAction {
|
|
238
|
-
type: "dismissNotification";
|
|
239
|
-
id: string;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
export interface SetRevealedSecretValueAction {
|
|
243
|
-
type: "setRevealedSecretValue";
|
|
244
|
-
id: string;
|
|
245
|
-
value: string;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
export interface SetPodMetricsAction {
|
|
249
|
-
type: "setPodMetrics";
|
|
250
|
-
metrics: AppState["podMetrics"];
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
export interface SetNodeMetricsAction {
|
|
254
|
-
type: "setNodeMetrics";
|
|
255
|
-
metrics: AppState["nodeMetrics"];
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
export interface SetHelmRollbackRevisionAction {
|
|
259
|
-
type: "setHelmRollbackRevision";
|
|
260
|
-
value: string;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
export type AppAction =
|
|
264
|
-
| SetActivePaneAction
|
|
265
|
-
| CyclePaneAction
|
|
266
|
-
| SetOverlayAction
|
|
267
|
-
| SetInspectorTabAction
|
|
268
|
-
| SetKubectlAvailableAction
|
|
269
|
-
| SetStatusMessageAction
|
|
270
|
-
| SetErrorAction
|
|
271
|
-
| SetContextsStatusAction
|
|
272
|
-
| SetContextsAction
|
|
273
|
-
| SetActiveContextAction
|
|
274
|
-
| SetNamespacesStatusAction
|
|
275
|
-
| SetNamespacesAction
|
|
276
|
-
| SetActiveNamespaceAction
|
|
277
|
-
| SetResourceKindsStatusAction
|
|
278
|
-
| SetResourceKindsAction
|
|
279
|
-
| SetSelectedKindAction
|
|
280
|
-
| SetResourcesStatusAction
|
|
281
|
-
| SetResourcesAction
|
|
282
|
-
| ToggleSelectedResourceAction
|
|
283
|
-
| ClearSelectedResourcesAction
|
|
284
|
-
| SetResourceFilterAction
|
|
285
|
-
| SetSelectedResourceNameAction
|
|
286
|
-
| SetSelectedResourceDetailStatusAction
|
|
287
|
-
| SetSelectedResourceDetailAction
|
|
288
|
-
| SetEventsStatusAction
|
|
289
|
-
| SetEventsAction
|
|
290
|
-
| ToggleRevealDetailLineAction
|
|
291
|
-
| AddActivePortForwardAction
|
|
292
|
-
| UpdateActivePortForwardAction
|
|
293
|
-
| RemoveActivePortForwardAction
|
|
294
|
-
| ClearTransientViewsAction
|
|
295
|
-
| SetLogsDataAction
|
|
296
|
-
| SetLogsContainerAction
|
|
297
|
-
| SetLogsOptionsAction
|
|
298
|
-
| PushNotificationAction
|
|
299
|
-
| DismissNotificationAction
|
|
300
|
-
| SetRevealedSecretValueAction
|
|
301
|
-
| SetPodMetricsAction
|
|
302
|
-
| SetNodeMetricsAction
|
|
303
|
-
| SetHelmRollbackRevisionAction;
|
|
304
|
-
|
|
305
47
|
const PANE_ORDER: PaneId[] = ["clusters", "resources", "inspector"];
|
|
306
48
|
|
|
307
49
|
export function reducer(state: AppState, action: AppAction): AppState {
|
|
@@ -352,7 +94,18 @@ export function reducer(state: AppState, action: AppAction): AppState {
|
|
|
352
94
|
selectedKind: action.selectedKind,
|
|
353
95
|
};
|
|
354
96
|
case "setSelectedKind":
|
|
355
|
-
return {
|
|
97
|
+
return {
|
|
98
|
+
...state,
|
|
99
|
+
selectedKind: action.kind,
|
|
100
|
+
resourceFilter: "",
|
|
101
|
+
selectedResourceNames: [],
|
|
102
|
+
resources: [],
|
|
103
|
+
resourcesStatus: "loading",
|
|
104
|
+
selectedResourceDetail: undefined,
|
|
105
|
+
selectedResourceDetailStatus: "idle",
|
|
106
|
+
events: [],
|
|
107
|
+
eventsStatus: "idle",
|
|
108
|
+
};
|
|
356
109
|
case "setResourcesStatus":
|
|
357
110
|
return { ...state, resourcesStatus: action.status };
|
|
358
111
|
case "setResources":
|
package/src/app/app.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type ScrollBoxRenderable } from "@opentui/core";
|
|
2
2
|
import { useRenderer, useTerminalDimensions } from "@opentui/react";
|
|
3
3
|
import { useCallback, useMemo, useReducer, useRef, useState } from "react";
|
|
4
4
|
|
|
@@ -14,12 +14,10 @@ import {
|
|
|
14
14
|
PANEL_BORDER_ACTIVE,
|
|
15
15
|
RESOURCE_SURFACE,
|
|
16
16
|
SURFACE,
|
|
17
|
-
SURFACE_ACCENT,
|
|
18
17
|
TEXT_MUTED,
|
|
19
18
|
TEXT_PRIMARY,
|
|
20
19
|
TEXT_SUBTLE,
|
|
21
20
|
paneBorder,
|
|
22
|
-
toneStyles,
|
|
23
21
|
} from "./theme";
|
|
24
22
|
import {
|
|
25
23
|
currentKindLabel,
|
|
@@ -31,9 +29,12 @@ import {
|
|
|
31
29
|
statusLine,
|
|
32
30
|
} from "./utils";
|
|
33
31
|
import { usePollingTick } from "./use-polling-tick";
|
|
32
|
+
import { useFooterHints, useFooterHintRows } from "./use-footer-hints";
|
|
34
33
|
import { InspectorBody, InspectorTabs, INSPECTOR_TABS } from "./components/inspector";
|
|
35
34
|
import { KindRows } from "./components/kind-rows";
|
|
36
35
|
import { ResourceRows } from "./components/resource-rows";
|
|
36
|
+
import { Header } from "./components/header";
|
|
37
|
+
import { Footer } from "./components/footer";
|
|
37
38
|
import {
|
|
38
39
|
DeleteConfirmOverlay,
|
|
39
40
|
HelmRollbackOverlay,
|
|
@@ -47,13 +48,7 @@ import {
|
|
|
47
48
|
import { PortForwardsTray } from "./components/port-forwards-tray";
|
|
48
49
|
import { NotificationTray } from "./components/notification-tray";
|
|
49
50
|
import { KubectlService } from "../lib/kubectl/kubectl-service";
|
|
50
|
-
import type {
|
|
51
|
-
ClusterContext,
|
|
52
|
-
NamespaceItem,
|
|
53
|
-
OverlayId,
|
|
54
|
-
PaneId,
|
|
55
|
-
ResourceDetailLine,
|
|
56
|
-
} from "../lib/k8s/types";
|
|
51
|
+
import type { ClusterContext, NamespaceItem, OverlayId, PaneId, ResourceDetailLine } from "../lib/k8s/types";
|
|
57
52
|
import type { NotificationTone } from "../lib/k8s/types";
|
|
58
53
|
|
|
59
54
|
import { useClipboard } from "./hooks/use-clipboard";
|
|
@@ -198,7 +193,13 @@ export function App() {
|
|
|
198
193
|
[state.activePortForwards],
|
|
199
194
|
);
|
|
200
195
|
|
|
201
|
-
const
|
|
196
|
+
const selectedKindLabel = useMemo(
|
|
197
|
+
() => currentKindLabel({ resourceKinds: state.resourceKinds, selectedKind: state.selectedKind }),
|
|
198
|
+
[state.resourceKinds, state.selectedKind],
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const footerHints = useFooterHints(filterMode, state, activeResourceRef, activeDetail, selectedResourcePortForwards, deleteTargets, kubectl);
|
|
202
|
+
const { footerHintsRow1, footerHintsRow2 } = useFooterHintRows(footerHints, dims.width);
|
|
202
203
|
|
|
203
204
|
// ── Hooks ─────────────────────────────────────────────────────────────────
|
|
204
205
|
|
|
@@ -334,168 +335,19 @@ export function App() {
|
|
|
334
335
|
}))
|
|
335
336
|
: [];
|
|
336
337
|
|
|
337
|
-
// ── Footer hints (context-aware) ─────────────────────────────────────────
|
|
338
|
-
|
|
339
|
-
const footerHints = useMemo((): [string, string][] => {
|
|
340
|
-
// Filter mode: only show filter-specific controls
|
|
341
|
-
if (filterMode) {
|
|
342
|
-
return [
|
|
343
|
-
[GLYPHS.enter, "confirm"],
|
|
344
|
-
[GLYPHS.esc, "cancel"],
|
|
345
|
-
["^U", "clear"],
|
|
346
|
-
];
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const isHelm = activeResourceRef?.kind.toLowerCase() === "helmreleases";
|
|
350
|
-
const hasResource = Boolean(activeResourceRef);
|
|
351
|
-
const hasPortForwards =
|
|
352
|
-
(activeDetail?.portForwards?.length ?? 0) > 0 || selectedResourcePortForwards.length > 0;
|
|
353
|
-
|
|
354
|
-
// Clusters pane: kind-list navigation only
|
|
355
|
-
if (state.activePane === "clusters") {
|
|
356
|
-
return [
|
|
357
|
-
[GLYPHS.tab, "panes"],
|
|
358
|
-
["↑↓", "kinds"],
|
|
359
|
-
["R", "refresh"],
|
|
360
|
-
];
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Inspector pane: navigation back, refresh
|
|
364
|
-
if (state.activePane === "inspector") {
|
|
365
|
-
return [
|
|
366
|
-
[GLYPHS.tab, "panes"],
|
|
367
|
-
["←", "back"],
|
|
368
|
-
["R", "refresh"],
|
|
369
|
-
];
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// Resources pane — base hints always present
|
|
373
|
-
const hints: [string, string][] = [
|
|
374
|
-
[GLYPHS.tab, "panes"],
|
|
375
|
-
["↑↓", "navigate"],
|
|
376
|
-
["/", "filter"],
|
|
377
|
-
["R", "refresh"],
|
|
378
|
-
];
|
|
379
|
-
|
|
380
|
-
if (!hasResource) return hints;
|
|
381
|
-
|
|
382
|
-
if (isHelm) {
|
|
383
|
-
hints.push(
|
|
384
|
-
["E", "edit values"],
|
|
385
|
-
...(deleteTargets.length > 0 ? [["D", "delete"] as [string, string]] : []),
|
|
386
|
-
["B", "rollback"],
|
|
387
|
-
["U", "upgrade"],
|
|
388
|
-
);
|
|
389
|
-
} else {
|
|
390
|
-
hints.push(["L", "logs"], ["E", "edit"], ["X", "shell"]);
|
|
391
|
-
if (hasPortForwards) hints.push(["F", "forward"]);
|
|
392
|
-
if (deleteTargets.length > 0) hints.push(["D", "delete"]);
|
|
393
|
-
if (activeResourceRef && kubectl.canScale(activeResourceRef)) hints.push(["⇧S", "scale"]);
|
|
394
|
-
if (activeResourceRef && kubectl.canRolloutRestart(activeResourceRef)) hints.push(["⇧R", "restart"]);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
return hints;
|
|
398
|
-
}, [
|
|
399
|
-
filterMode,
|
|
400
|
-
state.activePane,
|
|
401
|
-
activeResourceRef,
|
|
402
|
-
activeDetail,
|
|
403
|
-
selectedResourcePortForwards,
|
|
404
|
-
deleteTargets,
|
|
405
|
-
]);
|
|
406
|
-
|
|
407
|
-
// Split footerHints across two rows so hints never overflow.
|
|
408
|
-
// Row 1: primary hints, Row 2: overflow hints.
|
|
409
|
-
// Each badge: key.length + label.length + 3 chars; columnGap: 2 between items.
|
|
410
|
-
const { footerHintsRow1, footerHintsRow2 } = useMemo(() => {
|
|
411
|
-
const innerWidth = dims.width - 4; // 2 border + 2 padding
|
|
412
|
-
const avail1 = innerWidth;
|
|
413
|
-
const avail2 = innerWidth;
|
|
414
|
-
const row1: [string, string][] = [];
|
|
415
|
-
const row2: [string, string][] = [];
|
|
416
|
-
let used1 = 0;
|
|
417
|
-
let used2 = 0;
|
|
418
|
-
for (const [key, label] of footerHints) {
|
|
419
|
-
const w = key.length + label.length + 3;
|
|
420
|
-
const gap1 = row1.length === 0 ? 0 : 2;
|
|
421
|
-
const gap2 = row2.length === 0 ? 0 : 2;
|
|
422
|
-
if (used1 + gap1 + w <= avail1) {
|
|
423
|
-
used1 += gap1 + w;
|
|
424
|
-
row1.push([key, label]);
|
|
425
|
-
} else if (used2 + gap2 + w <= avail2) {
|
|
426
|
-
used2 += gap2 + w;
|
|
427
|
-
row2.push([key, label]);
|
|
428
|
-
}
|
|
429
|
-
// else drop — can't fit either row
|
|
430
|
-
}
|
|
431
|
-
return { footerHintsRow1: row1, footerHintsRow2: row2 };
|
|
432
|
-
}, [footerHints, dims.width]);
|
|
433
|
-
|
|
434
338
|
// ── Render ────────────────────────────────────────────────────────────────
|
|
435
339
|
|
|
436
340
|
return (
|
|
437
341
|
<box style={{ flexDirection: "column", flexGrow: 1, backgroundColor: SURFACE }}>
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
borderStyle: "rounded",
|
|
446
|
-
backgroundColor: SURFACE_ACCENT,
|
|
447
|
-
paddingLeft: 1,
|
|
448
|
-
paddingRight: 1,
|
|
449
|
-
justifyContent: "center",
|
|
450
|
-
alignItems: "center",
|
|
451
|
-
flexDirection: "row",
|
|
452
|
-
}}
|
|
453
|
-
>
|
|
454
|
-
<text fg={TEXT_SUBTLE} attributes={TextAttributes.DIM}>
|
|
455
|
-
{state.activeContext
|
|
456
|
-
? `${state.activeContext} / ${state.activeNamespace} / ${selectedKind?.name ?? "..."}`
|
|
457
|
-
: state.statusMessage}
|
|
458
|
-
</text>
|
|
459
|
-
</box>
|
|
342
|
+
<Header
|
|
343
|
+
activeContext={state.activeContext}
|
|
344
|
+
activeNamespace={state.activeNamespace}
|
|
345
|
+
selectedKindLabel={selectedKindLabel}
|
|
346
|
+
statusMessage={state.statusMessage}
|
|
347
|
+
onActivate={() => dispatch({ type: "setActivePane", pane: "clusters" })}
|
|
348
|
+
/>
|
|
460
349
|
|
|
461
|
-
|
|
462
|
-
<box
|
|
463
|
-
style={{
|
|
464
|
-
height: 4,
|
|
465
|
-
border: true,
|
|
466
|
-
borderColor: footerPalette.border,
|
|
467
|
-
borderStyle: "rounded",
|
|
468
|
-
backgroundColor: footerPalette.bg,
|
|
469
|
-
paddingLeft: 1,
|
|
470
|
-
paddingRight: 1,
|
|
471
|
-
flexDirection: "column",
|
|
472
|
-
}}
|
|
473
|
-
>
|
|
474
|
-
{/* Row 1: first set of key hints */}
|
|
475
|
-
<box style={{ flexDirection: "row", justifyContent: "flex-end", alignItems: "center", flexGrow: 1 }}>
|
|
476
|
-
<box style={{ flexDirection: "row", columnGap: 2 }}>
|
|
477
|
-
{footerHintsRow1.map(([key, label]) => (
|
|
478
|
-
<text key={`${key}:${label}`} fg={TEXT_SUBTLE} attributes={TEXT_MUTED}>
|
|
479
|
-
{"["}
|
|
480
|
-
<span fg={KEY_HINT}>{key}</span>
|
|
481
|
-
{`] ${label}`}
|
|
482
|
-
</text>
|
|
483
|
-
))}
|
|
484
|
-
</box>
|
|
485
|
-
</box>
|
|
486
|
-
{/* Row 2: overflow hints, right-aligned */}
|
|
487
|
-
<box style={{ flexDirection: "row", justifyContent: "flex-end", alignItems: "center", flexGrow: 1 }}>
|
|
488
|
-
<box style={{ flexDirection: "row", columnGap: 2 }}>
|
|
489
|
-
{footerHintsRow2.map(([key, label]) => (
|
|
490
|
-
<text key={`${key}:${label}`} fg={TEXT_SUBTLE} attributes={TEXT_MUTED}>
|
|
491
|
-
{"["}
|
|
492
|
-
<span fg={KEY_HINT}>{key}</span>
|
|
493
|
-
{`] ${label}`}
|
|
494
|
-
</text>
|
|
495
|
-
))}
|
|
496
|
-
</box>
|
|
497
|
-
</box>
|
|
498
|
-
</box>
|
|
350
|
+
<Footer hintRows={[footerHintsRow1, footerHintsRow2]} />
|
|
499
351
|
|
|
500
352
|
{/* Main content area */}
|
|
501
353
|
<box style={{ flexGrow: 1, flexDirection: "row", columnGap: 1, padding: 1, backgroundColor: SURFACE }}>
|
|
@@ -538,7 +390,7 @@ export function App() {
|
|
|
538
390
|
|
|
539
391
|
{/* Resources pane */}
|
|
540
392
|
<box
|
|
541
|
-
title={`${PANE_TITLES.resources}: ${
|
|
393
|
+
title={`${PANE_TITLES.resources}: ${selectedKindLabel}`}
|
|
542
394
|
onMouseDown={() => dispatch({ type: "setActivePane", pane: "resources" })}
|
|
543
395
|
style={{
|
|
544
396
|
flexGrow: 1,
|
|
@@ -569,7 +421,7 @@ export function App() {
|
|
|
569
421
|
flexDirection: "row",
|
|
570
422
|
}}
|
|
571
423
|
>
|
|
572
|
-
<text fg={TEXT_SUBTLE} attributes={
|
|
424
|
+
<text fg={TEXT_SUBTLE} attributes={TEXT_MUTED}>
|
|
573
425
|
Filter:
|
|
574
426
|
</text>
|
|
575
427
|
<input
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { TextAttributes } from "@opentui/core";
|
|
2
|
+
|
|
3
|
+
import { GLYPHS, KEY_HINT, TEXT_PRIMARY, TEXT_SUBTLE, toneStyles } from "../theme";
|
|
4
|
+
import type { EventItem, ResourceDetailLine, ResourceDetailSection } from "../../lib/k8s/types";
|
|
5
|
+
|
|
6
|
+
export interface EventListProps {
|
|
7
|
+
events: EventItem[];
|
|
8
|
+
status: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function EventList({ events, status }: EventListProps) {
|
|
12
|
+
if (status === "loading") {
|
|
13
|
+
return (
|
|
14
|
+
<text fg={TEXT_SUBTLE} attributes={TextAttributes.DIM}>
|
|
15
|
+
Loading events...
|
|
16
|
+
</text>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (events.length === 0) {
|
|
21
|
+
return (
|
|
22
|
+
<text fg={TEXT_SUBTLE} attributes={TextAttributes.DIM}>
|
|
23
|
+
No related events
|
|
24
|
+
</text>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<box style={{ flexDirection: "column", width: "100%" }}>
|
|
30
|
+
{events.map((event, index) => {
|
|
31
|
+
const isWarning = event.type.toLowerCase() === "warning";
|
|
32
|
+
const palette = toneStyles(isWarning ? "warning" : "info");
|
|
33
|
+
const typeGlyph = isWarning ? GLYPHS.warn : GLYPHS.dot;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<box key={`${event.reason}:${event.age}:${index}`} style={{ flexDirection: "column", marginBottom: 1 }}>
|
|
37
|
+
<text fg={palette.fg}>
|
|
38
|
+
<span fg={palette.border}>{typeGlyph}</span>
|
|
39
|
+
{` ${event.type} `}
|
|
40
|
+
<span fg={TEXT_SUBTLE}>{GLYPHS.sep}</span>
|
|
41
|
+
{` ${event.reason} ${event.age}`}
|
|
42
|
+
</text>
|
|
43
|
+
<text fg={TEXT_SUBTLE} attributes={TextAttributes.DIM}>{` ${event.source} ${event.message}`}</text>
|
|
44
|
+
</box>
|
|
45
|
+
);
|
|
46
|
+
})}
|
|
47
|
+
</box>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface DetailSectionsInternalProps {
|
|
52
|
+
sections: ResourceDetailSection[];
|
|
53
|
+
revealedIds: string[];
|
|
54
|
+
revealedSecretValues: Record<string, string>;
|
|
55
|
+
onToggleReveal: (id: string) => void;
|
|
56
|
+
onCopyText: (text: string, label?: string) => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function DetailSectionsInternal({ sections, revealedIds, revealedSecretValues, onToggleReveal, onCopyText }: DetailSectionsInternalProps) {
|
|
60
|
+
return (
|
|
61
|
+
<box style={{ flexDirection: "column", width: "100%" }}>
|
|
62
|
+
{sections.map((section) => (
|
|
63
|
+
<box key={section.title} style={{ flexDirection: "column", marginBottom: 1 }}>
|
|
64
|
+
<text fg={KEY_HINT}>
|
|
65
|
+
{"─ "}
|
|
66
|
+
<span fg={TEXT_PRIMARY} attributes={TextAttributes.BOLD}>{section.title}</span>
|
|
67
|
+
</text>
|
|
68
|
+
{section.lines.map((line, index) => {
|
|
69
|
+
const structuredLine: ResourceDetailLine | undefined =
|
|
70
|
+
typeof line === "string" ? undefined : line;
|
|
71
|
+
const lineId = structuredLine?.id ?? line as string;
|
|
72
|
+
const isRevealable = !!(structuredLine?.revealable && structuredLine.revealedText);
|
|
73
|
+
const isRevealed = isRevealable && revealedIds.includes(structuredLine!.id);
|
|
74
|
+
|
|
75
|
+
const fetchedValue = structuredLine ? revealedSecretValues[structuredLine.id] : undefined;
|
|
76
|
+
|
|
77
|
+
const rawText = structuredLine?.text ?? (line as string);
|
|
78
|
+
const displayText = isRevealable && isRevealed
|
|
79
|
+
? (fetchedValue ?? structuredLine!.revealedText ?? rawText)
|
|
80
|
+
: rawText;
|
|
81
|
+
const copyValue = fetchedValue ?? structuredLine?.revealedText ?? rawText;
|
|
82
|
+
const hasCopyButton = structuredLine !== undefined;
|
|
83
|
+
|
|
84
|
+
const copyLabel: string | undefined = structuredLine
|
|
85
|
+
? (() => {
|
|
86
|
+
const eqIdx = rawText.indexOf("=");
|
|
87
|
+
if (eqIdx > 0) return rawText.slice(0, eqIdx);
|
|
88
|
+
const spaceIdx = rawText.indexOf(" ");
|
|
89
|
+
return spaceIdx > 0 ? rawText.slice(0, spaceIdx) : rawText;
|
|
90
|
+
})()
|
|
91
|
+
: undefined;
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<box
|
|
95
|
+
key={`${section.title}:${index}:${lineId}`}
|
|
96
|
+
style={{ flexDirection: "row", width: "100%" }}
|
|
97
|
+
>
|
|
98
|
+
<box
|
|
99
|
+
onMouseDown={isRevealable ? () => onToggleReveal(structuredLine!.id) : undefined}
|
|
100
|
+
style={{ flexGrow: 1 }}
|
|
101
|
+
>
|
|
102
|
+
<text fg={TEXT_SUBTLE} attributes={TextAttributes.DIM}>
|
|
103
|
+
{" "}
|
|
104
|
+
{displayText}
|
|
105
|
+
</text>
|
|
106
|
+
</box>
|
|
107
|
+
{isRevealable && (
|
|
108
|
+
<box
|
|
109
|
+
onMouseDown={() => onToggleReveal(structuredLine!.id)}
|
|
110
|
+
style={{ paddingLeft: 1 }}
|
|
111
|
+
>
|
|
112
|
+
<text fg={KEY_HINT} attributes={TextAttributes.DIM}>
|
|
113
|
+
{isRevealed ? "[hide]" : "[reveal]"}
|
|
114
|
+
</text>
|
|
115
|
+
</box>
|
|
116
|
+
)}
|
|
117
|
+
{hasCopyButton && (
|
|
118
|
+
<box onMouseDown={() => onCopyText(copyValue, copyLabel)} style={{ paddingLeft: 1 }}>
|
|
119
|
+
<text fg={KEY_HINT} attributes={TextAttributes.DIM}>
|
|
120
|
+
{"[copy]"}
|
|
121
|
+
</text>
|
|
122
|
+
</box>
|
|
123
|
+
)}
|
|
124
|
+
</box>
|
|
125
|
+
);
|
|
126
|
+
})}
|
|
127
|
+
</box>
|
|
128
|
+
))}
|
|
129
|
+
</box>
|
|
130
|
+
);
|
|
131
|
+
}
|