@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
@@ -0,0 +1,172 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2
+ import { createPortal } from 'react-dom'
3
+ import { clsx } from 'clsx'
4
+ import { Crosshair, X } from 'lucide-react'
5
+ import { Badge, Input } from '@skyhook-io/k8s-ui'
6
+ import { useNavCustomization } from '../../context/NavCustomization'
7
+ import { useRegisterShortcut } from '../../hooks/useKeyboardShortcuts'
8
+ import { formatRate, searchEndpoints, type EndpointSummary, type TrafficFocus } from './trafficFilters'
9
+
10
+ interface TrafficFocusSearchProps {
11
+ endpoints: EndpointSummary[]
12
+ onFocus: (focus: TrafficFocus) => void
13
+ isRateBased: boolean
14
+ /** The pane the overlay covers; the viewport when absent. */
15
+ overlayContainer?: HTMLElement | null
16
+ /** 'toolbar' is the floating chip over the graph; 'panel' the wider button
17
+ * in the too-large panel. Both open the same overlay. */
18
+ variant?: 'toolbar' | 'panel'
19
+ }
20
+
21
+ export function TrafficFocusSearch({ endpoints, onFocus, isRateBased, overlayContainer, variant = 'toolbar' }: TrafficFocusSearchProps) {
22
+ const [isOpen, setIsOpen] = useState(false)
23
+ const [query, setQuery] = useState('')
24
+ const [selectedIndex, setSelectedIndex] = useState(0)
25
+ const inputRef = useRef<HTMLInputElement>(null)
26
+ const resultsRef = useRef<HTMLDivElement>(null)
27
+
28
+ const results = useMemo(() => (isOpen ? searchEndpoints(endpoints, query) : []), [isOpen, endpoints, query])
29
+
30
+ useEffect(() => setSelectedIndex(0), [results])
31
+ useEffect(() => {
32
+ const el = resultsRef.current?.children[selectedIndex] as HTMLElement | undefined
33
+ el?.scrollIntoView({ block: 'nearest' })
34
+ }, [selectedIndex])
35
+
36
+ const open = useCallback(() => {
37
+ setIsOpen(true)
38
+ setTimeout(() => inputRef.current?.focus(), 0)
39
+ }, [])
40
+ const close = useCallback(() => {
41
+ setIsOpen(false)
42
+ setQuery('')
43
+ }, [])
44
+ const choose = useCallback((e: EndpointSummary) => {
45
+ onFocus({ namespace: e.namespace, name: e.name })
46
+ close()
47
+ }, [onFocus, close])
48
+
49
+ // The view renders one of the two variants at a time, so whichever is shown
50
+ // owns the shortcut. An embedding host (Radar Hub) binds / to its own search.
51
+ const { embedded } = useNavCustomization()
52
+ useRegisterShortcut({
53
+ id: 'traffic-focus-search',
54
+ keys: '/',
55
+ description: 'Focus on a workload',
56
+ category: 'Search',
57
+ scope: 'traffic',
58
+ handler: open,
59
+ enabled: !embedded,
60
+ })
61
+
62
+ const onKeyDown = (e: React.KeyboardEvent) => {
63
+ if (e.key === 'Escape') {
64
+ e.preventDefault()
65
+ close()
66
+ return
67
+ }
68
+ if (results.length === 0) return
69
+ if (e.key === 'ArrowDown') {
70
+ e.preventDefault()
71
+ setSelectedIndex(i => Math.min(i + 1, results.length - 1))
72
+ } else if (e.key === 'ArrowUp') {
73
+ e.preventDefault()
74
+ setSelectedIndex(i => Math.max(i - 1, 0))
75
+ } else if (e.key === 'Enter') {
76
+ e.preventDefault()
77
+ if (results[selectedIndex]) choose(results[selectedIndex])
78
+ }
79
+ }
80
+
81
+ const portalTarget = overlayContainer ?? (typeof document !== 'undefined' ? document.body : null)
82
+
83
+ return (
84
+ <>
85
+ {variant === 'toolbar' ? (
86
+ <button
87
+ type="button"
88
+ onClick={open}
89
+ className="flex items-center gap-1.5 px-2 py-1 rounded-lg bg-theme-surface/90 backdrop-blur border border-theme-border text-[11px] text-theme-text-secondary hover:text-theme-text-primary transition-colors"
90
+ >
91
+ <Crosshair className="w-3 h-3" />
92
+ Focus on a workload
93
+ {!embedded && <kbd className="hidden sm:inline-flex items-center px-1 text-[10px] bg-theme-elevated rounded border border-theme-border-light">/</kbd>}
94
+ </button>
95
+ ) : (
96
+ <button
97
+ type="button"
98
+ onClick={open}
99
+ className="btn-brand-muted inline-flex items-center gap-1.5 text-xs px-2.5 py-1 rounded-md"
100
+ >
101
+ <Crosshair className="w-3.5 h-3.5" />
102
+ Focus on a workload…
103
+ </button>
104
+ )}
105
+
106
+ {isOpen && portalTarget && createPortal(
107
+ <div className={clsx(overlayContainer ? 'absolute' : 'fixed', 'inset-0 z-50 flex items-start justify-center pt-[10vh]')}>
108
+ <div className="absolute inset-0 bg-theme-base/60 backdrop-blur-sm" onClick={close} />
109
+ <div role="dialog" aria-label="Focus on a workload" className="relative w-full max-w-lg mx-4 bg-theme-surface border border-theme-border rounded-xl shadow-theme-lg overflow-hidden">
110
+ <div className="flex items-center gap-3 px-4 py-3 border-b border-theme-border">
111
+ <Crosshair className="w-5 h-5 text-theme-text-secondary" />
112
+ <Input
113
+ ref={inputRef}
114
+ value={query}
115
+ onChange={e => setQuery(e.target.value)}
116
+ onKeyDown={onKeyDown}
117
+ placeholder="Workload, service or external name…"
118
+ className="flex-1 bg-transparent text-theme-text-primary placeholder-theme-text-disabled outline-none text-sm"
119
+ autoFocus
120
+ />
121
+ {query && (
122
+ <button type="button" onClick={() => setQuery('')} aria-label="Clear" className="p-1 text-theme-text-secondary hover:text-theme-text-primary">
123
+ <X className="w-4 h-4" />
124
+ </button>
125
+ )}
126
+ <kbd className="px-1.5 py-0.5 text-xs text-theme-text-tertiary bg-theme-elevated rounded border border-theme-border-light">ESC</kbd>
127
+ </div>
128
+ <div ref={resultsRef} className="max-h-[50vh] overflow-y-auto">
129
+ {results.map((e, index) => (
130
+ <button
131
+ key={e.id}
132
+ type="button"
133
+ onClick={() => choose(e)}
134
+ // Not onMouseEnter: results re-rendering under a resting
135
+ // pointer would move the highlight away from what was typed.
136
+ onMouseMove={() => { if (index !== selectedIndex) setSelectedIndex(index) }}
137
+ className={clsx(
138
+ 'w-full flex items-center gap-3 px-4 py-2 text-left transition-colors',
139
+ index === selectedIndex ? 'bg-theme-elevated/60' : 'hover:bg-theme-elevated/30',
140
+ )}
141
+ >
142
+ <div className="flex-1 min-w-0">
143
+ <div className="flex items-center gap-2">
144
+ <span className="text-sm font-medium text-theme-text-primary truncate">{e.name}</span>
145
+ <span className="px-1.5 py-0.5 text-[10px] rounded bg-theme-elevated text-theme-text-secondary">{e.workloadKind || e.kind}</span>
146
+ </div>
147
+ {e.namespace && <div className="text-xs text-theme-text-tertiary truncate">{e.namespace}</div>}
148
+ </div>
149
+ <div className="flex items-center gap-1.5 shrink-0 text-[10px] tabular-nums">
150
+ {e.drops > 0 && <Badge severity="error" size="sm">{e.drops.toLocaleString()} dropped</Badge>}
151
+ {e.errors > 0 && <Badge severity="error" size="sm">{isRateBased ? `${formatRate(e.errors)}/s` : e.errors.toLocaleString()} 5xx</Badge>}
152
+ </div>
153
+ </button>
154
+ ))}
155
+ {results.length === 0 && (
156
+ <div className="px-4 py-6 text-center text-sm text-theme-text-tertiary">
157
+ {query
158
+ ? <>No traffic for &ldquo;{query}&rdquo; in this view</>
159
+ : 'Nothing in this view has traffic yet'}
160
+ </div>
161
+ )}
162
+ </div>
163
+ <div className="px-4 py-2 border-t border-theme-border text-xs text-theme-text-tertiary">
164
+ Shows the workload and everything it talks to. Only endpoints with traffic in this view are listed.
165
+ </div>
166
+ </div>
167
+ </div>,
168
+ portalTarget,
169
+ )}
170
+ </>
171
+ )
172
+ }
@@ -18,12 +18,12 @@ import '@xyflow/react/dist/style.css'
18
18
  import ELK from 'elkjs/lib/elk.bundled.js'
19
19
  import type { AggregatedFlow } from '../../types'
20
20
  import { clsx } from 'clsx'
21
- import { X, ArrowRight, Globe, Server, Activity, Puzzle } from 'lucide-react'
21
+ import { X, ArrowRight, Globe, Server, Activity, Puzzle, Crosshair } from 'lucide-react'
22
22
  import { isClusterAddon, type AddonMode } from './TrafficView'
23
23
  import { SEVERITY_BADGE, SEVERITY_DOT, SEVERITY_TEXT } from '@skyhook-io/k8s-ui/utils/badge-colors'
24
24
  import { getNamespaceColor } from '../../utils/traffic-colors'
25
25
  import { Tooltip } from '../ui/Tooltip'
26
- import { isRateBasedSource, isExternalKind, requestRateOf, errorRateOf, formatRate, displayVolume, latencyWeightOf } from './trafficFilters'
26
+ import { isRateBasedSource, isExternalKind, requestRateOf, errorRateOf, formatRate, displayVolume, latencyWeightOf, edgesOnPort, type TrafficFocus } from './trafficFilters'
27
27
 
28
28
  const elk = new ELK()
29
29
 
@@ -48,6 +48,8 @@ export interface TrafficGraphSelection {
48
48
  sourceId?: string
49
49
  destId?: string
50
50
  port?: number
51
+ // The graph draws traffic of unknown direction as its own edge.
52
+ directionUnknown?: boolean
51
53
  }
52
54
 
53
55
  interface TrafficGraphProps {
@@ -57,7 +59,13 @@ interface TrafficGraphProps {
57
59
  serviceCategories?: Map<string, string>
58
60
  addonMode?: AddonMode
59
61
  trafficSource?: string
62
+ /** The selection, owned by the view: the graph shows it and reports clicks. */
63
+ selection?: TrafficGraphSelection | null
60
64
  onSelectionChange?: (selection: TrafficGraphSelection | null) => void
65
+ /** Narrows the view to a node and its neighbors. */
66
+ onFocus?: (focus: TrafficFocus) => void
67
+ /** The focused node, which is not offered as a focus again. */
68
+ focusedId?: string
61
69
  }
62
70
 
63
71
  // Phase 2.1: Calculate edge width based on connection count (log scale)
@@ -146,10 +154,12 @@ interface TrafficNodeData extends Record<string, unknown> {
146
154
  namespace?: string
147
155
  kind: string
148
156
  workload?: string
157
+ workloadKind?: string
149
158
  connections?: number
150
159
  totalConnections?: number // Total connections for this node
151
160
  namespaceColor?: string // Background color for namespace grouping
152
161
  isHotPath?: boolean // Whether this node is on a hot path
162
+ isFocused?: boolean // The endpoint the view is focused on
153
163
  isAddonNode?: boolean // Whether this is a cluster addon node
154
164
  serviceCategory?: string // For external nodes: database, cloud, etc.
155
165
  ports?: PortInfo[] // All inbound ports sorted by connection count
@@ -205,7 +215,7 @@ function TrafficNode({ data }: { data: TrafficNodeData }) {
205
215
  : hasNamespaceColor
206
216
  ? 'border-white/20'
207
217
  : 'bg-theme-surface border-theme-border',
208
- data.isHotPath && 'ring-2 ring-orange-500/50'
218
+ data.isFocused ? 'ring-2 ring-skyhook-500' : data.isHotPath && 'ring-2 ring-orange-500/50'
209
219
  )}
210
220
  style={{
211
221
  width: NODE_WIDTH,
@@ -426,11 +436,13 @@ function formatBytes(bytes: number): string {
426
436
  function DetailsPanel({
427
437
  selection,
428
438
  onClose,
439
+ onFocus,
429
440
  flows,
430
441
  isRateBased,
431
442
  }: {
432
443
  selection: Selection
433
444
  onClose: () => void
445
+ onFocus?: () => void
434
446
  flows: AggregatedFlow[]
435
447
  // True for sources that measure a rate rather than counting connections —
436
448
  // Istio and Beyla both do. Changes the wording and the /s suffix.
@@ -539,12 +551,26 @@ function DetailsPanel({
539
551
  {isNode ? (nodeData?.kind === 'Internet' ? 'Internet Traffic' : nodeData?.kind === 'Addon' ? 'Cluster Addons' : 'Service Details') : 'Connection Details'}
540
552
  </span>
541
553
  </div>
542
- <button
543
- onClick={onClose}
544
- className="p-1 rounded hover:bg-theme-hover text-theme-text-secondary"
545
- >
546
- <X className="h-4 w-4" />
547
- </button>
554
+ <div className="flex items-center gap-1">
555
+ {onFocus && (
556
+ <Tooltip content="Show only this and what it talks to">
557
+ <button
558
+ type="button"
559
+ onClick={onFocus}
560
+ className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[11px] text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-hover"
561
+ >
562
+ <Crosshair className="h-3.5 w-3.5" /> Focus
563
+ </button>
564
+ </Tooltip>
565
+ )}
566
+ <button
567
+ onClick={onClose}
568
+ aria-label="Close"
569
+ className="p-1 rounded hover:bg-theme-hover text-theme-text-secondary"
570
+ >
571
+ <X className="h-4 w-4" />
572
+ </button>
573
+ </div>
548
574
  </div>
549
575
 
550
576
  {/* Content */}
@@ -569,7 +595,7 @@ function DetailsPanel({
569
595
  : isExternalKind(nodeData.kind)
570
596
  ? 'bg-yellow-500/20 text-yellow-400'
571
597
  : 'bg-blue-500/20 text-blue-400'
572
- )}>{nodeData.kind === 'Addon' ? 'Cluster Addons' : nodeData.kind}</span>
598
+ )}>{nodeData.kind === 'Addon' ? 'Cluster Addons' : nodeData.workloadKind || nodeData.kind}</span>
573
599
  </div>
574
600
  {nodeData.workload && nodeData.workload !== nodeData.label && (
575
601
  <div className="text-xs text-theme-text-secondary">
@@ -1085,6 +1111,26 @@ function FitViewOnChange({
1085
1111
  return null
1086
1112
  }
1087
1113
 
1114
+ function edgeSelection(edge: Edge, flow: AggregatedFlow | undefined): Selection {
1115
+ return {
1116
+ type: 'edge',
1117
+ id: edge.id,
1118
+ data: {
1119
+ source: edge.source,
1120
+ target: edge.target,
1121
+ port: flow?.port || 0,
1122
+ connections: flow?.connections || 0,
1123
+ protocol: flow?.protocol || 'tcp',
1124
+ flow,
1125
+ },
1126
+ }
1127
+ }
1128
+
1129
+ // The Internet node and the addon group stand for many endpoints at once.
1130
+ function isFocusable(id: string, data: TrafficNodeData | undefined): boolean {
1131
+ return id !== 'addon-group' && !!data?.kind && data.kind !== 'Internet' && data.kind !== 'Addon' && data.kind !== 'AddonInternet'
1132
+ }
1133
+
1088
1134
  // A pair can carry both oriented and unoriented traffic at the same port, and the
1089
1135
  // backend keeps those as separate aggregates because no single answer about the
1090
1136
  // direction is right for both. Without the suffix they would share an edge id and
@@ -1094,7 +1140,7 @@ function trafficEdgeId(sourceId: string, destId: string, flow: AggregatedFlow):
1094
1140
  return `${sourceId}->${destId}:${flow.port}${flow.directionUnknown ? ':unoriented' : ''}`
1095
1141
  }
1096
1142
 
1097
- export function TrafficGraph({ flows, hotPathThreshold = 0, showNamespaceGroups = false, serviceCategories, addonMode = 'show', trafficSource = '', onSelectionChange }: TrafficGraphProps) {
1143
+ export function TrafficGraph({ flows, hotPathThreshold = 0, showNamespaceGroups = false, serviceCategories, addonMode = 'show', trafficSource = '', selection: viewSelection, onSelectionChange, onFocus, focusedId }: TrafficGraphProps) {
1098
1144
  // Beyla is rate-based too: its Connections field carries requests per second,
1099
1145
  // not a connection count, so it needs the same label Istio gets.
1100
1146
  const isRateBased = isRateBasedSource(trafficSource)
@@ -1217,6 +1263,7 @@ export function TrafficGraph({ flows, hotPathThreshold = 0, showNamespaceGroups
1217
1263
  namespace: flow.source.namespace,
1218
1264
  kind: isAddonInternet ? 'AddonInternet' : flow.source.kind, // Keep AddonInternet kind for styling
1219
1265
  workload: flow.source.workload,
1266
+ workloadKind: flow.source.workloadKind,
1220
1267
  connections: connectionCounts.get(sourceId),
1221
1268
  totalConnections: totalConnections.get(sourceId),
1222
1269
  namespaceColor: showNamespaceGroups && !sourceIsAddon ? getNamespaceColor(flow.source.namespace) : undefined,
@@ -1249,6 +1296,7 @@ export function TrafficGraph({ flows, hotPathThreshold = 0, showNamespaceGroups
1249
1296
  namespace: flow.destination.namespace,
1250
1297
  kind: flow.destination.kind,
1251
1298
  workload: flow.destination.workload,
1299
+ workloadKind: flow.destination.workloadKind,
1252
1300
  connections: connectionCounts.get(destId),
1253
1301
  totalConnections: totalConnections.get(destId),
1254
1302
  namespaceColor: showNamespaceGroups ? getNamespaceColor(flow.destination.namespace) : undefined,
@@ -1279,6 +1327,7 @@ export function TrafficGraph({ flows, hotPathThreshold = 0, showNamespaceGroups
1279
1327
  namespace: flow.destination.namespace,
1280
1328
  kind: flow.destination.kind,
1281
1329
  workload: flow.destination.workload,
1330
+ workloadKind: flow.destination.workloadKind,
1282
1331
  connections: connectionCounts.get(destId),
1283
1332
  totalConnections: totalConnections.get(destId),
1284
1333
  namespaceColor: showNamespaceGroups && !destIsAddon ? getNamespaceColor(flow.destination.namespace) : undefined,
@@ -1599,6 +1648,43 @@ export function TrafficGraph({ flows, hotPathThreshold = 0, showNamespaceGroups
1599
1648
  const shouldFitViewRef = useRef(false)
1600
1649
  const prevFlowCountRef = useRef(flows.length)
1601
1650
 
1651
+ // Drawn per pod, a focused workload is each of its pods.
1652
+ const displayNodes = useMemo(() => {
1653
+ if (!focusedId) return nodes
1654
+ const isFocus = (n: Node<TrafficNodeData>) => n.id === focusedId ||
1655
+ (n.data.kind === 'Pod' && !!n.data.workload && `${n.data.namespace ?? ''}/${n.data.workload}` === focusedId)
1656
+ return nodes.map(n => (isFocus(n) ? { ...n, data: { ...n.data, isFocused: true } } : n))
1657
+ }, [nodes, focusedId])
1658
+
1659
+ // A new focus is a different graph even when it has as many edges.
1660
+ const prevFocusRef = useRef(focusedId)
1661
+ if (prevFocusRef.current !== focusedId) {
1662
+ prevFocusRef.current = focusedId
1663
+ shouldFitViewRef.current = true
1664
+ }
1665
+
1666
+ // The view owns the selection: it is set from outside the graph too (the
1667
+ // connections table selects an edge). The details are read
1668
+ // from the current layout, so a refresh updates them and a node that is no
1669
+ // longer drawn closes them.
1670
+ useEffect(() => {
1671
+ if (viewSelection === undefined) return
1672
+ if (!viewSelection) {
1673
+ setSelection(null)
1674
+ return
1675
+ }
1676
+ if (viewSelection.type === 'node' && viewSelection.nodeId) {
1677
+ const node = layoutedNodes.find(n => n.id === viewSelection.nodeId)
1678
+ setSelection(node ? { type: 'node', id: node.id, data: node.data } : null)
1679
+ return
1680
+ }
1681
+ const between = layoutedEdges.filter(e =>
1682
+ e.source === viewSelection.sourceId && e.target === viewSelection.destId &&
1683
+ !!flowByEdgeId.get(e.id)?.directionUnknown === !!viewSelection.directionUnknown)
1684
+ const [edge] = edgesOnPort(between, e => flowByEdgeId.get(e.id), viewSelection.port)
1685
+ setSelection(edge ? edgeSelection(edge, flowByEdgeId.get(edge.id)) : null)
1686
+ }, [viewSelection, layoutedNodes, layoutedEdges, flowByEdgeId])
1687
+
1602
1688
  // Update nodes and edges when layout changes
1603
1689
  useEffect(() => {
1604
1690
  // Check if flow count changed (filter/namespace change)
@@ -1622,19 +1708,8 @@ export function TrafficGraph({ flows, hotPathThreshold = 0, showNamespaceGroups
1622
1708
 
1623
1709
  const onEdgeClick: EdgeMouseHandler<Edge> = useCallback((_event, edge) => {
1624
1710
  const flow = flowByEdgeId.get(edge.id)
1625
- setSelection({
1626
- type: 'edge',
1627
- id: edge.id,
1628
- data: {
1629
- source: edge.source,
1630
- target: edge.target,
1631
- port: flow?.port || 0,
1632
- connections: flow?.connections || 0,
1633
- protocol: flow?.protocol || 'tcp',
1634
- flow,
1635
- },
1636
- })
1637
- onSelectionChange?.({ type: 'edge', sourceId: edge.source, destId: edge.target, port: flow?.port })
1711
+ setSelection(edgeSelection(edge, flow))
1712
+ onSelectionChange?.({ type: 'edge', sourceId: edge.source, destId: edge.target, port: flow?.port, directionUnknown: !!flow?.directionUnknown })
1638
1713
  }, [flowByEdgeId, onSelectionChange])
1639
1714
 
1640
1715
  const onPaneClick = useCallback(() => {
@@ -1645,7 +1720,7 @@ export function TrafficGraph({ flows, hotPathThreshold = 0, showNamespaceGroups
1645
1720
  return (
1646
1721
  <div className="w-full h-full relative">
1647
1722
  <ReactFlow
1648
- nodes={nodes}
1723
+ nodes={displayNodes}
1649
1724
  edges={edges}
1650
1725
  onNodesChange={onNodesChange}
1651
1726
  onEdgesChange={onEdgesChange}
@@ -1677,7 +1752,15 @@ export function TrafficGraph({ flows, hotPathThreshold = 0, showNamespaceGroups
1677
1752
  {selection && (
1678
1753
  <DetailsPanel
1679
1754
  selection={selection}
1680
- onClose={() => setSelection(null)}
1755
+ onClose={onPaneClick}
1756
+ onFocus={onFocus && selection.type === 'node' && selection.id !== focusedId && isFocusable(selection.id, selection.data as TrafficNodeData)
1757
+ ? () => {
1758
+ // The id is "ns/name"; an endpoint with no namespace is its
1759
+ // name alone, which may hold a slash of its own.
1760
+ const ns = (selection.data as TrafficNodeData).namespace || undefined
1761
+ onFocus({ namespace: ns, name: ns ? selection.id.slice(ns.length + 1) : selection.id })
1762
+ }
1763
+ : undefined}
1681
1764
  flows={flows}
1682
1765
  isRateBased={isRateBased}
1683
1766
  />
@@ -0,0 +1,150 @@
1
+ import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
2
+ import { ChevronDown, Network } from 'lucide-react'
3
+ import { pluralNoun } from '@skyhook-io/k8s-ui'
4
+ import { SEVERITY_TEXT } from '@skyhook-io/k8s-ui/utils/badge-colors'
5
+ import type { AggregatedFlow } from '../../types'
6
+ import { LargeClusterNamespacePicker } from '../shared/LargeClusterNamespacePicker'
7
+ import { TrafficConnectionsTable } from './TrafficConnectionsTable'
8
+ import { TrafficFocusSearch } from './TrafficFocusSearch'
9
+ import type { TrafficGraphSelection } from './TrafficGraph'
10
+ import { formatRate, namespaceOverBudget, type EndpointSummary, type NamespaceSummary, type TrafficFocus } from './trafficFilters'
11
+
12
+ const count = (n: number, noun: string) => `${n.toLocaleString()} ${pluralNoun(n, noun)}`
13
+
14
+ interface TrafficGraphTooLargeProps {
15
+ graph: { nodes: number; edges: number }
16
+ /** The focused endpoint's name, when the view is a neighborhood. */
17
+ focusName?: string
18
+ /** The sidebar can hide other namespaces' endpoints from this view. */
19
+ canFilterNamespaces?: boolean
20
+ onDrawAnyway?: () => void
21
+ /** Null when the namespace can't be changed from here (a single namespace
22
+ * in view, or a deployment scoped to one). */
23
+ namespaces: NamespaceSummary[] | null
24
+ onPickNamespace: (namespace: string) => void
25
+ endpoints: EndpointSummary[]
26
+ onFocus: (focus: TrafficFocus) => void
27
+ isRateBased: boolean
28
+ overlayContainer?: HTMLElement | null
29
+ flows: AggregatedFlow[]
30
+ selection: TrafficGraphSelection | null
31
+ onSelect: (selection: TrafficGraphSelection | null) => void
32
+ focusedId?: string
33
+ /** Shown above the explanation, in the layout rather than over it. */
34
+ notice?: ReactNode
35
+ }
36
+
37
+ function nextStep(focused: boolean, canPickNamespace: boolean, canFilterNamespaces: boolean): string {
38
+ if (focused) return 'Its connections are listed below, problems first. Focus on one of them, or narrow them with the traffic filters on the left.'
39
+ if (canPickNamespace) return 'They are listed below, problems first. Narrow to a namespace or focus on a workload to see a map.'
40
+ if (canFilterNamespaces) return 'They are listed below, problems first. Focus on a workload, or hide other namespaces in the Namespaces filter on the left.'
41
+ return 'They are listed below, problems first. Focus on a workload to see a map.'
42
+ }
43
+
44
+ /**
45
+ * Stands in for the map when it is too large to lay out: says why, offers the
46
+ * ways to narrow it, and lists the connections so the view still answers
47
+ * something.
48
+ */
49
+ export function TrafficGraphTooLarge(props: TrafficGraphTooLargeProps) {
50
+ const { graph, focusName, canFilterNamespaces, onDrawAnyway, namespaces, onPickNamespace, endpoints, onFocus, isRateBased, overlayContainer, notice } = props
51
+ const [pickerOpen, setPickerOpen] = useState(false)
52
+ const pickerRef = useRef<HTMLDivElement>(null)
53
+ useEffect(() => {
54
+ if (!pickerOpen) return
55
+ const handler = (e: MouseEvent) => {
56
+ if (pickerRef.current && !pickerRef.current.contains(e.target as Node)) setPickerOpen(false)
57
+ }
58
+ document.addEventListener('mousedown', handler, true)
59
+ return () => document.removeEventListener('mousedown', handler, true)
60
+ }, [pickerOpen])
61
+
62
+ const statsByName = useMemo(() => new Map((namespaces ?? []).map(ns => [ns.name, ns])), [namespaces])
63
+
64
+ return (
65
+ <div className="absolute inset-x-0 bottom-0 top-12 flex flex-col px-3 pb-3 gap-2">
66
+ {notice}
67
+ <div className="rounded-lg border border-theme-border bg-theme-surface px-4 py-3 shadow-theme-sm">
68
+ <div className="flex items-start gap-3">
69
+ <Network className="mt-0.5 h-5 w-5 shrink-0 text-theme-text-tertiary" />
70
+ <div className="min-w-0 flex-1">
71
+ <div className="text-sm font-medium text-theme-text-primary">
72
+ {focusName ? `${focusName} has too many connections to draw` : 'Too much traffic to draw at once'}
73
+ </div>
74
+ <p className="mt-0.5 text-xs text-theme-text-secondary">
75
+ {count(graph.nodes, 'endpoint')} and {count(graph.edges, 'connection')} — more than the map can lay out without freezing this tab.
76
+ {' '}{nextStep(!!focusName, !!namespaces?.length, !!canFilterNamespaces)}
77
+ </p>
78
+ <div className="mt-2 flex flex-wrap items-center gap-2">
79
+ {namespaces && namespaces.length > 0 && (
80
+ <div className="relative" ref={pickerRef}>
81
+ <button
82
+ type="button"
83
+ onClick={() => setPickerOpen(o => !o)}
84
+ aria-expanded={pickerOpen}
85
+ className="btn-brand-muted inline-flex items-center gap-1 text-xs px-2.5 py-1 rounded-md"
86
+ >
87
+ Narrow to a namespace <ChevronDown className="h-3.5 w-3.5" />
88
+ </button>
89
+ {pickerOpen && (
90
+ <div className="absolute left-0 top-full z-50 mt-1 w-80 rounded-lg border border-theme-border bg-theme-surface p-2 shadow-theme-lg">
91
+ <LargeClusterNamespacePicker
92
+ namespaces={namespaces}
93
+ ranked
94
+ onSelect={ns => {
95
+ setPickerOpen(false)
96
+ onPickNamespace(ns)
97
+ }}
98
+ renderMeta={name => {
99
+ const ns = statsByName.get(name)
100
+ if (!ns) return null
101
+ return (
102
+ <span className="flex shrink-0 items-center gap-1.5 text-[10px] tabular-nums text-theme-text-tertiary">
103
+ {ns.drops > 0 && <span className={SEVERITY_TEXT.error}>{ns.drops.toLocaleString()} dropped</span>}
104
+ {ns.errors > 0 && <span className={SEVERITY_TEXT.error}>{isRateBased ? `${formatRate(ns.errors)}/s` : ns.errors.toLocaleString()} 5xx</span>}
105
+ <span>{count(ns.endpoints, 'endpoint')}</span>
106
+ {namespaceOverBudget(ns) && <span className={SEVERITY_TEXT.warning}>too large to map</span>}
107
+ </span>
108
+ )
109
+ }}
110
+ />
111
+ <p className="mt-1 px-1 text-[10px] text-theme-text-tertiary">
112
+ Sets the namespace for all of Radar. Traffic to and from other namespaces stays in view.
113
+ </p>
114
+ </div>
115
+ )}
116
+ </div>
117
+ )}
118
+ <TrafficFocusSearch
119
+ endpoints={endpoints}
120
+ onFocus={onFocus}
121
+ isRateBased={isRateBased}
122
+ overlayContainer={overlayContainer}
123
+ variant="panel"
124
+ />
125
+ {onDrawAnyway && (
126
+ <button
127
+ type="button"
128
+ onClick={onDrawAnyway}
129
+ className="text-xs px-2.5 py-1 rounded-md border border-theme-border text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-hover"
130
+ >
131
+ Draw anyway
132
+ </button>
133
+ )}
134
+ </div>
135
+ </div>
136
+ </div>
137
+ </div>
138
+ <div className="min-h-0 flex-1 overflow-hidden rounded-lg border border-theme-border bg-theme-surface">
139
+ <TrafficConnectionsTable
140
+ flows={props.flows}
141
+ isRateBased={isRateBased}
142
+ selection={props.selection}
143
+ onSelect={props.onSelect}
144
+ onFocus={onFocus}
145
+ focusedId={props.focusedId}
146
+ />
147
+ </div>
148
+ </div>
149
+ )
150
+ }