@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
|
@@ -411,11 +411,11 @@ export function InstallWizard({ repo, chartName, version, source, repoUrl, defau
|
|
|
411
411
|
Cancel
|
|
412
412
|
</button>
|
|
413
413
|
{step === 'review' ? (
|
|
414
|
+
<Tooltip content={!canHelmWrite ? helmActReason : ''}>
|
|
414
415
|
<button
|
|
415
416
|
onClick={handleInstall}
|
|
416
417
|
disabled={!canInstall || isInstalling || !canHelmWrite}
|
|
417
|
-
className="flex items-center gap-2 px-4 py-2 text-sm font-medium btn-brand rounded-lg disabled:cursor-not-allowed"
|
|
418
|
-
title={!canHelmWrite ? helmActReason : undefined}
|
|
418
|
+
className="flex items-center gap-2 px-4 py-2 text-sm font-medium btn-brand rounded-lg disabled:cursor-not-allowed disabled:pointer-events-none"
|
|
419
419
|
>
|
|
420
420
|
{isInstalling ? (
|
|
421
421
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
@@ -424,6 +424,7 @@ export function InstallWizard({ repo, chartName, version, source, repoUrl, defau
|
|
|
424
424
|
)}
|
|
425
425
|
Install
|
|
426
426
|
</button>
|
|
427
|
+
</Tooltip>
|
|
427
428
|
) : (
|
|
428
429
|
<button
|
|
429
430
|
onClick={() => setStep(step === 'info' ? 'values' : 'review')}
|
|
@@ -13,6 +13,7 @@ import { useAvailablePorts } from '../../api/client'
|
|
|
13
13
|
import { apiUrl, getAuthHeaders, getCredentialsMode } from '../../api/config'
|
|
14
14
|
import { useNamespacedCapabilities } from '../../contexts/CapabilitiesContext'
|
|
15
15
|
import { pluralize } from '@skyhook-io/k8s-ui'
|
|
16
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
16
17
|
|
|
17
18
|
interface OwnedResourcesProps {
|
|
18
19
|
resources: HelmOwnedResource[]
|
|
@@ -103,13 +104,14 @@ export function OwnedResources({ resources, onNavigate }: OwnedResourcesProps) {
|
|
|
103
104
|
) : (
|
|
104
105
|
<span className="flex items-center gap-2">
|
|
105
106
|
Showing {filteredResources.length} of {resources.length} resources
|
|
107
|
+
<Tooltip content="Clear filter">
|
|
106
108
|
<button
|
|
107
109
|
onClick={() => setHealthFilter('all')}
|
|
108
110
|
className="p-0.5 text-theme-text-tertiary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
|
|
109
|
-
title="Clear filter"
|
|
110
111
|
>
|
|
111
112
|
<X className="w-3.5 h-3.5" />
|
|
112
113
|
</button>
|
|
114
|
+
</Tooltip>
|
|
113
115
|
</span>
|
|
114
116
|
)}
|
|
115
117
|
</div>
|
|
@@ -255,39 +257,44 @@ function ResourceItem({ resource, onNavigate }: ResourceItemProps) {
|
|
|
255
257
|
|
|
256
258
|
{/* Status badge */}
|
|
257
259
|
{resource.status && (
|
|
260
|
+
<Tooltip content={resource.message || resource.status}>
|
|
258
261
|
<span
|
|
259
262
|
className={clsx('badge-sm', getResourceStatusColor(resource.status || ''))}
|
|
260
|
-
title={resource.message || resource.status}
|
|
261
263
|
>
|
|
262
264
|
{resource.status}
|
|
263
265
|
</span>
|
|
266
|
+
</Tooltip>
|
|
264
267
|
)}
|
|
265
268
|
|
|
266
269
|
{/* Issue summary (e.g., "OOMKilled", "CrashLoopBackOff") */}
|
|
267
270
|
{resource.issue && (
|
|
271
|
+
<Tooltip content={resource.summary || resource.issue}>
|
|
268
272
|
<span
|
|
269
273
|
className="text-xs text-red-400"
|
|
270
|
-
title={resource.summary || resource.issue}
|
|
271
274
|
>
|
|
272
275
|
{resource.issue}
|
|
273
276
|
</span>
|
|
277
|
+
</Tooltip>
|
|
274
278
|
)}
|
|
275
279
|
|
|
276
280
|
{/* Error icon with message tooltip */}
|
|
277
281
|
{isError && resource.message && !resource.issue && (
|
|
278
|
-
<
|
|
282
|
+
<Tooltip content={resource.message}>
|
|
283
|
+
<span>
|
|
279
284
|
<AlertCircle className="w-3.5 h-3.5 text-red-400" />
|
|
280
285
|
</span>
|
|
286
|
+
</Tooltip>
|
|
281
287
|
)}
|
|
282
288
|
|
|
283
289
|
{canNavigate && (
|
|
290
|
+
<Tooltip content="View details">
|
|
284
291
|
<button
|
|
285
292
|
onClick={handleClick}
|
|
286
293
|
className="p-1 text-theme-text-tertiary opacity-0 group-hover:opacity-100 hover:text-theme-text-primary hover:bg-theme-elevated rounded transition-all"
|
|
287
|
-
title="View details"
|
|
288
294
|
>
|
|
289
295
|
<ExternalLink className="w-3.5 h-3.5" />
|
|
290
296
|
</button>
|
|
297
|
+
</Tooltip>
|
|
291
298
|
)}
|
|
292
299
|
</div>
|
|
293
300
|
</div>
|
|
@@ -383,35 +390,37 @@ function PodQuickActions({ namespace, podName, isRunning }: PodQuickActionsProps
|
|
|
383
390
|
|
|
384
391
|
{/* Terminal */}
|
|
385
392
|
{isRunning && canExec && (
|
|
393
|
+
<Tooltip content="Open terminal">
|
|
386
394
|
<button
|
|
387
395
|
onClick={(e) => { e.stopPropagation(); handleOpenTerminal() }}
|
|
388
396
|
disabled={isLoadingAction}
|
|
389
|
-
className="p-1 text-theme-text-tertiary hover:text-blue-400 hover:bg-blue-500/10 rounded transition-colors disabled:opacity-50"
|
|
390
|
-
title="Open terminal"
|
|
397
|
+
className="p-1 text-theme-text-tertiary hover:text-blue-400 hover:bg-blue-500/10 rounded transition-colors disabled:opacity-50 disabled:pointer-events-none"
|
|
391
398
|
>
|
|
392
399
|
<Terminal className="w-3.5 h-3.5" />
|
|
393
400
|
</button>
|
|
401
|
+
</Tooltip>
|
|
394
402
|
)}
|
|
395
403
|
|
|
396
404
|
{/* Logs */}
|
|
397
405
|
{canViewLogs && (
|
|
406
|
+
<Tooltip content="View logs">
|
|
398
407
|
<button
|
|
399
408
|
onClick={(e) => { e.stopPropagation(); handleOpenLogs() }}
|
|
400
409
|
disabled={isLoadingAction}
|
|
401
|
-
className="p-1 text-theme-text-tertiary hover:text-blue-400 hover:bg-blue-500/10 rounded transition-colors disabled:opacity-50"
|
|
402
|
-
title="View logs"
|
|
410
|
+
className="p-1 text-theme-text-tertiary hover:text-blue-400 hover:bg-blue-500/10 rounded transition-colors disabled:opacity-50 disabled:pointer-events-none"
|
|
403
411
|
>
|
|
404
412
|
<FileText className="w-3.5 h-3.5" />
|
|
405
413
|
</button>
|
|
414
|
+
</Tooltip>
|
|
406
415
|
)}
|
|
407
416
|
|
|
408
417
|
{/* Port Forward */}
|
|
409
418
|
{canPortForward && !portsLoading && ports.length > 0 && (
|
|
419
|
+
<Tooltip content={`Port forward :${ports[0].port}`}>
|
|
410
420
|
<button
|
|
411
421
|
onClick={(e) => { e.stopPropagation(); handlePortForward(ports[0].port) }}
|
|
412
422
|
disabled={startPortForward.isPending}
|
|
413
|
-
className="p-1 text-theme-text-tertiary hover:text-blue-400 hover:bg-blue-500/10 rounded transition-colors disabled:opacity-50"
|
|
414
|
-
title={`Port forward :${ports[0].port}`}
|
|
423
|
+
className="p-1 text-theme-text-tertiary hover:text-blue-400 hover:bg-blue-500/10 rounded transition-colors disabled:opacity-50 disabled:pointer-events-none"
|
|
415
424
|
>
|
|
416
425
|
{startPortForward.isPending ? (
|
|
417
426
|
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
|
@@ -419,6 +428,7 @@ function PodQuickActions({ namespace, podName, isRunning }: PodQuickActionsProps
|
|
|
419
428
|
<Plug className="w-3.5 h-3.5" />
|
|
420
429
|
)}
|
|
421
430
|
</button>
|
|
431
|
+
</Tooltip>
|
|
422
432
|
)}
|
|
423
433
|
</div>
|
|
424
434
|
)
|
|
@@ -449,11 +459,11 @@ function ServiceQuickActions({ namespace, serviceName }: ServiceQuickActionsProp
|
|
|
449
459
|
|
|
450
460
|
return (
|
|
451
461
|
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
462
|
+
<Tooltip content={`Port forward :${ports[0].port}`}>
|
|
452
463
|
<button
|
|
453
464
|
onClick={(e) => { e.stopPropagation(); handlePortForward(ports[0].port) }}
|
|
454
465
|
disabled={startPortForward.isPending}
|
|
455
|
-
className="p-1 text-theme-text-tertiary hover:text-blue-400 hover:bg-blue-500/10 rounded transition-colors disabled:opacity-50"
|
|
456
|
-
title={`Port forward :${ports[0].port}`}
|
|
466
|
+
className="p-1 text-theme-text-tertiary hover:text-blue-400 hover:bg-blue-500/10 rounded transition-colors disabled:opacity-50 disabled:pointer-events-none"
|
|
457
467
|
>
|
|
458
468
|
{startPortForward.isPending ? (
|
|
459
469
|
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
|
@@ -461,6 +471,7 @@ function ServiceQuickActions({ namespace, serviceName }: ServiceQuickActionsProp
|
|
|
461
471
|
<Plug className="w-3.5 h-3.5" />
|
|
462
472
|
)}
|
|
463
473
|
</button>
|
|
474
|
+
</Tooltip>
|
|
464
475
|
</div>
|
|
465
476
|
)
|
|
466
477
|
}
|
|
@@ -4,6 +4,7 @@ import { clsx } from 'clsx'
|
|
|
4
4
|
import type { HelmRevision } from '../../types'
|
|
5
5
|
import { getStatusColor, formatDate, formatAge } from './helm-utils'
|
|
6
6
|
import { SEVERITY_BADGE } from '../../utils/badge-colors'
|
|
7
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
7
8
|
|
|
8
9
|
interface RevisionHistoryProps {
|
|
9
10
|
history: HelmRevision[]
|
|
@@ -108,7 +109,7 @@ export function RevisionHistory({ history, currentRevision, onViewRevision, onCo
|
|
|
108
109
|
</div>
|
|
109
110
|
|
|
110
111
|
<div className="flex items-center gap-4 mt-1 text-xs text-theme-text-tertiary">
|
|
111
|
-
<
|
|
112
|
+
<Tooltip content={revision.updated}><span>{formatDate(revision.updated)}</span></Tooltip>
|
|
112
113
|
<span>{formatAge(revision.updated)} ago</span>
|
|
113
114
|
</div>
|
|
114
115
|
|
|
@@ -9,6 +9,7 @@ import { YamlEditor } from '../ui/YamlEditor'
|
|
|
9
9
|
import { useHelmPreviewValues, useHelmApplyValues } from '../../api/client'
|
|
10
10
|
import { useCanHelmAct } from '../../api/client'
|
|
11
11
|
import { ValuesDiffPreview } from './ValuesDiffPreview'
|
|
12
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
12
13
|
|
|
13
14
|
interface ValuesViewerProps {
|
|
14
15
|
values?: HelmValues
|
|
@@ -227,11 +228,11 @@ export function ValuesViewer({
|
|
|
227
228
|
)}
|
|
228
229
|
Preview
|
|
229
230
|
</button>
|
|
231
|
+
<Tooltip content={!canHelmWrite ? helmActReason : ''}>
|
|
230
232
|
<button
|
|
231
233
|
onClick={handleApply}
|
|
232
234
|
disabled={!!yamlError || applyMutation.isPending || !canHelmWrite}
|
|
233
|
-
className="flex items-center gap-1 px-2.5 py-1 text-xs btn-brand rounded disabled:cursor-not-allowed"
|
|
234
|
-
title={!canHelmWrite ? helmActReason : undefined}
|
|
235
|
+
className="flex items-center gap-1 px-2.5 py-1 text-xs btn-brand rounded disabled:cursor-not-allowed disabled:pointer-events-none"
|
|
235
236
|
>
|
|
236
237
|
{applyMutation.isPending ? (
|
|
237
238
|
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
|
@@ -240,6 +241,7 @@ export function ValuesViewer({
|
|
|
240
241
|
)}
|
|
241
242
|
Apply
|
|
242
243
|
</button>
|
|
244
|
+
</Tooltip>
|
|
243
245
|
</>
|
|
244
246
|
)}
|
|
245
247
|
</div>
|
|
@@ -219,24 +219,24 @@ export function ClusterHealthCard({
|
|
|
219
219
|
its top bar; rendering it again here is redundant and
|
|
220
220
|
makes the card feel like a label rather than content. */}
|
|
221
221
|
{!isCloud && (
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
// kubeconfig context path.
|
|
230
|
-
title={isInCluster ? undefined : cluster.name}
|
|
231
|
-
>
|
|
222
|
+
// In-cluster mode's cluster.name is the literal "in-cluster"
|
|
223
|
+
// sentinel, which would leak via the hover tooltip even though
|
|
224
|
+
// the visible text falls back to the platform label. Render no
|
|
225
|
+
// tooltip in that case; local mode keeps it so users can hover
|
|
226
|
+
// to see the full kubeconfig context path.
|
|
227
|
+
<Tooltip content={isInCluster ? '' : cluster.name} wrapperClassName="!block min-w-0">
|
|
228
|
+
<h2 className="text-xl font-semibold text-theme-text-primary truncate leading-tight mb-1.5">
|
|
232
229
|
{headlineName}
|
|
233
230
|
</h2>
|
|
231
|
+
</Tooltip>
|
|
234
232
|
)}
|
|
235
233
|
<div className="flex flex-col gap-0.5 text-xs text-theme-text-tertiary">
|
|
236
234
|
{(parsedContext.account || parsedContext.region) && (
|
|
237
|
-
<
|
|
235
|
+
<Tooltip content={[parsedContext.account, parsedContext.region].filter(Boolean).join(' · ')} wrapperClassName="min-w-0">
|
|
236
|
+
<span className="truncate font-mono">
|
|
238
237
|
{[parsedContext.account, parsedContext.region].filter(Boolean).join(' · ')}
|
|
239
238
|
</span>
|
|
239
|
+
</Tooltip>
|
|
240
240
|
)}
|
|
241
241
|
{cluster.version && (
|
|
242
242
|
<span>Kubernetes {cluster.version}</span>
|
|
@@ -247,12 +247,11 @@ export function ClusterHealthCard({
|
|
|
247
247
|
(in-cluster has no meaningful context name, cloud
|
|
248
248
|
shell already renders the canonical name). */}
|
|
249
249
|
{cluster.name && cluster.name !== headlineName && deployment.mode === 'local' && (
|
|
250
|
-
<
|
|
251
|
-
|
|
252
|
-
title={cluster.name}
|
|
253
|
-
>
|
|
250
|
+
<Tooltip content={cluster.name}>
|
|
251
|
+
<span className="font-mono text-[10px] text-theme-text-disabled break-all leading-snug pt-0.5">
|
|
254
252
|
{cluster.name}
|
|
255
253
|
</span>
|
|
254
|
+
</Tooltip>
|
|
256
255
|
)}
|
|
257
256
|
</div>
|
|
258
257
|
{nodeVersionSkew && (
|
|
@@ -290,9 +289,11 @@ export function ClusterHealthCard({
|
|
|
290
289
|
<Radio className="w-3.5 h-3.5 text-purple-400 animate-pulse shrink-0" />
|
|
291
290
|
<div className="flex flex-col gap-0.5 min-w-0 flex-1 text-left">
|
|
292
291
|
<span className="text-xs font-medium text-purple-400">MCP Server Live</span>
|
|
293
|
-
<
|
|
292
|
+
<Tooltip content={mcpUrl} wrapperClassName="min-w-0">
|
|
293
|
+
<span className="text-[10px] text-theme-text-tertiary truncate font-mono">
|
|
294
294
|
HTTP · {mcpUrl}
|
|
295
295
|
</span>
|
|
296
|
+
</Tooltip>
|
|
296
297
|
</div>
|
|
297
298
|
<Info className="w-3.5 h-3.5 text-purple-400/60 shrink-0" />
|
|
298
299
|
</button>
|
|
@@ -443,24 +444,26 @@ export function ClusterHealthCard({
|
|
|
443
444
|
{/* Left column: Warning indicators (aligned with cluster info) */}
|
|
444
445
|
<div className="flex flex-col justify-center gap-1 w-1/4 shrink-0 pr-4 border-r border-theme-border/50">
|
|
445
446
|
{health.warningEvents > 0 && (
|
|
447
|
+
<Tooltip content="Native Kubernetes Warning events (e.g., ImagePullBackOff, FailedScheduling)" wrapperClassName="w-fit">
|
|
446
448
|
<button
|
|
447
449
|
onClick={onWarningEventsClick}
|
|
448
|
-
title="Native Kubernetes Warning events (e.g., ImagePullBackOff, FailedScheduling)"
|
|
449
450
|
className="badge status-degraded w-fit gap-1.5 hover:opacity-80 transition-opacity"
|
|
450
451
|
>
|
|
451
452
|
<AlertTriangle className="w-3.5 h-3.5 shrink-0" />
|
|
452
453
|
<span><span className="font-mono">{health.warningEvents}</span> Warning Events</span>
|
|
453
454
|
</button>
|
|
455
|
+
</Tooltip>
|
|
454
456
|
)}
|
|
455
457
|
{issueCount > 0 && (
|
|
458
|
+
<Tooltip content="View grouped live operational issues" wrapperClassName="w-fit">
|
|
456
459
|
<button
|
|
457
460
|
onClick={onIssuesClick}
|
|
458
|
-
title="View grouped live operational issues"
|
|
459
461
|
className={clsx('badge w-fit gap-1.5 hover:opacity-80 transition-opacity', hasCriticalIssues ? 'status-unhealthy' : 'status-degraded')}
|
|
460
462
|
>
|
|
461
463
|
<AlertTriangle className="w-3.5 h-3.5 shrink-0" />
|
|
462
464
|
<span>{pluralize(issueCount, 'Active Issue')}</span>
|
|
463
465
|
</button>
|
|
466
|
+
</Tooltip>
|
|
464
467
|
)}
|
|
465
468
|
</div>
|
|
466
469
|
|
|
@@ -88,11 +88,13 @@ export function HelmSummary({ data, onNavigate }: HelmSummaryProps) {
|
|
|
88
88
|
<span className="text-xs font-medium text-theme-text-secondary">
|
|
89
89
|
{data.errorCode === 'unconfigured' ? 'Helm not configured' : 'Helm unavailable'}
|
|
90
90
|
</span>
|
|
91
|
-
<
|
|
91
|
+
<Tooltip content={data.error} wrapperClassName="!block mt-1 min-w-0 text-center">
|
|
92
|
+
<span className="text-[11px] text-center px-2 truncate max-w-full">
|
|
92
93
|
{data.errorCode === 'unconfigured'
|
|
93
94
|
? 'Set rbac.helm=true in the Radar Helm chart values.'
|
|
94
95
|
: data.error}
|
|
95
96
|
</span>
|
|
97
|
+
</Tooltip>
|
|
96
98
|
</div>
|
|
97
99
|
) : !data.releases || data.releases.length === 0 ? (
|
|
98
100
|
<div className="flex items-center justify-center h-full py-4 text-xs text-theme-text-tertiary">
|
|
@@ -2,6 +2,7 @@ import { useRef, useEffect, useState, useCallback } from 'react'
|
|
|
2
2
|
import { X, Copy, Check, Radio, Terminal, MessageSquare, Code2, ChevronRight, Pin } from 'lucide-react'
|
|
3
3
|
import { apiUrl, getAuthHeaders, getCredentialsMode } from '../../api/config'
|
|
4
4
|
import { MCP_TOOL_CATALOG } from './mcpToolCatalog'
|
|
5
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
5
6
|
|
|
6
7
|
interface MCPSetupDialogProps {
|
|
7
8
|
open: boolean
|
|
@@ -19,13 +20,14 @@ function CopyButton({ text }: { text: string }) {
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
return (
|
|
23
|
+
<Tooltip content="Copy to clipboard" position="left" wrapperClassName="absolute top-2 right-2">
|
|
22
24
|
<button
|
|
23
25
|
onClick={handleCopy}
|
|
24
|
-
className="
|
|
25
|
-
title="Copy to clipboard"
|
|
26
|
+
className="p-1.5 rounded-md bg-theme-elevated/50 hover:bg-theme-elevated text-theme-text-tertiary hover:text-theme-text-secondary transition-colors"
|
|
26
27
|
>
|
|
27
28
|
{copied ? <Check className="w-3.5 h-3.5 text-green-500" /> : <Copy className="w-3.5 h-3.5" />}
|
|
28
29
|
</button>
|
|
30
|
+
</Tooltip>
|
|
29
31
|
)
|
|
30
32
|
}
|
|
31
33
|
|
|
@@ -308,19 +310,23 @@ export function MCPSetupDialog({ open, onClose, mcpUrl }: MCPSetupDialogProps) {
|
|
|
308
310
|
<div className="flex items-center gap-2">
|
|
309
311
|
<code className="inline-code text-[11px]">{tool.name}</code>
|
|
310
312
|
{tool.write && (
|
|
311
|
-
<
|
|
313
|
+
<Tooltip content="Write tool — annotated as destructive">
|
|
314
|
+
<span className="badge-sm bg-amber-500/10 text-amber-600 dark:text-amber-400">
|
|
312
315
|
write
|
|
313
316
|
</span>
|
|
317
|
+
</Tooltip>
|
|
314
318
|
)}
|
|
315
319
|
</div>
|
|
316
320
|
<p className="text-[11px] text-theme-text-tertiary leading-relaxed">{tool.desc}</p>
|
|
317
321
|
{tool.params.length > 0 && (
|
|
318
322
|
<div className="flex flex-wrap gap-1.5 pt-0.5">
|
|
319
323
|
{tool.params.map((p) => (
|
|
320
|
-
<
|
|
324
|
+
<Tooltip key={p.arg} content={p.desc}>
|
|
325
|
+
<span className="inline-code text-[11px]">
|
|
321
326
|
<span>{p.arg}</span>
|
|
322
327
|
{p.required && <span className="text-red-400">*</span>}
|
|
323
328
|
</span>
|
|
329
|
+
</Tooltip>
|
|
324
330
|
))}
|
|
325
331
|
</div>
|
|
326
332
|
)}
|
|
@@ -3,6 +3,7 @@ import type { ReactNode } from 'react'
|
|
|
3
3
|
import { Home, Network, List, Clock, AlertTriangle, Package, GitBranch, Boxes, Activity, DollarSign, ShieldCheck, Settings, PanelLeftClose, PanelLeftOpen } from 'lucide-react'
|
|
4
4
|
import { clsx } from 'clsx'
|
|
5
5
|
import type { MainView } from '../../types'
|
|
6
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
6
7
|
|
|
7
8
|
// The views the rail can navigate to. Broader than k8s-ui's ExtendedMainView
|
|
8
9
|
// (which omits 'applications') — it mirrors the navigable subset of App.tsx's
|
|
@@ -131,11 +132,11 @@ export function PrimaryNavRail({ activeView, onNavigate, pinned, onTogglePinned,
|
|
|
131
132
|
forced slim (showPinToggle=false) — expanding there isn't available. */}
|
|
132
133
|
{showPinToggle && (
|
|
133
134
|
<div className="px-2 pb-2 pt-1 border-t border-theme-border/50">
|
|
135
|
+
<Tooltip content={pinned ? 'Collapse navigation' : 'Expand navigation'} position="right" wrapperClassName="!block w-full shrink-0">
|
|
134
136
|
<button
|
|
135
137
|
type="button"
|
|
136
138
|
onClick={onTogglePinned}
|
|
137
139
|
aria-label={pinned ? 'Collapse navigation' : 'Expand navigation'}
|
|
138
|
-
title={pinned ? 'Collapse navigation' : 'Expand navigation'}
|
|
139
140
|
className="group/pin relative flex h-9 w-full items-center rounded-md text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-secondary transition-colors"
|
|
140
141
|
>
|
|
141
142
|
<span className="flex w-10 shrink-0 items-center justify-center">
|
|
@@ -146,6 +147,7 @@ export function PrimaryNavRail({ activeView, onNavigate, pinned, onTogglePinned,
|
|
|
146
147
|
would contradict the "Expand navigation" label. */}
|
|
147
148
|
<span className={clsx('text-[13px] font-medium', !pinned && 'hidden')}>Collapse</span>
|
|
148
149
|
</button>
|
|
150
|
+
</Tooltip>
|
|
149
151
|
</div>
|
|
150
152
|
)}
|
|
151
153
|
</aside>
|
|
@@ -156,11 +158,11 @@ function BrandRow({ pinned, onNavigate }: { pinned: boolean; onNavigate: (view:
|
|
|
156
158
|
// Clickable brand = secondary home affordance (logo→home convention). The
|
|
157
159
|
// Home nav item below still carries the active state; the brand just navigates.
|
|
158
160
|
return (
|
|
161
|
+
<Tooltip content="Home" position="right" wrapperClassName="!block w-full shrink-0">
|
|
159
162
|
<button
|
|
160
163
|
type="button"
|
|
161
164
|
onClick={() => onNavigate('home')}
|
|
162
165
|
aria-label="Radar — go to home"
|
|
163
|
-
title="Home"
|
|
164
166
|
// Height matches the top bar header (App.tsx — items-center + py-2 = 51px)
|
|
165
167
|
// so the rail's brand divider and the header's bottom border form one line.
|
|
166
168
|
className="flex h-[51px] w-full items-center border-b border-theme-border/50 shrink-0 transition-opacity hover:opacity-80"
|
|
@@ -181,6 +183,7 @@ function BrandRow({ pinned, onNavigate }: { pinned: boolean; onNavigate: (view:
|
|
|
181
183
|
<span className="text-[9px] mt-0.5 tracking-wide uppercase text-theme-text-tertiary">by Skyhook</span>
|
|
182
184
|
</span>
|
|
183
185
|
</button>
|
|
186
|
+
</Tooltip>
|
|
184
187
|
)
|
|
185
188
|
}
|
|
186
189
|
|
|
@@ -4,6 +4,7 @@ import { clsx } from 'clsx'
|
|
|
4
4
|
import { useAvailablePorts, useClusterInfo, AvailablePort } from '../../api/client'
|
|
5
5
|
import { useStartPortForward } from './PortForwardManager'
|
|
6
6
|
import { validatePort } from '@skyhook-io/k8s-ui/utils/validators'
|
|
7
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
7
8
|
|
|
8
9
|
interface PortForwardButtonProps {
|
|
9
10
|
type: 'pod' | 'service'
|
|
@@ -206,31 +207,32 @@ export function PortForwardButton({
|
|
|
206
207
|
// If no ports available, show disabled button
|
|
207
208
|
if (!isLoading && ports.length === 0) {
|
|
208
209
|
return (
|
|
210
|
+
<Tooltip content="No ports available">
|
|
209
211
|
<button
|
|
210
212
|
disabled
|
|
211
213
|
className={clsx(
|
|
212
|
-
'flex items-center gap-2 px-3 py-2 bg-theme-elevated text-theme-text-primary text-sm rounded-lg opacity-50 cursor-not-allowed',
|
|
214
|
+
'flex items-center gap-2 px-3 py-2 bg-theme-elevated text-theme-text-primary text-sm rounded-lg opacity-50 cursor-not-allowed disabled:pointer-events-none',
|
|
213
215
|
className
|
|
214
216
|
)}
|
|
215
|
-
title="No ports available"
|
|
216
217
|
>
|
|
217
218
|
<Plug className="w-4 h-4" />
|
|
218
219
|
No Ports
|
|
219
220
|
</button>
|
|
221
|
+
</Tooltip>
|
|
220
222
|
)
|
|
221
223
|
}
|
|
222
224
|
|
|
223
225
|
// If only one port, forward directly on click (most common case)
|
|
224
226
|
if (ports.length === 1) {
|
|
225
227
|
return (
|
|
228
|
+
<Tooltip content={`Port forward to ${ports[0].port}`}>
|
|
226
229
|
<button
|
|
227
230
|
onClick={() => handlePortSelect(ports[0])}
|
|
228
231
|
disabled={isPending}
|
|
229
232
|
className={clsx(
|
|
230
|
-
'flex items-center gap-2 px-3 py-2 bg-theme-elevated text-theme-text-primary text-sm rounded-lg hover:bg-theme-hover transition-colors disabled:opacity-50',
|
|
233
|
+
'flex items-center gap-2 px-3 py-2 bg-theme-elevated text-theme-text-primary text-sm rounded-lg hover:bg-theme-hover transition-colors disabled:opacity-50 disabled:pointer-events-none',
|
|
231
234
|
className
|
|
232
235
|
)}
|
|
233
|
-
title={`Port forward to ${ports[0].port}`}
|
|
234
236
|
>
|
|
235
237
|
{isPending ? (
|
|
236
238
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
@@ -239,6 +241,7 @@ export function PortForwardButton({
|
|
|
239
241
|
)}
|
|
240
242
|
Forward :{ports[0].port}
|
|
241
243
|
</button>
|
|
244
|
+
</Tooltip>
|
|
242
245
|
)
|
|
243
246
|
}
|
|
244
247
|
|
|
@@ -269,6 +272,7 @@ export function PortForwardButton({
|
|
|
269
272
|
<div className="px-3 py-2 border-b border-theme-border">
|
|
270
273
|
<div className="text-xs text-theme-text-disabled mb-2">Listen on</div>
|
|
271
274
|
<div className="flex gap-1">
|
|
275
|
+
<Tooltip content="Only accessible from this machine" wrapperClassName="flex-1">
|
|
272
276
|
<button
|
|
273
277
|
onClick={(e) => { e.stopPropagation(); setListenAddress('127.0.0.1') }}
|
|
274
278
|
className={clsx(
|
|
@@ -277,11 +281,12 @@ export function PortForwardButton({
|
|
|
277
281
|
? 'btn-brand-toggle'
|
|
278
282
|
: 'bg-theme-elevated text-theme-text-tertiary hover:text-theme-text-primary'
|
|
279
283
|
)}
|
|
280
|
-
title="Only accessible from this machine"
|
|
281
284
|
>
|
|
282
285
|
<Monitor className="w-3 h-3" />
|
|
283
286
|
localhost
|
|
284
287
|
</button>
|
|
288
|
+
</Tooltip>
|
|
289
|
+
<Tooltip content="Accessible from other machines on the network" wrapperClassName="flex-1">
|
|
285
290
|
<button
|
|
286
291
|
onClick={(e) => { e.stopPropagation(); setListenAddress('0.0.0.0') }}
|
|
287
292
|
className={clsx(
|
|
@@ -290,11 +295,11 @@ export function PortForwardButton({
|
|
|
290
295
|
? 'bg-amber-600 text-white'
|
|
291
296
|
: 'bg-theme-elevated text-theme-text-tertiary hover:text-theme-text-primary'
|
|
292
297
|
)}
|
|
293
|
-
title="Accessible from other machines on the network"
|
|
294
298
|
>
|
|
295
299
|
<Globe className="w-3 h-3" />
|
|
296
300
|
all interfaces
|
|
297
301
|
</button>
|
|
302
|
+
</Tooltip>
|
|
298
303
|
</div>
|
|
299
304
|
</div>
|
|
300
305
|
)}
|
|
@@ -375,11 +380,11 @@ export function PortForwardInlineButton({
|
|
|
375
380
|
|
|
376
381
|
return (
|
|
377
382
|
<>
|
|
383
|
+
<Tooltip content={`Port forward ${port}`}>
|
|
378
384
|
<button
|
|
379
385
|
onClick={handleClick}
|
|
380
386
|
disabled={disabled || isPending}
|
|
381
|
-
className="inline-flex items-center gap-1 px-1.5 py-0.5 bg-theme-elevated hover:bg-accent-muted rounded text-xs transition-colors disabled:opacity-50 disabled:hover:bg-theme-elevated"
|
|
382
|
-
title={`Port forward ${port}`}
|
|
387
|
+
className="inline-flex items-center gap-1 px-1.5 py-0.5 bg-theme-elevated hover:bg-accent-muted rounded text-xs transition-colors disabled:opacity-50 disabled:hover:bg-theme-elevated disabled:pointer-events-none"
|
|
383
388
|
>
|
|
384
389
|
{port}/{protocol}
|
|
385
390
|
{isPending ? (
|
|
@@ -388,6 +393,7 @@ export function PortForwardInlineButton({
|
|
|
388
393
|
<Plug className="w-3 h-3" />
|
|
389
394
|
)}
|
|
390
395
|
</button>
|
|
396
|
+
</Tooltip>
|
|
391
397
|
{dialogInfo && (
|
|
392
398
|
<KubectlCommandDialog info={dialogInfo} onClose={() => setDialogInfo(null)} />
|
|
393
399
|
)}
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
type CategoryDef,
|
|
27
27
|
} from './PrometheusCharts'
|
|
28
28
|
import { RestartEventLane } from './RestartChart'
|
|
29
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
29
30
|
|
|
30
31
|
// Used when MetricsTabContent is in expanded (full-screen) mode. Drawer mode
|
|
31
32
|
// uses the single-chart tabbed `PrometheusCharts` instead — drawer width
|
|
@@ -288,7 +289,11 @@ function WorkloadHealthBadge({ kind, namespace, name }: { kind: string; namespac
|
|
|
288
289
|
// silently render as fine while we have no signal to display.
|
|
289
290
|
if (error && !data) {
|
|
290
291
|
const msg = error instanceof Error ? error.message : String(error)
|
|
291
|
-
return
|
|
292
|
+
return (
|
|
293
|
+
<Tooltip content={`Health check failed: ${msg}`}>
|
|
294
|
+
<span className={`badge badge-sm ${SEVERITY_BADGE.neutral}`}>Health unknown</span>
|
|
295
|
+
</Tooltip>
|
|
296
|
+
)
|
|
292
297
|
}
|
|
293
298
|
if (!data?.sampleAvailable || data.rows.length === 0) return null
|
|
294
299
|
|
|
@@ -320,7 +325,9 @@ function PanelError({ message }: { message: string }) {
|
|
|
320
325
|
return (
|
|
321
326
|
<div className={`flex flex-col items-center justify-center h-full min-h-[160px] ${SEVERITY_TEXT.warning} text-xs px-3 text-center`}>
|
|
322
327
|
Query failed
|
|
323
|
-
<
|
|
328
|
+
<Tooltip content={message} wrapperClassName="!block w-full">
|
|
329
|
+
<span className="text-theme-text-quaternary mt-0.5 line-clamp-2">{message}</span>
|
|
330
|
+
</Tooltip>
|
|
324
331
|
</div>
|
|
325
332
|
)
|
|
326
333
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useMemo } from 'react'
|
|
2
2
|
import { AlertCircle } from 'lucide-react'
|
|
3
3
|
import { usePrometheusResourceMetrics, usePrometheusStatus, type PrometheusSeries, type PrometheusTimeRange } from '../../api/client'
|
|
4
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* RestartEventLane — vertical markers at each restart event, on a dedicated
|
|
@@ -58,14 +59,14 @@ export function RestartEventLane({ kind, namespace, name, range = '1h' }: {
|
|
|
58
59
|
{restarts.map((r, i) => {
|
|
59
60
|
const left = `${Math.max(0, Math.min(100, ((r.timestamp - windowStart) / span) * 100))}%`
|
|
60
61
|
return (
|
|
61
|
-
<
|
|
62
|
+
<Tooltip
|
|
62
63
|
key={i}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
content={`${new Date(r.timestamp * 1000).toLocaleString()} · ${r.label}${r.value > 1 ? ` ×${r.value}` : ''}`}
|
|
65
|
+
wrapperClassName="absolute top-0 h-full w-px bg-amber-500/80"
|
|
66
|
+
wrapperStyle={{ left }}
|
|
66
67
|
>
|
|
67
68
|
<div className="absolute -top-0.5 left-1/2 -translate-x-1/2 w-1.5 h-1.5 rounded-full bg-amber-500" />
|
|
68
|
-
</
|
|
69
|
+
</Tooltip>
|
|
69
70
|
)
|
|
70
71
|
})}
|
|
71
72
|
</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ArrowRight, Check, Info, AlertTriangle } from 'lucide-react'
|
|
2
2
|
import { SEVERITY_TEXT, SEVERITY_BADGE, type Severity } from '@skyhook-io/k8s-ui/utils/badge-colors'
|
|
3
3
|
import { usePrometheusRightsizing, usePrometheusStatus, type RightsizingTone, type RightsizingRow } from '../../api/client'
|
|
4
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
4
5
|
|
|
5
6
|
const RIGHTSIZING_KINDS = new Set(['Deployment', 'StatefulSet', 'DaemonSet'])
|
|
6
7
|
|
|
@@ -38,7 +39,9 @@ export function RightsizingStrip({ kind, namespace, name }: {
|
|
|
38
39
|
<header className="flex items-center justify-between mb-1">
|
|
39
40
|
<h3 className="text-sm font-medium text-theme-text-primary">Right-sizing</h3>
|
|
40
41
|
</header>
|
|
41
|
-
<
|
|
42
|
+
<Tooltip content={msg}>
|
|
43
|
+
<p className="text-xs text-theme-text-tertiary">Right-sizing unavailable — Prometheus query failed.</p>
|
|
44
|
+
</Tooltip>
|
|
42
45
|
</section>
|
|
43
46
|
)
|
|
44
47
|
}
|
|
@@ -4,6 +4,7 @@ import { clsx } from 'clsx'
|
|
|
4
4
|
import { useState, useCallback } from 'react'
|
|
5
5
|
import type { TopologyNode, NodeKind, HealthStatus } from '../../types'
|
|
6
6
|
import { getKindBadgeBordered, healthToSeverity, SEVERITY_BADGE_BORDERED } from '../../utils/badge-colors'
|
|
7
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
7
8
|
|
|
8
9
|
interface ResourceDrawerProps {
|
|
9
10
|
node: TopologyNode
|
|
@@ -146,10 +147,10 @@ export const ResourceDrawer = memo(function ResourceDrawer({
|
|
|
146
147
|
<h2 className="text-lg font-semibold text-theme-text-primary truncate">
|
|
147
148
|
{node.name}
|
|
148
149
|
</h2>
|
|
150
|
+
<Tooltip content="Copy name">
|
|
149
151
|
<button
|
|
150
152
|
onClick={copyName}
|
|
151
153
|
className="p-1 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
|
|
152
|
-
title="Copy name"
|
|
153
154
|
>
|
|
154
155
|
{copied ? (
|
|
155
156
|
<Check className="w-4 h-4 text-green-400" />
|
|
@@ -157,6 +158,7 @@ export const ResourceDrawer = memo(function ResourceDrawer({
|
|
|
157
158
|
<Copy className="w-4 h-4" />
|
|
158
159
|
)}
|
|
159
160
|
</button>
|
|
161
|
+
</Tooltip>
|
|
160
162
|
</div>
|
|
161
163
|
</div>
|
|
162
164
|
<button
|