@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.
- package/package.json +8 -2
- package/src/App.tsx +58 -30
- 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 +3 -2
- 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/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 +19 -5
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
|
@@ -179,6 +179,32 @@ function AppInner() {
|
|
|
179
179
|
const capabilities = useCapabilitiesContext()
|
|
180
180
|
const openLocalTerminal = useOpenLocalTerminal()
|
|
181
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
|
+
)
|
|
182
208
|
const { pinned: navRailPinned, togglePinned: toggleNavRailPinned } = useNavRailPinned()
|
|
183
209
|
// Standalone Radar gets the left nav rail; embedded hosts (Radar Hub) own
|
|
184
210
|
// the left chrome via their own fleet rail and keep Radar's top-bar pills.
|
|
@@ -258,6 +284,20 @@ function AppInner() {
|
|
|
258
284
|
|
|
259
285
|
// Set mainView by navigating to the path
|
|
260
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
|
+
|
|
261
301
|
const path = view === 'home' ? '/' : `/${view}`
|
|
262
302
|
|
|
263
303
|
// Start fresh — keep only cross-view params (namespaces), discard all view-specific ones
|
|
@@ -275,32 +315,15 @@ function AppInner() {
|
|
|
275
315
|
}
|
|
276
316
|
|
|
277
317
|
navigate({ pathname: path, search: newParams.toString() })
|
|
278
|
-
}, [navigate, searchParams])
|
|
318
|
+
}, [navigate, searchParams, takeover, goHost])
|
|
279
319
|
|
|
280
320
|
// Cloud (embedded) takes over the "fleet-shaped" per-cluster views with its
|
|
281
321
|
// own fleet pages scoped to this cluster — owned by the host's left rail — so
|
|
282
|
-
// Radar drops the matching pills (see the nav below)
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
// getViewFromPath). `replace` (not assign) keeps the transient /<view> URL
|
|
288
|
-
// out of history so Back doesn't bounce off the redirect. Standalone OSS (no
|
|
289
|
-
// fleetTakeoverHref) is unaffected and renders the in-app view as before.
|
|
290
|
-
// Resolve every takeover target's host URL ONCE per render so the redirect
|
|
291
|
-
// effect, nav-pill filtering, inline-view gating, and the cert click handler
|
|
292
|
-
// all close over the SAME value — host callbacks aren't guaranteed idempotent
|
|
293
|
-
// (scope / flags / signed URLs can shift between calls). undefined = not taken
|
|
294
|
-
// over → Radar renders the view itself. `clusterChecksHref` is the deprecated
|
|
295
|
-
// pre-1.7 hook, folded into the 'checks' target for back-compat.
|
|
296
|
-
const fleetTakeoverHref = navCustomization.fleetTakeoverHref
|
|
297
|
-
const clusterChecksHref = navCustomization.clusterChecksHref
|
|
298
|
-
const takeover: Record<FleetTakeoverTarget, string | undefined> = {
|
|
299
|
-
issues: fleetTakeoverHref?.('issues'),
|
|
300
|
-
gitops: fleetTakeoverHref?.('gitops'),
|
|
301
|
-
checks: fleetTakeoverHref?.('checks') ?? clusterChecksHref?.(),
|
|
302
|
-
certs: fleetTakeoverHref?.('certs'),
|
|
303
|
-
}
|
|
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
|
+
//
|
|
304
327
|
// Has the host claimed this view? View-shaped targets only ('certs' has no
|
|
305
328
|
// Radar view — only its Home card consults `takeover`). Used to drop the nav
|
|
306
329
|
// pill and gate the inline view render in favor of the "Opening…" splash.
|
|
@@ -1307,14 +1330,15 @@ function AppInner() {
|
|
|
1307
1330
|
</span>
|
|
1308
1331
|
)}
|
|
1309
1332
|
{!connected && (
|
|
1333
|
+
<Tooltip content="Reconnect">
|
|
1310
1334
|
<button
|
|
1311
1335
|
onClick={reconnect}
|
|
1312
1336
|
disabled={isReconnecting}
|
|
1313
|
-
className="p-1 text-theme-text-secondary hover:text-theme-text-primary disabled:opacity-50"
|
|
1314
|
-
title="Reconnect"
|
|
1337
|
+
className="p-1 text-theme-text-secondary hover:text-theme-text-primary disabled:opacity-50 disabled:pointer-events-none"
|
|
1315
1338
|
>
|
|
1316
1339
|
<RefreshCw className={`w-3 h-3 ${isReconnecting ? 'animate-spin' : ''}`} />
|
|
1317
1340
|
</button>
|
|
1341
|
+
</Tooltip>
|
|
1318
1342
|
)}
|
|
1319
1343
|
</div>
|
|
1320
1344
|
{/* Port forwards indicator — shown only when sessions exist */}
|
|
@@ -1433,13 +1457,14 @@ function AppInner() {
|
|
|
1433
1457
|
|
|
1434
1458
|
{/* Local terminal */}
|
|
1435
1459
|
{capabilities.localTerminal && (
|
|
1460
|
+
<Tooltip content="Open local terminal">
|
|
1436
1461
|
<button
|
|
1437
1462
|
onClick={() => openLocalTerminal()}
|
|
1438
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"
|
|
1439
|
-
title="Open local terminal"
|
|
1440
1464
|
>
|
|
1441
1465
|
<SquareTerminal className="w-4 h-4" />
|
|
1442
1466
|
</button>
|
|
1467
|
+
</Tooltip>
|
|
1443
1468
|
)}
|
|
1444
1469
|
|
|
1445
1470
|
{/* Theme toggle — hidden in embedded mode. Host apps (e.g. Radar
|
|
@@ -1459,20 +1484,22 @@ function AppInner() {
|
|
|
1459
1484
|
old floating bottom-right pair. Settings moved to the rail bottom. */}
|
|
1460
1485
|
{showNavRail && (
|
|
1461
1486
|
<>
|
|
1487
|
+
<Tooltip content="Keyboard shortcuts (?)">
|
|
1462
1488
|
<button
|
|
1463
1489
|
onClick={() => setShowHelp(true)}
|
|
1464
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"
|
|
1465
|
-
title="Keyboard shortcuts (?)"
|
|
1466
1491
|
>
|
|
1467
1492
|
<HelpCircle className="w-4 h-4" />
|
|
1468
1493
|
</button>
|
|
1494
|
+
</Tooltip>
|
|
1495
|
+
<Tooltip content="Report a bug / Diagnostics">
|
|
1469
1496
|
<button
|
|
1470
1497
|
onClick={() => setShowDiagnostics(true)}
|
|
1471
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"
|
|
1472
|
-
title="Report a bug / Diagnostics"
|
|
1473
1499
|
>
|
|
1474
1500
|
<Bug className="w-4 h-4" />
|
|
1475
1501
|
</button>
|
|
1502
|
+
</Tooltip>
|
|
1476
1503
|
</>
|
|
1477
1504
|
)}
|
|
1478
1505
|
|
|
@@ -1641,7 +1668,7 @@ function AppInner() {
|
|
|
1641
1668
|
// belongs in history. Omitted → the card falls back to Radar's own
|
|
1642
1669
|
// TLS-secrets resource list.
|
|
1643
1670
|
onNavigateToCerts={
|
|
1644
|
-
takeover.certs ? () =>
|
|
1671
|
+
takeover.certs ? () => goHost(takeover.certs!) : undefined
|
|
1645
1672
|
}
|
|
1646
1673
|
/>
|
|
1647
1674
|
)}
|
|
@@ -2243,10 +2270,10 @@ function ThemeToggle() {
|
|
|
2243
2270
|
const { theme, toggleTheme } = useTheme()
|
|
2244
2271
|
|
|
2245
2272
|
return (
|
|
2273
|
+
<Tooltip content={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}>
|
|
2246
2274
|
<button
|
|
2247
2275
|
onClick={toggleTheme}
|
|
2248
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"
|
|
2249
|
-
title={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
|
|
2250
2277
|
>
|
|
2251
2278
|
{theme === 'dark' ? (
|
|
2252
2279
|
<Sun className="w-4 h-4" />
|
|
@@ -2254,6 +2281,7 @@ function ThemeToggle() {
|
|
|
2254
2281
|
<Moon className="w-4 h-4" />
|
|
2255
2282
|
)}
|
|
2256
2283
|
</button>
|
|
2284
|
+
</Tooltip>
|
|
2257
2285
|
)
|
|
2258
2286
|
}
|
|
2259
2287
|
|
|
@@ -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>}
|
|
@@ -316,15 +316,16 @@ export function ChartBrowser({ onChartSelect }: ChartBrowserProps) {
|
|
|
316
316
|
) : (
|
|
317
317
|
<div className="text-sm mt-1 flex flex-col items-center gap-2">
|
|
318
318
|
<p>Your repositories may be out of date.</p>
|
|
319
|
+
<Tooltip content={canHelmWrite ? 'Run helm repo update on every configured repository' : helmWriteReason}>
|
|
319
320
|
<button
|
|
320
321
|
onClick={handleUpdateAllRepos}
|
|
321
322
|
disabled={isBatchUpdating || !canHelmWrite}
|
|
322
|
-
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs btn-brand rounded disabled:opacity-50"
|
|
323
|
-
title={canHelmWrite ? 'Run helm repo update on every configured repository' : helmWriteReason}
|
|
323
|
+
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs btn-brand rounded disabled:opacity-50 disabled:pointer-events-none"
|
|
324
324
|
>
|
|
325
325
|
<RefreshCw className={`w-3.5 h-3.5 ${isBatchUpdating ? 'animate-spin' : ''}`} />
|
|
326
326
|
{isBatchUpdating ? 'Updating…' : 'Update all repositories'}
|
|
327
327
|
</button>
|
|
328
|
+
</Tooltip>
|
|
328
329
|
<button
|
|
329
330
|
onClick={() => setChartSource('artifacthub')}
|
|
330
331
|
className="text-xs text-blue-400 hover:underline"
|
|
@@ -436,14 +437,15 @@ function RepoDropdownItem({ repo, isSelected, onSelect, onUpdate, isUpdating, ca
|
|
|
436
437
|
</span>
|
|
437
438
|
)}
|
|
438
439
|
</button>
|
|
440
|
+
<Tooltip content={canUpdate ? "Update repository" : (cantUpdateReason ?? "Helm write permissions required")}>
|
|
439
441
|
<button
|
|
440
442
|
onClick={(e) => { e.stopPropagation(); onUpdate() }}
|
|
441
443
|
disabled={isUpdating || !canUpdate}
|
|
442
|
-
className="p-1 text-theme-text-tertiary hover:text-theme-text-primary opacity-0 group-hover:opacity-100 transition-opacity disabled:opacity-50"
|
|
443
|
-
title={canUpdate ? "Update repository" : (cantUpdateReason ?? "Helm write permissions required")}
|
|
444
|
+
className="p-1 text-theme-text-tertiary hover:text-theme-text-primary opacity-0 group-hover:opacity-100 transition-opacity disabled:opacity-50 disabled:pointer-events-none"
|
|
444
445
|
>
|
|
445
446
|
<RefreshCw className={clsx('w-3.5 h-3.5', isUpdating && 'animate-spin')} />
|
|
446
447
|
</button>
|
|
448
|
+
</Tooltip>
|
|
447
449
|
</div>
|
|
448
450
|
)
|
|
449
451
|
}
|
|
@@ -10,6 +10,7 @@ import { clsx } from 'clsx'
|
|
|
10
10
|
import { useHelmRelease, useHelmManifest, useHelmValues, useHelmManifestDiff, useHelmUpgradeInfo, useHelmUninstall, upgradeWithProgress, rollbackWithProgress } from '../../api/client'
|
|
11
11
|
import { useQueryClient } from '@tanstack/react-query'
|
|
12
12
|
import { ConfirmDialog } from '../ui/ConfirmDialog'
|
|
13
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
13
14
|
import { Markdown } from '../ui/Markdown'
|
|
14
15
|
import type { SelectedHelmRelease, HelmHook, ChartDependency } from '../../types'
|
|
15
16
|
import type { NavigateToResource } from '../../utils/navigation'
|
|
@@ -334,63 +335,72 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
334
335
|
checking...
|
|
335
336
|
</span>
|
|
336
337
|
) : upgradeError ? (
|
|
338
|
+
<Tooltip content={upgradeErrorMessage ?? ''}>
|
|
337
339
|
<span
|
|
338
340
|
className="badge bg-theme-hover/50 text-theme-text-secondary"
|
|
339
|
-
title={upgradeErrorMessage}
|
|
340
341
|
>
|
|
341
342
|
upgrade check failed
|
|
342
343
|
</span>
|
|
344
|
+
</Tooltip>
|
|
343
345
|
) : upgradeInfo?.updateAvailable ? (
|
|
346
|
+
<Tooltip content={canHelmWrite ? `Click to upgrade: ${upgradeInfo.currentVersion} → ${upgradeInfo.latestVersion}${upgradeInfo.repositoryName ? ` (${upgradeInfo.repositoryName})` : ''}` : helmActReason}>
|
|
344
347
|
<button
|
|
345
348
|
onClick={() => setShowUpgradeConfirm(true)}
|
|
346
349
|
disabled={!canHelmWrite}
|
|
347
350
|
className={clsx(
|
|
348
|
-
'badge transition-colors', SEVERITY_BADGE.warning,
|
|
351
|
+
'badge transition-colors disabled:pointer-events-none', SEVERITY_BADGE.warning,
|
|
349
352
|
canHelmWrite ? 'hover:bg-amber-500/30 cursor-pointer' : 'opacity-50 cursor-not-allowed'
|
|
350
353
|
)}
|
|
351
|
-
title={canHelmWrite ? `Click to upgrade: ${upgradeInfo.currentVersion} → ${upgradeInfo.latestVersion}${upgradeInfo.repositoryName ? ` (${upgradeInfo.repositoryName})` : ''}` : helmActReason}
|
|
352
354
|
>
|
|
353
355
|
<ArrowUpCircle className="w-3 h-3" />
|
|
354
356
|
{upgradeInfo.latestVersion}
|
|
355
357
|
</button>
|
|
358
|
+
</Tooltip>
|
|
356
359
|
) : upgradeInfo && !upgradeInfo.error ? (
|
|
357
|
-
<
|
|
360
|
+
<Tooltip content="Chart is up to date">
|
|
361
|
+
<span className={clsx('badge', SEVERITY_BADGE.success)}>
|
|
358
362
|
latest
|
|
359
363
|
</span>
|
|
364
|
+
</Tooltip>
|
|
360
365
|
) : upgradeInfo?.error ? (
|
|
366
|
+
<Tooltip content={upgradeInfo.error}>
|
|
361
367
|
<span
|
|
362
368
|
className="badge bg-theme-hover/50 text-theme-text-secondary"
|
|
363
|
-
title={upgradeInfo.error}
|
|
364
369
|
>
|
|
365
370
|
upstream unknown
|
|
366
371
|
</span>
|
|
372
|
+
</Tooltip>
|
|
367
373
|
) : null}
|
|
368
374
|
</div>
|
|
369
375
|
<div className="flex items-center gap-1">
|
|
376
|
+
<Tooltip content="Refresh">
|
|
370
377
|
<button
|
|
371
378
|
onClick={refetch}
|
|
372
379
|
disabled={isRefreshAnimating}
|
|
373
|
-
className="p-1.5 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded disabled:opacity-50"
|
|
374
|
-
title="Refresh"
|
|
380
|
+
className="p-1.5 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded disabled:opacity-50 disabled:pointer-events-none"
|
|
375
381
|
>
|
|
376
382
|
<RefreshCw className={clsx('w-4 h-4', isRefreshAnimating && 'animate-spin')} />
|
|
377
383
|
</button>
|
|
384
|
+
</Tooltip>
|
|
385
|
+
<Tooltip content={canHelmWrite ? 'Uninstall release' : helmActReason}>
|
|
378
386
|
<button
|
|
379
387
|
onClick={() => setShowUninstallConfirm(true)}
|
|
380
388
|
disabled={!canHelmWrite}
|
|
381
389
|
className={clsx(
|
|
382
|
-
'p-1.5 rounded',
|
|
390
|
+
'p-1.5 rounded disabled:pointer-events-none',
|
|
383
391
|
canHelmWrite
|
|
384
392
|
? 'text-theme-text-secondary hover:text-red-400 hover:bg-red-500/10'
|
|
385
393
|
: 'text-theme-text-disabled cursor-not-allowed'
|
|
386
394
|
)}
|
|
387
|
-
title={canHelmWrite ? 'Uninstall release' : helmActReason}
|
|
388
395
|
>
|
|
389
396
|
<Trash2 className="w-4 h-4" />
|
|
390
397
|
</button>
|
|
391
|
-
|
|
398
|
+
</Tooltip>
|
|
399
|
+
<Tooltip content="Close (Esc)">
|
|
400
|
+
<button onClick={onClose} className="p-1.5 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded">
|
|
392
401
|
<X className="w-4 h-4" />
|
|
393
402
|
</button>
|
|
403
|
+
</Tooltip>
|
|
394
404
|
</div>
|
|
395
405
|
</div>
|
|
396
406
|
|
|
@@ -399,28 +409,30 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
399
409
|
<div className="flex items-center gap-2">
|
|
400
410
|
<Package className="w-5 h-5 text-purple-400" />
|
|
401
411
|
<h2 className="text-lg font-semibold text-theme-text-primary truncate">{release.name}</h2>
|
|
412
|
+
<Tooltip content="Copy name" wrapperClassName="shrink-0">
|
|
402
413
|
<button
|
|
403
414
|
onClick={() => copyToClipboard(release.name, 'name')}
|
|
404
415
|
className="p-1 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded shrink-0"
|
|
405
|
-
title="Copy name"
|
|
406
416
|
>
|
|
407
417
|
{copied === 'name' ? <Check className="w-3.5 h-3.5 text-green-400" /> : <Copy className="w-3.5 h-3.5" />}
|
|
408
418
|
</button>
|
|
419
|
+
</Tooltip>
|
|
409
420
|
</div>
|
|
410
421
|
<p className="text-sm text-theme-text-tertiary">{release.namespace}</p>
|
|
411
422
|
{releaseDetail?.managedByFluxHelmRelease && (
|
|
423
|
+
<Tooltip content={`Installed by Flux helm-controller via HelmRelease ${releaseDetail.managedByFluxHelmRelease}. Changes here would be reverted at the next reconcile.`} wrapperClassName="mt-1">
|
|
412
424
|
<button
|
|
413
425
|
type="button"
|
|
414
426
|
onClick={() => {
|
|
415
427
|
const [ns, name] = releaseDetail.managedByFluxHelmRelease!.split('/')
|
|
416
428
|
navigate(`/gitops/detail/helmreleases/${encodeURIComponent(ns || '_')}/${encodeURIComponent(name)}`)
|
|
417
429
|
}}
|
|
418
|
-
className="
|
|
419
|
-
title={`Installed by Flux helm-controller via HelmRelease ${releaseDetail.managedByFluxHelmRelease}. Changes here would be reverted at the next reconcile.`}
|
|
430
|
+
className="inline-flex items-center gap-1 rounded border border-amber-500/40 bg-amber-500/10 px-1.5 py-0.5 text-[11px] text-amber-700 dark:text-amber-400 hover:bg-amber-500/20 transition-colors"
|
|
420
431
|
>
|
|
421
432
|
<GitBranch className="w-3 h-3" />
|
|
422
433
|
Managed by Flux · {releaseDetail.managedByFluxHelmRelease}
|
|
423
434
|
</button>
|
|
435
|
+
</Tooltip>
|
|
424
436
|
)}
|
|
425
437
|
</div>
|
|
426
438
|
|
|
@@ -226,14 +226,15 @@ export function HelmView({ namespaces, selectedRelease, onReleaseClick }: HelmVi
|
|
|
226
226
|
className="w-full max-w-md pl-10 pr-4 py-2 bg-theme-elevated border border-theme-border-light rounded-lg text-sm text-theme-text-primary placeholder-theme-text-disabled focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
227
227
|
/>
|
|
228
228
|
</div>
|
|
229
|
+
<Tooltip content="Refresh">
|
|
229
230
|
<button
|
|
230
231
|
onClick={handleRefresh}
|
|
231
232
|
disabled={isRefreshAnimating}
|
|
232
|
-
className="p-2 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded-lg disabled:opacity-50"
|
|
233
|
-
title="Refresh"
|
|
233
|
+
className="p-2 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded-lg disabled:opacity-50 disabled:pointer-events-none"
|
|
234
234
|
>
|
|
235
235
|
<RefreshCw className={clsx('w-4 h-4', isRefreshAnimating && 'animate-spin')} />
|
|
236
236
|
</button>
|
|
237
|
+
</Tooltip>
|
|
237
238
|
</div>
|
|
238
239
|
|
|
239
240
|
{/* Releases Table */}
|