@skyhook-io/radar-app 1.6.2 → 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.
- package/package.json +8 -2
- package/src/App.tsx +110 -45
- package/src/api/client.ts +10 -6
- package/src/components/ConnectionErrorView.tsx +5 -2
- package/src/components/DebugOverlay.tsx +4 -2
- package/src/components/UserMenu.tsx +5 -2
- package/src/components/audit/AuditSettingsDialog.tsx +14 -10
- package/src/components/audit/AuditView.tsx +3 -1
- package/src/components/compare/useCompareLauncher.tsx +6 -3
- package/src/components/cost/CostView.tsx +13 -4
- package/src/components/helm/ChartBrowser.tsx +6 -4
- package/src/components/helm/HelmReleaseDrawer.tsx +25 -13
- package/src/components/helm/HelmView.tsx +7 -6
- package/src/components/helm/InstallWizard.tsx +3 -2
- 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/ClusterHealthCard.tsx +21 -18
- package/src/components/home/HelmSummary.tsx +3 -1
- package/src/components/home/HomeView.tsx +9 -2
- package/src/components/home/MCPSetupDialog.tsx +10 -4
- package/src/components/nav/PrimaryNavRail.tsx +5 -2
- package/src/components/portforward/PortForwardButton.tsx +14 -8
- package/src/components/resource/PrometheusChartsGrid.tsx +9 -2
- 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/settings/SettingsDialog.tsx +371 -141
- package/src/components/traffic/TrafficFilterSidebar.tsx +8 -43
- package/src/components/traffic/TrafficFlowList.tsx +13 -4
- package/src/components/traffic/TrafficGraph.tsx +8 -3
- package/src/components/traffic/TrafficView.tsx +4 -2
- package/src/components/ui/Omnibar.tsx +0 -1
- package/src/components/workload/WorkloadView.tsx +15 -10
- package/src/context/NavCustomization.tsx +48 -9
- package/src/index.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyhook-io/radar-app",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Radar's full web UI as a reusable React component. Used by Radar's own binary and by external consumers like Radar Cloud.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"preview": "vite preview",
|
|
25
25
|
"tsc": "tsc --noEmit",
|
|
26
26
|
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
|
|
27
|
-
"lint": "eslint src
|
|
27
|
+
"lint": "eslint src && node scripts/check-no-native-title.mjs"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@fontsource-variable/dm-sans": "^5.2.8",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"tailwind-merge": ">=2"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
+
"@eslint/js": "^10.0.1",
|
|
54
55
|
"@playwright/test": "^1.59.1",
|
|
55
56
|
"@skyhook-io/k8s-ui": "*",
|
|
56
57
|
"@tailwindcss/typography": "^0.5.20",
|
|
@@ -63,6 +64,10 @@
|
|
|
63
64
|
"@xyflow/react": "^12.10.2",
|
|
64
65
|
"clsx": "^2.1.1",
|
|
65
66
|
"elkjs": "^0.11.1",
|
|
67
|
+
"eslint": "^10.5.0",
|
|
68
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
69
|
+
"eslint-plugin-react-refresh": "^0.5.3",
|
|
70
|
+
"globals": "^17.7.0",
|
|
66
71
|
"lucide-react": "^1.16.0",
|
|
67
72
|
"postcss": "^8.5.14",
|
|
68
73
|
"prettier": "^3.8.1",
|
|
@@ -72,6 +77,7 @@
|
|
|
72
77
|
"tailwind-merge": "^3.6.0",
|
|
73
78
|
"tailwindcss": "^4.3.1",
|
|
74
79
|
"typescript": "^6.0.2",
|
|
80
|
+
"typescript-eslint": "^8.62.0",
|
|
75
81
|
"vite": "^8.0.12"
|
|
76
82
|
},
|
|
77
83
|
"sideEffects": [
|
package/src/App.tsx
CHANGED
|
@@ -29,6 +29,7 @@ import { DURATION_DOCK } from '@skyhook-io/k8s-ui/utils/animation'
|
|
|
29
29
|
import { ContextSwitcher } from './components/ContextSwitcher'
|
|
30
30
|
import { NamespaceSwitcher, type NamespaceSwitcherHandle } from './components/NamespaceSwitcher'
|
|
31
31
|
import { useNavCustomization } from './context/NavCustomization'
|
|
32
|
+
import type { FleetTakeoverTarget } from './context/NavCustomization'
|
|
32
33
|
import { PrimaryNavRail } from './components/nav/PrimaryNavRail'
|
|
33
34
|
import { useNavRailPinned } from './hooks/useNavRailPinned'
|
|
34
35
|
import { useMediaQuery } from './hooks/useMediaQuery'
|
|
@@ -178,6 +179,32 @@ function AppInner() {
|
|
|
178
179
|
const capabilities = useCapabilitiesContext()
|
|
179
180
|
const openLocalTerminal = useOpenLocalTerminal()
|
|
180
181
|
const navCustomization = useNavCustomization()
|
|
182
|
+
// Hand off to a host-owned URL. The host's `onHostNavigate` (Radar Cloud's
|
|
183
|
+
// cross-tree swap) navigates same-document so the chrome morphs instead of
|
|
184
|
+
// cold-booting; without it we fall back to a hard `window.location` nav.
|
|
185
|
+
const goHost = useCallback(
|
|
186
|
+
(url: string) => {
|
|
187
|
+
if (navCustomization.onHostNavigate) navCustomization.onHostNavigate(url)
|
|
188
|
+
else window.location.assign(url)
|
|
189
|
+
},
|
|
190
|
+
[navCustomization],
|
|
191
|
+
)
|
|
192
|
+
// Resolve every host-takeover URL ONCE (memoized on navCustomization) so the
|
|
193
|
+
// setMainView intercept, redirect effect, nav-pill filtering, inline-view
|
|
194
|
+
// gating, and the cert click handler all consume the SAME value — host
|
|
195
|
+
// callbacks aren't guaranteed idempotent (scope / flags / signed URLs can
|
|
196
|
+
// shift between calls). undefined = not taken over → Radar renders the view
|
|
197
|
+
// itself. `clusterChecksHref` is the deprecated pre-1.7 hook, folded into the
|
|
198
|
+
// 'checks' target for back-compat.
|
|
199
|
+
const takeover: Record<FleetTakeoverTarget, string | undefined> = useMemo(
|
|
200
|
+
() => ({
|
|
201
|
+
issues: navCustomization.fleetTakeoverHref?.('issues'),
|
|
202
|
+
gitops: navCustomization.fleetTakeoverHref?.('gitops'),
|
|
203
|
+
checks: navCustomization.fleetTakeoverHref?.('checks') ?? navCustomization.clusterChecksHref?.(),
|
|
204
|
+
certs: navCustomization.fleetTakeoverHref?.('certs'),
|
|
205
|
+
}),
|
|
206
|
+
[navCustomization],
|
|
207
|
+
)
|
|
181
208
|
const { pinned: navRailPinned, togglePinned: toggleNavRailPinned } = useNavRailPinned()
|
|
182
209
|
// Standalone Radar gets the left nav rail; embedded hosts (Radar Hub) own
|
|
183
210
|
// the left chrome via their own fleet rail and keep Radar's top-bar pills.
|
|
@@ -257,6 +284,20 @@ function AppInner() {
|
|
|
257
284
|
|
|
258
285
|
// Set mainView by navigating to the path
|
|
259
286
|
const setMainView = useCallback((view: ExtendedMainView, params?: Record<string, string>) => {
|
|
287
|
+
// Host takeover: fleet-shaped views (issues/gitops/checks) are owned by the
|
|
288
|
+
// host's fleet pages. Hand straight to the host instead of navigating to
|
|
289
|
+
// our own /<view> first — that intermediate hop mounts the view machinery
|
|
290
|
+
// and flashes the "Opening…" splash before the redirect effect bounces out.
|
|
291
|
+
// Skipping it makes the hand-off a single smooth cross-tree swap. (Direct
|
|
292
|
+
// /<view> URL entry still funnels through the redirect effect below.)
|
|
293
|
+
if (view === 'issues' || view === 'gitops' || view === 'checks') {
|
|
294
|
+
const href = takeover[view]
|
|
295
|
+
if (href) {
|
|
296
|
+
goHost(href)
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
260
301
|
const path = view === 'home' ? '/' : `/${view}`
|
|
261
302
|
|
|
262
303
|
// Start fresh — keep only cross-view params (namespaces), discard all view-specific ones
|
|
@@ -274,23 +315,31 @@ function AppInner() {
|
|
|
274
315
|
}
|
|
275
316
|
|
|
276
317
|
navigate({ pathname: path, search: newParams.toString() })
|
|
277
|
-
}, [navigate, searchParams])
|
|
278
|
-
|
|
279
|
-
// Cloud (embedded)
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
|
|
318
|
+
}, [navigate, searchParams, takeover, goHost])
|
|
319
|
+
|
|
320
|
+
// Cloud (embedded) takes over the "fleet-shaped" per-cluster views with its
|
|
321
|
+
// own fleet pages scoped to this cluster — owned by the host's left rail — so
|
|
322
|
+
// Radar drops the matching pills (see the nav below). In-app nav hands off in
|
|
323
|
+
// setMainView (above); direct /<view> URL entry funnels through the redirect
|
|
324
|
+
// effect below. Both consume the memoized `takeover` resolved above. Standalone
|
|
325
|
+
// OSS (no fleetTakeoverHref) is unaffected and renders the in-app view.
|
|
326
|
+
//
|
|
327
|
+
// Has the host claimed this view? View-shaped targets only ('certs' has no
|
|
328
|
+
// Radar view — only its Home card consults `takeover`). Used to drop the nav
|
|
329
|
+
// pill and gate the inline view render in favor of the "Opening…" splash.
|
|
330
|
+
const isViewTakenOver = (view: ExtendedMainView): boolean =>
|
|
331
|
+
(view === 'issues' || view === 'gitops' || view === 'checks') && !!takeover[view]
|
|
332
|
+
// The host's URL for the CURRENT view, if taken over. Drives the redirect
|
|
333
|
+
// effect and the "Opening…" splash.
|
|
334
|
+
const viewTakeoverHref =
|
|
335
|
+
mainView === 'issues' || mainView === 'gitops' || mainView === 'checks'
|
|
336
|
+
? takeover[mainView]
|
|
337
|
+
: undefined
|
|
289
338
|
useEffect(() => {
|
|
290
|
-
if (
|
|
291
|
-
window.location.replace(
|
|
339
|
+
if (viewTakeoverHref) {
|
|
340
|
+
window.location.replace(viewTakeoverHref)
|
|
292
341
|
}
|
|
293
|
-
}, [
|
|
342
|
+
}, [viewTakeoverHref])
|
|
294
343
|
|
|
295
344
|
const [namespaces, setNamespaces] = useState<string[]>(getInitialState().namespaces)
|
|
296
345
|
// For large clusters: force SSE to reconnect with namespace filter
|
|
@@ -1281,14 +1330,15 @@ function AppInner() {
|
|
|
1281
1330
|
</span>
|
|
1282
1331
|
)}
|
|
1283
1332
|
{!connected && (
|
|
1333
|
+
<Tooltip content="Reconnect">
|
|
1284
1334
|
<button
|
|
1285
1335
|
onClick={reconnect}
|
|
1286
1336
|
disabled={isReconnecting}
|
|
1287
|
-
className="p-1 text-theme-text-secondary hover:text-theme-text-primary disabled:opacity-50"
|
|
1288
|
-
title="Reconnect"
|
|
1337
|
+
className="p-1 text-theme-text-secondary hover:text-theme-text-primary disabled:opacity-50 disabled:pointer-events-none"
|
|
1289
1338
|
>
|
|
1290
1339
|
<RefreshCw className={`w-3 h-3 ${isReconnecting ? 'animate-spin' : ''}`} />
|
|
1291
1340
|
</button>
|
|
1341
|
+
</Tooltip>
|
|
1292
1342
|
)}
|
|
1293
1343
|
</div>
|
|
1294
1344
|
{/* Port forwards indicator — shown only when sessions exist */}
|
|
@@ -1318,15 +1368,14 @@ function AppInner() {
|
|
|
1318
1368
|
// command palette (⌘K). Remove this comment to restore it.
|
|
1319
1369
|
{ view: 'checks' as const, icon: ShieldCheck, label: 'Checks' },
|
|
1320
1370
|
] as const)
|
|
1321
|
-
// In Cloud, Checks
|
|
1322
|
-
// left rail; the per-cluster view is just that fleet
|
|
1323
|
-
// to this cluster, so duplicating it as a peer pill here
|
|
1324
|
-
// second
|
|
1325
|
-
//
|
|
1326
|
-
// via the Home
|
|
1327
|
-
//
|
|
1328
|
-
|
|
1329
|
-
.filter(({ view }) => !(view === 'checks' && clusterChecksHref))
|
|
1371
|
+
// In Cloud, fleet-shaped views (Checks, Issues, GitOps) are owned by
|
|
1372
|
+
// the host's left rail; the per-cluster view is just that fleet page
|
|
1373
|
+
// filtered to this cluster, so duplicating it as a peer pill here
|
|
1374
|
+
// would be a second copy that teleports out of the cluster shell.
|
|
1375
|
+
// Drop any pill the host took over — cluster-scoped access stays
|
|
1376
|
+
// available via the Home cards (redirected by the takeover effect
|
|
1377
|
+
// above), ⌘K, and bookmarks. Standalone OSS keeps every pill.
|
|
1378
|
+
.filter(({ view }) => !isViewTakenOver(view))
|
|
1330
1379
|
.map(({ view, icon: Icon, label }) => (
|
|
1331
1380
|
<Tooltip key={view} content={label} delay={100} position="bottom">
|
|
1332
1381
|
<button
|
|
@@ -1382,8 +1431,6 @@ function AppInner() {
|
|
|
1382
1431
|
<div className="flex items-center gap-3 shrink-0">
|
|
1383
1432
|
<NamespaceSwitcher
|
|
1384
1433
|
ref={namespaceSwitcherRef}
|
|
1385
|
-
disabled={mainView === 'helm'}
|
|
1386
|
-
disabledTooltip="Helm view always shows all namespaces"
|
|
1387
1434
|
/>
|
|
1388
1435
|
|
|
1389
1436
|
|
|
@@ -1410,13 +1457,14 @@ function AppInner() {
|
|
|
1410
1457
|
|
|
1411
1458
|
{/* Local terminal */}
|
|
1412
1459
|
{capabilities.localTerminal && (
|
|
1460
|
+
<Tooltip content="Open local terminal">
|
|
1413
1461
|
<button
|
|
1414
1462
|
onClick={() => openLocalTerminal()}
|
|
1415
1463
|
className="p-1.5 rounded-md bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
1416
|
-
title="Open local terminal"
|
|
1417
1464
|
>
|
|
1418
1465
|
<SquareTerminal className="w-4 h-4" />
|
|
1419
1466
|
</button>
|
|
1467
|
+
</Tooltip>
|
|
1420
1468
|
)}
|
|
1421
1469
|
|
|
1422
1470
|
{/* Theme toggle — hidden in embedded mode. Host apps (e.g. Radar
|
|
@@ -1436,20 +1484,22 @@ function AppInner() {
|
|
|
1436
1484
|
old floating bottom-right pair. Settings moved to the rail bottom. */}
|
|
1437
1485
|
{showNavRail && (
|
|
1438
1486
|
<>
|
|
1487
|
+
<Tooltip content="Keyboard shortcuts (?)">
|
|
1439
1488
|
<button
|
|
1440
1489
|
onClick={() => setShowHelp(true)}
|
|
1441
1490
|
className="p-1.5 rounded-md bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
1442
|
-
title="Keyboard shortcuts (?)"
|
|
1443
1491
|
>
|
|
1444
1492
|
<HelpCircle className="w-4 h-4" />
|
|
1445
1493
|
</button>
|
|
1494
|
+
</Tooltip>
|
|
1495
|
+
<Tooltip content="Report a bug / Diagnostics">
|
|
1446
1496
|
<button
|
|
1447
1497
|
onClick={() => setShowDiagnostics(true)}
|
|
1448
1498
|
className="p-1.5 rounded-md bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
1449
|
-
title="Report a bug / Diagnostics"
|
|
1450
1499
|
>
|
|
1451
1500
|
<Bug className="w-4 h-4" />
|
|
1452
1501
|
</button>
|
|
1502
|
+
</Tooltip>
|
|
1453
1503
|
</>
|
|
1454
1504
|
)}
|
|
1455
1505
|
|
|
@@ -1611,6 +1661,15 @@ function AppInner() {
|
|
|
1611
1661
|
navigate({ pathname: `/resources/${kind}`, search: newParams.toString() })
|
|
1612
1662
|
}}
|
|
1613
1663
|
onNavigateToResource={navigateFromIssue}
|
|
1664
|
+
// Certs has no Radar view, so it can't ride the view-redirect effect
|
|
1665
|
+
// above — wire the Certificate Health card straight to the host's
|
|
1666
|
+
// fleet Certs page (scoped to this cluster) when claimed. `assign`
|
|
1667
|
+
// (not replace): the user is navigating forward from a card, so this
|
|
1668
|
+
// belongs in history. Omitted → the card falls back to Radar's own
|
|
1669
|
+
// TLS-secrets resource list.
|
|
1670
|
+
onNavigateToCerts={
|
|
1671
|
+
takeover.certs ? () => goHost(takeover.certs!) : undefined
|
|
1672
|
+
}
|
|
1614
1673
|
/>
|
|
1615
1674
|
)}
|
|
1616
1675
|
|
|
@@ -1746,10 +1805,9 @@ function AppInner() {
|
|
|
1746
1805
|
/>
|
|
1747
1806
|
)}
|
|
1748
1807
|
|
|
1749
|
-
{/* Helm view - always show all namespaces since releases span multiple ns */}
|
|
1750
1808
|
{mainView === 'helm' && (
|
|
1751
1809
|
<HelmView
|
|
1752
|
-
|
|
1810
|
+
namespaces={namespaces}
|
|
1753
1811
|
selectedRelease={selectedHelmRelease}
|
|
1754
1812
|
onReleaseClick={(ns, name, storageNamespace) => {
|
|
1755
1813
|
setSelectedHelmRelease({ namespace: ns, name, storageNamespace })
|
|
@@ -1765,8 +1823,9 @@ function AppInner() {
|
|
|
1765
1823
|
/>
|
|
1766
1824
|
)}
|
|
1767
1825
|
|
|
1768
|
-
{/* GitOps view
|
|
1769
|
-
|
|
1826
|
+
{/* GitOps view (inline only when the host hasn't taken it over — see
|
|
1827
|
+
the takeover splash below). */}
|
|
1828
|
+
{mainView === 'gitops' && !isViewTakenOver('gitops') && (
|
|
1770
1829
|
<GitOpsView
|
|
1771
1830
|
namespaces={namespaces}
|
|
1772
1831
|
onOpenResource={(resource) => {
|
|
@@ -1796,17 +1855,21 @@ function AppInner() {
|
|
|
1796
1855
|
<CostView onBack={() => setMainView('home')} />
|
|
1797
1856
|
)}
|
|
1798
1857
|
|
|
1799
|
-
{/*
|
|
1800
|
-
|
|
1801
|
-
splash instead of the
|
|
1802
|
-
nav lands.
|
|
1803
|
-
|
|
1858
|
+
{/* Takeover splash. When the host claims the current view via
|
|
1859
|
+
fleetTakeoverHref, the redirect effect above is mid-flight — render a
|
|
1860
|
+
brief splash instead of the inline view (which would flash + fire its
|
|
1861
|
+
own fetches) while the cross-document nav lands. Covers checks /
|
|
1862
|
+
issues / gitops with one block since only one view is active. */}
|
|
1863
|
+
{viewTakeoverHref && (
|
|
1804
1864
|
<div className="flex-1 flex flex-col items-center justify-center gap-3 bg-theme-base">
|
|
1805
1865
|
<img src={radarLoadingIcon} alt="" aria-hidden className="w-11 h-11" />
|
|
1806
|
-
<p className="text-sm text-theme-text-secondary">Opening
|
|
1866
|
+
<p className="text-sm text-theme-text-secondary">Opening…</p>
|
|
1807
1867
|
</div>
|
|
1808
1868
|
)}
|
|
1809
|
-
|
|
1869
|
+
|
|
1870
|
+
{/* Best practices detail view (inline only when the host hasn't taken
|
|
1871
|
+
Checks over — standalone OSS, or Cloud without a checks takeover). */}
|
|
1872
|
+
{mainView === 'checks' && !isViewTakenOver('checks') && (
|
|
1810
1873
|
<AuditView
|
|
1811
1874
|
namespaces={namespaces}
|
|
1812
1875
|
onNavigateToResource={navigateToResourceList}
|
|
@@ -1816,8 +1879,9 @@ function AppInner() {
|
|
|
1816
1879
|
{/* Issues — per-cluster live triage queue (hidden route: not yet in the
|
|
1817
1880
|
nav `views` list; reachable at /issues). Same shared <IssuesView> the
|
|
1818
1881
|
Hub fleet uses; a GitOps reconciler subject routes to its detail page,
|
|
1819
|
-
other resources open the standard resource view.
|
|
1820
|
-
|
|
1882
|
+
other resources open the standard resource view. Inline only when the
|
|
1883
|
+
host hasn't taken it over. */}
|
|
1884
|
+
{mainView === 'issues' && !isViewTakenOver('issues') && (
|
|
1821
1885
|
<IssuesPane
|
|
1822
1886
|
namespaces={namespaces}
|
|
1823
1887
|
onNavigateToResource={navigateFromIssue}
|
|
@@ -2206,10 +2270,10 @@ function ThemeToggle() {
|
|
|
2206
2270
|
const { theme, toggleTheme } = useTheme()
|
|
2207
2271
|
|
|
2208
2272
|
return (
|
|
2273
|
+
<Tooltip content={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}>
|
|
2209
2274
|
<button
|
|
2210
2275
|
onClick={toggleTheme}
|
|
2211
2276
|
className="p-1.5 rounded-md bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
2212
|
-
title={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
|
|
2213
2277
|
>
|
|
2214
2278
|
{theme === 'dark' ? (
|
|
2215
2279
|
<Sun className="w-4 h-4" />
|
|
@@ -2217,6 +2281,7 @@ function ThemeToggle() {
|
|
|
2217
2281
|
<Moon className="w-4 h-4" />
|
|
2218
2282
|
)}
|
|
2219
2283
|
</button>
|
|
2284
|
+
</Tooltip>
|
|
2220
2285
|
)
|
|
2221
2286
|
}
|
|
2222
2287
|
|
package/src/api/client.ts
CHANGED
|
@@ -2311,11 +2311,15 @@ export function useDrainNode() {
|
|
|
2311
2311
|
// Helm API hooks
|
|
2312
2312
|
// ============================================================================
|
|
2313
2313
|
|
|
2314
|
+
function helmNamespaceParams(namespaces: string[] = []) {
|
|
2315
|
+
return namespaces.length > 0 ? `?namespaces=${namespaces.join(',')}` : ''
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2314
2318
|
// List all Helm releases
|
|
2315
|
-
export function useHelmReleases(
|
|
2316
|
-
const params =
|
|
2319
|
+
export function useHelmReleases(namespaces: string[] = []) {
|
|
2320
|
+
const params = helmNamespaceParams(namespaces)
|
|
2317
2321
|
return useQuery<HelmRelease[]>({
|
|
2318
|
-
queryKey: ['helm-releases',
|
|
2322
|
+
queryKey: ['helm-releases', namespaces],
|
|
2319
2323
|
queryFn: () => fetchJSON(`/helm/releases${params}`),
|
|
2320
2324
|
staleTime: 30000, // 30 seconds
|
|
2321
2325
|
})
|
|
@@ -2393,10 +2397,10 @@ export function useHelmUpgradeInfo(namespace: string, name: string, enabled = tr
|
|
|
2393
2397
|
}
|
|
2394
2398
|
|
|
2395
2399
|
// Batch check for upgrade availability (for list view)
|
|
2396
|
-
export function useHelmBatchUpgradeInfo(
|
|
2397
|
-
const params =
|
|
2400
|
+
export function useHelmBatchUpgradeInfo(namespaces: string[] = [], enabled = true) {
|
|
2401
|
+
const params = helmNamespaceParams(namespaces)
|
|
2398
2402
|
return useQuery<BatchUpgradeInfo>({
|
|
2399
|
-
queryKey: ['helm-batch-upgrade-info',
|
|
2403
|
+
queryKey: ['helm-batch-upgrade-info', namespaces],
|
|
2400
2404
|
queryFn: () => fetchJSON(`/helm/upgrade-check${params}`),
|
|
2401
2405
|
enabled,
|
|
2402
2406
|
staleTime: 30000, // 30 seconds - keep in sync with release list
|
|
@@ -5,6 +5,7 @@ import { ContextSwitcher } from './ContextSwitcher'
|
|
|
5
5
|
import { parseContextName } from '../utils/context-name'
|
|
6
6
|
import { useOpenLocalTerminal, ClusterName } from '@skyhook-io/k8s-ui'
|
|
7
7
|
import { useAuthMe } from '../api/client'
|
|
8
|
+
import { Tooltip } from './ui/Tooltip'
|
|
8
9
|
|
|
9
10
|
interface ConnectionErrorViewProps {
|
|
10
11
|
connection: ConnectionState
|
|
@@ -137,18 +138,19 @@ function CopyableCommand({ command, onRunInTerminal }: { command: string; onRunI
|
|
|
137
138
|
{command}
|
|
138
139
|
</code>
|
|
139
140
|
{onRunInTerminal && (
|
|
141
|
+
<Tooltip content="Run in terminal" wrapperClassName="shrink-0">
|
|
140
142
|
<button
|
|
141
143
|
onClick={() => onRunInTerminal(command)}
|
|
142
144
|
className="shrink-0 text-theme-text-tertiary hover:text-theme-text-secondary transition-colors"
|
|
143
|
-
title="Run in terminal"
|
|
144
145
|
>
|
|
145
146
|
<TerminalSquare className="w-3.5 h-3.5" />
|
|
146
147
|
</button>
|
|
148
|
+
</Tooltip>
|
|
147
149
|
)}
|
|
150
|
+
<Tooltip content="Copy to clipboard" wrapperClassName="shrink-0">
|
|
148
151
|
<button
|
|
149
152
|
onClick={handleCopy}
|
|
150
153
|
className="shrink-0 text-theme-text-tertiary hover:text-theme-text-secondary transition-colors"
|
|
151
|
-
title="Copy to clipboard"
|
|
152
154
|
>
|
|
153
155
|
{copied ? (
|
|
154
156
|
<Check className="w-3.5 h-3.5 text-green-400" />
|
|
@@ -156,6 +158,7 @@ function CopyableCommand({ command, onRunInTerminal }: { command: string; onRunI
|
|
|
156
158
|
<Copy className="w-3.5 h-3.5" />
|
|
157
159
|
)}
|
|
158
160
|
</button>
|
|
161
|
+
</Tooltip>
|
|
159
162
|
</div>
|
|
160
163
|
)
|
|
161
164
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
2
|
import { Bug, X, ChevronDown, ChevronUp } from 'lucide-react'
|
|
3
3
|
import { useRuntimeStats } from '../api/client'
|
|
4
|
+
import { Tooltip } from './ui/Tooltip'
|
|
4
5
|
|
|
5
6
|
function formatUptime(seconds: number): string {
|
|
6
7
|
if (seconds < 60) return `${seconds}s`
|
|
@@ -17,13 +18,14 @@ export function DebugOverlay() {
|
|
|
17
18
|
|
|
18
19
|
if (!visible) {
|
|
19
20
|
return (
|
|
21
|
+
<Tooltip content="Show debug stats" position="left" wrapperClassName="fixed bottom-3 right-3 z-50">
|
|
20
22
|
<button
|
|
21
23
|
onClick={() => setVisible(true)}
|
|
22
|
-
className="
|
|
23
|
-
title="Show debug stats"
|
|
24
|
+
className="p-2 bg-theme-surface/90 border border-theme-border rounded-lg text-theme-text-tertiary hover:text-theme-text-secondary transition-colors"
|
|
24
25
|
>
|
|
25
26
|
<Bug className="w-4 h-4" />
|
|
26
27
|
</button>
|
|
28
|
+
</Tooltip>
|
|
27
29
|
)
|
|
28
30
|
}
|
|
29
31
|
|
|
@@ -2,6 +2,7 @@ import { useState, useRef, useEffect, useCallback } from 'react'
|
|
|
2
2
|
import { User, LogOut } from 'lucide-react'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
4
|
import { useAuthMe } from '../api/client'
|
|
5
|
+
import { Tooltip } from './ui/Tooltip'
|
|
5
6
|
import { useQueryClient } from '@tanstack/react-query'
|
|
6
7
|
|
|
7
8
|
interface UserMenuProps {
|
|
@@ -67,9 +68,9 @@ export function UserMenu({ variant = 'topbar', pinned = true }: UserMenuProps =
|
|
|
67
68
|
return (
|
|
68
69
|
<div ref={menuRef} className={clsx('relative', isRail && 'group/item', isRail && !pinned && 'w-10')}>
|
|
69
70
|
{isRail ? (
|
|
71
|
+
<Tooltip content={authMe.username} position="right" wrapperClassName="!block w-full">
|
|
70
72
|
<button
|
|
71
73
|
onClick={() => setIsOpen(!isOpen)}
|
|
72
|
-
title={authMe.username}
|
|
73
74
|
className={clsx(
|
|
74
75
|
'relative flex h-9 w-full items-center rounded-md text-sm font-medium text-theme-text-secondary hover:bg-theme-hover hover:text-theme-text-primary transition-colors',
|
|
75
76
|
!pinned && 'max-w-10 overflow-hidden',
|
|
@@ -78,14 +79,16 @@ export function UserMenu({ variant = 'topbar', pinned = true }: UserMenuProps =
|
|
|
78
79
|
<span className="flex w-10 shrink-0 items-center justify-center">{avatar}</span>
|
|
79
80
|
<span className={clsx('pr-3 truncate', !pinned && 'opacity-0')}>{authMe.username.split('@')[0]}</span>
|
|
80
81
|
</button>
|
|
82
|
+
</Tooltip>
|
|
81
83
|
) : (
|
|
84
|
+
<Tooltip content={authMe.username}>
|
|
82
85
|
<button
|
|
83
86
|
onClick={() => setIsOpen(!isOpen)}
|
|
84
87
|
className="w-7 h-7 rounded-full bg-blue-500/15 text-blue-500 flex items-center justify-center text-xs font-medium hover:bg-blue-500/25 transition-colors"
|
|
85
|
-
title={authMe.username}
|
|
86
88
|
>
|
|
87
89
|
{initials || <User className="w-3.5 h-3.5" />}
|
|
88
90
|
</button>
|
|
91
|
+
</Tooltip>
|
|
89
92
|
)}
|
|
90
93
|
|
|
91
94
|
{/* Slim-rail fly-out label (account row, collapsed) */}
|
|
@@ -4,6 +4,7 @@ import { clsx } from 'clsx'
|
|
|
4
4
|
import { useAuditSettings, useUpdateAuditSettings, useAudit, useCloudRole } from '../../api/client'
|
|
5
5
|
import type { CheckMeta } from '@skyhook-io/k8s-ui'
|
|
6
6
|
import { validateRFC1123Label, type ValidationResult } from '@skyhook-io/k8s-ui/utils/validators'
|
|
7
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
7
8
|
|
|
8
9
|
interface AuditSettingsDialogProps {
|
|
9
10
|
namespaces: string[]
|
|
@@ -192,6 +193,17 @@ export function AuditSettingsDialog({ namespaces, onClose }: AuditSettingsDialog
|
|
|
192
193
|
>
|
|
193
194
|
Cancel
|
|
194
195
|
</button>
|
|
196
|
+
<Tooltip
|
|
197
|
+
content={
|
|
198
|
+
!canEdit
|
|
199
|
+
? 'Audit settings can only be changed by owners'
|
|
200
|
+
: newNsError
|
|
201
|
+
? 'Fix or clear the pending namespace input before saving'
|
|
202
|
+
: newNsDuplicate
|
|
203
|
+
? 'Clear the duplicate pending input before saving'
|
|
204
|
+
: ''
|
|
205
|
+
}
|
|
206
|
+
>
|
|
195
207
|
<button
|
|
196
208
|
onClick={handleSave}
|
|
197
209
|
// Block save while the namespace input has unfixed pending
|
|
@@ -200,19 +212,11 @@ export function AuditSettingsDialog({ namespaces, onClose }: AuditSettingsDialog
|
|
|
200
212
|
disabled={
|
|
201
213
|
!canEdit || updateSettings.isPending || newNsError !== null || newNsDuplicate
|
|
202
214
|
}
|
|
203
|
-
|
|
204
|
-
!canEdit
|
|
205
|
-
? 'Audit settings can only be changed by owners'
|
|
206
|
-
: newNsError
|
|
207
|
-
? 'Fix or clear the pending namespace input before saving'
|
|
208
|
-
: newNsDuplicate
|
|
209
|
-
? 'Clear the duplicate pending input before saving'
|
|
210
|
-
: undefined
|
|
211
|
-
}
|
|
212
|
-
className="px-4 py-1.5 text-sm btn-brand rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
|
215
|
+
className="px-4 py-1.5 text-sm btn-brand rounded-lg disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none"
|
|
213
216
|
>
|
|
214
217
|
{updateSettings.isPending ? 'Saving...' : 'Save'}
|
|
215
218
|
</button>
|
|
219
|
+
</Tooltip>
|
|
216
220
|
</div>
|
|
217
221
|
</div>
|
|
218
222
|
</div>
|
|
@@ -4,6 +4,7 @@ import type { SelectedResource } from '../../types'
|
|
|
4
4
|
import { ChecksView, PaneLoader, PageHeader, type CheckResourceRef } from '@skyhook-io/k8s-ui'
|
|
5
5
|
import { ShieldCheck, Settings } from 'lucide-react'
|
|
6
6
|
import { AuditSettingsDialog } from './AuditSettingsDialog'
|
|
7
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
7
8
|
|
|
8
9
|
interface AuditViewProps {
|
|
9
10
|
namespaces: string[]
|
|
@@ -82,13 +83,14 @@ export function AuditView({ namespaces, onNavigateToResource }: AuditViewProps)
|
|
|
82
83
|
{ignoredCount > 0 && (
|
|
83
84
|
<button onClick={() => setShowSettings(true)} className="text-xs text-theme-text-tertiary hover:text-theme-text-secondary transition-colors">{ignoredCount} {ignoredCount === 1 ? 'namespace' : 'namespaces'} hidden</button>
|
|
84
85
|
)}
|
|
86
|
+
<Tooltip content="Checks settings">
|
|
85
87
|
<button
|
|
86
88
|
onClick={() => setShowSettings(true)}
|
|
87
89
|
className="p-2 rounded-lg hover:bg-theme-hover text-theme-text-tertiary hover:text-theme-text-secondary transition-colors"
|
|
88
|
-
title="Checks settings"
|
|
89
90
|
>
|
|
90
91
|
<Settings className="w-4 h-4" />
|
|
91
92
|
</button>
|
|
93
|
+
</Tooltip>
|
|
92
94
|
</>
|
|
93
95
|
}
|
|
94
96
|
/>
|
|
@@ -31,15 +31,18 @@ export function useCompareLauncher({ kind, namespace, name, group }: UseCompareL
|
|
|
31
31
|
const [open, setOpen] = useState(false)
|
|
32
32
|
const kindLower = kind.toLowerCase()
|
|
33
33
|
const { candidates, isPending, error } = useCompareCandidates(kindLower, group, open)
|
|
34
|
-
const { crossClusterCompareHref } = useNavCustomization()
|
|
34
|
+
const { crossClusterCompareHref, onHostNavigate } = useNavCustomization()
|
|
35
35
|
|
|
36
36
|
const onCompareTo = useCallback(() => setOpen(true), [])
|
|
37
37
|
|
|
38
38
|
const onCompareAcrossClusters = useCallback(() => {
|
|
39
39
|
if (!crossClusterCompareHref) return
|
|
40
40
|
const href = crossClusterCompareHref({ kind: kindLower, namespace, name, group })
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// Smooth same-document hand-off when the host supports it (Radar Cloud's
|
|
42
|
+
// cross-tree swap), else a hard nav — same contract as App.tsx's goHost.
|
|
43
|
+
if (onHostNavigate) onHostNavigate(href)
|
|
44
|
+
else window.location.assign(href)
|
|
45
|
+
}, [crossClusterCompareHref, onHostNavigate, kindLower, namespace, name, group])
|
|
43
46
|
|
|
44
47
|
const handlePick = useCallback(
|
|
45
48
|
(picked: CompareResourceRef) => {
|
|
@@ -4,6 +4,7 @@ import type { OpenCostNamespaceCost, OpenCostWorkloadCost, OpenCostNodeCost } fr
|
|
|
4
4
|
import { ArrowLeft, ChevronDown, ChevronRight, DollarSign, HelpCircle, Loader2, Server, X } from 'lucide-react'
|
|
5
5
|
import { PaneLoader } from '@skyhook-io/k8s-ui'
|
|
6
6
|
import { CostTrendChart } from './CostTrendChart'
|
|
7
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
7
8
|
|
|
8
9
|
interface CostViewProps {
|
|
9
10
|
onBack: () => void
|
|
@@ -177,8 +178,12 @@ export function CostView({ onBack }: CostViewProps) {
|
|
|
177
178
|
<div className="grid grid-cols-[minmax(180px,1fr)_90px_90px_80px_minmax(160px,1fr)_120px] gap-2 px-4 py-2 border-b border-theme-border text-[11px] font-medium text-theme-text-tertiary uppercase tracking-wider">
|
|
178
179
|
<span>Namespace</span>
|
|
179
180
|
<span className="text-right">Hourly</span>
|
|
180
|
-
<
|
|
181
|
-
<span className="
|
|
181
|
+
<Tooltip content="Projected from current hourly rate — not historical spend" wrapperClassName="!block text-right">
|
|
182
|
+
<span className="cursor-help">Monthly*</span>
|
|
183
|
+
</Tooltip>
|
|
184
|
+
<Tooltip content="% of reserved resources actually being used, weighted by cost" wrapperClassName="!block text-right">
|
|
185
|
+
<span className="cursor-help">Efficiency</span>
|
|
186
|
+
</Tooltip>
|
|
182
187
|
<span>CPU / Memory</span>
|
|
183
188
|
<span className="text-right">Cost Split</span>
|
|
184
189
|
</div>
|
|
@@ -368,7 +373,9 @@ function NodeCostTable({ nodes }: { nodes: OpenCostNodeCost[] }) {
|
|
|
368
373
|
<span>Node</span>
|
|
369
374
|
<span>Instance Type</span>
|
|
370
375
|
<span className="text-right">Hourly</span>
|
|
371
|
-
<
|
|
376
|
+
<Tooltip content="Projected from current hourly rate — not historical spend" wrapperClassName="!block text-right">
|
|
377
|
+
<span className="cursor-help">Monthly*</span>
|
|
378
|
+
</Tooltip>
|
|
372
379
|
<span className="text-right">CPU / Memory</span>
|
|
373
380
|
</div>
|
|
374
381
|
|
|
@@ -387,9 +394,11 @@ function NodeCostRow({ node }: { node: OpenCostNodeCost }) {
|
|
|
387
394
|
|
|
388
395
|
return (
|
|
389
396
|
<div className="grid grid-cols-[minmax(200px,1fr)_minmax(120px,1fr)_90px_100px_140px] gap-2 px-4 py-2.5">
|
|
390
|
-
<
|
|
397
|
+
<Tooltip content={node.name} wrapperClassName="!block min-w-0">
|
|
398
|
+
<span className="text-sm text-theme-text-primary truncate font-medium block">
|
|
391
399
|
{node.name}
|
|
392
400
|
</span>
|
|
401
|
+
</Tooltip>
|
|
393
402
|
<span className="text-xs text-theme-text-secondary truncate">
|
|
394
403
|
{node.instanceType || '-'}
|
|
395
404
|
{node.region && <span className="text-theme-text-quaternary ml-1.5">({node.region})</span>}
|