@skyhook-io/radar-app 1.16.3 → 1.17.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 (321) hide show
  1. package/README.md +1 -1
  2. package/package.json +3 -3
  3. package/src/App.tsx +138 -93
  4. package/src/RadarApp.tsx +27 -17
  5. package/src/api/actions.errorCode.test.ts +40 -0
  6. package/src/api/actions.test.ts +48 -0
  7. package/src/api/actions.ts +79 -0
  8. package/src/api/apiResources.navigation.test.tsx +232 -0
  9. package/src/api/apiResources.ts +46 -3
  10. package/src/api/client.goneResource.test.ts +49 -0
  11. package/src/api/client.helmSource.test.ts +39 -0
  12. package/src/api/client.namespaceSwitch.hook.test.tsx +54 -0
  13. package/src/api/client.namespaceSwitch.test.ts +107 -0
  14. package/src/api/client.ts +381 -47
  15. package/src/api/cnpg-ha.live.test.ts +140 -0
  16. package/src/api/cnpg-ha.test.ts +78 -0
  17. package/src/api/cnpg-ha.ts +19 -0
  18. package/src/api/cnpg-history.ts +123 -0
  19. package/src/api/cnpg-inspect.ts +102 -0
  20. package/src/api/cnpg-protection.ts +112 -0
  21. package/src/api/cnpg-recovery.test.tsx +60 -0
  22. package/src/api/cnpg-recovery.ts +262 -0
  23. package/src/api/cnpg-sessions.ts +175 -0
  24. package/src/api/cnpg-storage.ts +138 -0
  25. package/src/api/cnpg.legacy-reads.test.ts +43 -0
  26. package/src/api/cnpg.pooler-runtime.test.ts +15 -0
  27. package/src/api/cnpg.ts +428 -10
  28. package/src/api/diagnose.test.ts +40 -0
  29. package/src/api/diagnose.ts +12 -1
  30. package/src/api/radarFeatures.render.test.tsx +65 -3
  31. package/src/api/radarFeatures.ts +6 -0
  32. package/src/api/reviewedActionMutation.test.tsx +164 -0
  33. package/src/api/reviewedActionMutation.ts +66 -0
  34. package/src/api/timelineSource.test.ts +9 -0
  35. package/src/api/timelineSource.ts +8 -0
  36. package/src/api/traffic.sources.test.ts +55 -0
  37. package/src/api/traffic.ts +173 -14
  38. package/src/components/applications/ApplicationsView.tsx +3 -0
  39. package/src/components/audit/AuditView.tsx +9 -0
  40. package/src/components/capacity/CapacityActivity.tsx +3 -31
  41. package/src/components/capacity/CapacityDemand.tsx +4 -36
  42. package/src/components/capacity/CapacityOverview.tsx +217 -56
  43. package/src/components/capacity/CapacityPoolDetail.tsx +24 -62
  44. package/src/components/capacity/CapacityView.test.tsx +354 -1
  45. package/src/components/capacity/ClusterSchedulingCard.tsx +2 -10
  46. package/src/components/capacity/certaintyGlyph.test.tsx +2 -9
  47. package/src/components/capacity/routes.ts +42 -0
  48. package/src/components/capacity/shared.tsx +23 -150
  49. package/src/components/capacity/workspaceRoute.ts +18 -0
  50. package/src/components/cnpg/CNPGActionHost.compatibility.test.tsx +30 -0
  51. package/src/components/cnpg/CNPGArchivingRepair.tsx +191 -0
  52. package/src/components/cnpg/CNPGBlockingSessions.test.tsx +50 -0
  53. package/src/components/cnpg/CNPGBlockingSessions.tsx +333 -0
  54. package/src/components/cnpg/CNPGClusterActivity.test.tsx +11 -0
  55. package/src/components/cnpg/CNPGClusterActivity.tsx +18 -9
  56. package/src/components/cnpg/CNPGClusterLogs.compatibility.test.tsx +130 -0
  57. package/src/components/cnpg/CNPGClusterLogs.empty.test.tsx +29 -0
  58. package/src/components/cnpg/CNPGClusterLogs.test.ts +9 -0
  59. package/src/components/cnpg/CNPGClusterLogs.tsx +81 -24
  60. package/src/components/cnpg/CNPGClusterTabs.test.tsx +152 -0
  61. package/src/components/cnpg/CNPGClusterTabs.tsx +187 -0
  62. package/src/components/cnpg/CNPGClustersKindView.tsx +46 -0
  63. package/src/components/cnpg/CNPGConfiguration.test.tsx +223 -0
  64. package/src/components/cnpg/CNPGConfiguration.tsx +190 -0
  65. package/src/components/cnpg/CNPGCreateClusterDialog.test.tsx +113 -0
  66. package/src/components/cnpg/CNPGCreateClusterDialog.tsx +74 -0
  67. package/src/components/cnpg/CNPGDeclarations.test.ts +107 -0
  68. package/src/components/cnpg/CNPGDeclarations.tsx +236 -78
  69. package/src/components/cnpg/CNPGDetailPage.test.tsx +62 -0
  70. package/src/components/cnpg/CNPGDetailPage.tsx +159 -98
  71. package/src/components/cnpg/CNPGDrawerTrail.tsx +2 -2
  72. package/src/components/cnpg/CNPGInvestigationAction.test.tsx +30 -0
  73. package/src/components/cnpg/CNPGOperator.test.tsx +34 -0
  74. package/src/components/cnpg/CNPGOperator.tsx +78 -30
  75. package/src/components/cnpg/CNPGOperatorBanner.tsx +39 -0
  76. package/src/components/cnpg/CNPGOperatorConditions.test.tsx +45 -0
  77. package/src/components/cnpg/CNPGOperatorConditions.tsx +16 -0
  78. package/src/components/cnpg/CNPGOperatorConfig.test.tsx +27 -0
  79. package/src/components/cnpg/CNPGOperatorDiagnosis.tsx +247 -0
  80. package/src/components/cnpg/CNPGOverview.test.tsx +171 -0
  81. package/src/components/cnpg/CNPGOverview.tsx +238 -63
  82. package/src/components/cnpg/CNPGParametersInEffect.test.tsx +51 -0
  83. package/src/components/cnpg/CNPGParametersInEffect.tsx +120 -0
  84. package/src/components/cnpg/CNPGPerformance.prerequisite.test.tsx +48 -0
  85. package/src/components/cnpg/CNPGPerformance.test.tsx +73 -0
  86. package/src/components/cnpg/CNPGPerformance.tsx +477 -0
  87. package/src/components/cnpg/CNPGPooling.test.tsx +89 -0
  88. package/src/components/cnpg/CNPGPooling.tsx +97 -40
  89. package/src/components/cnpg/CNPGProtection.test.tsx +145 -0
  90. package/src/components/cnpg/CNPGProtection.tsx +230 -124
  91. package/src/components/cnpg/CNPGReplicationTab.test.tsx +87 -0
  92. package/src/components/cnpg/CNPGReplicationTab.tsx +212 -0
  93. package/src/components/cnpg/CNPGReplicationView.tsx +264 -0
  94. package/src/components/cnpg/CNPGStorage.test.tsx +140 -0
  95. package/src/components/cnpg/CNPGStorage.tsx +619 -0
  96. package/src/components/cnpg/CNPGSummaryHost.overview.test.tsx +44 -0
  97. package/src/components/cnpg/CNPGSummaryHost.test.tsx +23 -0
  98. package/src/components/cnpg/CNPGSummaryHost.tsx +228 -54
  99. package/src/components/cnpg/CNPGTrends.denied.test.tsx +33 -0
  100. package/src/components/cnpg/CNPGTrends.missing.test.tsx +69 -0
  101. package/src/components/cnpg/CNPGTrends.test.ts +38 -0
  102. package/src/components/cnpg/CNPGTrends.tsx +702 -0
  103. package/src/components/cnpg/CNPGView.tsx +34 -21
  104. package/src/components/cnpg/CNPGWorkspace.compatibility.test.tsx +90 -0
  105. package/src/components/cnpg/actions/CNPGClusterActions.dialog.test.tsx +79 -0
  106. package/src/components/cnpg/actions/CNPGClusterActions.test.tsx +142 -0
  107. package/src/components/cnpg/actions/CNPGClusterActions.tsx +853 -0
  108. package/src/components/cnpg/actions/CNPGConnectButton.read.test.tsx +40 -0
  109. package/src/components/cnpg/actions/CNPGConnectButton.test.ts +25 -0
  110. package/src/components/cnpg/actions/CNPGConnectButton.tsx +131 -0
  111. package/src/components/cnpg/actions/CNPGDestroyInstanceDialog.tsx +185 -0
  112. package/src/components/cnpg/actions/CNPGInstanceActions.test.tsx +38 -0
  113. package/src/components/cnpg/actions/CNPGInstanceActions.tsx +74 -0
  114. package/src/components/cnpg/actions/CNPGMaintenanceBanner.tsx +53 -0
  115. package/src/components/cnpg/actions/CNPGPoolerActions.test.tsx +35 -0
  116. package/src/components/cnpg/actions/CNPGPoolerActions.tsx +79 -0
  117. package/src/components/cnpg/actions/CNPGReportDialog.test.tsx +12 -0
  118. package/src/components/cnpg/actions/CNPGReportDialog.tsx +91 -0
  119. package/src/components/cnpg/actions/CNPGScheduleActions.run.test.tsx +58 -0
  120. package/src/components/cnpg/actions/CNPGScheduleActions.test.tsx +54 -0
  121. package/src/components/cnpg/actions/CNPGScheduleActions.tsx +208 -0
  122. package/src/components/cnpg/actions/actionModel.test.ts +111 -0
  123. package/src/components/cnpg/actions/actionModel.ts +127 -0
  124. package/src/components/cnpg/actions/renderCNPGHeaderActions.tsx +60 -0
  125. package/src/components/cnpg/actions/useCNPGWriteGuard.tsx +58 -0
  126. package/src/components/cnpg/actions/useOpenCNPGPsql.ts +22 -0
  127. package/src/components/cnpg/archivingRepair.test.ts +104 -0
  128. package/src/components/cnpg/archivingRepair.ts +145 -0
  129. package/src/components/cnpg/baseBackup.test.ts +36 -0
  130. package/src/components/cnpg/baseBackup.ts +36 -0
  131. package/src/components/cnpg/blocking.test.ts +59 -0
  132. package/src/components/cnpg/blocking.ts +131 -0
  133. package/src/components/cnpg/coverageText.test.ts +27 -0
  134. package/src/components/cnpg/creation/CNPGTargetFields.tsx +110 -0
  135. package/src/components/cnpg/creation/targetModel.test.ts +137 -0
  136. package/src/components/cnpg/creation/targetModel.ts +149 -0
  137. package/src/components/cnpg/fleetStatus.test.ts +46 -0
  138. package/src/components/cnpg/fleetStatus.ts +43 -0
  139. package/src/components/cnpg/formFields.ts +2 -0
  140. package/src/components/cnpg/grantText.test.tsx +11 -0
  141. package/src/components/cnpg/host.test.tsx +24 -0
  142. package/src/components/cnpg/host.tsx +101 -0
  143. package/src/components/cnpg/inspectModel.test.ts +120 -0
  144. package/src/components/cnpg/inspectModel.ts +140 -0
  145. package/src/components/cnpg/logicalSlots.test.ts +30 -0
  146. package/src/components/cnpg/logicalSlots.ts +29 -0
  147. package/src/components/cnpg/lsn.test.ts +40 -0
  148. package/src/components/cnpg/lsn.ts +43 -0
  149. package/src/components/cnpg/operations/CNPGFleetOperation.tsx +30 -0
  150. package/src/components/cnpg/operations/CNPGOperationTracker.test.tsx +247 -0
  151. package/src/components/cnpg/operations/CNPGOperationTracker.tsx +255 -0
  152. package/src/components/cnpg/operations/model.test.ts +510 -0
  153. package/src/components/cnpg/operations/model.ts +499 -0
  154. package/src/components/cnpg/operations/presentation.test.ts +80 -0
  155. package/src/components/cnpg/operations/presentation.ts +84 -0
  156. package/src/components/cnpg/operations/store.ts +98 -0
  157. package/src/components/cnpg/operatorStatus.test.ts +134 -0
  158. package/src/components/cnpg/operatorStatus.ts +194 -0
  159. package/src/components/cnpg/paths.test.ts +54 -0
  160. package/src/components/cnpg/paths.ts +47 -4
  161. package/src/components/cnpg/protection/CNPGAttachArchiveDialog.tsx +185 -0
  162. package/src/components/cnpg/protection/CNPGCreateScheduleDialog.tsx +196 -0
  163. package/src/components/cnpg/protection/CNPGProtectionSetup.test.tsx +121 -0
  164. package/src/components/cnpg/protection/CNPGProtectionSetup.tsx +452 -0
  165. package/src/components/cnpg/protection/CNPGScheduleInput.tsx +115 -0
  166. package/src/components/cnpg/protection/CNPGScheduleRepairButton.tsx +120 -0
  167. package/src/components/cnpg/protection/scheduleModel.test.ts +19 -0
  168. package/src/components/cnpg/protection/scheduleModel.ts +16 -0
  169. package/src/components/cnpg/recovery/CNPGRecoveryPoint.tsx +220 -0
  170. package/src/components/cnpg/recovery/CNPGRestoreButton.test.tsx +29 -0
  171. package/src/components/cnpg/recovery/CNPGRestoreButton.tsx +26 -0
  172. package/src/components/cnpg/recovery/CNPGRestoreChecks.test.tsx +23 -0
  173. package/src/components/cnpg/recovery/CNPGRestoreChecks.tsx +153 -0
  174. package/src/components/cnpg/recovery/CNPGRestoreDialog.back.test.tsx +149 -0
  175. package/src/components/cnpg/recovery/CNPGRestoreDialog.test.tsx +46 -0
  176. package/src/components/cnpg/recovery/CNPGRestoreDialog.tsx +333 -0
  177. package/src/components/cnpg/recovery/CNPGRestoreProgress.tsx +182 -0
  178. package/src/components/cnpg/recovery/CNPGRestoreValidation.tsx +216 -0
  179. package/src/components/cnpg/recovery/restoreModel.test.ts +709 -0
  180. package/src/components/cnpg/recovery/restoreModel.ts +685 -0
  181. package/src/components/cnpg/recovery/restoreOperation.ts +56 -0
  182. package/src/components/cnpg/refresh.test.ts +38 -0
  183. package/src/components/cnpg/refresh.ts +17 -0
  184. package/src/components/cnpg/refreshNotice.test.tsx +29 -0
  185. package/src/components/cnpg/routes.test.ts +34 -3
  186. package/src/components/cnpg/routes.ts +60 -48
  187. package/src/components/cnpg/runtimeAssessment.ts +198 -0
  188. package/src/components/cnpg/runtimeModel.test.ts +175 -0
  189. package/src/components/cnpg/runtimeModel.ts +196 -0
  190. package/src/components/cnpg/runtimeParts.test.tsx +17 -0
  191. package/src/components/cnpg/runtimeParts.tsx +79 -0
  192. package/src/components/cnpg/shared.test.tsx +59 -0
  193. package/src/components/cnpg/shared.tsx +100 -182
  194. package/src/components/cnpg/storageModel.test.ts +107 -0
  195. package/src/components/cnpg/storageModel.ts +101 -0
  196. package/src/components/cnpg/trendSamples.test.ts +148 -0
  197. package/src/components/cnpg/trendSamples.ts +222 -0
  198. package/src/components/cnpg/useCNPGClusterAssessment.ts +30 -0
  199. package/src/components/cnpg/useCNPGKubectlContext.test.tsx +45 -0
  200. package/src/components/cnpg/useCNPGKubectlContext.ts +13 -0
  201. package/src/components/cnpg/useCNPGNavigate.test.tsx +58 -0
  202. package/src/components/cnpg/useCNPGNavigate.ts +21 -0
  203. package/src/components/cnpg/useCNPGPoolerLive.ts +46 -0
  204. package/src/components/cnpg/useCNPGScreenParams.ts +24 -0
  205. package/src/components/cnpg/useCNPGSidebarWorkspace.ts +66 -16
  206. package/src/components/cnpg/workspaceRoute.ts +23 -0
  207. package/src/components/cost/ApplicationCostTab.test.ts +7 -0
  208. package/src/components/cost/ApplicationCostTab.tsx +7 -7
  209. package/src/components/cost/CostView.test.ts +2 -1
  210. package/src/components/cost/CostView.tsx +0 -2
  211. package/src/components/cost/WorkloadCostTab.tsx +2 -4
  212. package/src/components/cost/errors.ts +3 -1
  213. package/src/components/cost/source.test.ts +10 -0
  214. package/src/components/cost/source.ts +5 -1
  215. package/src/components/diagnose/AgentSetupNotice.test.tsx +58 -0
  216. package/src/components/diagnose/AgentSetupNotice.tsx +142 -51
  217. package/src/components/diagnose/ApplyDialog.ack.test.tsx +47 -0
  218. package/src/components/diagnose/ApplyDialog.test.tsx +59 -6
  219. package/src/components/diagnose/ApplyDialog.tsx +26 -28
  220. package/src/components/diagnose/DiagnoseContext.test.ts +43 -0
  221. package/src/components/diagnose/DiagnoseContext.tsx +136 -54
  222. package/src/components/diagnose/DiagnoseSurface.tsx +16 -3
  223. package/src/components/diagnose/InvestigationView.tsx +39 -2
  224. package/src/components/diagnose/LocalDiagnoseAction.tsx +40 -20
  225. package/src/components/diagnose/agentCatalog.ts +2 -2
  226. package/src/components/diagnose/diagnoseEvidenceTypes.ts +2 -0
  227. package/src/components/diagnose/investigationEvidence/adapters/issues.ts +8 -0
  228. package/src/components/diagnose/investigationEvidence/projection.test.ts +16 -0
  229. package/src/components/dock/BottomDock.tsx +2 -0
  230. package/src/components/dock/TerminalTab.test.tsx +17 -0
  231. package/src/components/dock/TerminalTab.tsx +7 -3
  232. package/src/components/dock/TrafficFlowListTab.tsx +27 -6
  233. package/src/components/execution/BatchExecutionView.tsx +11 -11
  234. package/src/components/gitops/ArgoResourceDiffLoader.tsx +4 -2
  235. package/src/components/gitops/GitOpsView.tsx +16 -23
  236. package/src/components/helm/ChartBrowser.tsx +3 -3
  237. package/src/components/helm/HelmCompareRoute.test.tsx +59 -0
  238. package/src/components/helm/HelmCompareRoute.tsx +57 -26
  239. package/src/components/helm/HelmReleaseDrawer.test.ts +55 -3
  240. package/src/components/helm/HelmReleaseDrawer.tsx +50 -12
  241. package/src/components/helm/RevisionHistory.tsx +3 -2
  242. package/src/components/helm/TrackChartSourceDialog.render.test.tsx +120 -0
  243. package/src/components/helm/TrackChartSourceDialog.test.ts +50 -0
  244. package/src/components/helm/TrackChartSourceDialog.tsx +153 -176
  245. package/src/components/helm/ValuesViewer.test.tsx +301 -0
  246. package/src/components/helm/ValuesViewer.tsx +169 -78
  247. package/src/components/home/ActivitySummary.tsx +2 -1
  248. package/src/components/home/ClusterHealthCard.tsx +7 -0
  249. package/src/components/home/HomeView.tsx +20 -9
  250. package/src/components/home/ProblemsPanel.test.tsx +81 -0
  251. package/src/components/home/TopologyPreview.tsx +2 -2
  252. package/src/components/home/mcpToolCatalog.ts +1 -1
  253. package/src/components/issues/IssuesPane.render.test.tsx +122 -0
  254. package/src/components/issues/IssuesPane.test.ts +65 -1
  255. package/src/components/issues/IssuesPane.tsx +188 -25
  256. package/src/components/issues/uncheckedSources.ts +21 -0
  257. package/src/components/resource/PVCUsageBar.render.test.tsx +32 -0
  258. package/src/components/resource/PVCUsageBar.test.tsx +24 -0
  259. package/src/components/resource/PVCUsageBar.tsx +15 -6
  260. package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +3 -1
  261. package/src/components/resource/WorkloadMetricsHelpDialog.tsx +1 -1
  262. package/src/components/resources/CompositeRenderer.tsx +4 -4
  263. package/src/components/resources/ResourceDetailDrawer.tsx +2 -2
  264. package/src/components/resources/ResourcesView.tsx +26 -12
  265. package/src/components/resources/renderers/KyvernoPolicyQueued.tsx +2 -1
  266. package/src/components/resources/renderers/NodeRenderer.tsx +7 -1
  267. package/src/components/resources/renderers/ServiceRenderer.test.tsx +81 -0
  268. package/src/components/resources/renderers/ServiceRenderer.tsx +19 -14
  269. package/src/components/settings/LocalIntegrationStatus.test.tsx +52 -0
  270. package/src/components/settings/LocalIntegrationStatus.tsx +3 -4
  271. package/src/components/settings/SettingsDialog.tsx +150 -34
  272. package/src/components/shared/LargeClusterNamespacePicker.tsx +11 -5
  273. package/src/components/timeline/TimelineList.tsx +25 -24
  274. package/src/components/timeline/TimelineLoadError.test.tsx +62 -0
  275. package/src/components/timeline/TimelineLoadError.tsx +96 -0
  276. package/src/components/timeline/TimelineRetentionNote.test.tsx +49 -0
  277. package/src/components/timeline/TimelineRetentionNote.tsx +20 -0
  278. package/src/components/timeline/TimelineView.tsx +25 -45
  279. package/src/components/timeline/ViewModeToggle.tsx +20 -0
  280. package/src/components/timeline/useStickyError.test.tsx +47 -0
  281. package/src/components/timeline/useStickyError.ts +15 -0
  282. package/src/components/traffic/TrafficConnectionsTable.tsx +144 -0
  283. package/src/components/traffic/TrafficFilterSidebar.tsx +22 -1
  284. package/src/components/traffic/TrafficFlowListContext.tsx +12 -4
  285. package/src/components/traffic/TrafficFocusSearch.tsx +172 -0
  286. package/src/components/traffic/TrafficGraph.tsx +109 -26
  287. package/src/components/traffic/TrafficGraphTooLarge.tsx +150 -0
  288. package/src/components/traffic/TrafficView.tsx +582 -149
  289. package/src/components/traffic/trafficFilters.test.ts +398 -1
  290. package/src/components/traffic/trafficFilters.ts +408 -0
  291. package/src/components/ui/DiagnosticsOverlay.tsx +34 -2
  292. package/src/components/ui/Markdown.test.tsx +65 -0
  293. package/src/components/ui/Markdown.tsx +144 -127
  294. package/src/components/whats-new/releaseNotes.ts +63 -1
  295. package/src/components/workload/WorkloadView.test.ts +11 -1
  296. package/src/components/workload/WorkloadView.tsx +162 -529
  297. package/src/components/workspace/layout.test.tsx +29 -0
  298. package/src/components/workspace/layout.tsx +259 -0
  299. package/src/components/workspace/table.ts +7 -0
  300. package/src/context/TimelineCustomization.tsx +15 -0
  301. package/src/contexts/CapabilitiesContext.tsx +1 -1
  302. package/src/hooks/useGitOpsWriteGuard.test.tsx +457 -0
  303. package/src/hooks/useGitOpsWriteGuard.ts +193 -0
  304. package/src/hooks/useResolvedGitOpsOwner.ts +256 -0
  305. package/src/integrations/resourceHost.test.tsx +55 -0
  306. package/src/integrations/resourceHost.ts +99 -0
  307. package/src/integrations/resourceHosts.test.tsx +141 -0
  308. package/src/integrations/resourceHosts.tsx +119 -0
  309. package/src/integrations/workspaceRoute.ts +20 -0
  310. package/src/integrations/workspaceRoutes.test.ts +40 -0
  311. package/src/integrations/workspaceRoutes.ts +22 -0
  312. package/src/integrations/workspaceScreens.tsx +14 -0
  313. package/src/utils/drawer-trail.ts +35 -0
  314. package/src/utils/kindFilterStore.test.ts +147 -0
  315. package/src/utils/kindFilterStore.ts +91 -0
  316. package/src/utils/navigation.test.ts +13 -1
  317. package/src/utils/navigation.ts +39 -2
  318. package/src/utils/page-links.ts +21 -0
  319. package/src/utils/tabStorage.ts +40 -0
  320. package/src/utils/viewMemory.test.ts +78 -0
  321. package/src/utils/viewMemory.ts +54 -0
@@ -1,13 +1,18 @@
1
1
  import { Loader2, Sparkles } from "lucide-react";
2
- import { useDiagnose, useDiagnoseLayout } from "./DiagnoseContext";
2
+ import {
3
+ type DiagnoseSetup,
4
+ useDiagnose,
5
+ useDiagnoseLayout,
6
+ } from "./DiagnoseContext";
3
7
  import { runTargetKey } from "./target";
4
8
  import { Tooltip } from "../ui/Tooltip";
5
9
  import type { RenderDiagnoseAction } from "../../context/DiagnoseCustomization";
6
10
 
7
11
  // The per-resource AI entry point. It no longer owns a panel — it just dispatches
8
12
  // to the single app-level AI surface (DiagnoseContext), opening a new investigation
9
- // for this resource. Self-hides when no agent CLI is present. Hosts can override
10
- // this slot with their own action.
13
+ // for this resource. Hidden only where investigations can't run here; with no
14
+ // agent CLI it stays, and opens the setup notice. Hosts can override this slot
15
+ // with their own action.
11
16
  //
12
17
  // Adaptive by health: on a resource with a live problem it reads as a prominent
13
18
  // "Investigate" action; when the resource is fine or health is unknown it shrinks
@@ -33,13 +38,13 @@ function DiagnoseResourceButton({
33
38
  // Hidden only when the feature can't work here (auth/cloud/--no-mcp). When it's
34
39
  // supported but no agent is installed we KEEP the button — clicking opens the
35
40
  // setup notice so the feature is discoverable rather than silently absent.
36
- if (d.setupState === "off") return null;
41
+ if (d.setupState === "off" || d.setupState === "unknown") return null;
37
42
  const ready = d.available;
38
43
  const problem = health === "problem";
39
44
  const running =
40
45
  ready && runningKeys.has(runTargetKey(kind, namespace, name, group ?? ""));
41
46
  const tooltip = !ready
42
- ? "Set up AI investigations — runs your own agent locally."
47
+ ? notReadyTooltip(d)
43
48
  : running
44
49
  ? `${d.agentLabel} is investigating this resource — click to watch it live.`
45
50
  : d.hosted
@@ -67,7 +72,7 @@ function DiagnoseResourceButton({
67
72
  }
68
73
  aria-label={
69
74
  !ready
70
- ? "Set up AI investigations"
75
+ ? notReadyTooltip(d)
71
76
  : running
72
77
  ? "Investigation running — click to view"
73
78
  : problem
@@ -122,18 +127,16 @@ export function IssueDiagnoseButton({
122
127
  name: string;
123
128
  }) {
124
129
  const d = useDiagnose();
125
- if (d.setupState === "off") return null;
130
+ if (d.setupState === "off" || d.setupState === "unknown") return null;
126
131
  const ready = d.available;
127
132
  return (
128
133
  <Tooltip
129
134
  content={
130
- d.setupState === "needs-restart"
131
- ? "Restart Radar to enable AI investigations — a supported agent is installed"
132
- : !ready
133
- ? "Set up AI investigations — install a local agent"
134
- : d.hosted
135
- ? `Sends this resource's context to ${d.agentLabel} for investigation`
136
- : `Runs ${d.agentLabel} on your machine to investigate this resource`
135
+ !ready
136
+ ? notReadyTooltip(d)
137
+ : d.hosted
138
+ ? `Sends this resource's context to ${d.agentLabel} for investigation`
139
+ : `Runs ${d.agentLabel} on your machine to investigate this resource`
137
140
  }
138
141
  position="left"
139
142
  >
@@ -158,12 +161,27 @@ export function IssueDiagnoseButton({
158
161
  );
159
162
  }
160
163
 
164
+ // The tooltip every AI entry point shows while investigations aren't runnable.
165
+ // It gives the same reason as the setup notice the button opens.
166
+ function notReadyTooltip(d: {
167
+ cliOverride: boolean;
168
+ setupState: DiagnoseSetup;
169
+ }): string {
170
+ if (d.cliOverride) {
171
+ return "AI investigations are off: can't run RADAR_AI_CLI_BIN";
172
+ }
173
+ if (d.setupState === "needs-restart") {
174
+ return "AI investigations aren't available right now";
175
+ }
176
+ return "Set up AI investigations";
177
+ }
178
+
161
179
  // Global top-bar entry into the AI surface (opens its Home / recent
162
- // investigations). Self-hides when no agent CLI is present.
180
+ // investigations). Hidden only where investigations can't run here.
163
181
  export function GlobalDiagnoseButton() {
164
182
  const d = useDiagnose();
165
183
  const { runningCount } = useDiagnoseLayout();
166
- if (d.setupState === "off") return null;
184
+ if (d.setupState === "off" || d.setupState === "unknown") return null;
167
185
  const ready = d.available;
168
186
  const agentSuffix = d.hosted
169
187
  ? `powered by ${d.agentLabel}`
@@ -172,7 +190,7 @@ export function GlobalDiagnoseButton() {
172
190
  <Tooltip
173
191
  content={
174
192
  !ready
175
- ? "Set up AI investigations — runs your own agent locally"
193
+ ? notReadyTooltip(d)
176
194
  : runningCount > 0
177
195
  ? `${runningCount} investigation${runningCount > 1 ? "s" : ""} running — ${agentSuffix}`
178
196
  : `AI investigations — ${agentSuffix}`
@@ -183,9 +201,11 @@ export function GlobalDiagnoseButton() {
183
201
  onClick={() => d.openWorkspace()}
184
202
  className="relative rounded-md bg-theme-elevated p-1.5 text-theme-text-secondary transition-colors hover:bg-theme-hover hover:text-theme-text-primary"
185
203
  aria-label={
186
- runningCount > 0
187
- ? `AI investigations (${runningCount} running)`
188
- : "AI investigations"
204
+ !ready
205
+ ? notReadyTooltip(d)
206
+ : runningCount > 0
207
+ ? `AI investigations (${runningCount} running)`
208
+ : "AI investigations"
189
209
  }
190
210
  >
191
211
  <Sparkles className="h-4 w-4 text-accent" />
@@ -13,7 +13,7 @@ export const SUPPORTED_AGENTS: AgentInstall[] = [
13
13
  name: "claude",
14
14
  label: "Claude Code",
15
15
  install: "npm install -g @anthropic-ai/claude-code",
16
- docs: "https://docs.claude.com/en/docs/claude-code/setup",
16
+ docs: "https://code.claude.com/docs/en/setup",
17
17
  },
18
18
  {
19
19
  name: "codex",
@@ -25,7 +25,7 @@ export const SUPPORTED_AGENTS: AgentInstall[] = [
25
25
  name: "cursor-agent",
26
26
  label: "Cursor CLI",
27
27
  install: "curl https://cursor.com/install -fsS | bash",
28
- docs: "https://docs.cursor.com/en/cli/overview",
28
+ docs: "https://cursor.com/docs/cli/overview",
29
29
  },
30
30
  {
31
31
  name: "opencode",
@@ -93,6 +93,8 @@ export interface DiagnosisResourceContext {
93
93
  };
94
94
  runsOn?: DiagnosisResourceRef;
95
95
  scaledBy?: DiagnosisScalerRef[];
96
+ dependencies?: DiagnosisResourceRef[];
97
+ dependents?: DiagnosisResourceRef[];
96
98
  statusSummary?: {
97
99
  phase?: string;
98
100
  conditions?: Array<{
@@ -71,6 +71,14 @@ export function adaptIssues(
71
71
  "truncated",
72
72
  );
73
73
  }
74
+ if (nonEmptyString(value.recent_changes_error)) {
75
+ builder.limit(
76
+ source,
77
+ "Issue-related changes",
78
+ value.recent_changes_error,
79
+ "error",
80
+ );
81
+ }
74
82
  if (value.correlation_truncated === true) {
75
83
  builder.limit(
76
84
  source,
@@ -247,6 +247,22 @@ describe("strict evidence adapters", () => {
247
247
  }),
248
248
  ]);
249
249
 
250
+ const unreadIssueFeed = project([
251
+ tool("issues-down", "issues", {
252
+ issues: [],
253
+ total: 0,
254
+ total_matched: 0,
255
+ recent_changes_error: "timeline history is unavailable",
256
+ }),
257
+ ]);
258
+ expect(unreadIssueFeed.limitations).toEqual([
259
+ expect.objectContaining({
260
+ source: "Issue-related changes",
261
+ kind: "error",
262
+ message: "timeline history is unavailable",
263
+ }),
264
+ ]);
265
+
250
266
  const filtered = project([
251
267
  tool("filtered", "get_resource", {
252
268
  resource: deployment,
@@ -17,6 +17,8 @@ function renderTabContent(tab: DockTab, isActive: boolean) {
17
17
  containerName={tab.containerName!}
18
18
  containers={tab.containers!}
19
19
  isActive={isActive}
20
+ shell={tab.shell}
21
+ note={tab.sessionNote}
20
22
  />
21
23
  )
22
24
  }
@@ -0,0 +1,17 @@
1
+ import { renderToString } from 'react-dom/server'
2
+ import { expect, it, vi } from 'vitest'
3
+ import type { TerminalTabProps } from '@skyhook-io/k8s-ui'
4
+ import { TerminalTab } from './TerminalTab'
5
+ const captured = vi.hoisted(() => ({ props: null as TerminalTabProps | null }))
6
+ vi.mock('@skyhook-io/k8s-ui', () => ({ TerminalTab: (props: TerminalTabProps) => { captured.props = props; return null } }))
7
+ vi.mock('../../api/config', () => ({ apiUrl: (path: string) => `/api${path}`, getWsUrl: (path: string) => `ws://localhost/api${path}` }))
8
+ it.each([undefined, '/bin/custom+command'])('forwards the executable and context through the shared terminal (%s)', async shell => {
9
+ renderToString(<TerminalTab namespace="app" podName="worker" containerName="app" containers={['app', 'sidecar']} shell={shell} note="Primary" />)
10
+ const props = captured.props!
11
+ expect(props.command).toBe(shell); expect(props.note).toBe('Primary')
12
+ expect(Boolean(props.createDebugContainer)).toBe(!shell)
13
+ const { wsUrl } = await props.createSession('app')
14
+ const url = new URL(wsUrl)
15
+ expect(url.pathname).toBe('/api/pods/app/worker/exec'); expect(url.searchParams.get('container')).toBe('app')
16
+ expect(url.searchParams.get('shell')).toBe(shell ?? null)
17
+ })
@@ -8,12 +8,14 @@ interface TerminalTabProps {
8
8
  containerName: string
9
9
  containers: string[]
10
10
  isActive?: boolean
11
+ shell?: string
12
+ note?: string
11
13
  }
12
14
 
13
- export function TerminalTab({ namespace, podName, containerName, containers, isActive }: TerminalTabProps) {
15
+ export function TerminalTab({ namespace, podName, containerName, containers, isActive, shell, note }: TerminalTabProps) {
14
16
  const createSession = (container: string) =>
15
17
  Promise.resolve({
16
- wsUrl: getWsUrl(`/pods/${namespace}/${podName}/exec?container=${container}`),
18
+ wsUrl: getWsUrl(`/pods/${namespace}/${podName}/exec?container=${container}${shell ? `&shell=${encodeURIComponent(shell)}` : ''}`),
17
19
  })
18
20
 
19
21
  const createDebugContainer = async (targetContainer: string) => {
@@ -37,7 +39,9 @@ export function TerminalTab({ namespace, podName, containerName, containers, isA
37
39
  containers={containers}
38
40
  isActive={isActive}
39
41
  createSession={createSession}
40
- createDebugContainer={createDebugContainer}
42
+ createDebugContainer={shell ? undefined : createDebugContainer}
43
+ note={note}
44
+ command={shell}
41
45
  />
42
46
  )
43
47
  }
@@ -1,18 +1,39 @@
1
1
  import { useTrafficFlowList } from '../traffic/TrafficFlowListContext'
2
2
  import { TrafficFlowList } from '../traffic/TrafficFlowList'
3
- import { List } from 'lucide-react'
3
+ import { List, Loader2 } from 'lucide-react'
4
4
 
5
5
  export function TrafficFlowListTab() {
6
- const { flows, responsesCallerOriented } = useTrafficFlowList()
6
+ const { flows, responsesCallerOriented, graphSelection, loading, note } = useTrafficFlowList()
7
7
 
8
- if (flows.length === 0) {
8
+ if (loading) {
9
9
  return (
10
10
  <div className="flex items-center justify-center h-full text-sm text-theme-text-tertiary gap-2">
11
- <List className="w-4 h-4" />
12
- Navigate to Traffic view to see flows
11
+ <Loader2 className="w-4 h-4 animate-spin" />
12
+ Loading flows for the selection…
13
+ </div>
14
+ )
15
+ }
16
+
17
+ if (flows.length === 0) {
18
+ return (
19
+ <div className="flex flex-col items-center justify-center h-full gap-1 px-4 text-center">
20
+ <div className="flex items-center gap-2 text-sm text-theme-text-tertiary">
21
+ <List className="w-4 h-4" />
22
+ {graphSelection ? 'No flow records for this selection' : 'Navigate to Traffic view to see flows'}
23
+ </div>
24
+ {note && <div className="text-[11px] text-theme-text-tertiary">{note}</div>}
13
25
  </div>
14
26
  )
15
27
  }
16
28
 
17
- return <TrafficFlowList flows={flows} responsesCallerOriented={responsesCallerOriented} />
29
+ return (
30
+ <div className="flex flex-col h-full">
31
+ {note && (
32
+ <div className="px-3 py-1 border-b border-theme-border text-[11px] text-theme-text-tertiary">{note}</div>
33
+ )}
34
+ <div className="relative flex-1 min-h-0">
35
+ <TrafficFlowList flows={flows} responsesCallerOriented={responsesCallerOriented} />
36
+ </div>
37
+ </div>
38
+ )
18
39
  }
@@ -389,10 +389,10 @@ export function BatchExecutionFullscreen({ kind, apiKind, namespace, name, resou
389
389
  </div>
390
390
  <div className="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs text-theme-text-tertiary">
391
391
  <span>{RUN_KIND_LABEL[selectedRun.kind] ?? selectedRun.kind}</span>
392
- {selectedRun.startedAt && <span>started {formatAge(selectedRun.startedAt)}</span>}
392
+ {selectedRun.startedAt && <span>started {formatTimeAgo(selectedRun.startedAt)}</span>}
393
393
  {selectedRun.trigger === 'manual' && <span>manual trigger</span>}
394
394
  {selectedRun.trigger === 'event' && <span>event triggered</span>}
395
- {selectedRun.scheduledAt && <span>scheduled {formatAge(selectedRun.scheduledAt)}</span>}
395
+ {selectedRun.scheduledAt && <span>scheduled {formatTimeAgo(selectedRun.scheduledAt)}</span>}
396
396
  {selectedResourceQuery.isFetching && <span>refreshing</span>}
397
397
  </div>
398
398
  </div>
@@ -568,8 +568,8 @@ function sourceFacts(kind: string, resource: any, runs: WorkloadRun[]) {
568
568
  duration: latest ? formatRunDuration(latest) : '',
569
569
  work: `${status.active?.length ?? 0} active`,
570
570
  facts: [
571
- ['Last schedule', status.lastScheduleTime ? formatAge(status.lastScheduleTime) : 'Never'],
572
- ['Last success', status.lastSuccessfulTime ? formatAge(status.lastSuccessfulTime) : 'Never'],
571
+ ['Last schedule', status.lastScheduleTime ? formatTimeAgo(status.lastScheduleTime) : 'Never'],
572
+ ['Last success', status.lastSuccessfulTime ? formatTimeAgo(status.lastSuccessfulTime) : 'Never'],
573
573
  ['Starting deadline', spec.startingDeadlineSeconds ? `${spec.startingDeadlineSeconds}s` : 'None'],
574
574
  ],
575
575
  definition,
@@ -818,15 +818,15 @@ function RunDetailList({ run, resource, workflowExecution, scheduledParent, memb
818
818
  ...(run.jobset?.replicatedJob ? [['Role', run.jobset?.replicatedJob]] as Array<[string, string]> : []),
819
819
  ...(run.jobset?.restartAttempt ? [['JobSet restart attempt', `#${run.jobset?.restartAttempt}`]] as Array<[string, string]> : []),
820
820
  ...(run.jobset?.jobRestartAttempt ? [['Job restart attempt', `#${run.jobset?.jobRestartAttempt}`]] as Array<[string, string]> : []),
821
- ['Started', run.startedAt ? formatAge(run.startedAt) : '-'],
822
- ['Finished', run.finishedAt ? formatAge(run.finishedAt) : run.active ? 'Running' : '-'],
821
+ ['Started', run.startedAt ? formatTimeAgo(run.startedAt) : '-'],
822
+ ['Finished', run.finishedAt ? formatTimeAgo(run.finishedAt) : run.active ? 'Running' : '-'],
823
823
  ['Duration', formatRunDuration(run) || '-'],
824
824
  ...(run.progress ? [['Progress', run.progress]] as Array<[string, string]> : []),
825
825
  ...(!memberCollection && (scheduledParent || run.trigger || run.scheduledAt) ? [
826
826
  ['Trigger', run.trigger === 'manual' ? 'Manual' : run.trigger === 'event' ? 'Event' : run.scheduledAt ? 'Cron schedule' : '-'],
827
827
  ] as Array<[string, string]> : []),
828
828
  ...(run.scheduledAt ? [
829
- ['Cron scheduled', formatAge(run.scheduledAt)],
829
+ ['Cron scheduled', formatTimeAgo(run.scheduledAt)],
830
830
  ['Start delay', formatScheduleDelay(run) || '-'],
831
831
  ] as Array<[string, string]> : []),
832
832
  ['Parallelism', run.parallelism ? String(run.parallelism) : '-'],
@@ -1008,7 +1008,7 @@ function RunActivityRow({ item, runMessage }: { item: WorkflowExecutionActivity;
1008
1008
  <div className="min-w-0 flex-1">
1009
1009
  <div className="flex items-start justify-between gap-3">
1010
1010
  <div className="truncate text-sm font-medium text-theme-text-primary">{item.label}</div>
1011
- <div className="shrink-0 text-xs text-theme-text-tertiary">{formatAge(item.at)}</div>
1011
+ <div className="shrink-0 text-xs text-theme-text-tertiary">{formatTimeAgo(item.at)}</div>
1012
1012
  </div>
1013
1013
  {item.detail && item.detail !== runMessage && item.tone !== 'danger' && <div className="mt-0.5 break-words text-xs text-theme-text-secondary">{item.detail}</div>}
1014
1014
  </div>
@@ -1539,10 +1539,10 @@ function formatRunDuration(run: WorkloadRun): string {
1539
1539
 
1540
1540
  function formatRunTime(run: WorkloadRun): string {
1541
1541
  const raw = run.startedAt || run.scheduledAt || run.finishedAt
1542
- return raw ? formatAge(raw) : ''
1542
+ return raw ? formatTimeAgo(raw) : ''
1543
1543
  }
1544
1544
 
1545
- function formatAge(value: string): string {
1545
+ function formatTimeAgo(value: string): string {
1546
1546
  const t = Date.parse(value)
1547
1547
  if (Number.isNaN(t)) return value
1548
1548
  const diff = Date.now() - t
@@ -1577,7 +1577,7 @@ function emptyRunsCopy(kind: string, resource: any): { headline: string; body: s
1577
1577
  }
1578
1578
  const lastScheduled = resource?.status?.lastScheduleTime || resource?.status?.lastScheduledTime
1579
1579
  if (lastScheduled) {
1580
- return { headline: 'No retained runs', body: `This resource last scheduled work ${formatAge(lastScheduled)}, but its ${runKindPluralForSchedule(kind)} are no longer retained in Kubernetes.` }
1580
+ return { headline: 'No retained runs', body: `This resource last scheduled work ${formatTimeAgo(lastScheduled)}, but its ${runKindPluralForSchedule(kind)} are no longer retained in Kubernetes.` }
1581
1581
  }
1582
1582
  return { headline: 'No runs yet', body: `Kubernetes does not currently have retained ${runKindPluralForSchedule(kind)} for this resource.` }
1583
1583
  }
@@ -1,23 +1,25 @@
1
1
  import { ArgoResourceDiff } from '@skyhook-io/k8s-ui'
2
- import type { GitOpsInsightRef } from '@skyhook-io/k8s-ui'
2
+ import type { GitOpsDrift, GitOpsInsightRef } from '@skyhook-io/k8s-ui'
3
3
  import { useArgoResourceDiff } from '../../api/client'
4
4
 
5
5
  interface ArgoResourceDiffLoaderProps {
6
6
  appNamespace: string
7
7
  appName: string
8
8
  resourceRef: GitOpsInsightRef
9
+ fallbackDrift?: GitOpsDrift
9
10
  }
10
11
 
11
12
  // Host wrapper: fetches the Argo CD resource diff and hands the data to the
12
13
  // pure k8s-ui presentation component. Mirrors the data-in-web / render-in-
13
14
  // k8s-ui split the resource renderers use for their host-wired data.
14
- export function ArgoResourceDiffLoader({ appNamespace, appName, resourceRef }: ArgoResourceDiffLoaderProps) {
15
+ export function ArgoResourceDiffLoader({ appNamespace, appName, resourceRef, fallbackDrift }: ArgoResourceDiffLoaderProps) {
15
16
  const { data, isLoading, error } = useArgoResourceDiff(appNamespace, appName, resourceRef)
16
17
  return (
17
18
  <ArgoResourceDiff
18
19
  diff={data ?? null}
19
20
  loading={isLoading}
20
21
  error={(error as Error | null)?.message ?? null}
22
+ fallbackDrift={fallbackDrift}
21
23
  />
22
24
  )
23
25
  }
@@ -25,7 +25,6 @@ import {
25
25
  isArgoOperationInProgress,
26
26
  isArgoSuspendedByRadar,
27
27
  gitOpsInsightChangeKey,
28
- initNavigationMap,
29
28
  kindToPluralWithGroup,
30
29
  normalizeArgoApplication,
31
30
  normalizeArgoApplicationSet,
@@ -72,7 +71,7 @@ import {
72
71
  useGitOpsTree,
73
72
  useResource,
74
73
  } from '../../api/client'
75
- import { useAPIResources } from '../../api/apiResources'
74
+ import { useAPIResources, GITOPS_KINDS } from '../../api/apiResources'
76
75
  import { useConnection } from '../../context/ConnectionContext'
77
76
  import { apiUrl, getAuthHeaders, getCredentialsMode } from '../../api/config'
78
77
  import { useRegisterShortcut } from '../../hooks/useKeyboardShortcuts'
@@ -84,17 +83,6 @@ import { RemoteDestinationCloudHint } from './RemoteDestinationCloudHint'
84
83
  const GITOPS_HEALTH_DOCS_URL = 'https://radarhq.io/docs/features/gitops#per-resource-health'
85
84
  import type { GitOpsHistoryItem } from '@skyhook-io/k8s-ui'
86
85
 
87
- const GITOPS_KINDS: APIResource[] = [
88
- { name: 'applications', kind: 'Application', group: 'argoproj.io', version: 'v1alpha1', namespaced: true, verbs: ['list', 'get'], isCrd: true },
89
- { name: 'applicationsets', kind: 'ApplicationSet', group: 'argoproj.io', version: 'v1alpha1', namespaced: true, verbs: ['list', 'get'], isCrd: true },
90
- { name: 'appprojects', kind: 'AppProject', group: 'argoproj.io', version: 'v1alpha1', namespaced: true, verbs: ['list', 'get'], isCrd: true },
91
- { name: 'kustomizations', kind: 'Kustomization', group: 'kustomize.toolkit.fluxcd.io', version: 'v1', namespaced: true, verbs: ['list', 'get'], isCrd: true },
92
- { name: 'helmreleases', kind: 'HelmRelease', group: 'helm.toolkit.fluxcd.io', version: 'v2', namespaced: true, verbs: ['list', 'get'], isCrd: true },
93
- { name: 'gitrepositories', kind: 'GitRepository', group: 'source.toolkit.fluxcd.io', version: 'v1', namespaced: true, verbs: ['list', 'get'], isCrd: true },
94
- { name: 'ocirepositories', kind: 'OCIRepository', group: 'source.toolkit.fluxcd.io', version: 'v1beta2', namespaced: true, verbs: ['list', 'get'], isCrd: true },
95
- { name: 'helmrepositories', kind: 'HelmRepository', group: 'source.toolkit.fluxcd.io', version: 'v1', namespaced: true, verbs: ['list', 'get'], isCrd: true },
96
- { name: 'alerts', kind: 'Alert', group: 'notification.toolkit.fluxcd.io', version: 'v1beta3', namespaced: true, verbs: ['list', 'get'], isCrd: true },
97
- ]
98
86
 
99
87
  type ArgoSyncDialogTarget =
100
88
  | { scope: 'application' }
@@ -164,9 +152,6 @@ function GitOpsTableView({ namespaces, onClearNamespaces }: { namespaces: string
164
152
  })
165
153
  }
166
154
 
167
- useEffect(() => {
168
- initNavigationMap([...(apiResources ?? []), ...GITOPS_KINDS])
169
- }, [apiResources])
170
155
 
171
156
  const hasGitOpsRowResource = useMemo(() => (
172
157
  hasAPIResource(apiResources, 'applications', 'argoproj.io') ||
@@ -816,8 +801,8 @@ function GitOpsDetailView({ namespaces, onOpenResource, onOpenSettings }: GitOps
816
801
  ) : undefined}
817
802
  focusKey={changesFocusKey}
818
803
  tree={tree}
819
- renderResourceDiff={isArgoApp ? (ref) => (
820
- <ArgoResourceDiffLoader appNamespace={namespace} appName={name} resourceRef={ref} />
804
+ renderResourceDiff={isArgoApp ? (ref, fallbackDrift) => (
805
+ <ArgoResourceDiffLoader appNamespace={namespace} appName={name} resourceRef={ref} fallbackDrift={fallbackDrift} />
821
806
  ) : undefined}
822
807
  onOpenSettings={onOpenSettings}
823
808
  settingsNote={settingsNote}
@@ -1097,18 +1082,26 @@ function newestConditionTime(resource: any): string {
1097
1082
  // many of them are healthy / drifted" at a glance, without making the user
1098
1083
  // count facets in the filter rail.
1099
1084
  function TopologyCounts({ tree }: { tree: GitOpsResourceTree }) {
1100
- const nodes = (tree.nodes ?? []).filter((n) => n.role !== 'group' && n.role !== 'root')
1085
+ const counted = (tree.nodes ?? []).filter((n) => n.role !== 'group' && n.role !== 'root')
1086
+ // Dependencies (Flux sources, charts, dependsOn targets) are consumed, not
1087
+ // managed: they get their own count and never inflate the managed totals.
1088
+ const nodes = counted.filter((n) => !n.dependency)
1089
+ const dependencies = counted.length - nodes.length
1101
1090
  const total = nodes.length
1102
- if (total === 0) return null
1103
- const healthy = nodes.filter((n) => (n.health || '').toLowerCase() === 'healthy').length
1104
- const degraded = nodes.filter((n) => {
1091
+ if (counted.length === 0) return null
1092
+ const isDegraded = (n: GitOpsTreeNode) => {
1105
1093
  const h = (n.health || '').toLowerCase()
1106
1094
  return h === 'degraded' || h === 'missing' || h === 'unhealthy'
1107
- }).length
1095
+ }
1096
+ const healthy = nodes.filter((n) => (n.health || '').toLowerCase() === 'healthy').length
1097
+ const degraded = nodes.filter(isDegraded).length
1098
+ const failingDependencies = counted.filter((n) => n.dependency && isDegraded(n)).length
1108
1099
  const outOfSync = nodes.filter((n) => (n.sync || '').toLowerCase() === 'outofsync').length
1109
1100
  return (
1110
1101
  <div className="hidden min-w-0 flex-1 items-center gap-3 truncate text-[11px] text-theme-text-tertiary sm:flex">
1111
1102
  <span><span className="text-theme-text-primary">{total}</span> resources</span>
1103
+ {dependencies > 0 && <span><span className="text-theme-text-primary">{dependencies}</span> {dependencies === 1 ? 'dependency' : 'dependencies'}</span>}
1104
+ {failingDependencies > 0 && <span className="flex items-center gap-1 font-medium text-red-600 dark:text-red-400"><span className="h-1.5 w-1.5 rounded-full bg-red-500" /> {failingDependencies} {failingDependencies === 1 ? 'dependency' : 'dependencies'} failing</span>}
1112
1105
  {healthy > 0 && <span className="flex items-center gap-1"><span className="h-1.5 w-1.5 rounded-full bg-emerald-500" /> {healthy} healthy</span>}
1113
1106
  {/* Bad-news counts use status colors on the number itself so the worst
1114
1107
  fact in the row visually pops, not just the dot next to it. */}
@@ -5,10 +5,10 @@ import { clsx } from 'clsx'
5
5
  import { useHelmRepositories, useSearchCharts, useUpdateRepository, useUpdateRepositorySilent, useArtifactHubSearch, type ArtifactHubSortOption } from '../../api/client'
6
6
  import { useCanHelmWrite, useIsAuthEnabled } from '../../contexts/CapabilitiesContext'
7
7
  import type { ChartInfo, HelmRepository, ArtifactHubChart, ChartSource } from '../../types'
8
- import { formatAge } from './helm-utils'
9
8
  import { SEVERITY_BADGE } from '../../utils/badge-colors'
10
9
  import { Tooltip } from '../ui/Tooltip'
11
10
  import { useToast } from '../ui/Toast'
11
+ import { formatRelativeAgeTime } from '@skyhook-io/k8s-ui/utils/format'
12
12
 
13
13
  interface ChartBrowserProps {
14
14
  onChartSelect: (repo: string, chart: string, version: string, source: ChartSource) => void
@@ -431,7 +431,7 @@ function RepoDropdownItem({ repo, isSelected, onSelect, onUpdate, isUpdating, ca
431
431
  {repo.name}
432
432
  {repo.lastUpdated && (
433
433
  <span className="text-xs text-theme-text-tertiary ml-2">
434
- {formatAge(repo.lastUpdated)}
434
+ {formatRelativeAgeTime(repo.lastUpdated)}
435
435
  </span>
436
436
  )}
437
437
  </button>
@@ -514,7 +514,7 @@ interface ArtifactHubChartCardProps {
514
514
  function ArtifactHubChartCard({ chart, onSelect }: ArtifactHubChartCardProps) {
515
515
  // Format Unix timestamp to relative age
516
516
  const lastUpdated = chart.updatedAt
517
- ? formatAge(new Date(chart.updatedAt * 1000).toISOString())
517
+ ? formatRelativeAgeTime(new Date(chart.updatedAt * 1000).toISOString())
518
518
  : null
519
519
 
520
520
  return (
@@ -0,0 +1,59 @@
1
+ // @vitest-environment jsdom
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+ import { describe, expect, it } from 'vitest'
4
+ import { CompareSummary } from './HelmCompareRoute'
5
+
6
+ const sameDiff = '--- revision-1\n+++ revision-2\n'
7
+ const changedDiff = '--- revision-1\n+++ revision-2\n@@ -1 +1 @@\n-old\n+new\n'
8
+
9
+ function renderSummary(overridesDiff: string, effectiveValuesDiff: string) {
10
+ document.body.innerHTML = renderToStaticMarkup(
11
+ <CompareSummary
12
+ revision1={1}
13
+ revision2={2}
14
+ manifestDiff={sameDiff}
15
+ manifestLoading={false}
16
+ manifestError={null}
17
+ overridesDiff={overridesDiff}
18
+ overridesLoading={false}
19
+ overridesError={null}
20
+ effectiveValuesDiff={effectiveValuesDiff}
21
+ effectiveValuesLoading={false}
22
+ effectiveValuesError={null}
23
+ notesDiff={sameDiff}
24
+ notesLoading={false}
25
+ notesError={null}
26
+ hooksLoading={false}
27
+ hooksError={null}
28
+ resourceLoading={false}
29
+ resourceError={null}
30
+ />,
31
+ )
32
+ }
33
+
34
+ function signalText(section: string) {
35
+ return document.querySelector(`a[href="#${section}"]`)?.textContent
36
+ }
37
+
38
+ describe('CompareSummary value signals', () => {
39
+ it('shows unchanged overrides and changed effective values when chart defaults change', () => {
40
+ renderSummary(sameDiff, changedDiff)
41
+
42
+ expect(signalText('overrides')).toBe('Overridessame')
43
+ expect(signalText('effective-values')).toBe('Effective values1 add / 1 remove')
44
+ })
45
+
46
+ it('shows both signals changed when an override changes effective values', () => {
47
+ renderSummary(changedDiff, changedDiff)
48
+
49
+ expect(signalText('overrides')).toBe('Overrides1 add / 1 remove')
50
+ expect(signalText('effective-values')).toBe('Effective values1 add / 1 remove')
51
+ })
52
+
53
+ it('shows both signals unchanged when both value sets are identical', () => {
54
+ renderSummary(sameDiff, sameDiff)
55
+
56
+ expect(signalText('overrides')).toBe('Overridessame')
57
+ expect(signalText('effective-values')).toBe('Effective valuessame')
58
+ })
59
+ })