@skyhook-io/radar-app 1.7.0 → 1.8.1
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 +9 -3
- package/src/App.tsx +150 -44
- package/src/api/client.ts +3 -3
- package/src/components/ConnectionErrorView.tsx +5 -2
- package/src/components/DebugOverlay.tsx +5 -3
- package/src/components/UserMenu.tsx +5 -2
- package/src/components/applications/ApplicationsView.tsx +1 -1
- package/src/components/audit/AuditSettingsDialog.tsx +14 -10
- package/src/components/audit/AuditView.tsx +3 -1
- package/src/components/compare/CompareViewRoute.tsx +13 -5
- package/src/components/compare/useCompareLauncher.tsx +6 -3
- package/src/components/cost/CostTrendChart.tsx +3 -3
- package/src/components/cost/CostView.tsx +14 -5
- package/src/components/helm/ChartBrowser.tsx +7 -5
- package/src/components/helm/HelmReleaseDrawer.tsx +25 -13
- package/src/components/helm/HelmView.tsx +3 -2
- package/src/components/helm/InstallWizard.tsx +4 -3
- package/src/components/helm/OwnedResources.tsx +24 -13
- package/src/components/helm/RevisionHistory.tsx +2 -1
- package/src/components/helm/ValuesViewer.tsx +4 -2
- package/src/components/home/ActivitySummary.tsx +4 -1
- package/src/components/home/ClusterHealthCard.tsx +21 -18
- package/src/components/home/HelmSummary.tsx +3 -1
- package/src/components/home/MCPSetupDialog.tsx +10 -4
- package/src/components/issues/IssuesPane.tsx +2 -2
- package/src/components/nav/PrimaryNavRail.tsx +5 -2
- package/src/components/portforward/PortForwardButton.tsx +14 -8
- package/src/components/portforward/PortForwardManager.tsx +70 -8
- package/src/components/resource/PrometheusChartsGrid.tsx +10 -3
- package/src/components/resource/RestartChart.tsx +6 -5
- package/src/components/resource/RightsizingStrip.tsx +4 -1
- package/src/components/resource-drawer/ResourceDrawer.tsx +3 -1
- package/src/components/resources/ImageFilesystemModal.tsx +16 -8
- package/src/components/resources/PodFilesystemModal.tsx +4 -2
- package/src/components/resources/ResourcesView.tsx +2 -0
- package/src/components/settings/MyPermissionsDialog.tsx +64 -4
- package/src/components/settings/SettingsDialog.tsx +371 -141
- package/src/components/shared/LargeClusterNamespacePicker.tsx +1 -1
- package/src/components/traffic/TrafficFilterSidebar.tsx +8 -43
- package/src/components/traffic/TrafficFlowList.tsx +13 -4
- package/src/components/traffic/TrafficGraph.tsx +37 -23
- package/src/components/traffic/TrafficView.tsx +7 -5
- package/src/components/ui/DiagnosticsOverlay.tsx +1 -1
- package/src/components/ui/Omnibar.tsx +0 -1
- package/src/components/workload/WorkloadView.tsx +32 -27
- package/src/context/NavCustomization.tsx +19 -5
- package/src/main.tsx +1 -1
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { memo, useState
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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 && (
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMemo, useEffect, useState, useCallback, useRef } from 'react'
|
|
1
|
+
import { useMemo, useEffect, useState, useCallback, useRef, type MutableRefObject } from 'react'
|
|
2
2
|
import {
|
|
3
3
|
ReactFlow,
|
|
4
4
|
Background,
|
|
@@ -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
|
-
<
|
|
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
|
-
<
|
|
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}
|
|
@@ -966,6 +971,33 @@ const nodeTypes = {
|
|
|
966
971
|
addonGroup: AddonGroupNode,
|
|
967
972
|
}
|
|
968
973
|
|
|
974
|
+
// Fits the view once nodes have been laid out. Module-scope (not defined inside
|
|
975
|
+
// TrafficGraph's render) so it keeps a stable identity — otherwise it remounts
|
|
976
|
+
// every render and its effect churns. Must render inside <ReactFlow> for the
|
|
977
|
+
// useReactFlow() context; the trigger state is passed in as props.
|
|
978
|
+
function FitViewOnChange({
|
|
979
|
+
shouldFitViewRef,
|
|
980
|
+
layoutedNodes,
|
|
981
|
+
}: {
|
|
982
|
+
shouldFitViewRef: MutableRefObject<boolean>
|
|
983
|
+
layoutedNodes: Node<TrafficNodeData>[]
|
|
984
|
+
}) {
|
|
985
|
+
const { fitView } = useReactFlow()
|
|
986
|
+
|
|
987
|
+
useEffect(() => {
|
|
988
|
+
if (shouldFitViewRef.current && layoutedNodes.length > 0) {
|
|
989
|
+
// Small delay to ensure nodes are rendered
|
|
990
|
+
const timer = setTimeout(() => {
|
|
991
|
+
fitView({ padding: 0.2, duration: 200 })
|
|
992
|
+
shouldFitViewRef.current = false
|
|
993
|
+
}, 50)
|
|
994
|
+
return () => clearTimeout(timer)
|
|
995
|
+
}
|
|
996
|
+
}, [fitView, layoutedNodes, shouldFitViewRef])
|
|
997
|
+
|
|
998
|
+
return null
|
|
999
|
+
}
|
|
1000
|
+
|
|
969
1001
|
export function TrafficGraph({ flows, hotPathThreshold = 0, showNamespaceGroups = false, serviceCategories, addonMode = 'show', trafficSource = '', onSelectionChange }: TrafficGraphProps) {
|
|
970
1002
|
const isIstio = trafficSource === 'istio'
|
|
971
1003
|
const connLabel = isIstio ? 'req/s' : 'conn'
|
|
@@ -1343,7 +1375,7 @@ export function TrafficGraph({ flows, hotPathThreshold = 0, showNamespaceGroups
|
|
|
1343
1375
|
}
|
|
1344
1376
|
|
|
1345
1377
|
// Build final edges list
|
|
1346
|
-
|
|
1378
|
+
const finalEdges = [...rawEdges]
|
|
1347
1379
|
|
|
1348
1380
|
// Add edge from addon-internet to addon-group if we have one
|
|
1349
1381
|
if (addonMode === 'group' && groupEdgeInfo) {
|
|
@@ -1487,24 +1519,6 @@ export function TrafficGraph({ flows, hotPathThreshold = 0, showNamespaceGroups
|
|
|
1487
1519
|
onSelectionChange?.(null)
|
|
1488
1520
|
}, [onSelectionChange])
|
|
1489
1521
|
|
|
1490
|
-
// FitView handler component - must be inside ReactFlow
|
|
1491
|
-
const FitViewOnChange = () => {
|
|
1492
|
-
const { fitView } = useReactFlow()
|
|
1493
|
-
|
|
1494
|
-
useEffect(() => {
|
|
1495
|
-
if (shouldFitViewRef.current && layoutedNodes.length > 0) {
|
|
1496
|
-
// Small delay to ensure nodes are rendered
|
|
1497
|
-
const timer = setTimeout(() => {
|
|
1498
|
-
fitView({ padding: 0.2, duration: 200 })
|
|
1499
|
-
shouldFitViewRef.current = false
|
|
1500
|
-
}, 50)
|
|
1501
|
-
return () => clearTimeout(timer)
|
|
1502
|
-
}
|
|
1503
|
-
}, [fitView, layoutedNodes])
|
|
1504
|
-
|
|
1505
|
-
return null
|
|
1506
|
-
}
|
|
1507
|
-
|
|
1508
1522
|
return (
|
|
1509
1523
|
<div className="w-full h-full relative">
|
|
1510
1524
|
<ReactFlow
|
|
@@ -1530,7 +1544,7 @@ export function TrafficGraph({ flows, hotPathThreshold = 0, showNamespaceGroups
|
|
|
1530
1544
|
>
|
|
1531
1545
|
<Background />
|
|
1532
1546
|
<Controls />
|
|
1533
|
-
<FitViewOnChange />
|
|
1547
|
+
<FitViewOnChange shouldFitViewRef={shouldFitViewRef} layoutedNodes={layoutedNodes} />
|
|
1534
1548
|
</ReactFlow>
|
|
1535
1549
|
|
|
1536
1550
|
{/* Legend */}
|
|
@@ -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'
|
|
@@ -619,13 +620,13 @@ export function TrafficView({ namespaces }: TrafficViewProps) {
|
|
|
619
620
|
|
|
620
621
|
// Toggle L7 filter helpers
|
|
621
622
|
const toggleL7Method = useCallback((method: string) => {
|
|
622
|
-
setL7Methods(prev => { const next = new Set(prev); next.has(method)
|
|
623
|
+
setL7Methods(prev => { const next = new Set(prev); if (next.has(method)) next.delete(method); else next.add(method); return next })
|
|
623
624
|
}, [])
|
|
624
625
|
const toggleL7StatusRange = useCallback((range: string) => {
|
|
625
|
-
setL7StatusRanges(prev => { const next = new Set(prev); next.has(range)
|
|
626
|
+
setL7StatusRanges(prev => { const next = new Set(prev); if (next.has(range)) next.delete(range); else next.add(range); return next })
|
|
626
627
|
}, [])
|
|
627
628
|
const toggleL7Verdict = useCallback((verdict: string) => {
|
|
628
|
-
setL7Verdicts(prev => { const next = new Set(prev); next.has(verdict)
|
|
629
|
+
setL7Verdicts(prev => { const next = new Set(prev); if (next.has(verdict)) next.delete(verdict); else next.add(verdict); return next })
|
|
629
630
|
}, [])
|
|
630
631
|
|
|
631
632
|
// Toggle namespace visibility
|
|
@@ -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}
|
|
@@ -99,7 +99,7 @@ export function DiagnosticsOverlay({ onClose, isOpen = true }: DiagnosticsOverla
|
|
|
99
99
|
{/* Content */}
|
|
100
100
|
<div className="overflow-y-auto flex-1 px-5 py-4 space-y-4">
|
|
101
101
|
{isLoading && (
|
|
102
|
-
<div className="text-sm text-theme-text-tertiary text-center py-8">Loading diagnostics
|
|
102
|
+
<div className="text-sm text-theme-text-tertiary text-center py-8">Loading diagnostics…</div>
|
|
103
103
|
)}
|
|
104
104
|
{error && (
|
|
105
105
|
<div className="text-sm text-red-400 text-center py-8">Failed to load diagnostics: {(error as Error).message}</div>
|
|
@@ -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}
|
|
@@ -197,45 +199,45 @@ function useActionsBarProps(kind: string, namespace: string, name: string) {
|
|
|
197
199
|
renderPortForward: ({ type, namespace: ns, name: n, className }: { type: 'pod' | 'service'; namespace: string; name: string; className?: string }) => (
|
|
198
200
|
<PortForwardButton type={type} namespace={ns} name={n} className={className} />
|
|
199
201
|
),
|
|
200
|
-
onDelete: (params:
|
|
202
|
+
onDelete: (params: Parameters<typeof deleteMutation.mutate>[0], callbacks?: { onSuccess?: () => void }) => deleteMutation.mutate(params, { onSuccess: callbacks?.onSuccess }),
|
|
201
203
|
isDeleting: deleteMutation.isPending,
|
|
202
204
|
cascadeDependents: cascadePreview?.dependents,
|
|
203
205
|
cascadeLoading,
|
|
204
|
-
onRestart: (params:
|
|
206
|
+
onRestart: (params: Parameters<typeof restartWorkloadMutation.mutate>[0]) => restartWorkloadMutation.mutate(params),
|
|
205
207
|
isRestarting: restartWorkloadMutation.isPending,
|
|
206
208
|
revisions: revisionsList,
|
|
207
209
|
revisionsLoading,
|
|
208
210
|
revisionsError: revisionsError ?? null,
|
|
209
|
-
onRollback: (params:
|
|
211
|
+
onRollback: (params: Parameters<typeof rollbackMutation.mutate>[0], callbacks?: { onSuccess?: () => void }) => rollbackMutation.mutate(params, { onSuccess: callbacks?.onSuccess }),
|
|
210
212
|
isRollingBack: rollbackMutation.isPending,
|
|
211
|
-
onTriggerCronJob: (params:
|
|
213
|
+
onTriggerCronJob: (params: Parameters<typeof triggerCronJobMutation.mutate>[0]) => triggerCronJobMutation.mutate(params),
|
|
212
214
|
isTriggeringCronJob: triggerCronJobMutation.isPending,
|
|
213
|
-
onSuspendCronJob: (params:
|
|
215
|
+
onSuspendCronJob: (params: Parameters<typeof suspendCronJobMutation.mutate>[0]) => suspendCronJobMutation.mutate(params),
|
|
214
216
|
isSuspendingCronJob: suspendCronJobMutation.isPending,
|
|
215
|
-
onResumeCronJob: (params:
|
|
217
|
+
onResumeCronJob: (params: Parameters<typeof resumeCronJobMutation.mutate>[0]) => resumeCronJobMutation.mutate(params),
|
|
216
218
|
isResumingCronJob: resumeCronJobMutation.isPending,
|
|
217
|
-
onFluxReconcile: (params:
|
|
219
|
+
onFluxReconcile: (params: Parameters<typeof fluxReconcileMutation.mutate>[0]) => fluxReconcileMutation.mutate(params),
|
|
218
220
|
isFluxReconciling: fluxReconcileMutation.isPending,
|
|
219
|
-
onFluxSyncWithSource: (params:
|
|
221
|
+
onFluxSyncWithSource: (params: Parameters<typeof fluxSyncWithSourceMutation.mutate>[0]) => fluxSyncWithSourceMutation.mutate(params),
|
|
220
222
|
isFluxSyncing: fluxSyncWithSourceMutation.isPending,
|
|
221
|
-
onFluxSuspend: (params:
|
|
223
|
+
onFluxSuspend: (params: Parameters<typeof fluxSuspendMutation.mutate>[0]) => fluxSuspendMutation.mutate(params),
|
|
222
224
|
isFluxSuspending: fluxSuspendMutation.isPending,
|
|
223
|
-
onFluxResume: (params:
|
|
225
|
+
onFluxResume: (params: Parameters<typeof fluxResumeMutation.mutate>[0]) => fluxResumeMutation.mutate(params),
|
|
224
226
|
isFluxResuming: fluxResumeMutation.isPending,
|
|
225
|
-
onArgoSync: (params:
|
|
227
|
+
onArgoSync: (params: Parameters<typeof argoSyncMutation.mutate>[0]) => argoSyncMutation.mutate(params),
|
|
226
228
|
isArgoSyncing: argoSyncMutation.isPending,
|
|
227
|
-
onArgoRefresh: (params:
|
|
229
|
+
onArgoRefresh: (params: Parameters<typeof argoRefreshMutation.mutate>[0]) => argoRefreshMutation.mutate(params),
|
|
228
230
|
isArgoRefreshing: argoRefreshMutation.isPending,
|
|
229
|
-
onArgoSuspend: (params:
|
|
231
|
+
onArgoSuspend: (params: Parameters<typeof argoSuspendMutation.mutate>[0]) => argoSuspendMutation.mutate(params),
|
|
230
232
|
isArgoSuspending: argoSuspendMutation.isPending,
|
|
231
|
-
onArgoResume: (params:
|
|
233
|
+
onArgoResume: (params: Parameters<typeof argoResumeMutation.mutate>[0]) => argoResumeMutation.mutate(params),
|
|
232
234
|
isArgoResuming: argoResumeMutation.isPending,
|
|
233
235
|
canNodeWrite,
|
|
234
|
-
onCordonNode: (params:
|
|
236
|
+
onCordonNode: (params: Parameters<typeof cordonMutation.mutate>[0]) => cordonMutation.mutate(params),
|
|
235
237
|
isCordoningNode: cordonMutation.isPending,
|
|
236
|
-
onUncordonNode: (params:
|
|
238
|
+
onUncordonNode: (params: Parameters<typeof uncordonMutation.mutate>[0]) => uncordonMutation.mutate(params),
|
|
237
239
|
isUncordoningNode: uncordonMutation.isPending,
|
|
238
|
-
onDrainNode: (params:
|
|
240
|
+
onDrainNode: (params: Parameters<typeof drainMutation.mutate>[0]) => drainMutation.mutate(params),
|
|
239
241
|
isDrainingNode: drainMutation.isPending,
|
|
240
242
|
}
|
|
241
243
|
}
|
|
@@ -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
|
-
<
|
|
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
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
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
|
package/src/main.tsx
CHANGED
|
@@ -130,7 +130,7 @@ document.execCommand = function (command: string, showUI?: boolean, value?: stri
|
|
|
130
130
|
dt.setData('text/plain', text)
|
|
131
131
|
const ev = new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true })
|
|
132
132
|
if (!el.dispatchEvent(ev)) return
|
|
133
|
-
} catch
|
|
133
|
+
} catch { /* ClipboardEvent dispatch failed, fall back to insertText */ }
|
|
134
134
|
_origExecCommand('insertText', false, text)
|
|
135
135
|
}).catch((err) => { console.warn('[Radar] Paste failed:', err) })
|
|
136
136
|
return true
|