@skyhook-io/radar-app 1.7.0 → 1.8.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 (35) hide show
  1. package/package.json +8 -2
  2. package/src/App.tsx +58 -30
  3. package/src/components/ConnectionErrorView.tsx +5 -2
  4. package/src/components/DebugOverlay.tsx +4 -2
  5. package/src/components/UserMenu.tsx +5 -2
  6. package/src/components/audit/AuditSettingsDialog.tsx +14 -10
  7. package/src/components/audit/AuditView.tsx +3 -1
  8. package/src/components/compare/useCompareLauncher.tsx +6 -3
  9. package/src/components/cost/CostView.tsx +13 -4
  10. package/src/components/helm/ChartBrowser.tsx +6 -4
  11. package/src/components/helm/HelmReleaseDrawer.tsx +25 -13
  12. package/src/components/helm/HelmView.tsx +3 -2
  13. package/src/components/helm/InstallWizard.tsx +3 -2
  14. package/src/components/helm/OwnedResources.tsx +24 -13
  15. package/src/components/helm/RevisionHistory.tsx +2 -1
  16. package/src/components/helm/ValuesViewer.tsx +4 -2
  17. package/src/components/home/ClusterHealthCard.tsx +21 -18
  18. package/src/components/home/HelmSummary.tsx +3 -1
  19. package/src/components/home/MCPSetupDialog.tsx +10 -4
  20. package/src/components/nav/PrimaryNavRail.tsx +5 -2
  21. package/src/components/portforward/PortForwardButton.tsx +14 -8
  22. package/src/components/resource/PrometheusChartsGrid.tsx +9 -2
  23. package/src/components/resource/RestartChart.tsx +6 -5
  24. package/src/components/resource/RightsizingStrip.tsx +4 -1
  25. package/src/components/resource-drawer/ResourceDrawer.tsx +3 -1
  26. package/src/components/resources/ImageFilesystemModal.tsx +16 -8
  27. package/src/components/resources/PodFilesystemModal.tsx +4 -2
  28. package/src/components/settings/SettingsDialog.tsx +371 -141
  29. package/src/components/traffic/TrafficFilterSidebar.tsx +8 -43
  30. package/src/components/traffic/TrafficFlowList.tsx +13 -4
  31. package/src/components/traffic/TrafficGraph.tsx +8 -3
  32. package/src/components/traffic/TrafficView.tsx +4 -2
  33. package/src/components/ui/Omnibar.tsx +0 -1
  34. package/src/components/workload/WorkloadView.tsx +15 -10
  35. package/src/context/NavCustomization.tsx +19 -5
@@ -1,5 +1,4 @@
1
- import { memo, useState, useRef } from 'react'
2
- import { createPortal } from 'react-dom'
1
+ import { memo, useState } from 'react'
3
2
  import {
4
3
  ChevronDown,
5
4
  Eye,
@@ -16,43 +15,7 @@ import { clsx } from 'clsx'
16
15
  import { SEVERITY_BADGE } from '@skyhook-io/k8s-ui/utils/badge-colors'
17
16
  import type { AddonMode } from './TrafficView'
18
17
  import { getNamespaceColor } from '../../utils/traffic-colors'
19
-
20
- // Fast tooltip component using portal to escape overflow
21
- function Tooltip({ children, content }: { children: React.ReactNode; content: string }) {
22
- const [show, setShow] = useState(false)
23
- const [pos, setPos] = useState({ x: 0, y: 0 })
24
- const ref = useRef<HTMLDivElement>(null)
25
-
26
- const handleMouseEnter = () => {
27
- if (ref.current) {
28
- const rect = ref.current.getBoundingClientRect()
29
- setPos({ x: rect.right + 8, y: rect.top + rect.height / 2 })
30
- }
31
- setShow(true)
32
- }
33
-
34
- return (
35
- <div
36
- ref={ref}
37
- className="inline-flex"
38
- onMouseEnter={handleMouseEnter}
39
- onMouseLeave={() => setShow(false)}
40
- >
41
- {children}
42
- {show && createPortal(
43
- <div
44
- className="fixed z-[9999] pointer-events-none"
45
- style={{ left: pos.x, top: pos.y, transform: 'translateY(-50%)' }}
46
- >
47
- <div className="bg-gray-900 text-white text-[10px] px-2 py-1.5 rounded shadow-lg max-w-[180px] leading-tight whitespace-normal">
48
- {content}
49
- </div>
50
- </div>,
51
- document.body
52
- )}
53
- </div>
54
- )
55
- }
18
+ import { Tooltip } from '../ui/Tooltip'
56
19
 
57
20
  // Connection threshold options
58
21
  const CONNECTION_THRESHOLDS = [
@@ -152,7 +115,7 @@ function ToggleOption({
152
115
  {label}
153
116
  </span>
154
117
  </button>
155
- <Tooltip content={description}>
118
+ <Tooltip content={description} position="right">
156
119
  <Info className="w-3 h-3 text-theme-text-tertiary hover:text-theme-text-secondary cursor-help" />
157
120
  </Tooltip>
158
121
  <button
@@ -225,29 +188,31 @@ export const TrafficFilterSidebar = memo(function TrafficFilterSidebar({
225
188
  <div className="px-3 py-2 border-b border-theme-border space-y-1.5">
226
189
  <div className="flex items-center gap-2">
227
190
  <Clock className="w-3.5 h-3.5 text-theme-text-tertiary" />
191
+ <Tooltip content="Show traffic from the selected time window" wrapperClassName="!flex flex-1">
228
192
  <select
229
193
  value={timeRange}
230
194
  onChange={(e) => setTimeRange(e.target.value)}
231
- title="Show traffic from the selected time window"
232
195
  className="flex-1 bg-theme-elevated text-theme-text-primary text-xs rounded px-2 py-1.5 border border-theme-border focus:outline-none focus:ring-1 focus:ring-blue-500"
233
196
  >
234
197
  {TIME_RANGES.map(({ value, label }) => (
235
198
  <option key={value} value={value}>{label}</option>
236
199
  ))}
237
200
  </select>
201
+ </Tooltip>
238
202
  </div>
239
203
  <div className="flex items-center gap-2">
240
204
  <Filter className="w-3.5 h-3.5 text-theme-text-tertiary" />
205
+ <Tooltip content="Hide low-traffic flows to reduce noise" wrapperClassName="!flex flex-1">
241
206
  <select
242
207
  value={minConnections}
243
208
  onChange={(e) => setMinConnections(Number(e.target.value))}
244
- title="Hide low-traffic flows to reduce noise"
245
209
  className="flex-1 bg-theme-elevated text-theme-text-primary text-xs rounded px-2 py-1.5 border border-theme-border focus:outline-none focus:ring-1 focus:ring-blue-500"
246
210
  >
247
211
  {CONNECTION_THRESHOLDS.map(({ value, label }) => (
248
212
  <option key={value} value={value}>{label}</option>
249
213
  ))}
250
214
  </select>
215
+ </Tooltip>
251
216
  </div>
252
217
  </div>
253
218
 
@@ -279,7 +244,7 @@ export const TrafficFilterSidebar = memo(function TrafficFilterSidebar({
279
244
  <div className="flex items-center gap-2 mb-1.5">
280
245
  <Puzzle className="w-3.5 h-3.5 text-theme-text-tertiary" />
281
246
  <span className="text-xs text-theme-text-primary">Cluster Addons</span>
282
- <Tooltip content="Monitoring, logging, cert-manager, etc. Excludes ingress controllers and service mesh.">
247
+ <Tooltip content="Monitoring, logging, cert-manager, etc. Excludes ingress controllers and service mesh." position="right">
283
248
  <Info className="w-3 h-3 text-theme-text-tertiary hover:text-theme-text-secondary cursor-help" />
284
249
  </Tooltip>
285
250
  </div>
@@ -8,6 +8,7 @@ import { pluralize } from '@skyhook-io/k8s-ui'
8
8
  import { useFlowSearch } from './TrafficFlowListContext'
9
9
  import { useQuery } from '@tanstack/react-query'
10
10
  import { fetchJSON } from '../../api/client'
11
+ import { Tooltip } from '../ui/Tooltip'
11
12
 
12
13
  // DNS response code names
13
14
  const DNS_RCODES: Record<number, string> = {
@@ -174,28 +175,36 @@ export function TrafficFlowList({ flows }: TrafficFlowListProps) {
174
175
  <span className="text-theme-text-tertiary tabular-nums whitespace-nowrap">{time}</span>
175
176
 
176
177
  {/* Source */}
177
- <span className="truncate text-theme-text-primary" title={flow.source.namespace ? `${flow.source.namespace}/${flow.source.name}` : flow.source.name}>
178
+ <Tooltip content={flow.source.namespace ? `${flow.source.namespace}/${flow.source.name}` : flow.source.name} wrapperClassName="min-w-0">
179
+ <span className="truncate text-theme-text-primary">
178
180
  {flow.source.name}
179
181
  </span>
182
+ </Tooltip>
180
183
 
181
184
  {/* Destination */}
182
- <span className="truncate text-theme-text-primary" title={flow.destination.namespace ? `${flow.destination.namespace}/${flow.destination.name}` : flow.destination.name}>
185
+ <Tooltip content={flow.destination.namespace ? `${flow.destination.namespace}/${flow.destination.name}` : flow.destination.name} wrapperClassName="min-w-0">
186
+ <span className="truncate text-theme-text-primary">
183
187
  {flow.destination.name}
184
188
  </span>
189
+ </Tooltip>
185
190
 
186
191
  {/* Request info */}
187
192
  <div className="flex items-center gap-1.5 min-w-0">
188
193
  {isHTTP && (
189
194
  <>
190
195
  <span className={clsx('shrink-0 badge badge-sm text-[10px]', SEVERITY_BADGE.info)}>{flow.httpMethod}</span>
191
- <span className="truncate text-theme-text-secondary" title={flow.httpPath}>{flow.httpPath}</span>
196
+ <Tooltip content={flow.httpPath ?? ''} wrapperClassName="min-w-0">
197
+ <span className="truncate text-theme-text-secondary">{flow.httpPath}</span>
198
+ </Tooltip>
192
199
  {flow.l7Type === 'REQUEST' && <span className={clsx('shrink-0 text-[9px]', SEVERITY_TEXT.warning)}>no response</span>}
193
200
  </>
194
201
  )}
195
202
  {isDNS && (
196
203
  <>
197
204
  <span className={clsx('shrink-0 badge badge-sm text-[10px]', SEVERITY_BADGE.neutral)}>DNS</span>
198
- <span className="truncate text-theme-text-secondary" title={flow.dnsQuery}>{flow.dnsQuery}</span>
205
+ <Tooltip content={flow.dnsQuery ?? ''} wrapperClassName="min-w-0">
206
+ <span className="truncate text-theme-text-secondary">{flow.dnsQuery}</span>
207
+ </Tooltip>
199
208
  </>
200
209
  )}
201
210
  {!isHTTP && !isDNS && (
@@ -22,6 +22,7 @@ import { X, ArrowRight, Globe, Server, Activity, Puzzle } from 'lucide-react'
22
22
  import { isClusterAddon, type AddonMode } from './TrafficView'
23
23
  import { SEVERITY_BADGE, SEVERITY_TEXT } from '@skyhook-io/k8s-ui/utils/badge-colors'
24
24
  import { getNamespaceColor } from '../../utils/traffic-colors'
25
+ import { Tooltip } from '../ui/Tooltip'
25
26
 
26
27
  const elk = new ELK()
27
28
 
@@ -845,7 +846,9 @@ function DetailsPanel({
845
846
  {edgeData.flow.topHTTPPaths.map((p, i) => (
846
847
  <div key={i} className="flex items-center gap-1.5 text-[10px]">
847
848
  <span className={clsx('shrink-0 px-1 py-0.5 rounded badge font-medium', SEVERITY_BADGE.info)}>{p.method}</span>
848
- <span className="text-theme-text-primary truncate flex-1" title={p.path}>{p.path || '/'}</span>
849
+ <Tooltip content={p.path} wrapperClassName="min-w-0 flex-1">
850
+ <span className="text-theme-text-primary truncate flex-1">{p.path || '/'}</span>
851
+ </Tooltip>
849
852
  <span className="shrink-0 text-theme-text-secondary">{p.count}</span>
850
853
  {p.avgMs ? <span className="shrink-0 text-theme-text-tertiary">{formatLatency(p.avgMs)}</span> : null}
851
854
  {p.errorPct ? <span className={clsx('shrink-0', p.errorPct > 10 ? SEVERITY_TEXT.error : SEVERITY_TEXT.warning)}>{p.errorPct.toFixed(0)}%err</span> : null}
@@ -862,7 +865,9 @@ function DetailsPanel({
862
865
  <div className="space-y-1 max-h-40 overflow-y-auto">
863
866
  {edgeData.flow.topDNSQueries.map((q, i) => (
864
867
  <div key={i} className="flex items-center gap-1.5 text-[10px]">
865
- <span className="text-theme-text-primary truncate flex-1" title={q.query}>{q.query}</span>
868
+ <Tooltip content={q.query} wrapperClassName="min-w-0 flex-1">
869
+ <span className="text-theme-text-primary truncate flex-1">{q.query}</span>
870
+ </Tooltip>
866
871
  <span className="shrink-0 text-theme-text-secondary">{q.count}</span>
867
872
  {q.nxCount ? <span className={clsx('shrink-0', SEVERITY_TEXT.warning)}>NX:{q.nxCount}</span> : null}
868
873
  {q.avgTTL ? <span className="shrink-0 text-theme-text-tertiary">TTL:{q.avgTTL}s</span> : null}
@@ -1343,7 +1348,7 @@ export function TrafficGraph({ flows, hotPathThreshold = 0, showNamespaceGroups
1343
1348
  }
1344
1349
 
1345
1350
  // Build final edges list
1346
- let finalEdges = [...rawEdges]
1351
+ const finalEdges = [...rawEdges]
1347
1352
 
1348
1353
  // Add edge from addon-internet to addon-group if we have one
1349
1354
  if (addonMode === 'group' && groupEdgeInfo) {
@@ -12,6 +12,7 @@ import { clsx } from 'clsx'
12
12
  import { useQueryClient } from '@tanstack/react-query'
13
13
  import { useDock } from '../dock'
14
14
  import { EmptyState, PaneLoader } from '@skyhook-io/k8s-ui'
15
+ import { Tooltip } from '../ui/Tooltip'
15
16
 
16
17
  // Addon types for filtering
17
18
  export type AddonMode = 'show' | 'group' | 'hide'
@@ -1117,11 +1118,12 @@ export function TrafficView({ namespaces }: TrafficViewProps) {
1117
1118
  {/* Top-right: stats + actions */}
1118
1119
  <div className="absolute top-3 right-3 z-10 flex items-center gap-2">
1119
1120
  {flowsData?.flows && flowsData.flows.length > 0 && (
1121
+ <Tooltip content="Open flow list in dock">
1120
1122
  <button onClick={openFlowListDock}
1121
- className="flex items-center gap-1 px-2 py-1 text-[10px] rounded-lg bg-theme-surface/90 backdrop-blur border border-theme-border text-theme-text-secondary hover:text-theme-text-primary transition-colors"
1122
- title="Open flow list in dock">
1123
+ className="flex items-center gap-1 px-2 py-1 text-[10px] rounded-lg bg-theme-surface/90 backdrop-blur border border-theme-border text-theme-text-secondary hover:text-theme-text-primary transition-colors">
1123
1124
  <List className="w-3 h-3" /> Flows
1124
1125
  </button>
1126
+ </Tooltip>
1125
1127
  )}
1126
1128
  <div className="flex items-center gap-1.5 px-2 py-1 rounded-lg bg-theme-surface/90 backdrop-blur border border-theme-border text-[10px] text-theme-text-tertiary">
1127
1129
  {flowStats.shown}/{flowStats.total}
@@ -283,7 +283,6 @@ export const Omnibar = forwardRef<OmnibarHandle, OmnibarProps>(function Omnibar(
283
283
  out.push({ id: 'view-all', kind: 'viewAll', query: queryString, count: searchData?.total_matched ?? resourceRows.length })
284
284
  }
285
285
  return out
286
- // eslint-disable-next-line react-hooks/exhaustive-deps
287
286
  }, [recentRows, leadingKinds, resourceRows, commandGroups, freeText, pills.length, searchActive, onViewAllResults, queryString, searchData])
288
287
  const viewAllRow = rows.find((r): r is Extract<Row, { kind: 'viewAll' }> => r.kind === 'viewAll')
289
288
 
@@ -43,6 +43,7 @@ import { useCanUpdateSecrets, useCanNodeWrite, useNamespacedCapabilities } from
43
43
  import { useOpenTerminal, useOpenLogs, useOpenWorkloadLogs, useOpenNodeTerminal } from '../dock'
44
44
  import { PortForwardButton } from '../portforward/PortForwardButton'
45
45
  import { useToast } from '../ui/Toast'
46
+ import { Tooltip } from '../ui/Tooltip'
46
47
  import { PodRenderer } from '../resources/renderers/PodRenderer'
47
48
  import { NodeRenderer } from '../resources/renderers/NodeRenderer'
48
49
  import { ServiceRenderer } from '../resources/renderers/ServiceRenderer'
@@ -98,14 +99,6 @@ export function WorkloadViewRoute({ onNavigateToResource }: WorkloadViewRoutePro
98
99
  const name = parts.slice(3).map(decode).join('/')
99
100
  const group = searchParams.get('apiGroup') || ''
100
101
 
101
- if (!kind || !namespace || !name) {
102
- return (
103
- <div className="flex items-center justify-center h-full text-theme-text-tertiary">
104
- Invalid workload URL
105
- </div>
106
- )
107
- }
108
-
109
102
  const handleBack = useCallback(() => {
110
103
  if (window.history.length > 1) {
111
104
  navigate(-1)
@@ -118,6 +111,15 @@ export function WorkloadViewRoute({ onNavigateToResource }: WorkloadViewRoutePro
118
111
  navigate(buildWorkloadPath(resource))
119
112
  }, [navigate])
120
113
 
114
+ // Hooks must run unconditionally — the invalid-URL guard comes after them.
115
+ if (!kind || !namespace || !name) {
116
+ return (
117
+ <div className="flex items-center justify-center h-full text-theme-text-tertiary">
118
+ Invalid workload URL
119
+ </div>
120
+ )
121
+ }
122
+
121
123
  return (
122
124
  <WorkloadView
123
125
  kind={kind}
@@ -879,15 +881,18 @@ function FluxSourceConsumersInner({ sourceKind, namespace, name }: { sourceKind:
879
881
  </h3>
880
882
  <div className="flex flex-wrap gap-1.5">
881
883
  {consumers.map((c) => (
882
- <button
884
+ <Tooltip
883
885
  key={`${c.kind}/${c.namespace}/${c.name}`}
886
+ content={`${c.kind} ${c.namespace}/${c.name}`}
887
+ >
888
+ <button
884
889
  onClick={() => navigate(`/gitops/detail/${c.plural}/${encodeURIComponent(c.namespace)}/${encodeURIComponent(c.name)}`)}
885
890
  className="inline-flex items-center gap-1.5 rounded border border-theme-border bg-theme-surface px-1.5 py-0.5 text-[11px] text-theme-text-secondary hover:border-skyhook-500/60 hover:text-skyhook-500 transition-colors"
886
- title={`${c.kind} ${c.namespace}/${c.name}`}
887
891
  >
888
892
  <span className="text-theme-text-tertiary">{c.kind === 'HelmRelease' ? 'HR' : 'K'}</span>
889
893
  <span>{c.namespace}/{c.name}</span>
890
894
  </button>
895
+ </Tooltip>
891
896
  ))}
892
897
  </div>
893
898
  </div>
@@ -54,11 +54,14 @@ interface NavCustomizationBase {
54
54
  * `clusterChecksHref` folded in here)
55
55
  * - 'certs' → the Certificate Health card
56
56
  *
57
- * View-shaped targets (issues / gitops / checks) are also honored for any
58
- * entry into that view ⌘K, bookmarks, deep links via a redirect effect
59
- * in App.tsx, using window.location.replace so the transient /<view> URL
60
- * stays out of history. 'certs' has no Radar view, so only the card consults
61
- * it (window.location.assign a real forward navigation the user initiated).
57
+ * View-shaped targets (issues / gitops / checks) are honored for every entry:
58
+ * in-app nav (Home cards, ⌘K, "view all") hands straight to the host from
59
+ * `setMainView` via `onHostNavigate` (smooth same-document hand-off, no
60
+ * intermediate /<view> mount); a direct /<view> URL (bookmark/deep link)
61
+ * funnels through a redirect effect that uses `window.location.replace` so
62
+ * the transient URL stays out of history. 'certs' has no Radar view, so only
63
+ * the card consults it. `onHostNavigate` is optional — without it everything
64
+ * falls back to `window.location` (a hard reload).
62
65
  */
63
66
  fleetTakeoverHref?: (target: FleetTakeoverTarget) => string | undefined;
64
67
  /**
@@ -68,6 +71,17 @@ interface NavCustomizationBase {
68
71
  * change. Remove in a major release once all consumers have migrated.
69
72
  */
70
73
  clusterChecksHref?: () => string;
74
+ /**
75
+ * Optional smooth navigator for host-owned URLs. When the host takes a
76
+ * destination over (`fleetTakeoverHref`, `crossClusterCompareHref`), Radar
77
+ * would otherwise hand off via `window.location` — a full document reload
78
+ * that cold-boots the host (white flash, re-auth, chrome teardown). A host
79
+ * that can navigate SAME-DOCUMENT (e.g. Radar Cloud's cross-tree swap with a
80
+ * View Transition) passes this so the hand-off morphs instead of reloading.
81
+ * Omitted → Radar falls back to `window.location` (hard nav), so standalone
82
+ * OSS / other hosts are unaffected.
83
+ */
84
+ onHostNavigate?: (url: string) => void;
71
85
  /**
72
86
  * Chrome level for embedded hosts. Default ('full', or omitted) renders
73
87
  * Radar's top bar + the view-switcher. 'none' suppresses BOTH — the host