@skyhook-io/k8s-ui 1.3.0 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/components/dock/BottomDock.tsx +23 -5
- package/src/components/dock/DockContext.tsx +9 -1
- package/src/components/logs/LogsViewer.tsx +8 -3
- package/src/components/logs/WorkloadLogsViewer.tsx +8 -4
- package/src/components/timeline/DiffViewer.tsx +131 -0
- package/src/components/timeline/TimelineList.tsx +817 -0
- package/src/components/timeline/index.ts +2 -0
- package/src/components/topology/GroupNode.tsx +189 -79
- package/src/components/topology/TopologyGraph.tsx +286 -52
- package/src/components/topology/layout.ts +288 -92
- package/src/components/topology/topology.css +111 -0
- package/src/components/ui/Toast.tsx +74 -27
- package/src/theme/variables.css +7 -2
- package/src/types/core.ts +39 -0
- package/src/utils/download.ts +18 -3
- package/tsconfig.json +0 -1
|
@@ -11,13 +11,22 @@ interface Toast {
|
|
|
11
11
|
type?: 'success' | 'info' | 'warning' | 'error'
|
|
12
12
|
position?: { x: number; y: number }
|
|
13
13
|
dismissing?: boolean
|
|
14
|
+
action?: { label: string; icon?: ReactNode; onClick: () => void }
|
|
15
|
+
onDetailClick?: () => void
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
interface ToastContextType {
|
|
17
|
-
showToast: (message: string, options?: {
|
|
19
|
+
showToast: (message: string, options?: {
|
|
20
|
+
detail?: string
|
|
21
|
+
command?: string
|
|
22
|
+
type?: Toast['type']
|
|
23
|
+
position?: { x: number; y: number }
|
|
24
|
+
action?: Toast['action']
|
|
25
|
+
onDetailClick?: () => void
|
|
26
|
+
}) => void
|
|
18
27
|
showCopied: (command: string, label?: string, event?: React.MouseEvent) => void
|
|
19
28
|
showError: (message: string, detail?: string) => void
|
|
20
|
-
showSuccess: (message: string, detail?: string) => void
|
|
29
|
+
showSuccess: (message: string, detail?: string, action?: Toast['action'], onDetailClick?: () => void) => void
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
const ToastContext = createContext<ToastContextType | null>(null)
|
|
@@ -26,7 +35,7 @@ const ToastContext = createContext<ToastContextType | null>(null)
|
|
|
26
35
|
class ToastManager {
|
|
27
36
|
private static instance: ToastManager
|
|
28
37
|
private showErrorFn: ((message: string, detail?: string) => void) | null = null
|
|
29
|
-
private showSuccessFn: ((message: string, detail?: string) => void) | null = null
|
|
38
|
+
private showSuccessFn: ((message: string, detail?: string, action?: Toast['action'], onDetailClick?: () => void) => void) | null = null
|
|
30
39
|
|
|
31
40
|
static getInstance(): ToastManager {
|
|
32
41
|
if (!ToastManager.instance) {
|
|
@@ -49,8 +58,8 @@ class ToastManager {
|
|
|
49
58
|
this.showErrorFn ? this.showErrorFn(message, detail) : console.error('[Toast]', message, detail)
|
|
50
59
|
}
|
|
51
60
|
|
|
52
|
-
showSuccess(message: string, detail?: string) {
|
|
53
|
-
this.showSuccessFn ? this.showSuccessFn(message, detail) : console.log('[Toast]', message, detail)
|
|
61
|
+
showSuccess(message: string, detail?: string, action?: Toast['action'], onDetailClick?: () => void) {
|
|
62
|
+
this.showSuccessFn ? this.showSuccessFn(message, detail, action, onDetailClick) : console.log('[Toast]', message, detail)
|
|
54
63
|
}
|
|
55
64
|
}
|
|
56
65
|
|
|
@@ -60,8 +69,8 @@ export function showApiError(message: string, detail?: string) {
|
|
|
60
69
|
toastManager.showError(message, detail)
|
|
61
70
|
}
|
|
62
71
|
|
|
63
|
-
export function showApiSuccess(message: string, detail?: string) {
|
|
64
|
-
toastManager.showSuccess(message, detail)
|
|
72
|
+
export function showApiSuccess(message: string, detail?: string, action?: Toast['action'], onDetailClick?: () => void) {
|
|
73
|
+
toastManager.showSuccess(message, detail, action, onDetailClick)
|
|
65
74
|
}
|
|
66
75
|
|
|
67
76
|
export function useToast() {
|
|
@@ -82,14 +91,21 @@ export function ToastProvider({ children }: { children: ReactNode }) {
|
|
|
82
91
|
}, DURATION_TOAST_EXIT)
|
|
83
92
|
}, [])
|
|
84
93
|
|
|
85
|
-
const showToast = useCallback((message: string, options?: {
|
|
94
|
+
const showToast = useCallback((message: string, options?: {
|
|
95
|
+
detail?: string
|
|
96
|
+
command?: string
|
|
97
|
+
type?: Toast['type']
|
|
98
|
+
position?: { x: number; y: number }
|
|
99
|
+
action?: Toast['action']
|
|
100
|
+
onDetailClick?: () => void
|
|
101
|
+
}) => {
|
|
86
102
|
const id = Math.random().toString(36).slice(2)
|
|
87
103
|
const toast: Toast = { id, message, ...options }
|
|
88
104
|
|
|
89
105
|
setToasts(prev => [...prev, toast])
|
|
90
106
|
|
|
91
|
-
// Auto-dismiss: errors stay longer (
|
|
92
|
-
const dismissTime = options?.type === 'error' ?
|
|
107
|
+
// Auto-dismiss: errors stay longer (10s), others 7s
|
|
108
|
+
const dismissTime = options?.type === 'error' ? 10000 : 7000
|
|
93
109
|
setTimeout(() => {
|
|
94
110
|
animateDismiss(id)
|
|
95
111
|
}, dismissTime)
|
|
@@ -112,8 +128,8 @@ export function ToastProvider({ children }: { children: ReactNode }) {
|
|
|
112
128
|
showToast(message, { detail, type: 'error' })
|
|
113
129
|
}, [showToast])
|
|
114
130
|
|
|
115
|
-
const showSuccess = useCallback((message: string, detail?: string) => {
|
|
116
|
-
showToast(message, { detail, type: 'success' })
|
|
131
|
+
const showSuccess = useCallback((message: string, detail?: string, action?: Toast['action'], onDetailClick?: () => void) => {
|
|
132
|
+
showToast(message, { detail, type: 'success', action, onDetailClick })
|
|
117
133
|
}, [showToast])
|
|
118
134
|
|
|
119
135
|
const dismissToast = useCallback((id: string) => {
|
|
@@ -143,7 +159,7 @@ function ToastItem({ toast, onDismiss }: { toast: Toast; onDismiss: () => void }
|
|
|
143
159
|
const style: React.CSSProperties = toast.position
|
|
144
160
|
? {
|
|
145
161
|
position: 'fixed',
|
|
146
|
-
left: Math.min(toast.position.x, window.innerWidth -
|
|
162
|
+
left: Math.min(toast.position.x, window.innerWidth - 520),
|
|
147
163
|
top: toast.position.y,
|
|
148
164
|
zIndex: 50,
|
|
149
165
|
}
|
|
@@ -160,13 +176,13 @@ function ToastItem({ toast, onDismiss }: { toast: Toast; onDismiss: () => void }
|
|
|
160
176
|
return (
|
|
161
177
|
<div
|
|
162
178
|
className={clsx(
|
|
163
|
-
'flex items-start gap-3 p-
|
|
164
|
-
'w-[
|
|
179
|
+
'flex items-start gap-3 p-4 rounded-lg shadow-2xl border backdrop-blur-sm',
|
|
180
|
+
'w-[480px] max-w-[calc(100vw-32px)]',
|
|
165
181
|
toast.dismissing ? 'animate-out' : 'animate-in',
|
|
166
182
|
isError
|
|
167
|
-
? 'bg-red-950
|
|
183
|
+
? 'bg-red-950 border-red-800/60'
|
|
168
184
|
: isSuccess
|
|
169
|
-
? 'bg-
|
|
185
|
+
? 'bg-emerald-950 border-emerald-800/50'
|
|
170
186
|
: 'bg-theme-surface border-theme-border'
|
|
171
187
|
)}
|
|
172
188
|
style={style}
|
|
@@ -175,46 +191,77 @@ function ToastItem({ toast, onDismiss }: { toast: Toast; onDismiss: () => void }
|
|
|
175
191
|
<div className={clsx(
|
|
176
192
|
'w-8 h-8 rounded-full flex items-center justify-center shrink-0',
|
|
177
193
|
isError ? 'bg-red-500/20' :
|
|
178
|
-
isSuccess ? 'bg-
|
|
194
|
+
isSuccess ? 'bg-emerald-500/20' : 'bg-blue-500/20'
|
|
179
195
|
)}>
|
|
180
196
|
{isError ? (
|
|
181
197
|
<AlertTriangle className="w-4 h-4 text-red-400" />
|
|
182
198
|
) : toast.command ? (
|
|
183
|
-
<Terminal className={clsx('w-4 h-4', isSuccess ? 'text-
|
|
199
|
+
<Terminal className={clsx('w-4 h-4', isSuccess ? 'text-emerald-400' : 'text-blue-400')} />
|
|
184
200
|
) : (
|
|
185
|
-
<Check className="w-4 h-4 text-
|
|
201
|
+
<Check className="w-4 h-4 text-emerald-400" />
|
|
186
202
|
)}
|
|
187
203
|
</div>
|
|
188
204
|
|
|
189
205
|
{/* Content */}
|
|
190
206
|
<div className="flex-1 min-w-0">
|
|
191
207
|
<div className="flex items-center gap-2">
|
|
192
|
-
<span className={clsx('text-sm font-medium', isError ? 'text-red-200' : isSuccess ? 'text-
|
|
208
|
+
<span className={clsx('text-sm font-medium', isError ? 'text-red-200' : isSuccess ? 'text-emerald-50' : 'text-theme-text-primary')}>
|
|
193
209
|
{toast.message}
|
|
194
210
|
</span>
|
|
195
|
-
{!isError && <Check className={clsx('w-3.5 h-3.5 shrink-0', isSuccess ? 'text-
|
|
211
|
+
{!isError && !toast.action && <Check className={clsx('w-3.5 h-3.5 shrink-0', isSuccess ? 'text-emerald-400' : 'text-green-400')} />}
|
|
196
212
|
</div>
|
|
197
213
|
{toast.detail && (
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
214
|
+
toast.onDetailClick ? (
|
|
215
|
+
<button
|
|
216
|
+
onClick={toast.onDetailClick}
|
|
217
|
+
className={clsx(
|
|
218
|
+
'mt-1.5 block text-xs font-mono break-all text-left rounded px-1.5 py-1 -ml-1.5 transition-colors',
|
|
219
|
+
isError
|
|
220
|
+
? 'text-red-300 hover:bg-red-900/50'
|
|
221
|
+
: isSuccess
|
|
222
|
+
? 'text-emerald-300/90 hover:text-emerald-200 hover:bg-emerald-900/50'
|
|
223
|
+
: 'text-theme-text-secondary hover:bg-theme-elevated'
|
|
224
|
+
)}
|
|
225
|
+
title="Click to open file"
|
|
226
|
+
>
|
|
227
|
+
{toast.detail}
|
|
228
|
+
</button>
|
|
229
|
+
) : (
|
|
230
|
+
<p className={clsx('mt-1 text-xs break-all', isError ? 'text-red-300/80' : isSuccess ? 'text-emerald-300/80' : 'text-theme-text-secondary')}>
|
|
231
|
+
{toast.detail}
|
|
232
|
+
</p>
|
|
233
|
+
)
|
|
201
234
|
)}
|
|
202
235
|
{toast.command && (
|
|
203
236
|
<code className="block mt-1.5 text-xs text-theme-text-secondary font-mono bg-theme-base rounded px-2 py-1.5 whitespace-pre-wrap break-all">
|
|
204
237
|
{toast.command}
|
|
205
238
|
</code>
|
|
206
239
|
)}
|
|
240
|
+
{toast.action && (
|
|
241
|
+
<button
|
|
242
|
+
onClick={toast.action.onClick}
|
|
243
|
+
className={clsx(
|
|
244
|
+
'mt-2.5 inline-flex items-center gap-1.5 text-xs font-medium px-3 py-1.5 rounded transition-colors',
|
|
245
|
+
isSuccess
|
|
246
|
+
? 'text-emerald-100 bg-emerald-800/50 hover:bg-emerald-700/50 border border-emerald-700/40'
|
|
247
|
+
: 'text-theme-text-secondary bg-theme-elevated hover:bg-theme-surface border border-theme-border'
|
|
248
|
+
)}
|
|
249
|
+
>
|
|
250
|
+
{toast.action.icon}
|
|
251
|
+
{toast.action.label}
|
|
252
|
+
</button>
|
|
253
|
+
)}
|
|
207
254
|
</div>
|
|
208
255
|
|
|
209
256
|
{/* Dismiss button */}
|
|
210
257
|
<button
|
|
211
258
|
onClick={onDismiss}
|
|
212
259
|
className={clsx(
|
|
213
|
-
'p-1 rounded shrink-0',
|
|
260
|
+
'p-1 rounded shrink-0 transition-colors',
|
|
214
261
|
isError
|
|
215
262
|
? 'text-red-400 hover:text-red-300 hover:bg-red-900/50'
|
|
216
263
|
: isSuccess
|
|
217
|
-
? 'text-
|
|
264
|
+
? 'text-emerald-500 hover:text-emerald-300 hover:bg-emerald-900/50'
|
|
218
265
|
: 'text-theme-text-tertiary hover:text-theme-text-primary hover:bg-theme-elevated'
|
|
219
266
|
)}
|
|
220
267
|
>
|
package/src/theme/variables.css
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
:root {
|
|
14
|
-
color-scheme: light
|
|
14
|
+
color-scheme: light;
|
|
15
15
|
|
|
16
16
|
/* ── Brand (refined blue — more life than slate, less generic than tech blue) ── */
|
|
17
17
|
--color-brand: light-dark(#4A7CC9, #6A9FE0);
|
|
@@ -128,8 +128,13 @@
|
|
|
128
128
|
--scrollbar-thumb-hover: light-dark(#AABACF, #484C55);
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
/* Dark overrides
|
|
131
|
+
/* Dark overrides — color-scheme drives light-dark() resolution.
|
|
132
|
+
We declare it in CSS (not just via JS inline style) because Lightning CSS
|
|
133
|
+
may compile light-dark() into a @media (prefers-color-scheme) polyfill
|
|
134
|
+
(e.g. on Node <22), which only responds to CSS color-scheme declarations.
|
|
135
|
+
Also guards against WKWebView quirks in the desktop app. */
|
|
132
136
|
.dark {
|
|
137
|
+
color-scheme: dark;
|
|
133
138
|
--shadow-sm: 0 1px 3px rgba(0,0,0,0.3), 0 1px 2px rgba(0,0,0,0.2), 0 0 0 1px rgba(255,255,255,0.06);
|
|
134
139
|
--shadow-md: 0 4px 12px -2px rgba(0,0,0,0.35), 0 2px 4px rgba(0,0,0,0.15), 0 0 0 1px rgba(255,255,255,0.06);
|
|
135
140
|
--shadow-lg: 0 12px 32px -6px rgba(0,0,0,0.3), 0 4px 8px rgba(0,0,0,0.1);
|
package/src/types/core.ts
CHANGED
|
@@ -710,6 +710,19 @@ export interface TrafficFlow {
|
|
|
710
710
|
httpMethod?: string
|
|
711
711
|
httpPath?: string
|
|
712
712
|
httpStatus?: number
|
|
713
|
+
latencyNs?: number
|
|
714
|
+
l7Type?: string // REQUEST, RESPONSE, SAMPLE
|
|
715
|
+
httpProtocol?: string // HTTP/1.1, HTTP/2
|
|
716
|
+
httpHeaders?: string[] // allowlisted headers as "key: value"
|
|
717
|
+
dnsQuery?: string
|
|
718
|
+
dnsIPs?: string[]
|
|
719
|
+
dnsTTL?: number
|
|
720
|
+
dnsRCode?: number
|
|
721
|
+
dnsQTypes?: string[]
|
|
722
|
+
trafficDirection?: string // ingress, egress
|
|
723
|
+
dropReasonDesc?: string
|
|
724
|
+
sourceService?: string
|
|
725
|
+
destService?: string
|
|
713
726
|
bytesSent: number
|
|
714
727
|
bytesRecv: number
|
|
715
728
|
connections: number
|
|
@@ -717,6 +730,23 @@ export interface TrafficFlow {
|
|
|
717
730
|
lastSeen: string // ISO date string
|
|
718
731
|
}
|
|
719
732
|
|
|
733
|
+
// HTTP path statistics for aggregated flows
|
|
734
|
+
export interface HTTPPathStat {
|
|
735
|
+
method: string
|
|
736
|
+
path: string
|
|
737
|
+
count: number
|
|
738
|
+
avgMs?: number
|
|
739
|
+
errorPct?: number // 4xx+5xx percentage
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// DNS query statistics for aggregated flows
|
|
743
|
+
export interface DNSQueryStat {
|
|
744
|
+
query: string
|
|
745
|
+
count: number
|
|
746
|
+
nxCount?: number // NXDOMAIN responses
|
|
747
|
+
avgTTL?: number
|
|
748
|
+
}
|
|
749
|
+
|
|
720
750
|
// Aggregated flow by service pair
|
|
721
751
|
export interface AggregatedFlow {
|
|
722
752
|
source: TrafficEndpoint
|
|
@@ -728,9 +758,18 @@ export interface AggregatedFlow {
|
|
|
728
758
|
bytesRecv: number
|
|
729
759
|
connections: number
|
|
730
760
|
lastSeen: string
|
|
761
|
+
l7Protocol?: string // HTTP, gRPC, DNS
|
|
731
762
|
requestCount?: number
|
|
732
763
|
errorCount?: number
|
|
733
764
|
avgLatencyMs?: number
|
|
765
|
+
latencyP50Ms?: number
|
|
766
|
+
latencyP95Ms?: number
|
|
767
|
+
latencyP99Ms?: number
|
|
768
|
+
httpStatusCounts?: Record<string, number> // "2xx": 150, "5xx": 3
|
|
769
|
+
topHTTPPaths?: HTTPPathStat[]
|
|
770
|
+
topDNSQueries?: DNSQueryStat[]
|
|
771
|
+
verdictCounts?: Record<string, number> // "forwarded": 500, "dropped": 3
|
|
772
|
+
dropReasons?: Record<string, number>
|
|
734
773
|
}
|
|
735
774
|
|
|
736
775
|
// Cluster info for traffic detection
|
package/src/utils/download.ts
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Triggers a file download in the browser.
|
|
3
|
+
* When `overrideFn` is provided (e.g. for native desktop save dialogs),
|
|
4
|
+
* it delegates to that instead of the default blob URL approach.
|
|
5
|
+
* The override must handle its own errors (e.g. via toast notifications).
|
|
6
|
+
*/
|
|
7
|
+
export function triggerDownload(
|
|
8
|
+
content: string,
|
|
9
|
+
mime: string,
|
|
10
|
+
filename: string,
|
|
11
|
+
overrideFn?: (content: string, mime: string, filename: string) => void,
|
|
12
|
+
): void {
|
|
13
|
+
if (overrideFn) {
|
|
14
|
+
overrideFn(content, mime, filename)
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
|
|
3
18
|
const blob = new Blob([content], { type: mime })
|
|
4
19
|
const url = URL.createObjectURL(blob)
|
|
5
20
|
const a = document.createElement('a')
|
|
@@ -11,7 +26,7 @@ export function triggerDownload(content: string, mime: string, filename: string)
|
|
|
11
26
|
document.body.appendChild(a)
|
|
12
27
|
a.click()
|
|
13
28
|
|
|
14
|
-
// Delay cleanup so
|
|
29
|
+
// Delay cleanup so the browser has time to initiate the download before the blob URL is revoked.
|
|
15
30
|
window.setTimeout(() => {
|
|
16
31
|
URL.revokeObjectURL(url)
|
|
17
32
|
a.remove()
|