@skyhook-io/radar-app 1.7.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/package.json +9 -3
  2. package/src/App.tsx +150 -44
  3. package/src/api/client.ts +3 -3
  4. package/src/components/ConnectionErrorView.tsx +5 -2
  5. package/src/components/DebugOverlay.tsx +5 -3
  6. package/src/components/UserMenu.tsx +5 -2
  7. package/src/components/applications/ApplicationsView.tsx +1 -1
  8. package/src/components/audit/AuditSettingsDialog.tsx +14 -10
  9. package/src/components/audit/AuditView.tsx +3 -1
  10. package/src/components/compare/CompareViewRoute.tsx +13 -5
  11. package/src/components/compare/useCompareLauncher.tsx +6 -3
  12. package/src/components/cost/CostTrendChart.tsx +3 -3
  13. package/src/components/cost/CostView.tsx +14 -5
  14. package/src/components/helm/ChartBrowser.tsx +7 -5
  15. package/src/components/helm/HelmReleaseDrawer.tsx +25 -13
  16. package/src/components/helm/HelmView.tsx +3 -2
  17. package/src/components/helm/InstallWizard.tsx +4 -3
  18. package/src/components/helm/OwnedResources.tsx +24 -13
  19. package/src/components/helm/RevisionHistory.tsx +2 -1
  20. package/src/components/helm/ValuesViewer.tsx +4 -2
  21. package/src/components/home/ActivitySummary.tsx +4 -1
  22. package/src/components/home/ClusterHealthCard.tsx +21 -18
  23. package/src/components/home/HelmSummary.tsx +3 -1
  24. package/src/components/home/MCPSetupDialog.tsx +10 -4
  25. package/src/components/issues/IssuesPane.tsx +2 -2
  26. package/src/components/nav/PrimaryNavRail.tsx +5 -2
  27. package/src/components/portforward/PortForwardButton.tsx +14 -8
  28. package/src/components/portforward/PortForwardManager.tsx +70 -8
  29. package/src/components/resource/PrometheusChartsGrid.tsx +10 -3
  30. package/src/components/resource/RestartChart.tsx +6 -5
  31. package/src/components/resource/RightsizingStrip.tsx +4 -1
  32. package/src/components/resource-drawer/ResourceDrawer.tsx +3 -1
  33. package/src/components/resources/ImageFilesystemModal.tsx +16 -8
  34. package/src/components/resources/PodFilesystemModal.tsx +4 -2
  35. package/src/components/resources/ResourcesView.tsx +2 -0
  36. package/src/components/settings/MyPermissionsDialog.tsx +64 -4
  37. package/src/components/settings/SettingsDialog.tsx +371 -141
  38. package/src/components/shared/LargeClusterNamespacePicker.tsx +1 -1
  39. package/src/components/traffic/TrafficFilterSidebar.tsx +8 -43
  40. package/src/components/traffic/TrafficFlowList.tsx +13 -4
  41. package/src/components/traffic/TrafficGraph.tsx +37 -23
  42. package/src/components/traffic/TrafficView.tsx +7 -5
  43. package/src/components/ui/DiagnosticsOverlay.tsx +1 -1
  44. package/src/components/ui/Omnibar.tsx +0 -1
  45. package/src/components/workload/WorkloadView.tsx +32 -27
  46. package/src/context/NavCustomization.tsx +19 -5
  47. package/src/main.tsx +1 -1
@@ -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
- window.location.assign(href)
42
- }, [crossClusterCompareHref, kindLower, namespace, name, group])
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) => {
@@ -34,7 +34,7 @@ export function CostTrendChart() {
34
34
  <div className="rounded-lg border border-theme-border bg-theme-surface/50 p-4">
35
35
  <div className="flex items-center justify-center h-[200px] text-theme-text-tertiary">
36
36
  <Loader2 className="w-5 h-5 animate-spin mr-2" />
37
- Loading cost trend...
37
+ Loading cost trend
38
38
  </div>
39
39
  </div>
40
40
  )
@@ -164,7 +164,7 @@ function StackedAreaChart({ series }: { series: OpenCostTrendSeries[] }) {
164
164
  })
165
165
 
166
166
  return { timestamps, stacked, minTs, maxTs, yMax, seriesLookups, toX, toY, yTicks, xTicks, paths }
167
- }, [series])
167
+ }, [series, plotHeight, plotWidth])
168
168
 
169
169
  // Hover data — depends on hoverX + chartData, must be a separate hook (called unconditionally)
170
170
  const hoverData = useMemo(() => {
@@ -193,7 +193,7 @@ function StackedAreaChart({ series }: { series: OpenCostTrendSeries[] }) {
193
193
  })
194
194
 
195
195
  return { ts: closestTs, x: toX(closestTs), total, points }
196
- }, [hoverX, chartData, series])
196
+ }, [hoverX, chartData, series, plotWidth])
197
197
 
198
198
  const handleMouseMove = useCallback((e: React.MouseEvent<SVGRectElement>) => {
199
199
  const svg = svgRef.current
@@ -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
- <span className="text-right cursor-help" title="Projected from current hourly rate — not historical spend">Monthly*</span>
181
- <span className="text-right cursor-help" title="% of reserved resources actually being used, weighted by cost">Efficiency</span>
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>
@@ -277,7 +282,7 @@ function WorkloadRows({ namespace }: { namespace: string }) {
277
282
  return (
278
283
  <div className="px-4 py-3 flex items-center gap-2 text-xs text-theme-text-tertiary bg-theme-elevated/30">
279
284
  <Loader2 className="w-3.5 h-3.5 animate-spin" />
280
- Loading workloads...
285
+ Loading workloads
281
286
  </div>
282
287
  )
283
288
  }
@@ -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
- <span className="text-right cursor-help" title="Projected from current hourly rate — not historical spend">Monthly*</span>
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
- <span className="text-sm text-theme-text-primary truncate font-medium" title={node.name}>
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>}
@@ -190,7 +190,7 @@ export function ChartBrowser({ onChartSelect }: ChartBrowserProps) {
190
190
  </button>
191
191
  <div className="border-t border-theme-border my-1" />
192
192
  {reposLoading ? (
193
- <div className="px-3 py-2 text-sm text-theme-text-tertiary">Loading...</div>
193
+ <div className="px-3 py-2 text-sm text-theme-text-tertiary">Loading…</div>
194
194
  ) : repositories?.length === 0 ? (
195
195
  <div className="px-3 py-2 text-sm text-theme-text-tertiary">No repositories configured</div>
196
196
  ) : (
@@ -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
- <span className={clsx('badge', SEVERITY_BADGE.success)} title="Chart is up to date">
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
- <button onClick={onClose} className="p-1.5 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded" title="Close (Esc)">
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="mt-1 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"
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 */}
@@ -191,7 +191,7 @@ export function InstallWizard({ repo, chartName, version, source, repoUrl, defau
191
191
  } finally {
192
192
  setIsInstalling(false)
193
193
  }
194
- }, [releaseName, namespace, chartName, version, repo, valuesYaml, createNamespace, onSuccess, isLocal, artifactHubDetail, queryClient])
194
+ }, [releaseName, namespace, chartName, version, repo, repoUrl, valuesYaml, createNamespace, onSuccess, isLocal, artifactHubDetail, queryClient])
195
195
 
196
196
  // Validate release name + namespace before letting the user
197
197
  // advance. Without this, a name like "Invalid Name With Spaces!"
@@ -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
- <span title={resource.message}>
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
- <span title={revision.updated}>{formatDate(revision.updated)}</span>
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>
@@ -82,6 +82,9 @@ export function ActivitySummary({ namespaces, topology, onNavigate }: ActivitySu
82
82
  limit: 1000,
83
83
  })
84
84
 
85
+ // Intentionally re-sample 'now' only when events refresh (not every render),
86
+ // so the timeline window stays stable between data updates.
87
+ // eslint-disable-next-line react-hooks/exhaustive-deps
85
88
  const now = useMemo(() => Date.now(), [events])
86
89
  const spanMs = SPAN_MINUTES * 60 * 1000
87
90
  const startTime = now - spanMs
@@ -125,7 +128,7 @@ export function ActivitySummary({ namespaces, topology, onNavigate }: ActivitySu
125
128
  <div className="flex-1 min-h-0 overflow-hidden px-4 py-1.5">
126
129
  {isLoading ? (
127
130
  <div className="flex items-center justify-center h-full py-4 text-xs text-theme-text-tertiary">
128
- Loading...
131
+ Loading
129
132
  </div>
130
133
  ) : error ? (
131
134
  <div className="flex items-center justify-center h-full py-4 text-xs text-theme-text-tertiary">
@@ -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
- <h2
223
- className="text-xl font-semibold text-theme-text-primary truncate mb-1.5 leading-tight"
224
- // In-cluster mode's cluster.name is the literal "in-cluster"
225
- // sentinel, which would leak via the browser hover tooltip
226
- // even though the visible text falls back to the platform
227
- // label. Drop the title attribute entirely in that case;
228
- // local mode keeps it so users can hover to see the full
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
- <span className="truncate font-mono" title={[parsedContext.account, parsedContext.region].filter(Boolean).join(' · ')}>
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
- <span
251
- className="font-mono text-[10px] text-theme-text-disabled break-all leading-snug pt-0.5"
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
- <span className="text-[10px] text-theme-text-tertiary truncate font-mono" title={mcpUrl}>
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