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
|
@@ -9,15 +9,13 @@ import {
|
|
|
9
9
|
TEXT_PRIMARY,
|
|
10
10
|
TEXT_SUBTLE,
|
|
11
11
|
} from "../theme";
|
|
12
|
-
import {
|
|
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
|
-
<
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
detail.yaml
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
{
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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 {
|
|
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
|
|
13
|
+
return null;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
// Show at most
|
|
15
|
-
const visible = notifications.slice(-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
width:
|
|
23
|
+
bottom: 5,
|
|
24
|
+
right: 1,
|
|
25
|
+
width: NOTIFICATION_WIDTH_PCT,
|
|
24
26
|
flexDirection: "column",
|
|
25
|
-
rowGap:
|
|
27
|
+
rowGap: 0,
|
|
26
28
|
}}
|
|
27
29
|
>
|
|
28
|
-
{visible.map((notification) =>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
47
|
-
<text fg={TEXT_SUBTLE} attributes={
|
|
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={
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|
-
<
|
|
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={
|
|
20
|
+
<text fg={TEXT_SUBTLE} attributes={ATTR_DIM}>
|
|
45
21
|
{"Leave revision empty to roll back to previous version"}
|
|
46
22
|
</text>
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
}
|