@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.
@@ -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?: { detail?: string; command?: string; type?: Toast['type']; position?: { x: number; y: number } }) => void
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?: { detail?: string; command?: string; type?: Toast['type']; position?: { x: number; y: number } }) => {
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 (8s), others 5s
92
- const dismissTime = options?.type === 'error' ? 8000 : 5000
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 - 420),
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-3 rounded-lg shadow-2xl border',
164
- 'w-[400px] max-w-[calc(100vw-32px)]',
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/90 border-red-800/50'
183
+ ? 'bg-red-950 border-red-800/60'
168
184
  : isSuccess
169
- ? 'bg-green-950/90 border-green-700/50'
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-green-500/20' : 'bg-blue-500/20'
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-green-400' : 'text-blue-400')} />
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-green-400" />
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-green-100' : 'text-theme-text-primary')}>
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-green-300' : 'text-green-400')} />}
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
- <p className={clsx('mt-1 text-xs', isError ? 'text-red-300/80' : isSuccess ? 'text-green-300/80' : 'text-theme-text-secondary')}>
199
- {toast.detail}
200
- </p>
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-green-400 hover:text-green-300 hover:bg-green-900/50'
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
  >
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  :root {
14
- color-scheme: light dark;
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 for comma-separated values that can't use light-dark() */
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
@@ -1,5 +1,20 @@
1
- /** Triggers a file download in the browser or desktop webview. */
2
- export function triggerDownload(content: string, mime: string, filename: string): void {
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 embedded webviews still have time to consume the blob URL.
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()
package/tsconfig.json CHANGED
@@ -14,7 +14,6 @@
14
14
  "noUnusedParameters": true,
15
15
  "noFallthroughCasesInSwitch": true,
16
16
  "skipLibCheck": true,
17
- "baseUrl": ".",
18
17
  "paths": {
19
18
  "@skyhook/k8s-ui": ["./src/index.ts"],
20
19
  "@skyhook/k8s-ui/*": ["./src/*"]