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.
Files changed (72) hide show
  1. package/README.md +1 -1
  2. package/package.json +13 -10
  3. package/src/app/__tests__/app-state.test.ts +48 -6
  4. package/src/app/__tests__/utils.test.ts +6 -15
  5. package/src/app/app-actions.ts +5 -0
  6. package/src/app/app-state.ts +11 -2
  7. package/src/app/app.tsx +57 -46
  8. package/src/app/components/detail-sections.tsx +2 -2
  9. package/src/app/components/footer.tsx +23 -30
  10. package/src/app/components/inspector.tsx +40 -83
  11. package/src/app/components/kind-rows.tsx +1 -1
  12. package/src/app/components/notification-card.tsx +145 -0
  13. package/src/app/components/notification-tray.tsx +19 -38
  14. package/src/app/components/overlays/delete-confirm-overlay.tsx +15 -17
  15. package/src/app/components/overlays/helm-rollback-overlay.tsx +12 -66
  16. package/src/app/components/overlays/help-overlay.tsx +171 -0
  17. package/src/app/components/overlays/index.ts +4 -1
  18. package/src/app/components/overlays/logs-dialog.tsx +47 -67
  19. package/src/app/components/overlays/notification-history-overlay.tsx +79 -0
  20. package/src/app/components/overlays/port-forward-list-overlay.tsx +85 -0
  21. package/src/app/components/overlays/port-forward-overlay.tsx +16 -56
  22. package/src/app/components/overlays/scale-dialog.tsx +12 -67
  23. package/src/app/components/overlays/select-overlay.tsx +5 -14
  24. package/src/app/components/overlays/shared.tsx +85 -6
  25. package/src/app/components/resource-rows.tsx +1 -1
  26. package/src/app/constants.ts +24 -0
  27. package/src/app/hooks/keyboard/filter-handlers.ts +2 -1
  28. package/src/app/hooks/keyboard/global-handlers.ts +30 -21
  29. package/src/app/hooks/keyboard/helm-handlers.ts +14 -9
  30. package/src/app/hooks/keyboard/keys.ts +18 -0
  31. package/src/app/hooks/keyboard/logs-handlers.ts +3 -1
  32. package/src/app/hooks/keyboard/navigation-handlers.ts +5 -4
  33. package/src/app/hooks/keyboard/overlay-handlers.ts +11 -7
  34. package/src/app/hooks/keyboard/port-forward-handlers.ts +19 -14
  35. package/src/app/hooks/keyboard/shell-edit-handlers.ts +32 -16
  36. package/src/app/hooks/use-app-keyboard.ts +22 -2
  37. package/src/app/hooks/use-app-side-effects.ts +10 -7
  38. package/src/app/hooks/use-clipboard.ts +8 -10
  39. package/src/app/hooks/use-data-fetching.ts +10 -6
  40. package/src/app/hooks/use-log-stream.ts +8 -4
  41. package/src/app/hooks/use-notifications.ts +92 -0
  42. package/src/app/hooks/use-port-forward.ts +19 -4
  43. package/src/app/persistence.ts +7 -3
  44. package/src/app/syntax-theme.ts +31 -0
  45. package/src/app/theme.ts +2 -3
  46. package/src/app/use-footer-hints.ts +21 -16
  47. package/src/app/utils.ts +1 -9
  48. package/src/assets/tree-sitter/yaml/highlights.scm +79 -0
  49. package/src/assets/tree-sitter/yaml/tree-sitter-yaml.wasm +0 -0
  50. package/src/index.tsx +22 -2
  51. package/src/lib/k8s/__tests__/k8s-format.test.ts +17 -0
  52. package/src/lib/k8s/__tests__/resource-detail-builder.test.ts +4 -6
  53. package/src/lib/k8s/__tests__/resource-parser.test.ts +40 -2
  54. package/src/lib/k8s/detail-builders/event-builder.ts +18 -12
  55. package/src/lib/k8s/detail-builders/hpa-cronjob-builder.ts +34 -31
  56. package/src/lib/k8s/detail-builders/node-builder.ts +22 -11
  57. package/src/lib/k8s/detail-builders/overview-builder.ts +4 -17
  58. package/src/lib/k8s/detail-builders/pod-builder.ts +52 -24
  59. package/src/lib/k8s/detail-builders/rbac-builder.ts +20 -29
  60. package/src/lib/k8s/k8s-format.ts +14 -9
  61. package/src/lib/k8s/resource-detail-builder.ts +4 -7
  62. package/src/lib/k8s/resource-parser.ts +17 -2
  63. package/src/lib/k8s/types.ts +12 -4
  64. package/src/lib/kubectl/__tests__/metrics-utils.test.ts +9 -0
  65. package/src/lib/kubectl/kubectl-helpers.ts +16 -11
  66. package/src/lib/kubectl/kubectl-service.ts +38 -54
  67. package/src/lib/kubectl/kubectl-types.ts +10 -1
  68. package/src/lib/kubectl/metrics-utils.ts +21 -7
  69. package/src/lib/kubectl/spawn-utils.ts +50 -11
  70. package/src/app/__tests__/components/inspector-tokens.test.ts +0 -101
  71. package/src/app/components/inspector-tokens.ts +0 -93
  72. package/src/app/components/port-forwards-tray.tsx +0 -57
@@ -9,15 +9,13 @@ import {
9
9
  TEXT_PRIMARY,
10
10
  TEXT_SUBTLE,
11
11
  } from "../theme";
12
- import { tokenizeYamlLine, tokenizeDescribeLine } from "./inspector-tokens";
12
+ import { yamlSyntaxStyle } from "../syntax-theme";
13
13
  import { EventList, DetailSectionsInternal } from "./detail-sections";
14
14
  import type { EventItem, InspectorTab, PaneId, ResourceDetail } from "../../lib/k8s/types";
15
15
 
16
16
  export const INSPECTOR_TABS: Array<{ id: InspectorTab; label: string; hotkey: string }> = [
17
17
  { id: "summary", label: "Summary", hotkey: "s" },
18
18
  { id: "yaml", label: "YAML", hotkey: "y" },
19
- { id: "events", label: "Events", hotkey: "v" },
20
- { id: "describe", label: "Describe", hotkey: "i" },
21
19
  ];
22
20
 
23
21
  export interface InspectorTabsProps {
@@ -101,81 +99,33 @@ export function InspectorBody({
101
99
  return (
102
100
  <box style={{ flexGrow: 1, flexDirection: "column" }}>
103
101
  <box style={{ height: 1 }} />
104
- <scrollbox
102
+ <box
105
103
  focused={active}
106
- scrollX
107
- horizontalScrollbarOptions={{ showArrows: false }}
108
104
  onMouseDown={() => {
109
105
  onActivate();
110
106
  if (detail?.yaml) onCopyText(detail.yaml, "YAML");
111
107
  }}
112
108
  flexGrow={1}
113
109
  >
114
- <box style={{ flexDirection: "column", paddingRight: 1 }}>
115
- {detail ? (
116
- detail.yaml.split("\n").map((line, index) => {
117
- const tokens = tokenizeYamlLine(line);
118
- return (
119
- <box key={`${index}:${line}`} style={{ flexDirection: "row" }}>
120
- {tokens.map((token, tokenIndex) => (
121
- <text key={tokenIndex} fg={token.fg} wrapMode="none">
122
- {token.text}
123
- </text>
124
- ))}
125
- </box>
126
- );
127
- })
128
- ) : detailStatus === "loading" ? (
129
- <text fg={TEXT_SUBTLE} attributes={TextAttributes.DIM}>
130
- Loading YAML...
131
- </text>
132
- ) : (
133
- <text fg={TEXT_SUBTLE} attributes={TextAttributes.DIM}>
134
- No YAML available
135
- </text>
136
- )}
137
- </box>
138
- </scrollbox>
139
- </box>
140
- );
141
- }
142
-
143
- if (activeTab === "describe") {
144
- return (
145
- <box style={{ flexGrow: 1, flexDirection: "column" }}>
146
- <box style={{ height: 1 }} />
147
- <scrollbox
148
- focused={active}
149
- scrollX
150
- horizontalScrollbarOptions={{ showArrows: false }}
151
- onMouseDown={onActivate}
152
- flexGrow={1}
153
- >
154
- <box style={{ flexDirection: "column", paddingRight: 1 }}>
155
- {detail?.describe ? (
156
- detail.describe.split("\n").map((line, index) => {
157
- const tokens = tokenizeDescribeLine(line);
158
- return (
159
- <box key={`${index}:${line}`} style={{ flexDirection: "row" }}>
160
- {tokens.map((token, tokenIndex) => (
161
- <text key={tokenIndex} fg={token.fg} wrapMode="none">
162
- {token.text}
163
- </text>
164
- ))}
165
- </box>
166
- );
167
- })
168
- ) : detailStatus === "loading" ? (
169
- <text fg={TEXT_SUBTLE} attributes={TextAttributes.DIM}>
170
- Loading describe output...
171
- </text>
172
- ) : (
173
- <text fg={TEXT_SUBTLE} attributes={TextAttributes.DIM}>
174
- No describe output available
175
- </text>
176
- )}
177
- </box>
178
- </scrollbox>
110
+ {detail ? (
111
+ <code
112
+ content={detail.yaml}
113
+ filetype="yaml"
114
+ syntaxStyle={yamlSyntaxStyle}
115
+ drawUnstyledText
116
+ wrapMode="none"
117
+ style={{ width: "100%", height: "100%" }}
118
+ />
119
+ ) : detailStatus === "loading" ? (
120
+ <text fg={TEXT_SUBTLE} attributes={TextAttributes.DIM}>
121
+ Loading YAML...
122
+ </text>
123
+ ) : (
124
+ <text fg={TEXT_SUBTLE} attributes={TextAttributes.DIM}>
125
+ No YAML available
126
+ </text>
127
+ )}
128
+ </box>
179
129
  </box>
180
130
  );
181
131
  }
@@ -185,23 +135,30 @@ export function InspectorBody({
185
135
  <box style={{ height: 1 }} />
186
136
  <scrollbox focused={active} onMouseDown={onActivate} flexGrow={1}>
187
137
  <box style={{ flexDirection: "column", width: "100%", paddingRight: 1 }}>
188
- {activeTab === "events" ? (
189
- <EventList events={events} status={eventsStatus} />
190
- ) : detail ? (
191
- <DetailSectionsInternal
192
- sections={detail.summarySections}
193
- revealedIds={revealedDetailLineIds}
194
- revealedSecretValues={revealedSecretValues}
195
- onToggleReveal={onToggleReveal}
196
- onCopyText={onCopyText}
197
- />
138
+ {detail ? (
139
+ <>
140
+ <DetailSectionsInternal
141
+ sections={detail.summarySections}
142
+ revealedIds={revealedDetailLineIds}
143
+ revealedSecretValues={revealedSecretValues}
144
+ onToggleReveal={onToggleReveal}
145
+ onCopyText={onCopyText}
146
+ />
147
+ <box style={{ flexDirection: "column", marginTop: 1, marginBottom: 1 }}>
148
+ <text fg={KEY_HINT}>
149
+ {"─ "}
150
+ <span fg={TEXT_PRIMARY} attributes={TextAttributes.BOLD}>Recent Events</span>
151
+ </text>
152
+ <EventList events={events} status={eventsStatus} />
153
+ </box>
154
+ </>
198
155
  ) : detailStatus === "loading" ? (
199
156
  <text fg={TEXT_SUBTLE} attributes={TextAttributes.DIM}>
200
157
  Loading resource detail...
201
158
  </text>
202
159
  ) : (
203
- fallbackLines.map((line) => (
204
- <text key={line} fg={TEXT_SUBTLE} attributes={TextAttributes.DIM}>
160
+ fallbackLines.map((line, index) => (
161
+ <text key={`${index}:${line}`} fg={TEXT_SUBTLE} attributes={TextAttributes.DIM}>
205
162
  {line}
206
163
  </text>
207
164
  ))
@@ -17,7 +17,7 @@ export function KindRows({ resourceKinds, selectedKind, onSelect, onActivate }:
17
17
 
18
18
  useEffect(() => {
19
19
  scrollRef.current?.scrollChildIntoView(`kind:${selectedKind}`);
20
- }, [selectedKind]);
20
+ }, [selectedKind, resourceKinds]);
21
21
 
22
22
  if (resourceKinds.length === 0) {
23
23
  return (
@@ -0,0 +1,145 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { TextAttributes } from "@opentui/core";
3
+
4
+ import { GLYPHS, KEY_HINT, OVERLAY_SURFACE, TEXT_PRIMARY, TEXT_SUBTLE, toneStyles } from "../theme";
5
+ import {
6
+ NOTIFICATION_PROGRESS_INTERVAL_MS,
7
+ NOTIFICATION_SLIDE_FRAMES,
8
+ NOTIFICATION_SLIDE_INTERVAL_MS,
9
+ TOAST_DURATION_MS,
10
+ } from "../constants";
11
+ import type { Notification } from "../../lib/k8s/types";
12
+
13
+ export interface NotificationCardProps {
14
+ notification: Notification;
15
+ onDismiss: (id: string) => void;
16
+ onAction: (notificationId: string, actionId: string) => void;
17
+ }
18
+
19
+ const TONE_ICONS: Record<Notification["tone"], string> = {
20
+ info: GLYPHS.dotEmpty,
21
+ success: GLYPHS.dot,
22
+ warning: GLYPHS.warn,
23
+ danger: GLYPHS.cross,
24
+ };
25
+
26
+ const SLIDE_DISTANCE = 15;
27
+
28
+ function easeOut(t: number): number {
29
+ return 1 - Math.pow(1 - t, 2);
30
+ }
31
+
32
+ export function NotificationCard({ notification, onDismiss, onAction }: NotificationCardProps) {
33
+ const palette = toneStyles(notification.tone);
34
+ const icon = TONE_ICONS[notification.tone];
35
+
36
+ const [phase, setPhase] = useState<"entering" | "visible" | "exiting">("entering");
37
+ const [slideOffset, setSlideOffset] = useState(SLIDE_DISTANCE);
38
+ const [progress, setProgress] = useState(1);
39
+
40
+ const slideFrameRef = useRef(0);
41
+ const progressRef = useRef<ReturnType<typeof setInterval> | undefined>(undefined);
42
+
43
+ // Slide-in animation
44
+ useEffect(() => {
45
+ const interval = setInterval(() => {
46
+ slideFrameRef.current += 1;
47
+ const frame = slideFrameRef.current;
48
+ if (frame >= NOTIFICATION_SLIDE_FRAMES) {
49
+ setSlideOffset(0);
50
+ setPhase("visible");
51
+ clearInterval(interval);
52
+ return;
53
+ }
54
+ const t = frame / NOTIFICATION_SLIDE_FRAMES;
55
+ setSlideOffset(Math.round(SLIDE_DISTANCE * (1 - easeOut(t))));
56
+ }, NOTIFICATION_SLIDE_INTERVAL_MS);
57
+ return () => clearInterval(interval);
58
+ }, []);
59
+
60
+ // Progress bar (non-persistent only)
61
+ useEffect(() => {
62
+ if (notification.persistent || phase !== "visible") return;
63
+
64
+ const startTime = Date.now();
65
+ progressRef.current = setInterval(() => {
66
+ const elapsed = Date.now() - startTime;
67
+ const remaining = Math.max(0, 1 - elapsed / TOAST_DURATION_MS);
68
+ setProgress(remaining);
69
+ if (remaining <= 0) {
70
+ if (progressRef.current) clearInterval(progressRef.current);
71
+ }
72
+ }, NOTIFICATION_PROGRESS_INTERVAL_MS);
73
+
74
+ return () => {
75
+ if (progressRef.current) clearInterval(progressRef.current);
76
+ };
77
+ }, [notification.persistent, phase]);
78
+
79
+ const handleDismiss = (): void => {
80
+ if (phase === "exiting") return;
81
+ if (progressRef.current) clearInterval(progressRef.current);
82
+ setPhase("exiting");
83
+ slideFrameRef.current = 0;
84
+ const interval = setInterval(() => {
85
+ slideFrameRef.current += 1;
86
+ const frame = slideFrameRef.current;
87
+ if (frame >= NOTIFICATION_SLIDE_FRAMES) {
88
+ clearInterval(interval);
89
+ onDismiss(notification.id);
90
+ return;
91
+ }
92
+ const t = frame / NOTIFICATION_SLIDE_FRAMES;
93
+ setSlideOffset(Math.round(SLIDE_DISTANCE * easeOut(t)));
94
+ }, NOTIFICATION_SLIDE_INTERVAL_MS);
95
+ };
96
+
97
+ const barWidth = Math.round(progress * 20);
98
+
99
+ return (
100
+ <box
101
+ style={{
102
+ border: true,
103
+ borderColor: palette.border,
104
+ borderStyle: "rounded",
105
+ backgroundColor: OVERLAY_SURFACE,
106
+ paddingLeft: 1,
107
+ paddingRight: 1,
108
+ flexDirection: "column",
109
+ marginLeft: slideOffset,
110
+ }}
111
+ onMouseDown={handleDismiss}
112
+ >
113
+ <box style={{ flexDirection: "row", alignItems: "center" }}>
114
+ <text fg={palette.fg}>
115
+ {icon}
116
+ </text>
117
+ <text fg={TEXT_PRIMARY} wrapMode="word" style={{ flexGrow: 1, marginLeft: 1 }}>
118
+ {notification.message}
119
+ </text>
120
+ </box>
121
+ {notification.actions.length > 0 ? (
122
+ <box style={{ flexDirection: "row", columnGap: 2, marginTop: 0 }}>
123
+ {notification.actions.map((action) => (
124
+ <text
125
+ key={action.id}
126
+ fg={KEY_HINT}
127
+ attributes={TextAttributes.BOLD}
128
+ onMouseDown={(e: unknown) => {
129
+ (e as { stopPropagation?: () => void })?.stopPropagation?.();
130
+ onAction(notification.id, action.id);
131
+ }}
132
+ >
133
+ {`[${action.label}]`}
134
+ </text>
135
+ ))}
136
+ {!notification.persistent && (
137
+ <text fg={TEXT_SUBTLE} attributes={TextAttributes.DIM}>
138
+ {`${"█".repeat(barWidth)}${"░".repeat(20 - barWidth)}`}
139
+ </text>
140
+ )}
141
+ </box>
142
+ ) : null}
143
+ </box>
144
+ );
145
+ }
@@ -1,59 +1,40 @@
1
- import { GLYPHS, OVERLAY_SURFACE, TEXT_SUBTLE, toneStyles } from "../theme";
1
+ import { MAX_VISIBLE_NOTIFICATIONS, NOTIFICATION_WIDTH_PCT } from "../constants";
2
2
  import type { Notification } from "../../lib/k8s/types";
3
+ import { NotificationCard } from "./notification-card";
3
4
 
4
5
  export interface NotificationTrayProps {
5
6
  notifications: Notification[];
6
7
  onDismiss: (id: string) => void;
8
+ onAction: (notificationId: string, actionId: string) => void;
7
9
  }
8
10
 
9
- export function NotificationTray({ notifications, onDismiss }: NotificationTrayProps) {
11
+ export function NotificationTray({ notifications, onDismiss, onAction }: NotificationTrayProps) {
10
12
  if (notifications.length === 0) {
11
- return undefined;
13
+ return null;
12
14
  }
13
15
 
14
- // Show at most 3, most recent last (rendered top-to-bottom)
15
- const visible = notifications.slice(-3);
16
+ // Show at most MAX_VISIBLE_NOTIFICATIONS, most recent last (rendered bottom-to-top)
17
+ const visible = notifications.slice(-MAX_VISIBLE_NOTIFICATIONS);
16
18
 
17
19
  return (
18
20
  <box
19
21
  style={{
20
22
  position: "absolute",
21
- left: "55%",
22
- top: 3,
23
- width: "44%",
23
+ bottom: 5,
24
+ right: 1,
25
+ width: NOTIFICATION_WIDTH_PCT,
24
26
  flexDirection: "column",
25
- rowGap: 1,
27
+ rowGap: 0,
26
28
  }}
27
29
  >
28
- {visible.map((notification) => {
29
- const palette = toneStyles(notification.tone);
30
-
31
- return (
32
- <box
33
- key={notification.id}
34
- style={{
35
- border: true,
36
- borderColor: palette.border,
37
- borderStyle: "rounded",
38
- backgroundColor: OVERLAY_SURFACE,
39
- paddingLeft: 1,
40
- paddingRight: 1,
41
- paddingTop: 0,
42
- paddingBottom: 0,
43
- flexDirection: "row",
44
- justifyContent: "space-between",
45
- alignItems: "center",
46
- }}
47
- >
48
- <text fg={palette.fg} wrapMode="word" style={{ flexShrink: 1, flexGrow: 1 }}>
49
- {notification.message}
50
- </text>
51
- <text fg={TEXT_SUBTLE} onMouseDown={() => onDismiss(notification.id)}>
52
- {` ${GLYPHS.stop}`}
53
- </text>
54
- </box>
55
- );
56
- })}
30
+ {visible.map((notification) => (
31
+ <NotificationCard
32
+ key={notification.id}
33
+ notification={notification}
34
+ onDismiss={onDismiss}
35
+ onAction={onAction}
36
+ />
37
+ ))}
57
38
  </box>
58
39
  );
59
40
  }
@@ -3,13 +3,12 @@ import { TextAttributes } from "@opentui/core";
3
3
  import {
4
4
  DANGER,
5
5
  GLYPHS,
6
- KEY_HINT,
7
6
  OVERLAY_SURFACE,
8
- TEXT_MUTED,
7
+ ATTR_DIM,
9
8
  TEXT_SUBTLE,
10
9
  toneStyles,
11
10
  } from "../../theme";
12
- import { DIALOG_LEFT, DIALOG_TOP, DIALOG_WIDTH } from "./shared";
11
+ import { DIALOG_LEFT, DIALOG_TOP, DIALOG_WIDTH, KeyHintRow } from "./shared";
13
12
  import { statusLine } from "../../utils";
14
13
  import type { ResourceRef } from "../../../lib/k8s/types";
15
14
 
@@ -20,7 +19,11 @@ export interface DeleteConfirmOverlayProps {
20
19
  export function DeleteConfirmOverlay({ targets }: DeleteConfirmOverlayProps) {
21
20
  const palette = toneStyles("danger");
22
21
  const title =
23
- targets.length <= 1 ? `Delete ${statusLine({ ref: targets[0] })}?` : `Delete ${targets.length} selected resources?`;
22
+ targets.length === 0
23
+ ? "No selection"
24
+ : targets.length === 1
25
+ ? `Delete ${statusLine({ ref: targets[0] })}?`
26
+ : `Delete ${targets.length} selected resources?`;
24
27
  const overlayHeight = Math.min(22, Math.max(9, targets.length + 8));
25
28
 
26
29
  return (
@@ -43,8 +46,12 @@ export function DeleteConfirmOverlay({ targets }: DeleteConfirmOverlayProps) {
43
46
  <text fg={palette.fg} attributes={TextAttributes.BOLD}>
44
47
  {title}
45
48
  </text>
46
- {targets.length <= 1 ? (
47
- <text fg={TEXT_SUBTLE} attributes={TEXT_MUTED}>
49
+ {targets.length === 0 ? (
50
+ <text fg={TEXT_SUBTLE} attributes={ATTR_DIM}>
51
+ Nothing selected for deletion.
52
+ </text>
53
+ ) : targets.length === 1 ? (
54
+ <text fg={TEXT_SUBTLE} attributes={ATTR_DIM}>
48
55
  Runs kubectl delete on the current selection.
49
56
  </text>
50
57
  ) : (
@@ -54,7 +61,7 @@ export function DeleteConfirmOverlay({ targets }: DeleteConfirmOverlayProps) {
54
61
  <text
55
62
  key={`${target.kind}:${target.namespace ?? "cluster"}:${target.name}`}
56
63
  fg={TEXT_SUBTLE}
57
- attributes={TEXT_MUTED}
64
+ attributes={ATTR_DIM}
58
65
  >
59
66
  <span fg={DANGER}>{GLYPHS.cross}</span>
60
67
  {" "}
@@ -64,16 +71,7 @@ export function DeleteConfirmOverlay({ targets }: DeleteConfirmOverlayProps) {
64
71
  </box>
65
72
  </scrollbox>
66
73
  )}
67
- <box style={{ flexDirection: "row", columnGap: 2, marginTop: 1, justifyContent: "flex-end" }}>
68
- <text fg={TEXT_SUBTLE} attributes={TEXT_MUTED}>
69
- <span fg={DANGER}>{GLYPHS.enter}</span>
70
- {" confirm"}
71
- </text>
72
- <text fg={TEXT_SUBTLE} attributes={TEXT_MUTED}>
73
- <span fg={KEY_HINT}>{GLYPHS.esc}</span>
74
- {" cancel"}
75
- </text>
76
- </box>
74
+ <KeyHintRow hints={[[GLYPHS.enter, "confirm"], [GLYPHS.esc, "cancel"]]} />
77
75
  </box>
78
76
  );
79
77
  }
@@ -1,16 +1,7 @@
1
1
  import { TextAttributes } from "@opentui/core";
2
2
 
3
- import {
4
- FILTER_BACKGROUND,
5
- GLYPHS,
6
- KEY_HINT,
7
- OVERLAY_SURFACE,
8
- PANEL_BORDER_ACTIVE,
9
- TEXT_MUTED,
10
- TEXT_SUBTLE,
11
- toneStyles,
12
- } from "../../theme";
13
- import { DIALOG_LEFT, DIALOG_TOP, DIALOG_WIDTH } from "./shared";
3
+ import { GLYPHS, ATTR_DIM, TEXT_SUBTLE, toneStyles } from "../../theme";
4
+ import { DialogFrame, KeyHintRow, TextInput } from "./shared";
14
5
 
15
6
  export interface HelmRollbackOverlayProps {
16
7
  releaseName: string;
@@ -22,65 +13,20 @@ export function HelmRollbackOverlay({ releaseName, revisionValue, onChange }: He
22
13
  const palette = toneStyles("warning");
23
14
 
24
15
  return (
25
- <box
26
- style={{
27
- position: "absolute",
28
- left: DIALOG_LEFT,
29
- top: DIALOG_TOP,
30
- width: DIALOG_WIDTH,
31
- height: 12,
32
- border: true,
33
- borderStyle: "rounded",
34
- borderColor: palette.border,
35
- backgroundColor: OVERLAY_SURFACE,
36
- padding: 1,
37
- flexDirection: "column",
38
- }}
39
- title="Helm Rollback"
40
- >
16
+ <DialogFrame title="Helm Rollback" tone="warning" height={12}>
41
17
  <text fg={palette.fg} attributes={TextAttributes.BOLD}>
42
18
  {releaseName}
43
19
  </text>
44
- <text fg={TEXT_SUBTLE} attributes={TEXT_MUTED}>
20
+ <text fg={TEXT_SUBTLE} attributes={ATTR_DIM}>
45
21
  {"Leave revision empty to roll back to previous version"}
46
22
  </text>
47
- <box
48
- style={{
49
- border: true,
50
- borderColor: PANEL_BORDER_ACTIVE,
51
- borderStyle: "rounded",
52
- backgroundColor: FILTER_BACKGROUND,
53
- paddingLeft: 1,
54
- paddingRight: 1,
55
- marginTop: 1,
56
- height: 3,
57
- justifyContent: "center",
58
- alignItems: "center",
59
- flexDirection: "row",
60
- }}
61
- >
62
- <text fg={TEXT_SUBTLE} attributes={TextAttributes.DIM}>
63
- Revision:
64
- </text>
65
- <input
66
- focused
67
- value={revisionValue}
68
- placeholder="(previous)"
69
- style={{ flexGrow: 1, marginLeft: 1 }}
70
- onInput={onChange}
71
- onChange={onChange}
72
- />
73
- </box>
74
- <box style={{ flexDirection: "row", columnGap: 2, marginTop: 1, justifyContent: "flex-end" }}>
75
- <text fg={TEXT_SUBTLE} attributes={TEXT_MUTED}>
76
- <span fg={KEY_HINT}>{GLYPHS.enter}</span>
77
- {" rollback"}
78
- </text>
79
- <text fg={TEXT_SUBTLE} attributes={TEXT_MUTED}>
80
- <span fg={KEY_HINT}>{GLYPHS.esc}</span>
81
- {" cancel"}
82
- </text>
83
- </box>
84
- </box>
23
+ <TextInput
24
+ label="Revision:"
25
+ value={revisionValue}
26
+ placeholder="(previous)"
27
+ onChange={onChange}
28
+ />
29
+ <KeyHintRow hints={[[GLYPHS.enter, "rollback"], [GLYPHS.esc, "cancel"]]} />
30
+ </DialogFrame>
85
31
  );
86
32
  }