@skyhook-io/radar-app 0.2.2 → 0.3.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 (177) hide show
  1. package/README.md +7 -1
  2. package/package.json +33 -25
  3. package/src/App.tsx +1449 -382
  4. package/src/RadarApp.tsx +132 -19
  5. package/src/api/apiResources.ts +1 -1
  6. package/src/api/client.argoResourceSync.test.ts +69 -0
  7. package/src/api/client.delta.test.ts +89 -0
  8. package/src/api/client.deltaSync.test.ts +216 -0
  9. package/src/api/client.metrics.test.ts +106 -0
  10. package/src/api/client.rightsizing.test.ts +32 -0
  11. package/src/api/client.ts +2730 -271
  12. package/src/api/client.yaml.test.ts +45 -0
  13. package/src/api/diagnose.ts +289 -0
  14. package/src/api/quotas.ts +16 -0
  15. package/src/api/rbac.ts +57 -0
  16. package/src/api/timelineSource.test.ts +217 -0
  17. package/src/api/timelineSource.ts +582 -0
  18. package/src/components/ConnectionErrorView.tsx +186 -70
  19. package/src/components/ContextSwitcher.tsx +63 -18
  20. package/src/components/DebugOverlay.tsx +5 -3
  21. package/src/components/NamespaceSwitcher.tsx +41 -0
  22. package/src/components/UserMenu.tsx +69 -21
  23. package/src/components/applications/ApplicationsView.tsx +936 -0
  24. package/src/components/audit/AuditSettingsDialog.tsx +79 -17
  25. package/src/components/audit/AuditView.tsx +65 -62
  26. package/src/components/compare/CompareViewRoute.tsx +124 -0
  27. package/src/components/compare/useCompareCandidates.ts +27 -0
  28. package/src/components/compare/useCompareLauncher.tsx +79 -0
  29. package/src/components/cost/ApplicationCostTab.test.ts +204 -0
  30. package/src/components/cost/ApplicationCostTab.tsx +571 -0
  31. package/src/components/cost/CostTrendChart.tsx +106 -75
  32. package/src/components/cost/CostView.test.ts +12 -0
  33. package/src/components/cost/CostView.tsx +507 -223
  34. package/src/components/cost/CostViewTabs.test.tsx +21 -0
  35. package/src/components/cost/CostViewTabs.tsx +40 -0
  36. package/src/components/cost/CurrentAllocationUse.test.ts +21 -0
  37. package/src/components/cost/CurrentAllocationUse.tsx +126 -0
  38. package/src/components/cost/WorkloadCostTab.test.ts +153 -0
  39. package/src/components/cost/WorkloadCostTab.tsx +372 -0
  40. package/src/components/cost/cloud-console.test.ts +39 -0
  41. package/src/components/cost/cloud-console.ts +81 -0
  42. package/src/components/cost/errors.ts +8 -0
  43. package/src/components/cost/format.test.ts +27 -0
  44. package/src/components/cost/format.ts +46 -0
  45. package/src/components/cost/kinds.ts +5 -0
  46. package/src/components/curl/ServiceCurlButton.tsx +445 -0
  47. package/src/components/diagnose/AISettings.tsx +147 -0
  48. package/src/components/diagnose/DiagnoseContext.tsx +495 -0
  49. package/src/components/diagnose/DiagnoseSurface.tsx +394 -0
  50. package/src/components/diagnose/Home.tsx +163 -0
  51. package/src/components/diagnose/InvestigationView.tsx +622 -0
  52. package/src/components/diagnose/LocalDiagnoseAction.tsx +162 -0
  53. package/src/components/diagnose/launch.ts +65 -0
  54. package/src/components/diagnose/parts.tsx +1756 -0
  55. package/src/components/dock/BottomDock.tsx +2 -3
  56. package/src/components/dock/DockContext.tsx +1 -0
  57. package/src/components/dock/TerminalTab.tsx +1 -1
  58. package/src/components/dock/WorkloadLogsTab.tsx +21 -5
  59. package/src/components/dock/index.ts +1 -1
  60. package/src/components/execution/BatchExecutionView.test.ts +170 -0
  61. package/src/components/execution/BatchExecutionView.tsx +1329 -0
  62. package/src/components/execution/batch-run-actions.test.ts +48 -0
  63. package/src/components/execution/batch-run-actions.ts +24 -0
  64. package/src/components/execution/batch-timeline.test.ts +57 -0
  65. package/src/components/execution/batch-timeline.ts +46 -0
  66. package/src/components/execution/execution-definition.test.ts +208 -0
  67. package/src/components/execution/execution-definition.ts +245 -0
  68. package/src/components/gitops/ArgoResourceDiffLoader.tsx +23 -0
  69. package/src/components/gitops/GitOpsView.tsx +1042 -0
  70. package/src/components/gitops/RevisionMetaChip.tsx +63 -0
  71. package/src/components/helm/ChartBrowser.tsx +87 -31
  72. package/src/components/helm/HelmCompareRoute.tsx +1341 -0
  73. package/src/components/helm/HelmReleaseDrawer.test.ts +17 -0
  74. package/src/components/helm/HelmReleaseDrawer.tsx +1073 -102
  75. package/src/components/helm/HelmView.tsx +237 -96
  76. package/src/components/helm/InstallWizard.tsx +94 -38
  77. package/src/components/helm/ManifestDiffViewer.tsx +8 -27
  78. package/src/components/helm/OwnedResources.tsx +34 -59
  79. package/src/components/helm/RevisionHistory.tsx +52 -3
  80. package/src/components/helm/RoleGatedPanel.tsx +3 -3
  81. package/src/components/helm/TrackChartSourceDialog.tsx +185 -0
  82. package/src/components/helm/ValuesDiffPreview.tsx +17 -7
  83. package/src/components/helm/ValuesViewer.tsx +49 -53
  84. package/src/components/helm/helm-utils.ts +4 -0
  85. package/src/components/home/ActivitySummary.tsx +4 -1
  86. package/src/components/home/ClusterHealthCard.tsx +56 -42
  87. package/src/components/home/CostCard.tsx +21 -36
  88. package/src/components/home/GitOpsControllersCard.tsx +110 -0
  89. package/src/components/home/HelmSummary.tsx +3 -1
  90. package/src/components/home/HomeView.tsx +339 -105
  91. package/src/components/home/MCPSetupDialog.tsx +29 -87
  92. package/src/components/home/TrafficSummary.tsx +2 -2
  93. package/src/components/home/mcpToolCatalog.ts +333 -0
  94. package/src/components/issues/IssuesPane.tsx +151 -0
  95. package/src/components/logs/LogsViewer.tsx +4 -1
  96. package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +135 -0
  97. package/src/components/logs/WorkloadLogsViewer.tsx +4 -1
  98. package/src/components/nav/PrimaryNavRail.tsx +285 -0
  99. package/src/components/portforward/PortForwardButton.tsx +118 -47
  100. package/src/components/portforward/PortForwardManager.tsx +253 -131
  101. package/src/components/resource/HPACharts.tsx +237 -0
  102. package/src/components/resource/PVCUsageBar.tsx +59 -0
  103. package/src/components/resource/PrometheusCharts.tsx +160 -584
  104. package/src/components/resource/PrometheusChartsGrid.tsx +270 -0
  105. package/src/components/resource/RestartChart.tsx +133 -0
  106. package/src/components/resource/RightsizingStrip.test.ts +109 -0
  107. package/src/components/resource/RightsizingStrip.tsx +363 -0
  108. package/src/components/resource-drawer/ResourceDrawer.tsx +3 -1
  109. package/src/components/resources/CompositeRenderer.tsx +101 -0
  110. package/src/components/resources/ImageFilesystemModal.tsx +19 -12
  111. package/src/components/resources/PodFilesystemModal.tsx +6 -5
  112. package/src/components/resources/ResourceDetailDrawer.tsx +13 -3
  113. package/src/components/resources/ResourcesView.tsx +194 -17
  114. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +1 -0
  115. package/src/components/resources/renderers/HPARenderer.tsx +20 -1
  116. package/src/components/resources/renderers/NamespaceRenderer.tsx +31 -0
  117. package/src/components/resources/renderers/NodeRenderer.tsx +10 -4
  118. package/src/components/resources/renderers/PVCRenderer.tsx +19 -1
  119. package/src/components/resources/renderers/PodRenderer.tsx +30 -6
  120. package/src/components/resources/renderers/RoleBindingRenderer.tsx +45 -1
  121. package/src/components/resources/renderers/RoleRenderer.tsx +27 -1
  122. package/src/components/resources/renderers/ServiceAccountRenderer.tsx +28 -1
  123. package/src/components/resources/renderers/ServiceRenderer.tsx +81 -8
  124. package/src/components/resources/renderers/WorkloadRenderer.tsx +51 -4
  125. package/src/components/resources/renderers/index.ts +2 -0
  126. package/src/components/resources/resource-utils.ts +2 -1
  127. package/src/components/rightsizing/RightsizingScanView.tsx +938 -0
  128. package/src/components/rightsizing/copy.test.ts +56 -0
  129. package/src/components/rightsizing/model.test.ts +227 -0
  130. package/src/components/rightsizing/model.ts +158 -0
  131. package/src/components/rightsizing/presentation.test.ts +104 -0
  132. package/src/components/rightsizing/presentation.ts +94 -0
  133. package/src/components/settings/MyPermissionsDialog.tsx +241 -0
  134. package/src/components/settings/SettingsDialog.tsx +1505 -165
  135. package/src/components/shared/CreateResourceDialog.tsx +9 -2
  136. package/src/components/shared/LargeClusterNamespacePicker.tsx +3 -3
  137. package/src/components/timeline/LocalTimelineScrubber.tsx +212 -0
  138. package/src/components/timeline/RetainedTimelineScrubber.tsx +311 -0
  139. package/src/components/timeline/TimelineList.tsx +86 -13
  140. package/src/components/timeline/TimelineSwimlanes.tsx +9 -1299
  141. package/src/components/timeline/TimelineView.tsx +873 -24
  142. package/src/components/timeline/TimelineView.urlparams.test.ts +335 -0
  143. package/src/components/traffic/TrafficFilterSidebar.tsx +10 -45
  144. package/src/components/traffic/TrafficFlowList.tsx +29 -15
  145. package/src/components/traffic/TrafficGraph.tsx +42 -24
  146. package/src/components/traffic/TrafficView.tsx +32 -19
  147. package/src/components/ui/CommandPalette.tsx +8 -215
  148. package/src/components/ui/DiagnosticsOverlay.tsx +219 -9
  149. package/src/components/ui/Markdown.tsx +3 -3
  150. package/src/components/ui/Omnibar.tsx +602 -0
  151. package/src/components/ui/RadarOmnibar.tsx +52 -0
  152. package/src/components/ui/SearchSyntaxHelp.tsx +89 -0
  153. package/src/components/ui/ShortcutHelpOverlay.tsx +3 -2
  154. package/src/components/ui/UpdateNotification.tsx +48 -36
  155. package/src/components/ui/command-items.ts +178 -0
  156. package/src/components/workload/WorkloadView.tsx +1342 -158
  157. package/src/context/ConnectionContext.tsx +146 -21
  158. package/src/context/DiagnoseCustomization.tsx +93 -0
  159. package/src/context/NavCustomization.tsx +75 -0
  160. package/src/context/TimelineSource.tsx +50 -0
  161. package/src/contexts/CapabilitiesContext.tsx +32 -8
  162. package/src/filter/FilterLocationBridge.tsx +30 -0
  163. package/src/hooks/useClusterLoadState.ts +73 -0
  164. package/src/hooks/useDocumentTitle.ts +25 -0
  165. package/src/hooks/useEventSource.ts +6 -0
  166. package/src/hooks/useKeyboardShortcuts.tsx +1 -0
  167. package/src/hooks/useMediaQuery.ts +21 -0
  168. package/src/hooks/useNavRailPinned.ts +46 -0
  169. package/src/hooks/useRecentResources.ts +49 -0
  170. package/src/index.css +162 -1
  171. package/src/index.ts +73 -1
  172. package/src/main.tsx +7 -5
  173. package/src/types/clusterLoadState.ts +33 -0
  174. package/src/types.ts +2 -0
  175. package/src/utils/auditBadges.ts +53 -0
  176. package/src/utils/navigation.ts +64 -1
  177. package/src/components/ui/NamespaceSelector.tsx +0 -436
@@ -1,5 +1,5 @@
1
- import { useState } from 'react'
2
- import type { DashboardResponse, DashboardMetrics, DashboardCRDCount, DashboardProblem } from '../../api/client'
1
+ import { useState, type ReactNode } from 'react'
2
+ import type { DashboardResponse, DashboardMetrics, DashboardCRDCount } from '../../api/client'
3
3
  import { HealthRing } from './HealthRing'
4
4
  import {
5
5
  AlertTriangle, CheckCircle, XCircle,
@@ -23,12 +23,16 @@ interface ClusterHealthCardProps {
23
23
  metrics: DashboardMetrics | null
24
24
  metricsServerAvailable: boolean
25
25
  topCRDs?: DashboardCRDCount[] // Loaded lazily, may be undefined
26
- problems: DashboardProblem[]
26
+ issueCount: number
27
+ hasCriticalIssues: boolean
27
28
  nodeVersionSkew: DashboardResponse['nodeVersionSkew']
28
29
  onNavigateToKind: (kind: string, group?: string) => void
29
30
  onNavigateToView: () => void
30
31
  onWarningEventsClick?: () => void
31
- onUnhealthyClick?: () => void
32
+ onIssuesClick?: () => void
33
+ // Freshness/refresh control for the dashboard poll — rendered under the
34
+ // cluster metadata so the overview carries a freshness signal without a band.
35
+ freshness?: ReactNode
32
36
  }
33
37
 
34
38
  function getMetricsInstallHint(platform: string): string {
@@ -52,7 +56,7 @@ function MetricsUnavailableHint({ platform, metricsServerAvailable }: { platform
52
56
  content={
53
57
  <div className="space-y-1">
54
58
  <div className="font-medium">How to fix</div>
55
- <div>{isPreInstalled ? hint : <>Install by running:<br /><code className="text-[10px] opacity-80">{hint}</code></>}</div>
59
+ <div>{isPreInstalled ? hint : <>Install by running:<br /><code className="inline-code text-[10px] opacity-80">{hint}</code></>}</div>
56
60
  </div>
57
61
  }
58
62
  position="bottom"
@@ -113,12 +117,14 @@ export function ClusterHealthCard({
113
117
  metrics,
114
118
  metricsServerAvailable,
115
119
  topCRDs: _topCRDs,
116
- problems,
120
+ issueCount,
121
+ hasCriticalIssues,
117
122
  nodeVersionSkew,
118
123
  onNavigateToKind,
119
124
  onNavigateToView,
120
125
  onWarningEventsClick,
121
- onUnhealthyClick,
126
+ onIssuesClick,
127
+ freshness,
122
128
  }: ClusterHealthCardProps) {
123
129
  void _topCRDs // Reserved for future CRD display
124
130
 
@@ -217,24 +223,24 @@ export function ClusterHealthCard({
217
223
  its top bar; rendering it again here is redundant and
218
224
  makes the card feel like a label rather than content. */}
219
225
  {!isCloud && (
220
- <h2
221
- className="text-xl font-semibold text-theme-text-primary truncate mb-1.5 leading-tight"
222
- // In-cluster mode's cluster.name is the literal "in-cluster"
223
- // sentinel, which would leak via the browser hover tooltip
224
- // even though the visible text falls back to the platform
225
- // label. Drop the title attribute entirely in that case;
226
- // local mode keeps it so users can hover to see the full
227
- // kubeconfig context path.
228
- title={isInCluster ? undefined : cluster.name}
229
- >
226
+ // In-cluster mode's cluster.name is the literal "in-cluster"
227
+ // sentinel, which would leak via the hover tooltip even though
228
+ // the visible text falls back to the platform label. Render no
229
+ // tooltip in that case; local mode keeps it so users can hover
230
+ // to see the full kubeconfig context path.
231
+ <Tooltip content={isInCluster ? '' : cluster.name} wrapperClassName="!block min-w-0">
232
+ <h2 className="text-xl font-semibold text-theme-text-primary truncate leading-tight mb-1.5">
230
233
  {headlineName}
231
234
  </h2>
235
+ </Tooltip>
232
236
  )}
233
237
  <div className="flex flex-col gap-0.5 text-xs text-theme-text-tertiary">
234
238
  {(parsedContext.account || parsedContext.region) && (
235
- <span className="truncate font-mono" title={[parsedContext.account, parsedContext.region].filter(Boolean).join(' · ')}>
239
+ <Tooltip content={[parsedContext.account, parsedContext.region].filter(Boolean).join(' · ')} wrapperClassName="min-w-0">
240
+ <span className="truncate font-mono">
236
241
  {[parsedContext.account, parsedContext.region].filter(Boolean).join(' · ')}
237
242
  </span>
243
+ </Tooltip>
238
244
  )}
239
245
  {cluster.version && (
240
246
  <span>Kubernetes {cluster.version}</span>
@@ -245,14 +251,14 @@ export function ClusterHealthCard({
245
251
  (in-cluster has no meaningful context name, cloud
246
252
  shell already renders the canonical name). */}
247
253
  {cluster.name && cluster.name !== headlineName && deployment.mode === 'local' && (
248
- <span
249
- className="font-mono text-[10px] text-theme-text-disabled break-all leading-snug pt-0.5"
250
- title={cluster.name}
251
- >
254
+ <Tooltip content={cluster.name}>
255
+ <span className="font-mono text-[10px] text-theme-text-disabled break-all leading-snug pt-0.5">
252
256
  {cluster.name}
253
257
  </span>
258
+ </Tooltip>
254
259
  )}
255
260
  </div>
261
+ {freshness && <div className="mt-2">{freshness}</div>}
256
262
  {nodeVersionSkew && (
257
263
  <Tooltip
258
264
  content={
@@ -288,9 +294,11 @@ export function ClusterHealthCard({
288
294
  <Radio className="w-3.5 h-3.5 text-purple-400 animate-pulse shrink-0" />
289
295
  <div className="flex flex-col gap-0.5 min-w-0 flex-1 text-left">
290
296
  <span className="text-xs font-medium text-purple-400">MCP Server Live</span>
291
- <span className="text-[10px] text-theme-text-tertiary truncate font-mono" title={mcpUrl}>
297
+ <Tooltip content={mcpUrl} wrapperClassName="min-w-0">
298
+ <span className="text-[10px] text-theme-text-tertiary truncate font-mono">
292
299
  HTTP · {mcpUrl}
293
300
  </span>
301
+ </Tooltip>
294
302
  </div>
295
303
  <Info className="w-3.5 h-3.5 text-purple-400/60 shrink-0" />
296
304
  </button>
@@ -389,12 +397,14 @@ export function ClusterHealthCard({
389
397
  <Cpu className="w-3.5 h-3.5 text-theme-text-tertiary" />
390
398
  CPU
391
399
  </div>
392
- <ResourceBar
393
- label="Used"
394
- used={formatCPUMillicores(metrics.cpu.usageMillis)}
395
- total={formatCPUMillicores(metrics.cpu.capacityMillis)}
396
- percent={metrics.cpu.usagePercent}
397
- />
400
+ {metricsServerAvailable && (
401
+ <ResourceBar
402
+ label="Used"
403
+ used={formatCPUMillicores(metrics.cpu.usageMillis)}
404
+ total={formatCPUMillicores(metrics.cpu.capacityMillis)}
405
+ percent={metrics.cpu.usagePercent}
406
+ />
407
+ )}
398
408
  <ResourceBar
399
409
  label="Requested"
400
410
  used={formatCPUMillicores(metrics.cpu.requestsMillis)}
@@ -409,12 +419,14 @@ export function ClusterHealthCard({
409
419
  <MemoryStick className="w-3.5 h-3.5 text-theme-text-tertiary" />
410
420
  Memory
411
421
  </div>
412
- <ResourceBar
413
- label="Used"
414
- used={formatMemoryMiB(metrics.memory.usageMillis)}
415
- total={formatMemoryMiB(metrics.memory.capacityMillis)}
416
- percent={metrics.memory.usagePercent}
417
- />
422
+ {metricsServerAvailable && (
423
+ <ResourceBar
424
+ label="Used"
425
+ used={formatMemoryMiB(metrics.memory.usageMillis)}
426
+ total={formatMemoryMiB(metrics.memory.capacityMillis)}
427
+ percent={metrics.memory.usagePercent}
428
+ />
429
+ )}
418
430
  <ResourceBar
419
431
  label="Requested"
420
432
  used={formatMemoryMiB(metrics.memory.requestsMillis)}
@@ -423,7 +435,7 @@ export function ClusterHealthCard({
423
435
  />
424
436
  </div>
425
437
  )}
426
- {!metrics?.cpu && !metrics?.memory && (
438
+ {!metricsServerAvailable && (
427
439
  <MetricsUnavailableHint platform={cluster.platform} metricsServerAvailable={metricsServerAvailable} />
428
440
  )}
429
441
  </div>
@@ -437,24 +449,26 @@ export function ClusterHealthCard({
437
449
  {/* Left column: Warning indicators (aligned with cluster info) */}
438
450
  <div className="flex flex-col justify-center gap-1 w-1/4 shrink-0 pr-4 border-r border-theme-border/50">
439
451
  {health.warningEvents > 0 && (
452
+ <Tooltip content="Native Kubernetes Warning events (e.g., ImagePullBackOff, FailedScheduling)" wrapperClassName="w-fit">
440
453
  <button
441
454
  onClick={onWarningEventsClick}
442
- title="Native Kubernetes Warning events (e.g., ImagePullBackOff, FailedScheduling)"
443
455
  className="badge status-degraded w-fit gap-1.5 hover:opacity-80 transition-opacity"
444
456
  >
445
457
  <AlertTriangle className="w-3.5 h-3.5 shrink-0" />
446
458
  <span><span className="font-mono">{health.warningEvents}</span> Warning Events</span>
447
459
  </button>
460
+ </Tooltip>
448
461
  )}
449
- {problems.length > 0 && (
462
+ {issueCount > 0 && (
463
+ <Tooltip content="View grouped live operational issues" wrapperClassName="w-fit">
450
464
  <button
451
- onClick={onUnhealthyClick}
452
- title="View timeline of unhealthy/degraded workload events"
453
- className="badge status-unhealthy w-fit gap-1.5 hover:opacity-80 transition-opacity"
465
+ onClick={onIssuesClick}
466
+ className={clsx('badge w-fit gap-1.5 hover:opacity-80 transition-opacity', hasCriticalIssues ? 'status-unhealthy' : 'status-degraded')}
454
467
  >
455
468
  <AlertTriangle className="w-3.5 h-3.5 shrink-0" />
456
- <span>View unhealthy workload events</span>
469
+ <span>{pluralize(issueCount, 'Active Issue')}</span>
457
470
  </button>
471
+ </Tooltip>
458
472
  )}
459
473
  </div>
460
474
 
@@ -1,6 +1,12 @@
1
1
  import type { OpenCostSummary } from '../../api/client'
2
2
  import { useOpenCostSummary } from '../../api/client'
3
3
  import { DollarSign } from 'lucide-react'
4
+ import {
5
+ formatCostPerHour,
6
+ formatProjectedDailyRate,
7
+ formatProjectedMonthlyCost,
8
+ formatProjectedMonthlyRate,
9
+ } from '../cost/format'
4
10
 
5
11
  export function CostCard({ onNavigate }: { onNavigate?: () => void }) {
6
12
  const { data } = useOpenCostSummary()
@@ -15,7 +21,6 @@ export function CostCard({ onNavigate }: { onNavigate?: () => void }) {
15
21
 
16
22
  function CostCardContent({ data, onNavigate }: { data: OpenCostSummary; onNavigate?: () => void }) {
17
23
  const hourlyCost = data.totalHourlyCost ?? 0
18
- const monthlyCost = hourlyCost * 730
19
24
  const namespaces = data.namespaces ?? []
20
25
  const topNamespaces = namespaces.slice(0, 5)
21
26
 
@@ -30,10 +35,10 @@ function CostCardContent({ data, onNavigate }: { data: OpenCostSummary; onNaviga
30
35
  <div className="flex flex-col h-full w-full">
31
36
  <div className="flex items-center justify-between px-5 py-3 border-b border-theme-border/50">
32
37
  <div className="flex items-center gap-2">
33
- <DollarSign className="w-4 h-4 text-indigo-500" />
34
- <span className="text-xs font-semibold uppercase tracking-wider text-indigo-500">Cost Insights</span>
38
+ <DollarSign className="w-4 h-4 text-accent-text" />
39
+ <span className="text-xs font-semibold uppercase tracking-wider text-accent-text">Cost Insights</span>
35
40
  {namespaces.length > 0 && (
36
- <span className="badge-sm bg-indigo-100 text-indigo-700 border-indigo-300 dark:bg-indigo-950/50 dark:text-indigo-400 dark:border-indigo-700/40">
41
+ <span className="badge-sm border border-theme-border bg-accent-muted text-accent-text">
37
42
  {namespaces.length} ns
38
43
  </span>
39
44
  )}
@@ -45,13 +50,14 @@ function CostCardContent({ data, onNavigate }: { data: OpenCostSummary; onNaviga
45
50
  <div className="flex items-baseline gap-3 mb-3">
46
51
  <div className="flex items-baseline gap-1">
47
52
  <span className="text-2xl font-bold text-theme-text-primary tabular-nums">
48
- {formatCost(hourlyCost)}
53
+ {formatProjectedMonthlyCost(hourlyCost)}
49
54
  </span>
50
- <span className="text-xs text-theme-text-tertiary">/hr</span>
55
+ <span className="text-xs text-theme-text-tertiary">/mo</span>
51
56
  </div>
52
- <div className="flex items-baseline gap-1 text-theme-text-secondary">
53
- <span className="text-sm font-medium tabular-nums">~{formatCost(monthlyCost)}</span>
54
- <span className="text-[10px] text-theme-text-tertiary">/mo</span>
57
+ <div className="flex items-baseline gap-1.5 text-theme-text-secondary">
58
+ <span className="text-xs font-medium tabular-nums">{formatProjectedDailyRate(hourlyCost)}</span>
59
+ <span className="text-[10px] text-theme-text-quaternary">·</span>
60
+ <span className="text-xs font-medium tabular-nums">{formatCostPerHour(hourlyCost)}</span>
55
61
  </div>
56
62
  </div>
57
63
 
@@ -63,30 +69,25 @@ function CostCardContent({ data, onNavigate }: { data: OpenCostSummary; onNaviga
63
69
  <div key={ns.name} className="flex items-center gap-2">
64
70
  <span className="text-[11px] text-theme-text-secondary truncate w-24 shrink-0">{ns.name}</span>
65
71
  <div className="flex-1 h-2 rounded-full overflow-hidden bg-theme-hover">
66
- <div
67
- className="h-full rounded-full bg-indigo-500/60"
68
- style={{ width: `${Math.max(pct, 2)}%` }}
69
- />
72
+ <div className="h-full rounded-full bg-indigo-500/60" style={{ width: `${Math.max(pct, 2)}%` }} />
70
73
  </div>
71
- <span className="text-[10px] text-theme-text-tertiary tabular-nums w-14 text-right shrink-0">
72
- {formatCost(ns.hourlyCost)}/h
74
+ <span className="text-[10px] text-theme-text-tertiary tabular-nums w-20 text-right shrink-0">
75
+ {formatProjectedMonthlyRate(ns.hourlyCost)}
73
76
  </span>
74
77
  </div>
75
78
  )
76
79
  })}
77
80
  {namespaces.length > 5 && (
78
- <span className="text-[10px] text-theme-text-tertiary">
79
- +{namespaces.length - 5} more namespaces
80
- </span>
81
+ <span className="text-[10px] text-theme-text-tertiary">+{namespaces.length - 5} more namespaces</span>
81
82
  )}
82
83
  </div>
83
84
  </div>
84
85
 
85
86
  <div className="px-4 py-1.5 border-t border-theme-border/50 flex items-center justify-between">
86
87
  <span className="text-[10px] text-theme-text-tertiary">
87
- {data.currency ?? 'USD'} &middot; {data.window ?? '1h'} window
88
+ {data.currency ?? 'USD'} &middot; projected monthly from {data.window ?? '1h'} window
88
89
  </span>
89
- <span className="flex items-center gap-1.5 text-[10px] font-semibold uppercase tracking-wider text-indigo-500">
90
+ <span className="flex items-center gap-1.5 text-[10px] font-semibold uppercase tracking-wider text-accent-text">
90
91
  OpenCost
91
92
  </span>
92
93
  </div>
@@ -94,19 +95,3 @@ function CostCardContent({ data, onNavigate }: { data: OpenCostSummary; onNaviga
94
95
  </div>
95
96
  )
96
97
  }
97
-
98
- function formatCost(value: number): string {
99
- if (value >= 1000) {
100
- return `$${(value / 1000).toFixed(1)}k`
101
- }
102
- if (value >= 1) {
103
- return `$${value.toFixed(2)}`
104
- }
105
- if (value >= 0.01) {
106
- return `$${value.toFixed(3)}`
107
- }
108
- if (value > 0) {
109
- return `$${value.toFixed(4)}`
110
- }
111
- return '$0.00'
112
- }
@@ -0,0 +1,110 @@
1
+ import { GitBranch, AlertCircle, CheckCircle2 } from 'lucide-react'
2
+ import type { DashboardGitOpsControllers, DashboardGitOpsController } from '../../api/client'
3
+ import { clsx } from 'clsx'
4
+
5
+ interface GitOpsControllersCardProps {
6
+ data: DashboardGitOpsControllers
7
+ onNavigate: () => void
8
+ }
9
+
10
+ // GitOpsControllersCard surfaces Argo CD / Flux controller pod health
11
+ // on the Home dashboard so an operator can spot "source-controller is
12
+ // CrashLoopBackOff" before drilling into individual GitOps applications
13
+ // and seeing the *symptoms* (apps stuck OutOfSync, sources unfetched).
14
+ //
15
+ // Capability-gated: the parent only renders this card when the backend
16
+ // actually detected controllers in the cluster (DashboardResponse.gitopsControllers
17
+ // is null on non-GitOps clusters). The card itself doesn't have an
18
+ // "empty state" branch — by the time we get here, we have something to show.
19
+ export function GitOpsControllersCard({ data, onNavigate }: GitOpsControllersCardProps) {
20
+ const headerTone =
21
+ data.status === 'crashing'
22
+ ? 'text-red-500'
23
+ : data.status === 'degraded'
24
+ ? 'text-amber-400'
25
+ : 'text-emerald-500'
26
+
27
+ const headerLabel =
28
+ data.status === 'crashing'
29
+ ? 'Controllers crashing'
30
+ : data.status === 'degraded'
31
+ ? 'Controllers degraded'
32
+ : 'Controllers healthy'
33
+
34
+ // Group controllers by tool so the card reads as two sections (Argo +
35
+ // Flux) when both are installed. Operators with only one tool see a
36
+ // single-section card without empty placeholders.
37
+ const argo = data.controllers.filter((c) => c.tool === 'argocd')
38
+ const flux = data.controllers.filter((c) => c.tool === 'fluxcd')
39
+
40
+ return (
41
+ <button
42
+ type="button"
43
+ onClick={onNavigate}
44
+ className="group h-[260px] rounded-xl bg-theme-surface shadow-theme-sm hover:-translate-y-1 hover:shadow-theme-md transition-all duration-200 text-left animate-fade-in-up"
45
+ >
46
+ <div className="flex flex-col h-full w-full">
47
+ <div className="flex items-center justify-between px-5 py-3 border-b border-theme-border/50">
48
+ <div className="flex items-center gap-2">
49
+ <GitBranch className={clsx('h-4 w-4', headerTone)} />
50
+ <span className={clsx('text-xs font-semibold uppercase tracking-wider', headerTone)}>
51
+ GitOps Controllers
52
+ </span>
53
+ </div>
54
+ <span className={clsx('text-[11px] font-medium', headerTone)}>{headerLabel}</span>
55
+ </div>
56
+
57
+ <div className="flex-1 min-h-0 overflow-y-auto px-5 py-3 flex flex-col gap-3">
58
+ {argo.length > 0 && <ControllerSection label="Argo CD" controllers={argo} />}
59
+ {flux.length > 0 && <ControllerSection label="Flux CD" controllers={flux} />}
60
+ </div>
61
+ </div>
62
+ </button>
63
+ )
64
+ }
65
+
66
+ function ControllerSection({ label, controllers }: { label: string; controllers: DashboardGitOpsController[] }) {
67
+ return (
68
+ <div>
69
+ <div className="mb-1 text-[10px] font-medium uppercase tracking-wide text-theme-text-tertiary">{label}</div>
70
+ <div className="flex flex-col gap-1">
71
+ {controllers.map((c) => (
72
+ <ControllerRow key={`${c.tool}-${c.name}`} c={c} />
73
+ ))}
74
+ </div>
75
+ </div>
76
+ )
77
+ }
78
+
79
+ function ControllerRow({ c }: { c: DashboardGitOpsController }) {
80
+ // Per-row tone matches the per-controller status. We don't reuse
81
+ // mapHealthToTone because the controller status vocabulary
82
+ // (healthy/degraded/crashing/pending) is different from resource
83
+ // health (Healthy/Degraded/Missing/etc). Mapping inline keeps the
84
+ // intent obvious.
85
+ const tone =
86
+ c.status === 'crashing'
87
+ ? 'text-red-500'
88
+ : c.status === 'degraded' || c.status === 'pending'
89
+ ? 'text-amber-400'
90
+ : 'text-emerald-500'
91
+ const Icon = c.status === 'crashing' ? AlertCircle : c.status === 'degraded' || c.status === 'pending' ? AlertCircle : CheckCircle2
92
+ return (
93
+ <div className="flex items-center justify-between gap-2 text-[12px]">
94
+ <div className="flex min-w-0 items-center gap-1.5">
95
+ <Icon className={clsx('h-3 w-3 shrink-0', tone)} />
96
+ <span className="truncate text-theme-text-primary">{c.name}</span>
97
+ </div>
98
+ <div className="flex shrink-0 items-center gap-1.5 text-[11px] text-theme-text-secondary">
99
+ <span>
100
+ {c.ready}/{c.total} ready
101
+ </span>
102
+ {c.crashReason && (
103
+ <span className={clsx('rounded border px-1 py-px text-[10px] font-medium', 'border-red-500/40 bg-red-500/10 text-red-400')}>
104
+ {c.crashReason}
105
+ </span>
106
+ )}
107
+ </div>
108
+ </div>
109
+ )
110
+ }
@@ -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
- <span className="text-[11px] mt-1 text-center px-2 truncate max-w-full" title={data.error}>
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">